Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .changeset/object-view-form-title-i18n-3462.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
'@object-ui/plugin-view': patch
'@object-ui/i18n': patch
---

Localize the create / edit / view form title `ObjectView` builds itself
(objectui#3462)

The same family as #3426 / PR #3457 and #3459 / PR #3464, one call site further
in. `ObjectView.getFormTitle()` string-built its three verbs in TypeScript:

case 'create': return `Create ${objectLabel}`;
case 'edit': return `Edit ${objectLabel}`;
case 'view': return `View ${objectLabel}`;

so a Chinese session whose object is labelled 联系人 read a drawer headed
**"View 联系人"** — an English verb glued onto a localized label. All three
consumers are visible chrome: `renderDrawerForm`'s `DrawerTitle`,
`renderModalForm`'s `DialogTitle`, and the `title` prop handed to
`NavigationOverlay` in the `popover` branch (a host-supplied `title` displaces
the overlay's own `resolvedTitle` default, so it is what the user sees).

The bar to reach it is lower than #3459's split panel: `ObjectViewSchema.layout`
already defaults to `'drawer'`, and `navigation` is a declared authorable input
on the registered `object-view` block whose `mode` union carries `drawer`,
`modal` and `popover`. A row click under any of them sets `formMode: 'view'` and
opens the container. `app-shell`'s wrapper pinning `layout: 'page'` is one host
overriding a registered block, not proof the branch is dead.

## What changed

The three verb branches resolve `form.createTitle` / `form.editTitle` /
`form.viewTitle`.

**No new key family was minted.** `form.createTitle` (`'Create {{object}}'`) and
`form.editTitle` (`'Edit {{object}}'`) already ship in all ten packs and are
already how `app-shell` heads the PAGE-mode record form
(`RecordFormPage.tsx`, `AppContent.tsx`). The drawer / modal / popover titles are
the same heading on a different surface, so they resolve the same keys — a
parallel per-plugin family would have guaranteed the two spellings drift, which
is what the sibling issues were about. Only the third verb had no sibling:
`form.viewTitle` is added to all ten packs, following each pack's existing
arrangement for its create/edit twins rather than a translated-verb-plus-label
concatenation (de puts the verb last, ja/zh use particles and no space).

`VIEW_DEFAULT_TRANSLATIONS` in `ObjectView.tsx` gains the three English entries,
which is what `createSafeTranslation` falls back to with no `I18nProvider`
mounted.

Two branches stay literal on purpose and are pinned by tests: `schema.form.title`
(the author wrote a title, so the author's title wins, in every locale) and the
`default` branch (bare object label, no verb to translate).

## Visible English change

None. Every branch is byte-identical in English — `Create Contacts`,
`Edit Contacts`, `View Contacts` — with and without a provider, so e2e specs and
host tests that address this chrome by its English name keep addressing it. The
provider-less path has its own test file, kept separate because
`initReactI18next` registers its instance as a module global that outlives
`cleanup()`.

The toolbar's create BUTTON keeps resolving `console.objectView.new`
("New" / 新建) and was deliberately not reused for the heading: a button verb and
a title are different contexts, and folding them together is how the next drift
of this shape would start.
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const ar = {
stepOf: "الخطوة {{current}} من {{total}}",
createTitle: "إنشاء {{object}}",
editTitle: "تعديل {{object}}",
viewTitle: "عرض {{object}}",
saveRecord: "حفظ السجل",
create: "إنشاء",
update: "تحديث",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const de = {
stepOf: "Schritt {{current}} von {{total}}",
createTitle: "{{object}} erstellen",
editTitle: "{{object}} bearbeiten",
viewTitle: "{{object}} anzeigen",
saveRecord: "Datensatz speichern",
create: "Erstellen",
update: "Aktualisieren",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const en = {
stepOf: 'Step {{current}} of {{total}}',
createTitle: 'Create {{object}}',
editTitle: 'Edit {{object}}',
viewTitle: 'View {{object}}',
saveRecord: 'Save',
create: 'Create',
update: 'Update',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ const es = {
stepOf: "Paso {{current}} de {{total}}",
createTitle: "Crear {{object}}",
editTitle: "Editar {{object}}",
viewTitle: "Ver {{object}}",
saveRecord: "Guardar registro",
create: "Crear",
update: "Actualizar",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const fr = {
stepOf: "Étape {{current}} sur {{total}}",
createTitle: "Créer {{object}}",
editTitle: "Modifier {{object}}",
viewTitle: "Afficher {{object}}",
saveRecord: "Enregistrer",
create: "Créer",
update: "Mettre à jour",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const ja = {
stepOf: "ステップ {{current}} / {{total}}",
createTitle: "{{object}}を作成",
editTitle: "{{object}}を編集",
viewTitle: "{{object}}を表示",
saveRecord: "レコードを保存",
create: "作成",
update: "更新",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const ko = {
stepOf: "{{total}}단계 중 {{current}}단계",
createTitle: "{{object}} 생성",
editTitle: "{{object}} 편집",
viewTitle: "{{object}} 보기",
saveRecord: "레코드 저장",
create: "생성",
update: "업데이트",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const pt = {
stepOf: "Etapa {{current}} de {{total}}",
createTitle: "Criar {{object}}",
editTitle: "Editar {{object}}",
viewTitle: "Ver {{object}}",
saveRecord: "Salvar registro",
create: "Criar",
update: "Atualizar",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const ru = {
stepOf: "Шаг {{current}} из {{total}}",
createTitle: "Создать {{object}}",
editTitle: "Редактировать {{object}}",
viewTitle: "Просмотреть {{object}}",
saveRecord: "Сохранить запись",
create: "Создать",
update: "Обновить",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ const zh = {
stepOf: '第{{current}}步,共{{total}}步',
createTitle: '新建{{object}}',
editTitle: '编辑{{object}}',
viewTitle: '查看{{object}}',
saveRecord: '保存',
create: '创建',
update: '更新',
Expand Down
79 changes: 58 additions & 21 deletions packages/plugin-view/src/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,47 @@ function useCreateVerb(): string {
}

/**
* English fallback for the split-mode record-detail heading (objectui#3459,
* following #3426's shape).
* English fallbacks for the headings this view resolves through `t()`
* (objectui#3459 for the split-mode record-detail heading, objectui#3462 for
* the create/edit/view form titles — both following #3426's shape).
*
* Borrowed from the `detail.*` namespace rather than minted as
* `view.recordDetail`: `NavigationOverlay` — the very component this heading is
* handed to — already resolves that namespace, as do `ListView` / `ObjectGrid`
* / `ObjectKanban` / `ObjectTree`, and one heading on one control should not
* get several translations that can drift apart. The entry must exist HERE too
* — a provider-less host never reaches the locale packs, and
* `createSafeTranslation`'s fallback is what interpolates `{{label}}`.
* Every entry must exist HERE as well as in the locale packs: a provider-less
* host never reaches the packs, and `createSafeTranslation`'s fallback is what
* interpolates the placeholder.
*
* It doubles as the probe key: under a provider `t()` returns the pack's
* template (≠ the key) so the real translator is used; with no provider the key
* comes back unchanged and this map supplies the English.
* ── Why these keys, and not new ones ──────────────────────────────────────
* `detail.recordDetailWithLabel` is borrowed from the `detail.*` namespace
* rather than minted as `view.recordDetail`: `NavigationOverlay` — the very
* component that heading is handed to — already resolves that namespace, as do
* `ListView` / `ObjectGrid` / `ObjectKanban` / `ObjectTree`, and one heading on
* one control should not get several translations that can drift apart.
*
* `form.createTitle` / `form.editTitle` are reused for the same reason, and are
* not new: all ten packs already carry them, and `app-shell` already heads the
* PAGE-mode record form with exactly these (`RecordFormPage.tsx`,
* `AppContent.tsx`). The drawer / modal / popover titles below are the same
* heading on a different surface, so they resolve the same keys — minting a
* parallel `console.objectView.*` family would have guaranteed the two spellings
* drift (zh already distinguishes 新建 from 创建). Only the third verb,
* `form.viewTitle`, had no sibling; it was added to all ten packs.
*
* Note the placeholder is `{{object}}`, not `{{label}}` — that is the variable
* the existing `form.*Title` family declares, and the pack-vs-en placeholder
* parity guard compares placeholder sets per key.
*
* `detail.recordDetailWithLabel` doubles as the probe key: under a provider
* `t()` returns the pack's template (≠ the key) so the real translator is used;
* with no provider the key comes back unchanged and this map supplies the
* English.
*/
const VIEW_DEFAULT_TRANSLATIONS: Record<string, string> = {
'detail.recordDetailWithLabel': '{{label}} Detail',
// Byte-for-byte the strings `getFormTitle` used to build with a template
// literal, so an English session and every e2e spec that addresses this
// chrome by name see no change at all.
'form.createTitle': 'Create {{object}}',
'form.editTitle': 'Edit {{object}}',
'form.viewTitle': 'View {{object}}',
};

const useObjectViewTranslation = createSafeTranslation(
Expand Down Expand Up @@ -271,10 +295,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
onViewAction,
}) => {
const createVerb = useCreateVerb();
// Heading of the split-mode record-detail panel (see the split branch far
// below). Declared with the other top-level hooks so it stays above every
// conditional return — rules-of-hooks.
const { t: tDetail } = useObjectViewTranslation();
// Headings this view owns: the split-mode record-detail panel (see the split
// branch far below) and the create/edit/view form titles (`getFormTitle`).
// Declared with the other top-level hooks so it stays above every conditional
// return — rules-of-hooks.
const { t: tView } = useObjectViewTranslation();
const [objectSchema, setObjectSchema] = useState<Record<string, unknown> | null>(null);
// Assigned in the render body (not in an effect) so the fetchData effect always
// reads the latest objectSchema without needing it as a dependency. This matches
Expand Down Expand Up @@ -872,14 +897,26 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
};
};

// Get form title based on mode
// Get form title based on mode.
//
// objectui#3462: the three verbs used to be string-built (`` `View ${label}` ``),
// so a zh session reading a drawer opened by a row click was headed
// "View 联系人" — an English verb glued onto a localized label. They resolve
// `form.{create,edit,view}Title` now, which is the SAME key family `app-shell`
// already uses for the page-mode record form, so the four surfaces cannot
// drift. German compounds and ja/zh particle order all sit inside the
// template, which is why this is a key and not a verb lookup + concatenation.
//
// Two branches stay literal on purpose:
// - `schema.form?.title` — the author wrote a title, so use the author's.
// - `default` — returns the object label alone, no verb to translate.
const getFormTitle = (): string => {
if (schema.form?.title) return schema.form.title;
const objectLabel = (objectSchema?.label as string) || schema.objectName;
switch (formMode) {
case 'create': return `Create ${objectLabel}`;
case 'edit': return `Edit ${objectLabel}`;
case 'view': return `View ${objectLabel}`;
case 'create': return tView('form.createTitle', { object: objectLabel });
case 'edit': return tView('form.editTitle', { object: objectLabel });
case 'view': return tView('form.viewTitle', { object: objectLabel });
default: return objectLabel;
}
};
Expand Down Expand Up @@ -1188,7 +1225,7 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
choose its own word order (de hyphenates, ja/zh need a
possessive particle). English output is byte-identical
(`Contacts Detail`), with or without an `I18nProvider`. */
title={tDetail('detail.recordDetailWithLabel', { label: objectLabel })}
title={tView('detail.recordDetailWithLabel', { label: objectLabel })}
mainContent={<div className="h-full overflow-auto">{renderContent()}</div>}
>
{renderOverlayDetail}
Expand Down
Loading
Loading