diff --git a/example/integration/pagebuilder_category_i18n_test.go b/example/integration/pagebuilder_category_i18n_test.go new file mode 100644 index 00000000..642f2b9e --- /dev/null +++ b/example/integration/pagebuilder_category_i18n_test.go @@ -0,0 +1,60 @@ +package integration_test + +import ( + "net/http" + "testing" + + . "github.com/qor5/web/v3/multipartestutils" + + "github.com/qor5/admin/v3/example/admin" + "github.com/qor5/admin/v3/presets/actions" +) + +// TestPageCategoryI18n covers KGM-3903: +// In the Japanese UI, the Page Categories "New" form rendered its field labels +// (Name / Path / Description) in English, because the field-level translations +// (PageCategoriesName / PageCategoriesPath / PageCategoriesDescription) were not +// registered under presets.ModelsI18nModuleKey, so i18n.PT fell back to the +// English humanized field names. +func TestPageCategoryI18n(t *testing.T) { + h := admin.TestHandler(TestDB, nil) + dbr, _ := TestDB.DB() + + cases := []TestCase{ + { + Name: "Category New form in Japanese shows Japanese field labels", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(actions.New). + BuildEventFuncRequest() + req.Header.Set("Accept-Language", "ja") + return req + }, + // Field labels must be the Japanese translations, in editing field order. + ExpectPortalUpdate0ContainsInOrder: []string{"名前", "パス", "説明"}, + }, + { + Name: "Category New form in English shows English field labels", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(actions.New). + BuildEventFuncRequest() + req.Header.Set("Accept-Language", "en") + return req + }, + ExpectPortalUpdate0ContainsInOrder: []string{"Name", "Path", "Description"}, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + RunCase(t, c, h) + }) + } +} diff --git a/example/integration/pagebuilder_perm_test.go b/example/integration/pagebuilder_perm_test.go index 96beffef..188e7ab5 100644 --- a/example/integration/pagebuilder_perm_test.go +++ b/example/integration/pagebuilder_perm_test.go @@ -68,6 +68,25 @@ func TestPageBuilderPerm(t *testing.T) { }, ExpectPageBodyContainsInOrder: []string{"permission denied"}, }, + { + // KGM-4190: the permission-denied message must be localized, not the + // raw English perm.PermissionDenied.Error() string. + Name: "Container Header Update permission denied in Japanese", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderContainerTestData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/headers"). + EventFunc(actions.Update). + Query(presets.ParamID, "10"). + AddField("Color", "white"). + BuildEventFuncRequest() + req.Header.Set("Accept-Language", "ja") + return req + }, + ExpectPageBodyContainsInOrder: []string{"権限がありません"}, + ExpectPageBodyNotContains: []string{"permission denied"}, + }, } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { diff --git a/pagebuilder/builder.go b/pagebuilder/builder.go index f955d2d2..1c742fa9 100644 --- a/pagebuilder/builder.go +++ b/pagebuilder/builder.go @@ -683,8 +683,25 @@ func (b *Builder) defaultCategoryInstall(pb *presets.Builder, pm *presets.ModelB }) eb := pm.Editing("Name", "Path", "Description") + // Translate the editing form field labels from the page builder's own i18n + // messages, mirroring the listing column labels above. Without this the labels + // fall back to the English humanized field names in non-English locales, because + // the default i18n.PT lookup goes through presets.ModelsI18nModuleKey, which only + // the host app can register (KGM-3903). + categoryFieldLabel := presets.WrapperFieldLabel(func(evCtx *web.EventContext, obj interface{}, field *presets.FieldContext) (map[string]string, error) { + msgr := i18n.MustGetModuleMessages(evCtx.R, I18nPageBuilderKey, Messages_en_US).(*Messages) + return map[string]string{ + "Name": msgr.ListHeaderName, + "Path": msgr.ListHeaderPath, + "Description": msgr.ListHeaderDescription, + }, nil + }) + eb.Field("Name").LazyWrapComponentFunc(categoryFieldLabel) + eb.Field("Description").LazyWrapComponentFunc(categoryFieldLabel) eb.Field("Path").LazyWrapComponentFunc(func(in presets.FieldComponentFunc) presets.FieldComponentFunc { return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { + msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages) + field.Label = msgr.ListHeaderPath comp := in(obj, field, ctx) if p, ok := comp.(*vx.VXFieldBuilder); ok { p.Attr(presets.VFieldError(field.Name, strings.TrimPrefix(field.Value(obj).(string), "/"), field.Errors)...). diff --git a/presets/detailing.go b/presets/detailing.go index 35e3194b..fd2defd0 100644 --- a/presets/detailing.go +++ b/presets/detailing.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/qor5/web/v3" - "github.com/qor5/x/v3/perm" v "github.com/qor5/x/v3/ui/vuetify" h "github.com/theplant/htmlgo" @@ -195,7 +194,7 @@ func (b *DetailingBuilder) defaultPageFunc(ctx *web.EventContext) (r web.PageRes } if b.mb.Info().Verifier().Do(PermGet).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - r.Body = h.Div(h.Text(perm.PermissionDenied.Error())) + r.Body = h.Div(h.Text(MustGetMessages(ctx.R).PermissionDenied)) return } @@ -300,7 +299,7 @@ func (b *DetailingBuilder) WrapIdCurrentActive(w func(IdCurrentActiveProcessor) func (b *DetailingBuilder) showInDrawer(ctx *web.EventContext) (r web.EventResponse, err error) { if b.mb.Info().Verifier().Do(PermGet).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } onChangeEvent := fmt.Sprintf("if (vars.%s) { vars.%s.detailing=true };", VarsPresetsDataChanged, VarsPresetsDataChanged) diff --git a/presets/editing.go b/presets/editing.go index a095f13f..617cb75a 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -197,7 +197,7 @@ func (b *EditingBuilder) WrapIdCurrentActive(w func(in IdCurrentActiveProcessor) func (b *EditingBuilder) formNew(ctx *web.EventContext) (r web.EventResponse, err error) { if b.mb.Info().Verifier().Do(PermCreate).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } @@ -215,7 +215,7 @@ func (b *EditingBuilder) formNew(ctx *web.EventContext) (r web.EventResponse, er func (b *EditingBuilder) formEdit(ctx *web.EventContext) (r web.EventResponse, err error) { if b.mb.Info().Verifier().Do(PermGet).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } if b.idCurrentActiveProcessor != nil { @@ -468,7 +468,7 @@ func (b *EditingBuilder) doValidate(ctx *web.EventContext) (r web.EventResponse, } vErrSetter := vErr if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - vErr.GlobalError(perm.PermissionDenied.Error()) + vErr.GlobalError(MustGetMessages(ctx.R).PermissionDenied) return } if usingB.Validator != nil { @@ -481,7 +481,7 @@ func (b *EditingBuilder) doValidate(ctx *web.EventContext) (r web.EventResponse, func (b *EditingBuilder) doDelete(ctx *web.EventContext) (r web.EventResponse, err1 error) { if b.mb.Info().Verifier().Do(PermDelete).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } @@ -671,7 +671,11 @@ func (b *EditingBuilder) UpdateOverlayContent( if err != nil { if _, ok := err.(*web.ValidationErrors); !ok { vErr := &web.ValidationErrors{} - vErr.GlobalError(err.Error()) + msg := err.Error() + if errors.Is(err, perm.PermissionDenied) { + msg = MustGetMessages(ctx.R).PermissionDenied + } + vErr.GlobalError(msg) ctx.Flash = vErr } } diff --git a/presets/listeditor.go b/presets/listeditor.go index bbf761b0..e75068c7 100644 --- a/presets/listeditor.go +++ b/presets/listeditor.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/qor5/web/v3" - "github.com/qor5/x/v3/perm" . "github.com/qor5/x/v3/ui/vuetify" "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" @@ -285,7 +284,7 @@ func addListItemRow(mb *ModelBuilder) web.EventFunc { obj, _ := me.FetchAndUnmarshal(id, false, ctx) if mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), ColorError) + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, ColorError) return r, nil } @@ -309,7 +308,7 @@ func removeListItemRow(mb *ModelBuilder) web.EventFunc { obj, _ := me.FetchAndUnmarshal(id, false, ctx) if mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), ColorError) + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, ColorError) return r, nil } @@ -337,7 +336,7 @@ func sortListItems(mb *ModelBuilder) web.EventFunc { obj, _ := me.FetchAndUnmarshal(id, false, ctx) if mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), ColorError) + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, ColorError) return r, nil } diff --git a/presets/messages.go b/presets/messages.go index 0eedb419..a440837e 100644 --- a/presets/messages.go +++ b/presets/messages.go @@ -100,7 +100,8 @@ type Messages struct { LeaveBeforeUnsubmit string - RecordNotFound string + RecordNotFound string + PermissionDenied string } func (msgr *Messages) CreatingObjectTitle(modelName string) string { @@ -252,7 +253,8 @@ var Messages_en_US = &Messages{ LeaveBeforeUnsubmit: "If you leave before submitting the form, you will lose all the unsaved input.", - RecordNotFound: "record not found", + RecordNotFound: "record not found", + PermissionDenied: "permission denied", } var Messages_zh_CN = &Messages{ @@ -350,7 +352,8 @@ var Messages_zh_CN = &Messages{ LeaveBeforeUnsubmit: "如果您在提交表单之前离开,您将丢失所有未保存的输入。", - RecordNotFound: "记录未找到", + RecordNotFound: "记录未找到", + PermissionDenied: "没有权限", } var Messages_ja_JP = &Messages{ @@ -448,5 +451,6 @@ var Messages_ja_JP = &Messages{ LeaveBeforeUnsubmit: "フォームを送信する前に離れると、すべての未保存の入力が失われます。", - RecordNotFound: "レコードが見つかりません", + RecordNotFound: "レコードが見つかりません", + PermissionDenied: "権限がありません", } diff --git a/presets/presets.go b/presets/presets.go index b49b1d95..80d95652 100644 --- a/presets/presets.go +++ b/presets/presets.go @@ -939,7 +939,7 @@ func (b *Builder) defaultLayout(in web.PageFunc, cfg *LayoutConfig) (out web.Pag var innerPr web.PageResponse innerPr, err = in(ctx) if errors.Is(err, perm.PermissionDenied) { - pr.Body = h.Text(perm.PermissionDenied.Error()) + pr.Body = h.Text(MustGetMessages(ctx.R).PermissionDenied) return pr, nil } if err != nil { @@ -1005,7 +1005,7 @@ func (b *Builder) PlainLayout(in web.PageFunc) (out web.PageFunc) { var innerPr web.PageResponse innerPr, err = in(ctx) if err == perm.PermissionDenied { - pr.Body = h.Text(perm.PermissionDenied.Error()) + pr.Body = h.Text(MustGetMessages(ctx.R).PermissionDenied) return pr, nil } if err != nil { diff --git a/presets/section.go b/presets/section.go index 372d3aba..dbdf5080 100644 --- a/presets/section.go +++ b/presets/section.go @@ -12,7 +12,6 @@ import ( "github.com/qor5/web/v3" "github.com/qor5/x/v3/i18n" - "github.com/qor5/x/v3/perm" . "github.com/qor5/x/v3/ui/vuetify" "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" @@ -1008,7 +1007,7 @@ func (b *SectionBuilder) EditDetailField(ctx *web.EventContext) (r web.EventResp } if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } @@ -1056,7 +1055,7 @@ func (b *SectionBuilder) SaveDetailField(ctx *web.EventContext) (r web.EventResp } if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } vErrSetter := b.editingFB.Unmarshal(obj, b.mb.Info(), true, ctx) @@ -1175,7 +1174,7 @@ func (b *SectionBuilder) ValidateDetailField(ctx *web.EventContext) (r web.Event } vErrSetter := vErr if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - vErr.GlobalError(perm.PermissionDenied.Error()) + vErr.GlobalError(MustGetMessages(ctx.R).PermissionDenied) return } if b.validator != nil { @@ -1215,7 +1214,7 @@ func (b *SectionBuilder) EditDetailListField(ctx *web.EventContext) (r web.Event } if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } @@ -1269,7 +1268,7 @@ func (b *SectionBuilder) SaveDetailListField(ctx *web.EventContext) (r web.Event } if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } @@ -1384,7 +1383,7 @@ func (b *SectionBuilder) DeleteDetailListField(ctx *web.EventContext) (r web.Eve } if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return } @@ -1439,7 +1438,7 @@ func (b *SectionBuilder) CreateDetailListField(ctx *web.EventContext) (r web.Eve } if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { - ShowMessage(&r, perm.PermissionDenied.Error(), "warning") + ShowMessage(&r, MustGetMessages(ctx.R).PermissionDenied, "warning") return }