diff --git a/blueprint/auth/interfaces/direct.go b/blueprint/auth/interfaces/direct.go index eaa996d3..a76408c8 100644 --- a/blueprint/auth/interfaces/direct.go +++ b/blueprint/auth/interfaces/direct.go @@ -54,29 +54,29 @@ func (ap *DirectAuthProvider) Signin(c *gin.Context) { directAPISigninByField := core.CurrentConfig.D.Uadmin.DirectAPISigninByField db.Db.Model(core.User{}).Where(fmt.Sprintf("%s = ?", directAPISigninByField), json.SigninField).First(&user) if user.ID == 0 { - c.JSON(http.StatusBadRequest, core.APIBadResponse("login credentials are incorrect")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("login_credentials_incorrect", "login credentials are incorrect")) return } if !user.Active { - c.JSON(http.StatusBadRequest, core.APIBadResponse("this user is inactive")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("user_inactive", "this user is inactive")) return } if !user.IsPasswordUsable { - c.JSON(http.StatusBadRequest, core.APIBadResponse("this user doesn't have a password")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("password_is_not_configured", "this user doesn't have a password")) return } err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(json.Password+user.Salt)) if err != nil { - c.JSON(http.StatusBadRequest, core.APIBadResponse("login credentials are incorrect")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("login_credentials_incorrect", "login credentials are incorrect")) return } if user.GeneratedOTPToVerify != "" { if json.OTP == "" { - c.JSON(http.StatusBadRequest, core.APIBadResponse("otp is required")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("otp_required", "otp is required")) return } if user.GeneratedOTPToVerify != json.OTP { - c.JSON(http.StatusBadRequest, core.APIBadResponse("otp provided by user is wrong")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("otp_is_wrong", "otp provided by user is wrong")) return } user.GeneratedOTPToVerify = "" @@ -214,7 +214,7 @@ func (ap *DirectAuthProvider) IsAuthenticated(c *gin.Context) { return } if sessionAdapter.IsExpired() { - c.JSON(http.StatusBadRequest, core.APIBadResponse("session expired")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("session_expired", "session expired")) return } c.JSON(http.StatusOK, GetUserForAPI(sessionAdapter.GetUser())) diff --git a/blueprint/auth/interfaces/direct_for_admin.go b/blueprint/auth/interfaces/direct_for_admin.go index cc9a2630..dec5210d 100644 --- a/blueprint/auth/interfaces/direct_for_admin.go +++ b/blueprint/auth/interfaces/direct_for_admin.go @@ -52,24 +52,24 @@ func (ap *DirectAuthForAdminProvider) Signin(c *gin.Context) { var user = core.GenerateUserModel() db.Model(core.User{}).Where(&core.User{Username: json.SigninField}).First(user) if user.GetID() == 0 { - c.JSON(http.StatusBadRequest, core.APIBadResponse("login credentials are incorrect.")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("login_credentials_incorrect", "login credentials are incorrect.")) return } if !user.GetActive() { - c.JSON(http.StatusBadRequest, core.APIBadResponse("this user is inactive")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("user_inactive", "this user is inactive")) return } if !user.GetIsSuperUser() && !user.GetIsStaff() { - c.JSON(http.StatusBadRequest, core.APIBadResponse("this user doesn't have an access to admin panel")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("user_has_no_access_to_admin_panel", "this user doesn't have an access to admin panel")) return } if !user.GetIsPasswordUsable() { - c.JSON(http.StatusBadRequest, core.APIBadResponse("this user doesn't have a password")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("password_is_not_configured", "this user doesn't have a password")) return } err := bcrypt.CompareHashAndPassword([]byte(user.GetPassword()), []byte(json.Password+user.GetSalt())) if err != nil { - c.JSON(http.StatusBadRequest, core.APIBadResponse("login credentials are incorrect.")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("login_credentials_incorrect", "login credentials are incorrect.")) return } sessionAdapterRegistry := sessionsblueprint.ConcreteBlueprint.SessionAdapterRegistry @@ -206,7 +206,7 @@ func (ap *DirectAuthForAdminProvider) IsAuthenticated(c *gin.Context) { return } if sessionAdapter.IsExpired() { - c.JSON(http.StatusBadRequest, core.APIBadResponse("session expired")) + c.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("session_expired", "session expired")) return } c.JSON(http.StatusOK, getUserForUadminPanel(sessionAdapter.GetUser())) diff --git a/blueprint/language/language.go b/blueprint/language/language.go index 864759f8..8be5cd3e 100644 --- a/blueprint/language/language.go +++ b/blueprint/language/language.go @@ -1,7 +1,6 @@ package language import ( - "errors" "fmt" "github.com/gin-gonic/gin" "github.com/sergeyglazyrindev/uadmin/blueprint/language/migrations" @@ -47,7 +46,7 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { lang := &core.Language{} uadminDatabase.Db.Where(&core.Language{Default: true}).First(lang) if lang.ID != 0 && ID != strconv.Itoa(int(lang.ID)) { - return errors.New("only one default language could be configured") + return core.NewHTTPErrorResponse("only_one_default_language_allowed", "only one default language could be configured") } return nil }) diff --git a/blueprint/sessions/interfaces/db.go b/blueprint/sessions/interfaces/db.go index dc36b35b..3e4dd8f7 100644 --- a/blueprint/sessions/interfaces/db.go +++ b/blueprint/sessions/interfaces/db.go @@ -1,7 +1,6 @@ package interfaces import ( - "fmt" "github.com/sergeyglazyrindev/uadmin/core" "time" ) @@ -59,7 +58,7 @@ func (s *DbSession) GetByKey(sessionKey string) (ISessionProvider, error) { var session core.Session db.Db.Model(&core.Session{}).Where(&core.Session{Key: sessionKey}).Preload("User").First(&session) if session.ID == 0 { - return nil, fmt.Errorf("no session with key %s found", sessionKey) + return nil, core.NewHTTPErrorResponse("session_not_found", "no session with key %s found", sessionKey) } return &DbSession{ session: &session, diff --git a/blueprint/sessions/sessions.go b/blueprint/sessions/sessions.go index 7a680f98..9974bc3c 100644 --- a/blueprint/sessions/sessions.go +++ b/blueprint/sessions/sessions.go @@ -26,7 +26,7 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { return } contentType := c.Request.Header.Get("Content-Type") - if contentType == "application/json" { + if strings.Contains(contentType, "application/json") { c.Next() return } @@ -84,7 +84,7 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { return } contentType := c.Request.Header.Get("Content-Type") - if contentType == "application/json" { + if strings.Contains(contentType, "application/json") { c.Next() return } diff --git a/blueprint/user/user.go b/blueprint/user/user.go index 26a0f303..873d9a7a 100644 --- a/blueprint/user/user.go +++ b/blueprint/user/user.go @@ -71,7 +71,7 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { user := core.GenerateUserModel() db.Model(core.GenerateUserModel()).Where(&core.User{Email: json.Email}).First(user) if user.GetID() == 0 { - ctx.JSON(http.StatusBadRequest, core.APIBadResponse("User with this email not found")) + ctx.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("user_not_found", "User with this email not found")) return } templateWriter := bytes.NewBuffer([]byte{}) @@ -135,11 +135,11 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { var oneTimeAction core.OneTimeAction db.Model(core.OneTimeAction{}).Where(&core.OneTimeAction{Code: json.Code, IsUsed: false}).Preload("User").First(&oneTimeAction) if oneTimeAction.ID == 0 { - ctx.JSON(http.StatusBadRequest, core.APIBadResponse("No such code found")) + ctx.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("code_not_found", "No such code found")) return } if oneTimeAction.ExpiresOn.Before(time.Now()) { - ctx.JSON(http.StatusBadRequest, core.APIBadResponse("Code is expired")) + ctx.JSON(http.StatusBadRequest, core.APIBadResponseWithCode("code_expired", "Code is expired")) return } passwordValidationStruct := &PasswordValidationStruct{ @@ -369,14 +369,14 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { return ret } userGroupsWidget.LeftSelectTitle = "Available groups" - userGroupsWidget.LeftSelectHelp = "This is the list of available groups. You may choose some by selecting them in the box below and then clicking the \"Choose\" arrow between the two boxes." - userGroupsWidget.LeftSearchSelectHelp = "Type into this box to filter down the list of available groups." + userGroupsWidget.LeftSelectHelp = "available_groups_left" + userGroupsWidget.LeftSearchSelectHelp = "available_groups_search_help" userGroupsWidget.LeftHelpChooseAll = "Click to choose all groups at once." userGroupsWidget.RightSelectTitle = "Chosen groups" - userGroupsWidget.RightSelectHelp = "This is the list of chosen groups. You may remove some by selecting them in the box below and then clicking the \"Remove\" arrow between the two boxes." + userGroupsWidget.RightSelectHelp = "chosen_groups_left" userGroupsWidget.RightSearchSelectHelp = "" userGroupsWidget.RightHelpChooseAll = "Click to remove all chosen groups at once." - userGroupsWidget.HelpText = "The groups this user belongs to. A user will get all permissions granted to each of their groups. Hold down \"Control\", or \"Command\" on a Mac, to select more than one." + userGroupsWidget.HelpText = "group_widget_help" permissionsField, _ := form.FieldRegistry.GetByName("Permissions") permissionsField.SetUpField = func(w core.IWidget, modelI interface{}, v interface{}, afo core.IAdminFilterObjects) error { model := modelI.(*core.User) @@ -414,14 +414,14 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { return ret } permissionsWidget.LeftSelectTitle = "Available user permissions" - permissionsWidget.LeftSelectHelp = "This is the list of available user permissions. You may choose some by selecting them in the box below and then clicking the \"Choose\" arrow between the two boxes." - permissionsWidget.LeftSearchSelectHelp = "Type into this box to filter down the list of available user permissions." + permissionsWidget.LeftSelectHelp = "available_permissions_left" + permissionsWidget.LeftSearchSelectHelp = "available_permissions_search_help" permissionsWidget.LeftHelpChooseAll = "Click to choose all user permissions at once." permissionsWidget.RightSelectTitle = "Chosen user permissions" - permissionsWidget.RightSelectHelp = "This is the list of chosen user permissions. You may remove some by selecting them in the box below and then clicking the \"Remove\" arrow between the two boxes." + permissionsWidget.RightSelectHelp = "chosen_permissions_left" permissionsWidget.RightSearchSelectHelp = "" permissionsWidget.RightHelpChooseAll = "Click to remove all chosen user permissions at once." - permissionsWidget.HelpText = "Specific permissions for this user. Hold down \"Control\", or \"Command\" on a Mac, to select more than one." + permissionsWidget.HelpText = "permission_widget_help" permissionsWidget.PopulateRightSide = func() []*core.SelectOptGroup { ret := make([]*core.SelectOptGroup, 0) user := modelI.(*core.User) @@ -545,14 +545,14 @@ func (b Blueprint) InitRouter(app core.IApp, group *gin.RouterGroup) { return ret } permissionsWidget.LeftSelectTitle = "Available permissions" - permissionsWidget.LeftSelectHelp = "This is the list of available permissions. You may choose some by selecting them in the box below and then clicking the \"Choose\" arrow between the two boxes." - permissionsWidget.LeftSearchSelectHelp = "Type into this box to filter down the list of available user permissions." + permissionsWidget.LeftSelectHelp = "available_permissions_left" + permissionsWidget.LeftSearchSelectHelp = "available_permissions_search_help" permissionsWidget.LeftHelpChooseAll = "Click to choose all user permissions at once." permissionsWidget.RightSelectTitle = "Chosen permissions" - permissionsWidget.RightSelectHelp = "This is the list of chosen permissions. You may remove some by selecting them in the box below and then clicking the \"Remove\" arrow between the two boxes." + permissionsWidget.RightSelectHelp = "chosen_permissions_left" permissionsWidget.RightSearchSelectHelp = "" permissionsWidget.RightHelpChooseAll = "Click to remove all chosen permissions at once." - permissionsWidget.HelpText = "Specific permissions for this user. Hold down \"Control\", or \"Command\" on a Mac, to select more than one." + permissionsWidget.HelpText = "permission_widget_help" permissionsWidget.PopulateRightSide = func() []*core.SelectOptGroup { ret := make([]*core.SelectOptGroup, 0) user := modelI.(*core.UserGroup) @@ -622,7 +622,7 @@ func (b Blueprint) InitApp(app core.IApp) { if cUsers == 0 { return nil } - return fmt.Errorf("user with name %s is already registered", i.(string)) + return core.NewHTTPErrorResponse("user_name_already_registered", "user with name %s is already registered", i.(string)) }) core.UadminValidatorRegistry.AddValidator("email-unique", func(i interface{}, o interface{}) error { @@ -634,7 +634,7 @@ func (b Blueprint) InitApp(app core.IApp) { if cUsers == 0 { return nil } - return fmt.Errorf("user with email %s is already registered", i.(string)) + return core.NewHTTPErrorResponse("user_email_already_registered", "user with email %s is already registered", i.(string)) }) core.UadminValidatorRegistry.AddValidator("username-uadmin", func(i interface{}, o interface{}) error { @@ -642,7 +642,7 @@ func (b Blueprint) InitApp(app core.IApp) { maxLength := core.CurrentConfig.D.Auth.MaxUsernameLength currentUsername := i.(string) if maxLength < len(currentUsername) || len(currentUsername) < minLength { - return fmt.Errorf("length of the username has to be between %d and %d symbols", minLength, maxLength) + return core.NewHTTPErrorResponse("username_length_error", "length of the username has to be between %s and %s symbols", strconv.Itoa(minLength), strconv.Itoa(maxLength)) } return nil }) @@ -650,10 +650,10 @@ func (b Blueprint) InitApp(app core.IApp) { core.UadminValidatorRegistry.AddValidator("password-uadmin", func(i interface{}, o interface{}) error { passwordStruct := o.(PasswordValidationStruct) if passwordStruct.Password != passwordStruct.ConfirmedPassword { - return fmt.Errorf("password doesn't equal to confirmed password") + return core.NewHTTPErrorResponse("password_not_equal", "password doesn't equal to confirmed password") } if len(passwordStruct.Password) < core.CurrentConfig.D.Auth.MinPasswordLength { - return fmt.Errorf("length of the password has to be at least %d symbols", core.CurrentConfig.D.Auth.MinPasswordLength) + return core.NewHTTPErrorResponse("password_length_error", "length of the password has to be at least %d symbols", strconv.Itoa(core.CurrentConfig.D.Auth.MinPasswordLength)) } return nil }) diff --git a/core/admin_dashboard_panel.go b/core/admin_dashboard_panel.go index bd6571f9..83b6ae66 100644 --- a/core/admin_dashboard_panel.go +++ b/core/admin_dashboard_panel.go @@ -2,7 +2,6 @@ package core import ( "bytes" - "errors" "fmt" excelize1 "github.com/360EntSecGroup-Skylar/excelize/v2" "github.com/gin-gonic/gin" @@ -310,7 +309,7 @@ func (dap *DashboardAdminPanel) RegisterHTTPHandlers(router *gin.Engine) { c.ListEditableFormsForInlines.AddForInlineWholeCollection(inline.Prefix, inlineListEditableCollection) } if !successfulInline { - return errors.New("error while submitting inlines") + return NewHTTPErrorResponse("error_inline_submit", "error while submitting inlines") } if ctx.Query("_popup") == "1" { mID := GetID(reflect.ValueOf(modelToSave)) @@ -330,7 +329,7 @@ func (dap *DashboardAdminPanel) RegisterHTTPHandlers(router *gin.Engine) { } return nil } - return errors.New("not successful form validation") + return NewHTTPErrorResponse("not_successful_form_validation", "not successful form validation") }) if err != nil { form.FormError.GeneralErrors = append(form.FormError.GeneralErrors, err) diff --git a/core/admin_model_actions.go b/core/admin_model_actions.go index 9f8842e7..66755ea9 100644 --- a/core/admin_model_actions.go +++ b/core/admin_model_actions.go @@ -53,7 +53,7 @@ func init() { } if removalConfirmed != "" { query := ctx.Request.URL.Query() - query.Set("message", "Objects were removed succesfully") + query.Set("message", Tf(c.Language.Code, "Objects were removed succesfully")) ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/%s/%s/?%s", CurrentConfig.D.Uadmin.RootAdminURL, ap.ParentPage.Slug, ap.ModelName, query.Encode())) return nil } diff --git a/core/admin_page.go b/core/admin_page.go index 4c590472..77f37014 100644 --- a/core/admin_page.go +++ b/core/admin_page.go @@ -220,7 +220,7 @@ func (ap *AdminPage) HandleModelAction(modelActionName string, ctx *gin.Context) PopulateTemplateContextForAdminPanel(ctx, adminContext, adminRequestParams) afo := ap.GetQueryset(adminContext, ap, adminRequestParams) var json1 ModelActionRequestParams - if ctx.GetHeader("Content-Type") == "application/json" { + if strings.Contains(ctx.GetHeader("Content-Type"), "application/json") { if err := ctx.ShouldBindJSON(&json1); err != nil { ctx.JSON(http.StatusBadRequest, APIBadResponse(err.Error())) return @@ -241,7 +241,7 @@ func (ap *AdminPage) HandleModelAction(modelActionName string, ctx *gin.Context) primaryKeyField, _ := ap.Form.FieldRegistry.GetPrimaryKey() afo.FilterByMultipleIds(primaryKeyField, json1.RealObjectIds) modelAction, _ := ap.ModelActionsRegistry.GetModelActionByName(modelActionName) - if ctx.GetHeader("Content-Type") == "application/json" { + if strings.Contains(ctx.GetHeader("Content-Type"), "application/json") { _, affectedRows := modelAction.Handler(ap, afo, ctx) ctx.JSON(http.StatusOK, gin.H{"Affected": strconv.Itoa(int(affectedRows))}) } else { diff --git a/core/admin_page_inlines.go b/core/admin_page_inlines.go index 0b30edb9..8f9941aa 100644 --- a/core/admin_page_inlines.go +++ b/core/admin_page_inlines.go @@ -1,7 +1,6 @@ package core import ( - "errors" "fmt" "html/template" "mime/multipart" @@ -56,6 +55,9 @@ func (api *AdminPageInline) RenderExampleForm(adminContext IAdminContext) templa func1 := make(template.FuncMap) path := "admin/inlineexampleform" templateName := CurrentConfig.GetPathToTemplate(path) + templateRenderer.AddFuncMap("Translate", func(v interface{}) string { + return Tf(adminContext.GetLanguage().Code, v) + }) return templateRenderer.RenderAsString( templateName, c, FuncMap, func1, @@ -65,6 +67,14 @@ func (api *AdminPageInline) RenderExampleForm(adminContext IAdminContext) templa func (api *AdminPageInline) GetFormForExample(adminContext IAdminContext) *FormListEditable { modelI, _ := api.GenerateModelI(nil) form := api.ListDisplay.BuildListEditableFormForNewModel(adminContext, "toreplacewithid", modelI) + r := NewTemplateRenderer("") + r.AddFuncMap("Translate", func(v interface{}) string { + return Tf(adminContext.GetLanguage().Code, v) + }) + for _, field := range form.FieldRegistry.GetAllFields() { + field.FieldConfig.Widget.RenderUsingRenderer(r) + } + // return r.RenderAsString(templateName, data, baseFuncMap) return form } @@ -160,7 +170,7 @@ func (api *AdminPageInline) ProceedRequest(afo IAdminFilterObjects, f *multipart } } if err { - return collection, errors.New("error while validating inlines") + return collection, NewHTTPErrorResponse("inline_validation_error", "error while validating inlines") } return collection, nil } diff --git a/core/blueprint_interfaces.go b/core/blueprint_interfaces.go index 7acbd392..089b5710 100644 --- a/core/blueprint_interfaces.go +++ b/core/blueprint_interfaces.go @@ -1,6 +1,7 @@ package core import ( + "encoding/json" "errors" "fmt" mapset "github.com/deckarep/golang-set" @@ -288,6 +289,18 @@ func (r BlueprintRegistry) InitializeRouting(app IApp, router *gin.Engine) { "message": "pong", }) }) + router.GET("/localization/", func(ctx *gin.Context) { + type Context struct { + AdminContext + } + c := &Context{} + adminRequestParams := NewAdminRequestParamsFromGinContext(ctx) + PopulateTemplateContextForAdminPanel(ctx, c, adminRequestParams) + langMap := ReadLocalization(c.GetLanguage().Code) + langMapB, _ := json.Marshal(langMap) + ctx.Header("Content-Type", "application/javascript") + ctx.String(200, fmt.Sprintf("setLocalization(%s)", string(langMapB))) + }) router.POST("/testcsrf/", func(c *gin.Context) { c.String(200, "csrf token test passed") }) diff --git a/core/config_interfaces.go b/core/config_interfaces.go index 2c9fe14c..700d5bec 100644 --- a/core/config_interfaces.go +++ b/core/config_interfaces.go @@ -161,6 +161,7 @@ type UadminConfig struct { TemplatesFS embed.FS OverridenTemplatesFS *embed.FS LocalizationFS embed.FS + CustomLocalizationFS *embed.FS RequiresCsrfCheck func(c *gin.Context) bool PatternsToIgnoreCsrfCheck *list.List ErrorHandleFunc func(int, string, string) diff --git a/core/form.go b/core/form.go index bf1cb689..dff4887a 100644 --- a/core/form.go +++ b/core/form.go @@ -2,7 +2,6 @@ package core import ( "bytes" - "fmt" "github.com/davecgh/go-spew/spew" "gorm.io/gorm" "html/template" @@ -41,7 +40,7 @@ type GrouppedFieldsRegistry struct { func (tfr *GrouppedFieldsRegistry) GetGroupByName(name string) (*GrouppedFields, error) { gf, ok := tfr.GrouppedFields[name] if !ok { - return nil, fmt.Errorf("no field %s found", name) + return nil, NewHTTPErrorResponse("field_not_found", "no field %s found", name) } return gf, nil } @@ -133,12 +132,12 @@ func (f *Form) Render() template.HTML { } } } - err := RenderHTMLAsString(templateWriter, CurrentConfig.GetPathToTemplate(path), data2, FuncMap, funcs1) - if err != nil { - Trail(CRITICAL, "Error while parsing include of the template %s", "form/grouprow") - return "" - } - ret = append(ret, templateWriter.String()) + //err := f.Renderer.RenderAsString(CurrentConfig.GetPathToTemplate(path), data2, FuncMap, funcs1) + //if err != nil { + // Trail(CRITICAL, "Error while parsing include of the template %s", "form/grouprow") + // return "" + //} + ret = append(ret, string(f.Renderer.RenderAsString(CurrentConfig.GetPathToTemplate(path), data2, FuncMap, funcs1))) } } return template.HTML(strings.Join(ret, "\n")) @@ -175,6 +174,12 @@ func (f *Form) Render() template.HTML { func1["GetRenderContext"] = func() *FormRenderContext { return f.RenderContext } + f.Renderer.AddFuncMap("Translate", func(v interface{}) string { + return Tf(f.RenderContext.Context.GetLanguage().Code, v) + }) + for _, field := range f.FieldRegistry.GetAllFields() { + field.FieldConfig.Widget.RenderUsingRenderer(f.Renderer) + } func1["RenderFieldGroups"] = RenderFieldGroups(func1) path := "form" if f.ForAdminPanel { @@ -227,7 +232,9 @@ func (f *Form) ProceedRequest(form *multipart.Form, gormModel interface{}, admin } modelF := model.FieldByName(field.Name) if !modelF.IsValid() { - formError.AddGeneralError(fmt.Errorf("not valid field %s for model", field.Name)) + formError.AddGeneralError( + NewHTTPErrorResponse("field_invalid", "not valid field %s for model", field.Name), + ) continue } if !formError.IsEmpty() { @@ -241,7 +248,9 @@ func (f *Form) ProceedRequest(form *multipart.Form, gormModel interface{}, admin continue } if !modelF.CanSet() { - formError.AddGeneralError(fmt.Errorf("can't set field %s for model", field.Name)) + formError.AddGeneralError( + NewHTTPErrorResponse("cant_set_field", "can't set field %s for model", field.Name), + ) continue } err := SetUpStructField(modelF, field.FieldConfig.Widget.GetOutputValue()) diff --git a/core/form_fields.go b/core/form_fields.go index 98696b7f..3ca894cc 100644 --- a/core/form_fields.go +++ b/core/form_fields.go @@ -275,7 +275,7 @@ type FieldRegistry struct { func (fr *FieldRegistry) GetByName(name string) (*Field, error) { f, ok := fr.Fields[name] if !ok { - return nil, fmt.Errorf("no field %s found", name) + return nil, NewHTTPErrorResponse("field_not_found", "no field %s found", name) } return f, nil } diff --git a/core/form_list_editable.go b/core/form_list_editable.go index ecc70f4a..e87390e0 100644 --- a/core/form_list_editable.go +++ b/core/form_list_editable.go @@ -90,11 +90,15 @@ func (f *FormListEditable) ProceedRequest(form *multipart.Form, gormModel interf continue } if !modelF.IsValid() { - formError.AddGeneralError(fmt.Errorf("not valid field %s for model", field.Name)) + formError.AddGeneralError( + NewHTTPErrorResponse("field_invalid", "not valid field %s for model", field.Name), + ) continue } if !modelF.CanSet() { - formError.AddGeneralError(fmt.Errorf("can't set field %s for model", field.Name)) + formError.AddGeneralError( + NewHTTPErrorResponse("cant_set_field", "can't set field %s for model", field.Name), + ) continue } if field.SetUpField != nil { diff --git a/core/form_widget.go b/core/form_widget.go index a19ed056..8fa29ff5 100644 --- a/core/form_widget.go +++ b/core/form_widget.go @@ -2,7 +2,6 @@ package core import ( "encoding/json" - "errors" "fmt" "github.com/asaskevich/govalidator" "gorm.io/gorm" @@ -309,7 +308,7 @@ func (w *Widget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, rend } w.SetValue(v[0]) if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } w.SetOutputValue(v[0]) return nil @@ -357,7 +356,7 @@ func (w *Widget) Render(formRenderContext *FormRenderContext, currentField *Fiel data := w.GetDataForRendering(formRenderContext, currentField) data["Type"] = w.GetWidgetType() data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *Widget) GetDataForRendering(formRenderContext *FormRenderContext, currentField *Field) WidgetData { @@ -382,9 +381,15 @@ func (w *Widget) GetDataForRendering(formRenderContext *FormRenderContext, curre } } -func RenderWidget(renderer ITemplateRenderer, templateName string, data map[string]interface{}, baseFuncMap template.FuncMap) template.HTML { +func RenderWidget(formRenderContext *FormRenderContext, renderer ITemplateRenderer, templateName string, data map[string]interface{}, baseFuncMap template.FuncMap) template.HTML { if renderer == nil { r := NewTemplateRenderer("") + r.AddFuncMap("Translate", func(v interface{}) string { + if formRenderContext.Context != nil && formRenderContext.Context.GetLanguage() != nil { + return Tf(formRenderContext.Context.GetLanguage().Code, v) + } + return v.(string) + }) return r.RenderAsString(templateName, data, baseFuncMap) } return renderer.RenderAsString( @@ -417,7 +422,7 @@ func (tw *TextWidget) Render(formRenderContext *FormRenderContext, currentField data := tw.Widget.GetDataForRendering(formRenderContext, currentField) data["Type"] = tw.GetWidgetType() data["ShowOnlyHtmlInput"] = tw.ShowOnlyHTMLInput - return RenderWidget(tw.Renderer, tw.GetTemplateName(), data, tw.BaseFuncMap) // tw.Value, tw.Widget.GetAttrs() + return RenderWidget(formRenderContext, tw.Renderer, tw.GetTemplateName(), data, tw.BaseFuncMap) // tw.Value, tw.Widget.GetAttrs() } type DynamicWidget struct { @@ -518,7 +523,7 @@ func (w *FkLinkWidget) Render(formRenderContext *FormRenderContext, currentField data := w.Widget.GetDataForRendering(formRenderContext, currentField) data["Type"] = w.GetWidgetType() data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) // tw.Value, tw.Widget.GetAttrs() + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) // tw.Value, tw.Widget.GetAttrs() } type NumberWidget struct { @@ -546,7 +551,7 @@ func (w *NumberWidget) Render(formRenderContext *FormRenderContext, currentField data := w.Widget.GetDataForRendering(formRenderContext, currentField) data["Type"] = w.GetWidgetType() data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *NumberWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -559,10 +564,10 @@ func (w *NumberWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects } w.SetValue(v[0]) if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } if !govalidator.IsInt(v[0]) { - return errors.New("should be a number") + return NewHTTPErrorResponse("should_be_number", "should be a number") } w.SetOutputValue(w.TransformValueForOutput(v[0])) return nil @@ -622,7 +627,7 @@ func (w *EmailWidget) Render(formRenderContext *FormRenderContext, currentField data := w.Widget.GetDataForRendering(formRenderContext, currentField) data["Type"] = w.GetWidgetType() data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *EmailWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -635,10 +640,10 @@ func (w *EmailWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, } w.SetValue(v[0]) if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } if !govalidator.IsEmail(v[0]) { - return errors.New("should be an email") + return NewHTTPErrorResponse("should_be_email", "should be an email") } w.SetOutputValue(v[0]) return nil @@ -687,7 +692,7 @@ func (w *URLWidget) Render(formRenderContext *FormRenderContext, currentField *F } else { data["ChangeLabel"] = w.ChangeLabel } - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *URLWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -700,7 +705,7 @@ func (w *URLWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, r } w.SetValue(v[0]) if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } url := v[0] if w.AppendHTTPSAutomatically { @@ -710,7 +715,7 @@ func (w *URLWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, r } } if !govalidator.IsURL(url) { - return errors.New("should be an url") + return NewHTTPErrorResponse("should_be_url", "should be an url") } w.SetOutputValue(v[0]) return nil @@ -742,7 +747,7 @@ func (w *PasswordWidget) Render(formRenderContext *FormRenderContext, currentFie data["Type"] = w.GetWidgetType() data["DisplayName"] = w.FieldDisplayName data["Value"] = "" - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *PasswordWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -758,10 +763,10 @@ func (w *PasswordWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjec return fmt.Errorf("no field with name %s has been submitted", w.FieldDisplayName) } if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } if len(v[0]) < CurrentConfig.D.Auth.MinPasswordLength { - return fmt.Errorf("length of the password has to be at least %d symbols", CurrentConfig.D.Auth.MinPasswordLength) + return NewHTTPErrorResponse("password_length_error", "length of the password has to be at least %d symbols", strconv.Itoa(CurrentConfig.D.Auth.MinPasswordLength)) } w.SetOutputValue(v[0]) return nil @@ -791,7 +796,7 @@ func (w *HiddenWidget) Render(formRenderContext *FormRenderContext, currentField data := w.Widget.GetDataForRendering(formRenderContext, currentField) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *HiddenWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -804,7 +809,7 @@ func (w *HiddenWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects } w.SetValue(v[0]) if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } w.SetOutputValue(v[0]) return nil @@ -838,7 +843,7 @@ func (w *DateWidget) Render(formRenderContext *FormRenderContext, currentField * } data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *DateWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -851,7 +856,7 @@ func (w *DateWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, } w.DateValue = v[0] if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } d, err := time.Parse(CurrentConfig.D.Uadmin.DateFormat, v[0]) if err != nil { @@ -907,7 +912,7 @@ func (w *DateTimeWidget) Render(formRenderContext *FormRenderContext, currentFie } data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *DateTimeWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -920,7 +925,7 @@ func (w *DateTimeWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjec } w.DateTimeValue = v[0] if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } d, err := time.Parse(CurrentConfig.D.Uadmin.DateTimeFormat, v[0]) if err != nil { @@ -958,7 +963,7 @@ func (w *TimeWidget) Render(formRenderContext *FormRenderContext, currentField * } data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *TimeWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -971,7 +976,7 @@ func (w *TimeWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, } w.TimeValue = v[0] if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } d, err := time.Parse(CurrentConfig.D.Uadmin.TimeFormat, v[0]) if err != nil { @@ -1005,7 +1010,7 @@ func (w *TextareaWidget) Render(formRenderContext *FormRenderContext, currentFie data := w.Widget.GetDataForRendering(formRenderContext, currentField) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *TextareaWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1018,7 +1023,7 @@ func (w *TextareaWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjec } w.SetValue(v[0]) if w.Required && v[0] == "" { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } w.SetOutputValue(v[0]) return nil @@ -1058,7 +1063,7 @@ func (w *CheckboxWidget) Render(formRenderContext *FormRenderContext, currentFie data := w.Widget.GetDataForRendering(formRenderContext, currentField) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *CheckboxWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1151,7 +1156,7 @@ func (w *SelectWidget) Render(formRenderContext *FormRenderContext, currentField data := w.GetDataForRendering(formRenderContext, currentField) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *SelectWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1181,7 +1186,7 @@ func (w *SelectWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects } w.SetValue(v[0]) if foundNotExistent { - return fmt.Errorf("value %s is not valid for the field %s", notExistentValue, w.FieldDisplayName) + return NewHTTPErrorResponse("value_invalid_for_field", "not valid value %s for the field %s", notExistentValue, w.FieldDisplayName) } w.SetOutputValue(v[0]) return nil @@ -1319,7 +1324,7 @@ func (w *ForeignKeyWidget) Render(formRenderContext *FormRenderContext, currentF data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["AddNewLink"] = w.AddNewLink data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *ForeignKeyWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1365,10 +1370,10 @@ func (w *ForeignKeyWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObj modelI, _ := w.GenerateModelInterface() queryset.Model(modelI).Count(&c) if c == 0 { - return fmt.Errorf("no object found to be used for this field") + return NewHTTPErrorResponse("object_not_found", "no object found to be used for this field") } if foundNotExistent { - return fmt.Errorf("value %s is not valid for the field %s", notExistentValue, w.FieldDisplayName) + return NewHTTPErrorResponse("value_invalid_for_field", "not valid value %s for the field %s", notExistentValue, w.FieldDisplayName) } queryset.LoadDataForModelByID(modelI, v[0]) w.SetOutputValue(modelI) @@ -1477,7 +1482,7 @@ func (w *ContentTypeSelectorWidget) Render(formRenderContext *FormRenderContext, data := w.GetDataForRendering(formRenderContext, currentField) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *ContentTypeSelectorWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1567,7 +1572,7 @@ func (w *NullBooleanWidget) Render(formRenderContext *FormRenderContext, current data := w.GetDataForRendering(formRenderContext, currentField) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *NullBooleanWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1595,7 +1600,7 @@ func (w *NullBooleanWidget) ProceedForm(form *multipart.Form, afo IAdminFilterOb } w.SetValue(v[0]) if foundNotExistent { - return fmt.Errorf("value %s is not valid for the field %s", notExistentValue, w.FieldDisplayName) + return NewHTTPErrorResponse("value_invalid_for_field", "not valid value %s for the field %s", notExistentValue, w.FieldDisplayName) } w.SetOutputValue(v[0]) return nil @@ -1654,7 +1659,7 @@ func (w *SelectMultipleWidget) Render(formRenderContext *FormRenderContext, curr data := w.GetDataForRendering(formRenderContext) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *SelectMultipleWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1682,7 +1687,7 @@ func (w *SelectMultipleWidget) ProceedForm(form *multipart.Form, afo IAdminFilte } w.SetValue(v) if foundNotExistent { - return fmt.Errorf("value %s is not valid for the field %s", notExistentValue, w.FieldDisplayName) + return NewHTTPErrorResponse("value_invalid_for_field", "not valid value %s for the field %s", notExistentValue, w.FieldDisplayName) } w.SetOutputValue(v) return nil @@ -1773,7 +1778,7 @@ func (w *RadioSelectWidget) Render(formRenderContext *FormRenderContext, current data := w.GetDataForRendering(formRenderContext) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *RadioSelectWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1801,7 +1806,7 @@ func (w *RadioSelectWidget) ProceedForm(form *multipart.Form, afo IAdminFilterOb } w.SetValue(v[0]) if foundNotExistent { - return fmt.Errorf("value %s is not valid for the field %s", notExistentValue, w.FieldDisplayName) + return NewHTTPErrorResponse("value_invalid_for_field", "not valid value %s for the field %s", notExistentValue, w.FieldDisplayName) } w.SetOutputValue(v[0]) return nil @@ -1870,7 +1875,7 @@ func (w *CheckboxSelectMultipleWidget) Render(formRenderContext *FormRenderConte data := w.GetDataForRendering(formRenderContext) data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *CheckboxSelectMultipleWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -1898,7 +1903,7 @@ func (w *CheckboxSelectMultipleWidget) ProceedForm(form *multipart.Form, afo IAd } w.SetValue(v) if foundNotExistent { - return fmt.Errorf("value %s is not valid for the field %s", notExistentValue, w.FieldDisplayName) + return NewHTTPErrorResponse("value_invalid_for_field", "not valid value %s for the field %s", notExistentValue, w.FieldDisplayName) } w.SetOutputValue(v) return nil @@ -1941,7 +1946,7 @@ func (w *FileWidget) Render(formRenderContext *FormRenderContext, currentField * data["Value"] = w.Value data["ShowOnlyHtmlInput"] = w.ShowOnlyHTMLInput data["Type"] = w.GetWidgetType() - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *FileWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -2051,7 +2056,7 @@ func (w *ClearableFileWidget) Render(formRenderContext *FormRenderContext, curre data["Id"] = w.ID data["ClearCheckboxLabel"] = w.ClearCheckboxLabel data["InputText"] = w.InputText - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *ClearableFileWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -2142,7 +2147,7 @@ func (w *MultipleInputHiddenWidget) Render(formRenderContext *FormRenderContext, subwidgets = append(subwidgets, vd) } data["Subwidgets"] = subwidgets - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *MultipleInputHiddenWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -2281,7 +2286,7 @@ func (w *ChooseFromSelectWidget) Render(formRenderContext *FormRenderContext, cu vd2["SelectorGeneralClass"] = "selector-chosen related-target" subwidgets = append(subwidgets, vd2) data["Subwidgets"] = subwidgets - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *ChooseFromSelectWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -2379,7 +2384,7 @@ func (w *SplitDateTimeWidget) Render(formRenderContext *FormRenderContext, curre vd1["TemplateName"] = templateName subwidgets = append(subwidgets, vd1) data["Subwidgets"] = subwidgets - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *SplitDateTimeWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -2388,16 +2393,16 @@ func (w *SplitDateTimeWidget) ProceedForm(form *multipart.Form, afo IAdminFilter } vDate, ok := form.Value[w.GetHTMLInputName()+"_date"] if !ok { - return fmt.Errorf("no date has been submitted for field %s", w.FieldDisplayName) + return NewHTTPErrorResponse("no_date", "no date has been submitted for field %s", w.FieldDisplayName) } w.DateValue = vDate[0] vTime, ok := form.Value[w.GetHTMLInputName()+"_time"] if !ok { - return fmt.Errorf("no time has been submitted for field %s", w.FieldDisplayName) + return NewHTTPErrorResponse("no_time", "no time has been submitted for field %s", w.FieldDisplayName) } w.TimeValue = vTime[0] if w.Required && (vDate[0] == "" || vTime[0] == "") { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } d, err := time.Parse(w.DateFormat, vDate[0]) if err != nil { @@ -2483,7 +2488,7 @@ func (w *SplitHiddenDateTimeWidget) Render(formRenderContext *FormRenderContext, vd1["TemplateName"] = templateName subwidgets = append(subwidgets, vd1) data["Subwidgets"] = subwidgets - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *SplitHiddenDateTimeWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -2492,16 +2497,16 @@ func (w *SplitHiddenDateTimeWidget) ProceedForm(form *multipart.Form, afo IAdmin } vDate, ok := form.Value[w.GetHTMLInputName()+"_date"] if !ok { - return fmt.Errorf("no date has been submitted for field %s", w.FieldDisplayName) + return NewHTTPErrorResponse("no_date", "no date has been submitted for field %s", w.FieldDisplayName) } w.DateValue = vDate[0] vTime, ok := form.Value[w.GetHTMLInputName()+"_time"] if !ok { - return fmt.Errorf("no time has been submitted for field %s", w.FieldDisplayName) + return NewHTTPErrorResponse("no_time", "no time has been submitted for field %s", w.FieldDisplayName) } w.TimeValue = vTime[0] if w.Required && (vDate[0] == "" || vTime[0] == "") { - return fmt.Errorf("field %s is required", w.FieldDisplayName) + return NewHTTPErrorResponse("field_required", "field %s is required", w.FieldDisplayName) } d, err := time.Parse(w.DateFormat, vDate[0]) if err != nil { @@ -2686,7 +2691,7 @@ func (w *SelectDateWidget) Render(formRenderContext *FormRenderContext, currentF } } data["Subwidgets"] = subwidgets - return RenderWidget(w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) + return RenderWidget(formRenderContext, w.Renderer, w.GetTemplateName(), data, w.BaseFuncMap) } func (w *SelectDateWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObjects, renderContext *FormRenderContext) error { @@ -2695,33 +2700,33 @@ func (w *SelectDateWidget) ProceedForm(form *multipart.Form, afo IAdminFilterObj } vYear, ok := form.Value[w.GetHTMLInputName()+"_year"] if !ok { - return fmt.Errorf("no year has been submitted for field %s", w.FieldDisplayName) + return NewHTTPErrorResponse("no_year", "no year has been submitted for field %s", w.FieldDisplayName) } w.YearValue = vYear[0] vMonth, ok := form.Value[w.GetHTMLInputName()+"_month"] if !ok { - return fmt.Errorf("no month has been submitted for field %s", w.FieldDisplayName) + return NewHTTPErrorResponse("no_month", "no month has been submitted for field %s", w.FieldDisplayName) } w.MonthValue = vMonth[0] vDay, ok := form.Value[w.GetHTMLInputName()+"_day"] if !ok { - return fmt.Errorf("no day has been submitted for field %s", w.FieldDisplayName) + return NewHTTPErrorResponse("no_day", "no day has been submitted for field %s", w.FieldDisplayName) } w.DayValue = vDay[0] if w.Required && (w.YearValue == "" || w.MonthValue == "" || w.DayValue == "") { - return fmt.Errorf("either year, month, value is empty") + return NewHTTPErrorResponse("either_year_month_value_empty", "either year, month, value is empty") } day, err := strconv.Atoi(w.DayValue) if err != nil { - return fmt.Errorf("incorrect day") + return NewHTTPErrorResponse("incorrect_day", "incorrect day") } month, err := strconv.Atoi(w.MonthValue) if err != nil { - return fmt.Errorf("incorrect month") + return NewHTTPErrorResponse("incorrect_month", "incorrect month") } year, err := strconv.Atoi(w.YearValue) if err != nil { - return fmt.Errorf("incorrect year") + return NewHTTPErrorResponse("incorrect_year", "incorrect year") } d := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) w.SetOutputValue(&d) diff --git a/core/http_responses.go b/core/http_responses.go index c2dfbb5d..dea98c9f 100644 --- a/core/http_responses.go +++ b/core/http_responses.go @@ -1,6 +1,9 @@ package core -import "github.com/gin-gonic/gin" +import ( + "encoding/json" + "github.com/gin-gonic/gin" +) func APINoMethodFound() gin.H { return APIBadResponse("invalid_action") @@ -10,6 +13,25 @@ func APIBadResponse(error string) gin.H { return gin.H{"error": error} } +type HTTPErrorResponse struct { + Code string + Message string + Params []interface{} +} + +func (her *HTTPErrorResponse) Error() string { + ret, _ := json.Marshal(her) + return string(ret) +} + +func NewHTTPErrorResponse(code string, message string, params ...interface{}) *HTTPErrorResponse { + return &HTTPErrorResponse{Code: code, Message: message, Params: params} +} + +func APIBadResponseWithCode(code string, error string, params ...string) gin.H { + return gin.H{"Code": code, "Message": error, "Params": params} +} + func APISuccessResp() gin.H { return gin.H{"status": true} } diff --git a/core/template.go b/core/template.go index ee9fa6d3..a52394d1 100644 --- a/core/template.go +++ b/core/template.go @@ -35,6 +35,9 @@ func (tr *TemplateRenderer) Render(ctx *gin.Context, path string, data interface if len(data1) == 1 { data2 = data1[0] } + for rendererTemplateFuncName, rendererTemplateFunc := range tr.funcMap { + funcs1[rendererTemplateFuncName] = rendererTemplateFunc + } return tr.RenderAsString(CurrentConfig.GetPathToTemplate(templateName), data2, baseFuncMap, funcs1) } } @@ -49,6 +52,9 @@ func (tr *TemplateRenderer) Render(ctx *gin.Context, path string, data interface funcs1 = funcs[0] funcs1["PageTitle"] = PageTitle } + for rendererTemplateFuncName, rendererTemplateFunc := range tr.funcMap { + funcs1[rendererTemplateFuncName] = rendererTemplateFunc + } funcs1["Include"] = Include(funcs1) RenderHTML(ctx, path, data, baseFuncMap, funcs1) } @@ -60,6 +66,9 @@ func (tr *TemplateRenderer) RenderAsString(path string, data interface{}, baseFu if len(data1) == 1 { data2 = data1[0] } + for rendererTemplateFuncName, rendererTemplateFunc := range tr.funcMap { + funcs1[rendererTemplateFuncName] = rendererTemplateFunc + } return tr.RenderAsString(CurrentConfig.GetPathToTemplate(templateName), data2, baseFuncMap, funcs1) } } @@ -74,6 +83,9 @@ func (tr *TemplateRenderer) RenderAsString(path string, data interface{}, baseFu funcs1 = funcs[0] funcs1["PageTitle"] = PageTitle } + for rendererTemplateFuncName, rendererTemplateFunc := range tr.funcMap { + funcs1[rendererTemplateFuncName] = rendererTemplateFunc + } funcs1["Include"] = Include(funcs1) templateWriter := bytes.NewBuffer([]byte{}) RenderHTMLAsString(templateWriter, path, data, baseFuncMap, funcs1) diff --git a/core/template_funcs.go b/core/template_funcs.go index 93ce4bdf..26fd3d11 100644 --- a/core/template_funcs.go +++ b/core/template_funcs.go @@ -86,6 +86,9 @@ var FuncMap = template.FuncMap{ "GenerateAttrs": GenerateAttrs, "GetDisplayName": GetDisplayName, "SplitCamelCase": HumanizeCamelCase, + //"Translate": func (v interface{}) string { + // return v.(string) + //}, //"CSRF": func() string { // return "dfsafsa" // // return authapi.GetSession(r) diff --git a/core/translation.go b/core/translation.go index cadfd745..978fd93d 100644 --- a/core/translation.go +++ b/core/translation.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" + "reflect" "strings" ) @@ -52,6 +53,42 @@ type translationLoaded map[string]string var langMapCache map[string]translationLoaded +func ReadLocalization(languageCode string) translationLoaded { + langMap, ok := langMapCache[languageCode] + if ok { + return langMap + } + ret := make(translationLoaded) + langFile, err := CurrentConfig.LocalizationFS.ReadFile(fmt.Sprintf("localization/%s.json", languageCode)) + if err != nil { + if CurrentConfig.CustomLocalizationFS != nil { + langFile, err := CurrentConfig.CustomLocalizationFS.ReadFile(fmt.Sprintf("localization/%s.json", languageCode)) + if err == nil { + err = json.Unmarshal(langFile, &ret) + } + } + } else { + err = json.Unmarshal(langFile, &ret) + if err != nil { + Trail(ERROR, "Unable to unmarshal json file with language (%s)", err) + } else { + if CurrentConfig.CustomLocalizationFS != nil { + langFile, err := CurrentConfig.CustomLocalizationFS.ReadFile(fmt.Sprintf("localization/%s.json", languageCode)) + if err == nil { + ret1 := make(translationLoaded) + err = json.Unmarshal(langFile, &ret1) + if err == nil { + for term, translated := range ret1 { + ret[term] = translated + } + } + } + } + } + } + langMapCache[languageCode] = ret + return ret +} const translateMe = "Translate me ---> " // @todo, redo @@ -64,53 +101,60 @@ const translateMe = "Translate me ---> " // the default language. // term (string): The term to translate. // args (...interface{}): Is a list of args to fill the term with place holders -func Tf(path string, lang string, term string, args ...interface{}) string { +func Tf(lang string, iTerm interface{}, args ...interface{}) string { + term := "" + iTermReflectV := reflect.ValueOf(iTerm) + httpErrorResponse := false + itemV := &HTTPErrorResponse{} + if iTermReflectV.Kind() == reflect.String { + term = iTerm.(string) + } else { + if itemV1, ok := iTerm.(*HTTPErrorResponse); ok { + httpErrorResponse = true + itemV = itemV1 + term = itemV1.Code + } + + } if lang == "" { lang = GetDefaultLanguage().Code } // Check if the path if for an existing model schema - pathParts := strings.Split(path, "/") - isSchemaFile := false - if len(pathParts) > 2 { - path = strings.Join(pathParts[0:2], "/") - isSchemaFile = true - } if langMapCache == nil { langMapCache = make(map[string]translationLoaded) } langMap, ok := langMapCache[lang] if !ok { - langFile, err := CurrentConfig.LocalizationFS.ReadFile(fmt.Sprintf("localization/%s.json", lang)) - if err != nil { - Trail(ERROR, "Unable to unmarshal json file with language (%s)", err) - } else { - err = json.Unmarshal(langFile, &langMap) - if err != nil { - Trail(ERROR, "Unable to unmarshal json file with language (%s)", err) - } else { - langMapCache[lang] = langMap - } - } + langMap = ReadLocalization(lang) } // If the term exists, then return it if val, ok := langMap[term]; ok { + if httpErrorResponse && len(itemV.Params) > 0 { + return fmt.Sprintf(strings.TrimPrefix(val, translateMe), itemV.Params...) + } return strings.TrimPrefix(val, translateMe) } - if !isSchemaFile { - // If the term exists, then return it - if val, ok := langMap[term]; ok { - return strings.TrimPrefix(val, translateMe) - } - - // If it doesn't exist then add it to the file - if lang != "en" { + // If it doesn't exist then add it to the file + if lang != "en" { + if httpErrorResponse { + langMap[term] = translateMe + itemV.Message + if len(itemV.Params) > 0 { + return fmt.Sprintf(itemV.Message, itemV.Params...) + } + } else { langMap[term] = translateMe + term - Trail(WARNING, "Unknown term %s", term) - return translateMe + term } + Trail(WARNING, "Unknown term %s", term) + return translateMe + term + } + if httpErrorResponse { + langMap[term] = itemV.Message + } else { langMap[term] = term - return term + } + if httpErrorResponse && len(itemV.Params) > 0 { + return fmt.Sprintf(itemV.Message, itemV.Params...) } return term } diff --git a/language.go b/language.go index 98e7bdba..779b000e 100644 --- a/language.go +++ b/language.go @@ -270,6 +270,7 @@ Please provide flag -c which is code of the language if !found { return fmt.Errorf("language %s doesn't exists", opts.Code) } + core.Trail(core.OK, "Language has been added, active it in admin panel") return nil } diff --git a/localization/en.json b/localization/en.json index 582a6ccb..6986120b 100644 --- a/localization/en.json +++ b/localization/en.json @@ -22,6 +22,7 @@ "systemconfigurations":"System Configurations", "opendoordelay":"Open Door Delay", "dashboard":"Dashboard", + "Dashboard":"Dashboard", "menuname":"Menu Name", "cat":"Category", "groupname":"Group Name", @@ -98,12 +99,17 @@ "city":"City", "new":"New", "save":"Save", + "password": "Password", "saveandaddanother":"Save and Add Another", "saveandcontinue":"Save and Continue", "changepassword":"Change Password", "logout":"Logout", "email":"Email", + "email address":"Email Address", + "forgot password":"Forgot Password", + "send request":"Send Request", "lastlogin":"Last Login", + "login": "Login", "savechanges":"Save Changes", "oldpassword":"Old Password", "newpassword":"New Password", @@ -539,5 +545,13 @@ "seriessetupwizard":"SERIES SETUP WIZARD", "royaltyfreeseries":"Royalty Free series", "pagenotfound": "Page not found", - "signin": "Sign in" + "signin": "Sign in", + "available_groups_left": "This is the list of available groups. You may choose some by selecting them in the box below and then clicking the \"Choose\" arrow between the two boxes.", + "available_groups_search_left": "Type into this box to filter down the list of available groups.", + "chosen_groups_left": "This is the list of chosen groups. You may remove some by selecting them in the box below and then clicking the \"Remove\" arrow between the two boxes.", + "group_widget_help": "The groups this user belongs to. A user will get all permissions granted to each of their groups. Hold down \"Control\", or \"Command\" on a Mac, to select more than one.", + "available_permissions_left": "This is the list of available user permissions. You may choose some by selecting them in the box below and then clicking the \"Choose\" arrow between the two boxes.", + "available_permissions_search_help": "Type into this box to filter down the list of available user permissions.", + "chosen_permissions_left": "This is the list of chosen user permissions. You may remove some by selecting them in the box below and then clicking the \"Remove\" arrow between the two boxes.", + "permission_widget_help": "Specific permissions for this user. Hold down \"Control\", or \"Command\" on a Mac, to select more than one." } \ No newline at end of file diff --git a/localization/ru.json b/localization/ru.json new file mode 100644 index 00000000..3f76ae34 --- /dev/null +++ b/localization/ru.json @@ -0,0 +1,557 @@ +{ + "system":"Система", + "systeminformation":"Системная информация", + "version":"Версия", + "uptime":"Время работы", + "ip":"IP", + "time":"Время", + "temperature":"Температура", + "systemload":"Системная загрузка", + "memory":"Память", + "storage":"Хранилище", + "backupandrestore":"Сделать резервную копию и Восстановить", + "backupnow":"Сделать резервную копию сейчас", + "restore":"Восстановить", + "restorefromfile":"Восстановить из файла", + "choosefile":"Выбрать файл", + "restorenow":"Восстановить сейчас", + "updates":"Обновления", + "systemupdate":"Обновление системы", + "updatefromfile":"Обновить из файла", + "updatenow":"Обновить сейчас", + "systemconfigurations":"Системные конфигурации", + "opendoordelay":"Открытая дверь задержка", + "dashboard":"Панель управления", + "Dashboard":"Панель управления", + "menuname":"Название меню", + "cat":"Категория", + "groupname":"Имя группы", + "building":"Строительство", + "name":"Имя", + "logo":"Лого", + "backgroundimage":"Фоновая картинка", + "description":"Описание", + "greetings":"Поздравляем", + "currencyid":"Валюта", + "currency":"Валюта", + "numbeofroom":"Количество комнат", + "filter":"Фильтровать", + "addnew":"Добавить новый", + "excel":"Excel", + "deleteselected":"Удалить выбранные", + "usergroup":"Группа пользователя", + "useraccount":"Аккаунт пользователя", + "firstname":"Имя", + "lastname":"Фамилия", + "username":"Никнейм юзера", + "active":"Активный", + "superadmin":"Супер Админ", + "remoteaccess":"Удаленный Доступ", + "roomname":"Имя комнаты", + "defaultwifivoucher":"Wifi ваучер по умолчанию", + "biometricip":"Биометричное IP", + "donotdisturbip":"Не отвлекать IP", + "roomnumber":"Номер комнаты", + "room":"Комната", + "genre":"Жанр", + "image":"Картинка", + "displayorder":"Порядок отображения", + "movie":"Видео", + "title":"Заголовок", + "Plot":"График", + "year":"Год", + "rating":"Рейтинг", + "runtime":"Выполнение", + "price":"Цена", + "browsemovie":"Найти видео", + "browsetrailer":"Найти трейлер", + "browsesubtitle":"Найти подзаголовок", + "middlename":"Отчество", + "screenname":"Никнейм пользователя", + "url":"Ссылка", + "extras":"Дополнительно", + "actor":"Актор", + "director":"Директор", + "transaction":"Транзакция", + "customerinformation":"Информация заказчика", + "wifivoucher":"Wifi Ваучер", + "tooltip":"Всплывающая подсказка", + "series":"Сериалы", + "vattype":"Ват тип", + "season":"Сезон", + "number":"Номер", + "menucategory":"Категория меню", + "isdisplayed":"Отображается ?", + "onchangeitempriority":"Приоритет элементов при изменении", + "menuitem":"Элемент меню", + "kitchenlocation":"Расположение кухни", + "code":"Код", + "spicylevel":"Уровень остроты", + "spa":"Спа", + "housekeeping":"Забота о дома", + "noavailablehousekeeping":"Не доступна забота о доме", + "servicename":"Имя сервиса", + "chauffeur":"Шофер", + "chauffeurcategory":"Категория шофера", + "type":"Тип", + "transmission":"Трансмиссия", + "engine":"Двигатель", + "city":"Город", + "new":"Новый", + "save":"Сохранить", + "password": "Пароль", + "saveandaddanother":"Сохранить и Добавить другой", + "saveandcontinue":"Сохранить и продолжить", + "changepassword":"Изменить пароль", + "logout":"Выйти", + "email":"Емейл", + "email address":"Емейл адрес", + "forgot password":"Забыл пароль", + "send request":"Отправить запрос", + "lastlogin":"Последнее время посещения", + "login": "Время посещения", + "savechanges":"Сохранить изменения", + "oldpassword":"Старый пароль", + "newpassword":"Новый пароль", + "confirmpassword":"Подтвердить пароль", + "applychanges":"Применить изменения", + "close":"Закрыть", + "profile":"Профиль", + "history":"История", + "browse":"Найти", + "onewaytrip":"Путешествие в одну сторону", + "roundtrip":"Круговое путешествие", + "hotelname":"Имя гостиницы", + "nameofcurrency":"Валюта", + "symbolofcurrency":"Символ валюты", + "welcometo":"Добро пожаловать", + "dear":"Дорогой", + "welcometoyour":"Добро пожаловать в ваш", + "haveagreatdaymessage":"Удачного дня и насладитесь вашим временем в", + "pendingcharges":"Ожидающие оплаты", + "localweather":"Локальная погода", + "localdate":"Локальное время и дата", + "runningbill":"Текущий счет", + "guestprofile":"Гостевой профиль", + "help":"Помощь", + "yourdigitalconcierge":"Ваш цифровой консьерж", + "goodday":"Хороший день", + "home":"Домой", + "menu":"Меню", + "movies":"Фильмы", + "moviecategories":"Категории фильмов", + "availedmovies":"Мои фильмы", + "noavailedmovies":"Нет купленных фильмов", + "watchnow":"Наблюдать за", + "trailer":"Трейлер", + "usearrow":"Используйте удаленные элементы управления для навигации", + "wouldyoulike":"Хотите ли вы посмотреть это кино ?", + "purchase":"Купить", + "cancel":"Отменить", + "loading":"Загрузить", + "premium":"Премиум", + "owned":"Владея", + "movietitle":"Заголовок фильма", + "dismiss":"Отклонить", + "watchforfree":"Смотреть бесплатно", + "lockunlockmaindoor":"Разблокировать/Заблокировать главную дверь", + "lock":"Заблокировать", + "unlock":"Разблокировать", + "unlockmessage":"Ваша главная дверь разблокирована", + "lockmessage":"Ваша главная дверь заблокирована.", + "donotdisturb":"Не беспокоить", + "yourwifivoucher":"Ваш Wifi ваучер", + "on":"Включить", + "off":"Выключить", + "roomservice":"Сервис комнатный", + "mealcategories" : "Категории еды", + "allergyfilter":"Аллергии", + "mealcart":"Тележка для еды", + "status":"Статус", + "meal":"Еда", + "quantity":"Количество", + "totalitem":"Всего элементов", + "subtotal":"Подзаголовок", + "vat":"ВАТ", + "grandtotal":"Общая сумма", + "sendorder":"Отправить заказ", + "clearorder":"Очистить заказ", + "spicylevelfilter":"Уровень остроты", + "hi":"Привет гость", + "spicymessage":"Мы будем отображать элементы в зависимости от выбранного уровня остроты из списка внизу.", + "all":"Все", + "extraspicy":"Сильно острый", + "spicy":"Острый", + "mildspicy":"Умеренно острый", + "notspicy":"Не острый", + "ok":"Ок", + "editquantity":"Редактировать количество", + "upsizeclickmessage":"Кликните на элементах внизу для увеличения вашего", + "back":"Назад", + "nodontupsize":"Нет, Не увеличивайте", + "switchbundled":"Переключиться на комплексное питание", + "bundledmessage":"лучше сочетается с приведенным элементом, кликните каждый элемент для добавления/удаления их в ваш заказ.", + "allergymessage":"Меню, которые могут вызвать аллергию не будут отображены, основываясь на выбранных аллергенах/ингридиентах.", + "nojustproceed":"Нет, Просто продолжить", + "ordercomponent":"Порядок компонента", + "chooseyour":"Выберите ваш", + "removeorder":"Удалить заказ", + "pleasereview":"Пожалуйста проверьте ваш заказ.", + "proceed":"Продолжить", + "selectorder":"Выберите порядок", + "clearordermessage":"Вы уверены, что вы хотите очистить ваш заказ/ы?", + "no":"Нет", + "yes":"Да", + "done":"Выполнено", + "removeordermessage":"Вы уверены, что вы хотите удалить ваш заказ?", + "upsell":"Допродать", + "tvseries":"Телевизионные сериалы", + "seriescategories":"Категории сериалов", + "availedseries":"Мои сериалы", + "noavailedseries":"Нет купленных сериалов", + "seriestitle":"Заголовок сериалов", + "confirmwatchseries":"Хотите ли вы смотреть телевизионные сериалы?", + "youcanenjoy":"Вы можете насладиться и посмотреть все сезоны", + "continue":"Продолжить", + "wakeupcall":"Проснись Звонок", + "wakeupcalllist":"Проснись Список Звокнов", + "pressentertoedit":"Нажмите Enter для редактирования/удаления", + "setwakeuptime":"Поставить время пробуждения", + "hour":"Час", + "hours":"Часы", + "minute":"Минута", + "minutes":"Минуты", + "month":"Месяц", + "day":"День", + "days":"Дни", + "reset":"Сброс", + "confirmwakeupsave":"Сохранить звонок для пробуждения?", + "wakeupcallsaved":"Звонок для пробуждения сохранен", + "sucesswakeupmessage" : "Наша гостиничная обслуга позвонит вам в", + "removewakeupcallmessage":"УДАЛИТЬ ЗВОНОК ДЛЯ ПРОБУЖДЕНИЯ?", + "remove":"Удалить", + "edit":"Редактировать", + "ambiance":"Обстановка", + "viewbill":"Посмотреть счет", + "amount":"Количество", + "datepurchased":"Время покупки", + "destination":"Назначение", + "traveloption":"Опция путешествия", + "dateavailed":"Время покупки", + "clock":"Часы", + "noavailablecountry":"Нет доступных стран", + "servicecategories":"Категории услуг", + "availedservices":"Мои услуги", + "noavailedservices":"Нет приобретенных услуг", + "service":"Услуга", + "confirmavailservice":"Вы уверены что хотите купить эту услугу?", + "shallbeaddedtobill":"Это будет добавлено в ваш счет.", + "confirm":"Подтвердить", + "thankyou":"Спасибо", + "servicesuccessmessage":"Наша гостиничная обслуга скоро прийдет", + "vehiclecategories":"Категории машин", + "availedvehicles":"Мои машины", + "vehicletype":"Тип машины", + "seater":"Сиделка", + "select":"Выберите", + "pickdestination":"Выберите ваше направление", + "wheredoyouwanttogo":"Куда вы хотите поехать?", + "chauffeurifnoneoftheabove":"Для запросов пожалуйста свяжитесь с нашим гостевым сервисом.", + "traveltype":"Тип путешествия", + "noavailedvehicle":"Нет арендованной машины", + "noavailable":"Не доступно", + "noavailablevehiclecategory":"Нет доступных категорий машин", + "noavailablevehicle":"Нет доступных машин", + "noavailablecity":"Нет доступных городов", + "noavailabledestination":"Нет доступных направлений", + "spaservice":"Услуга СПА", + "noavailablespa":"СПА не доступно", + "spacategories":"Категории СПА", + "availedspaservice":"Мой СПА сервис", + "noavailedspa":"Нету приобретенных СПА сервисов", + "availedpromotions":"Мое специальное предложение", + "noavailedspecialoffer":"Нет приобретенных специальных предложений", + "noavailablespecialoffer":"Нет доступных специальных предложений", + "specialoffer":"Специальное предложение", + "about":"О", + "mainmenu":"Главное меню", + "finish":"Закончить", + "vehicle":"Машина", + "summary":"Суммарный", + "chauffeursuccessmessage":"Ваша транзакция была размещена", + "traveltypemessage":"Какую опцию путешествий вы предпочтете ?", + "thankyoumessage":"Спасибо за то что воспользованим специальным предложением, ограниченным во времени.", + "relaycontroller":"Реле контроллер", + "allswitch":"Все переключатели", + "devicename":"Имя устройства", + "onoff":"Вкл./Выкл.", + "schedule":"Расписание", + "addschedule":"Добавить новый", + "editschedule":"Редактировать расписание", + "setdate":"Set Установить дату", + "ampm":"Утро/Вечер", + "removeschedule":"Удалить расписание", + "relaysucesssavemessage":"Расписание сохранено успешно", + "chat":"Чат", + "selectpreferredlanguage":"Выберите ваш предпочитаемый язык", + "selectlanguage":"Выберите язык", + "typeamessage":"Напишите сообщение...", + "send":"Отправить", + "conversation":"Разговор", + "language":"Язык", + "unreadclient":"Непрочитанный клиент", + "unreadadmin":"Непрочитанный админ", + "conversationmessage":"Сообщение", + "message":"Сообщение", + "user":"Пользователь", + "capacity":"Объем", + "chauffeurprice":"Цена шофера", + "cityplace":"Место в городе", + "onewaytripprice":"Стоимость путешествия в одну сторону", + "roundtripprice":"Стоимость кругового путешествия", + "chauffeurtransactionline":"Линия транзакции шофера", + "chauffeurtransactionprice":"Стоимость транзакции шофера", + "traveltime":"Время путешествия", + "distance":"Расстояние", + "country":"Страна", + "timezone":"Временная зона", + "default":"По умолчанию", + "extra":"Экстра", + "video":"Видео", + "episode":"Эпизод", + "browseseries":"Найти сериал", + "seriesmediaserver":"Медиа сервер сериалов", + "releasedate":"Время публикации", + "filmrating":"Рейтинг фильма", + "mediadir":"Каталог медиа", + "drive":"Диск", + "port":"Порт", + "mediaformat":"Формат медиа", + "format":"Форма", + "mediaserver":"Сервер медиа", + "filebrowseapi":"Файл API", + "isactive":"Доступен", + "plot":"График", + "movietransactionline":"Линия транзакции фильма", + "seriestransactionline":"Сериал транзакции линия Transaction Line", + "housekeepingtransactionline":"Линия транзакции домосодержания", + "icon":"Иконка", + "action":"Действие", + "position":"Позиция", + "excludemobile":"Исключая мобильный", + "excludetv":"Исключая телевизор", + "notification":"Уведомление", + "notificationtype":"Тип уведомления", + "acknowledged":"Принято", + "promo":"Промо", + "relay":"Радиотрансляция", + "relaybrand":"Бренд радиотрансляции", + "relayschedule":"Расписание радиотрансляции", + "relaypin":"Пин-Код радиотрансляции", + "datetime":"Время дата", + "state":"Состояние", + "relaydevice":"Устройство радиотрансляции", + "relaydevicecategory":"Категория устройства радиотрансляции", + "relaydevicelocation":"Местоположение устройства радиотрансляции", + "pin":"Номер порта", + "location":"Местоположение", + "spatransactionline":"Линия транзакции СПА", + "specialofferimage":"Картинка специального предложения", + "specialoffertransaction":"Транзакция специального предложения", + "numberofroom":"Количество комнат", + "symbol":"Символ", + "device":"Устройство", + "devicetype":"Тип устройства", + "size":"Размер", + "order":"Порядок", + "isdefault":"По умолчанию", + "advertisement":"Реклама", + "suggested":"Предложенный", + "menuitemsize":"Кол-во меню элементов", + "menuitemcomponent":"Элемент меню", + "component":"Компонент", + "canchange":"Может изменить", + "canupsize":"Может увеличить количество", + "menuitemingredient":"Ингредиент элемента меню", + "ingredient":"Ингредиент", + "isrequired":"Обязателен", + "menuitemtimedordering":"Временной порядок элементов меню", + "fromtime":"Со времени", + "totime":"До времени", + "menuiteminventory":"Инвентарь элементов меню", + "inventoryitem":"Элемент инвентаря", + "unitofmeasure":"Единица измерения", + "menuitemspecialrequest":"Элемент меню специальный запрос", + "exclusive":"Экслюзивный", + "optional":"Опциональный", + "translate":"Перевести", + "text":"Текст", + "allergy":"Аллергия", + "createtime":"Время создания", + "ordertype":"Тип заказа", + "ordersource":"Источник заказа", + "total":"Всего", + "change":"Изменить", + "headcount":"Кол-во умерших", + "seniorcount":"Кол-во пенсионеров", + "isservicechargevated":"Подвергнут ли платеж за услугу ВАТ налогу", + "paid":"Оплачено", + "totalseniordiscount":"Скидка всего пенсионерам", + "totalpwddiscount":"Скидка всего PWD", + "totalregulardiscount":"Скидка регулярная всего", + "billouttime":"Время до оплаты", + "overallsubtotal":"Общий промежуточный итог", + "expirationdate":"Дата просрочки", + "relatedorder":"Связанный заказ", + "vatexcepmtsc":"ВАТ освобожденнный сервисный сбор", + "scnetsales":"Чистая плата за обслуживание", + "regularsales":"Регулярные продажи", + "deductiondueto":"Удержание из за", + "deliverycharge":"Стоимость доставки", + "orderitem":"Элемент заказа", + "unitprice":"Цена одного", + "addonprice":"Цена дополнения", + "remarks":"Замечания", + "specialrequest":"Специальный запрос", + "ordertime":"Время заказа", + "servedtime":"Время обслуживания", + "preparedtime":"Время приготовления", + "orderitemcomponent":"Элемент заказа", + "additionalprice":"Дополнительная цена", + "orderitemcomponentallergies":"Элемент меню аллергии", + "categories":"Категории", + "availeditems":"Мои элементы", + "item":"Элемент", + "ordersuccessmessage":"Заказ успешен.", + "avail":"Купить", + "store":"Сохранить", + "notransaction":"Нет транзакции", + "selectdestination":"Выберите направление", + "selecthourlyrate":"Выберите стоимость часа работы", + "choosehourlyrate":"Выберите кол-во часов", + "howlong":"Как долго вы собираетесь арендовать машину?", + "noofhours":"Кол-во часов", + "details":"Детали", + "noavailablecopy":"Нет доступной копии", + "internalerror":"Внутренняя ошибка возникла", + "onceyouorder":"Когда вы подтвердите, ваш заказ/ы не можно будет изменить или рефанднуть. Хотите ли вы разместить заказ/ы сейчас ?", + "invoice":"Инвойс", + "maindetails":"Главные детали", + "guestname":"Имя гостя", + "package":"Пакет", + "totalbill":"Всего стоимость", + "billbreakdown":"Разъяснение счета", + "notifications":"Уведомления", + "callservice":"Колл услуга", + "requestcall":"Запросите звонок", + "requestinroomservice":"Закажите услугу в комнату", + "requestcallresponse":"Один из наших гостевых сервисов позвонят вам очень скоро.", + "requestinroomserviceresponse":"Один из наших гостевых сервисов прийдет очень скоро.", + "checkout":"Выписаться", + "checkoutmessage":"Вы уверены что хотите выписаться ?", + "extendstay":"Продлить пребывание", + "daysand":"День(дни) и", + "timeremaining":"Оставшееся время", + "howmanyhoursdoyouwantoextend":"На сколько часов вы хотите продлить?", + "extendstayrequestwasacceptedthankyou":"Ваш запрос на продление пребывания был принят.Спасибо", + "sorryextendstayrequestwasrejected":"Приносим извинения, но ваш запрос на продление пребывания был отклонен.", + "wasacceptedthankyou":"был принят. Спасибо", + "Sorry":"извините", + "personneliscurrentlynotavailable":"Персонал в настоящее время недоступен", + "invalidnotifid":"Неправильный идентификатор уведомления", + "accepted":"принято", + "rejected":"отклонено", + "areyousureyyouwanttocheckout":"Вы уверены что хотите выписаться ?", + "next":"Следующий", + "Step":"Шаг", + "of":"из", + "setupwizard":"Помощник установки", + "hotelsetupwizard":"Помощник установки гостиницы", + "hotelsetup":"Установка гостиницы", + "setupyourdigiButlerinjustafewminutes":"Установите ваш DigiButler всего лишь за несколько минут", + "hotellogo":"Логотип гостиницы", + "hotelbackgroundimage":"Картинка фона гостиницы", + "roompackagesetupwizard":"Пакет установки комнаты - помощник", + "packagename":"Имя пакета", + "mealsetupwizard":"Установщик еды", + "appetizers":"Закуски", + "cocktails":"Коктейли", + "breakfast&snack":"Завтрак и снэки", + "champagne":"Шампанское", + "entree":"Вход", + "coffee":"Кофн", + "sandwiches":"Сэндвичи", + "whiskeys":"Виски", + "salads":"Салаты", + "hottea":"Горячий чай", + "wines":"Вино", + "beer":"Пиво", + "noodlesandrice":"Макароны и рис", + "juices":"Соки", + "rum":"Ром", + "incanrefreshments":"в банке с напитками", + "Desserts":"Дессерты", + "smoothies":"Смузи", + "vodka":"Водка", + "cognacandbrandy":"Коньяк и бренди", + "mealname":"Название блюда", + "mealdescriptionoptional":"Описание блюда (опционально)", + "mealprice":"Цена блюда", + "mealimageoptional":"Картинка блюда (опционально)", + "mealcategory":"Категория блюда", + "skip":"Пропустить", + "housekeepingsetupwizard":"Помощник установки домохозяйства", + "housekeepingsetup": "Установщик домохозяйства", + "cleanroom":"Очистите комнату", + "laundrypickup":"Вывоз белья", + "ironing":"Глажка", + "turndownservice" :"Выключить сервис", + "afterswimming":"После плавания", + "traypickup":"Подбор лотка", + "spasetupwizard":"Помощник установки СПА", + "spaservicesetup":"Помощник установки СПА", + "spaservicename":"Имя услуги СПА", + "spaimageoptional":"Картинка СПА (опционально)", + "chauffeursetupwizard":"Помощник установки шофера", + "chauffeursetup":"Установка шофера", + "chauffeurcategories":"Категории шоферов", + "midsizeeconomy":"Средняя экономия", + "fullsizeeconomy":"Полноценная экономия", + "Midsizeluxury":"Среднего размера люкс", + "Luxury":"Люкс", + "vehiclename":"Имя машины", + "chauffeurimageoptional":"Картинка шофера (опционально)", + "specialofferssetupwizard":"Специальные предложения конфигуратор", + "specialofferssetup":"Специальные предложения конфигуратор", + "offername":"Имя предложения", + "offerbannerimage":"Имя баннера предложения", + "shopsetupwizard":"Помощник установки магазина", + "shopsetup":"Установка магазина", + "storename":"Имя магазина", + "storeimage":"Картинка магазина", + "insertthreecategories":"Добавьте три категории ", + "firstcategory":"Первая категория", + "firstcategoryimage":"Картинка первой категории", + "secondcategory":"Вторя категория", + "secondcategoryimage":"Картинка второй категории", + "thirdcategory":"Третья категория", + "thirdcategoryimage":"Картинка третьей категории", + "itemnameone":"Имя элемента 1", + "itemprice":"Цена элемента", + "shopcategory":"Категория магазина", + "itemimageone":"Картинка элемента 1", + "moviesetupwizard":"Установка фильма", + "royaltyfreemovies":"Фильмы без лицензионных отчислений", + "seriessetupwizard":"Сериалы - конфигуратор", + "royaltyfreeseries":"Сериалы без лицензионных отчислений", + "pagenotfound": "Страница не найдена", + "signin": "Залогиньтесь", + "available_groups_left": "Это список доступных групп. Вы можете выбрать некоторые, путем выборки их внизу, затем кликая на \"Выбрать\" стрелку между двумя контейнерами.", + "available_groups_search_left": "Начните печатать в этом поле для фильтрования списка доступных групп.", + "chosen_groups_left": "Это список выбранных групп. Вы можете удалить некоторые, выбрав их и затем кликая кнопку \"Удалить\" между двумя контейнерами.", + "group_widget_help": "Группы, к которым пользователь принадлежит. Пользователь получит все права, которые предоставлены каждой из этих групп. Нажмите \"Control\", илм \"Command\" на Маке, для того чтобы выбрать больше чем одну.", + "available_permissions_left": "Это список доступных прав. Вы можете выбрать некоторые, путем выборки их внизу, затем кликая на \"Выбрать\" стрелку между двумя контейнерами.", + "available_permissions_search_help": "Начните печатать в этом поле для фильтрования списка доступных прав.", + "chosen_permissions_left": "Это список выбранных прав пользователя. Вы можете удалить некоторые, выбрав их и затем кликая кнопку \"Удалить\" между двумя контейнерами.", + "permission_widget_help": "Специальные права, которыми обладает пользователь. Нажмите \"Control\", илм \"Command\" на Маке, для того чтобы выбрать больше чем одну." +} \ No newline at end of file diff --git a/static/uadmin/assets/admin/main.css b/static/uadmin/assets/admin/main.css index 2efb4929..d00858ef 100644 --- a/static/uadmin/assets/admin/main.css +++ b/static/uadmin/assets/admin/main.css @@ -847,4 +847,8 @@ a.selector-clearall { } .uploaded-file { margin-top: 5px; +} +.uadmin-alert { + display: flex; + align-items: center; } \ No newline at end of file diff --git a/static/uadmin/assets/admin/main.js b/static/uadmin/assets/admin/main.js index cd35c0bf..e819f207 100644 --- a/static/uadmin/assets/admin/main.js +++ b/static/uadmin/assets/admin/main.js @@ -92,14 +92,14 @@ function setHomeTabs(me, container, extclass){ content += '

'; content += ' '; + content += ' data-toggle="tooltip" data-placement="top" title="'+Translate(tooltip)+'" >'; } else { content += '>'; } content += '
'; content += '
'; content += '
'; - content += ' '+name+''; + content += ' '+Translate(name)+''; content += '
'; content += '
'; content += '
'; diff --git a/static/uadmin/js/sprintf.js b/static/uadmin/js/sprintf.js new file mode 100644 index 00000000..81800557 --- /dev/null +++ b/static/uadmin/js/sprintf.js @@ -0,0 +1,3 @@ +/*! sprintf-js v1.1.2 | Copyright (c) 2007-present, Alexandru Mărășteanu | BSD-3-Clause */ +!function(){"use strict";var g={not_string:/[^s]/,not_bool:/[^t]/,not_type:/[^T]/,not_primitive:/[^v]/,number:/[diefg]/,numeric_arg:/[bcdiefguxX]/,json:/[j]/,not_json:/[^j]/,text:/^[^\x25]+/,modulo:/^\x25{2}/,placeholder:/^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,key:/^([a-z_][a-z_\d]*)/i,key_access:/^\.([a-z_][a-z_\d]*)/i,index_access:/^\[(\d+)\]/,sign:/^[+-]/};function y(e){return function(e,t){var r,n,i,s,a,o,p,c,l,u=1,f=e.length,d="";for(n=0;n>>0).toString(8);break;case"s":r=String(r),r=s.precision?r.substring(0,s.precision):r;break;case"t":r=String(!!r),r=s.precision?r.substring(0,s.precision):r;break;case"T":r=Object.prototype.toString.call(r).slice(8,-1).toLowerCase(),r=s.precision?r.substring(0,s.precision):r;break;case"u":r=parseInt(r,10)>>>0;break;case"v":r=r.valueOf(),r=s.precision?r.substring(0,s.precision):r;break;case"x":r=(parseInt(r,10)>>>0).toString(16);break;case"X":r=(parseInt(r,10)>>>0).toString(16).toUpperCase()}g.json.test(s.type)?d+=r:(!g.number.test(s.type)||c&&!s.sign?l="":(l=c?"+":"-",r=r.toString().replace(g.sign,"")),o=s.pad_char?"0"===s.pad_char?"0":s.pad_char.charAt(1):" ",p=s.width-(l+r).length,a=s.width&&0

404

-

{{Tf "uadmin/system" .Language.Code "pagenotfound"}}

+

{{Tf .Language.Code "pagenotfound"}}


diff --git a/templates/uadmin/default/admin/form.html b/templates/uadmin/default/admin/form.html index 94261ee2..26eb8c5c 100644 --- a/templates/uadmin/default/admin/form.html +++ b/templates/uadmin/default/admin/form.html @@ -3,7 +3,7 @@ {{ if .FormError }} {{ range $generalError := .FormError.GeneralErrors }}
- {{ $generalError }} + {{ Translate $generalError }}
{{ end }} {{ end }} @@ -16,7 +16,7 @@
diff --git a/templates/uadmin/default/admin/form_edit.html b/templates/uadmin/default/admin/form_edit.html index f67d2771..418e7748 100644 --- a/templates/uadmin/default/admin/form_edit.html +++ b/templates/uadmin/default/admin/form_edit.html @@ -5,7 +5,7 @@ {{ if .FormError }} {{ range $generalError := .FormError.GeneralErrors }}
- {{ $generalError }} + {{ Translate $generalError }}
{{ end }} {{ end }} diff --git a/templates/uadmin/default/admin/widgets/checkbox.html b/templates/uadmin/default/admin/widgets/checkbox.html index 146c0ce8..a0f2f69e 100644 --- a/templates/uadmin/default/admin/widgets/checkbox.html +++ b/templates/uadmin/default/admin/widgets/checkbox.html @@ -2,7 +2,7 @@ {{ if ne .Type "hidden" }} {{ if .FieldDisplayName }} -