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
43 changes: 43 additions & 0 deletions .changeset/objectview-value-language-3582.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@object-ui/i18n': patch
---

`console.objectView.systemViewReadonly` and `console.objectView.expandToPage`
are translated in the eight packs that stored English for them, so a Japanese,
Korean, German, French, Spanish, Portuguese, Russian or Arabic session reads
the system-view hint and the expand affordance in its own language (#3582).

This is a different defect class from #3546's ledger, and no gate in the repo
could see it. There the key was *missing* from a pack and `fallbackLng: 'en'`
rendered English; here the key was **present in all ten** and eight of them
stored English as the value. `all-locales-key-parity` compares key *sets* and
placeholder *shape* — identical before and after this change, and neither key
interpolates anything. `scripts/check-i18n-call-site-keys.mjs` and its baseline
ratchet ask whether a `t()` key exists in `en`; it did, so these two were never
in the 258.

`systemViewReadonly` carried the sharper half: the eight packs did not hold
`en`'s sentence, they held one `en` had already abandoned. `en` says the view
is read-only; the eight said `System view defined in code - duplicate to
customize.` — pointing eight locales at a duplicate-to-customize path the
product no longer presents. They are translated against `en`'s **current**
read-only meaning, not against the stale English they replaced.

Each value is built from terminology the same pack already uses rather than
invented: `view.readonlyTooltip` supplies "system view" and
`console.objectView.cannotEditMetaView` (landed in #3583) supplies "defined in
code", so the new hint agrees with the copy beside it in every pack. For
`expandToPage`, `detail.openAsFullPage` is the identical English sentence one
namespace over and was already translated everywhere — `en` and `zh` hold their
two byte-identical to each other, so the eight now do too, and one locale
cannot end up with two different words for one action.

`en` and `zh` are unchanged, byte for byte. No key is added or removed —
the diff is 16 values in 8 files. A new
`objectView-value-language-3582.test.ts` pins the `en` literal (so the next
rewording of `en` fails loudly instead of silently orphaning nine
translations), asserts that no pack but `en` serves either English spelling,
and requires the zh/ja/ko/ru/ar values to contain characters of their own
script. The repo-wide "no ASCII English sentence in a non-Latin pack" gate that
#3582 also sketched is deliberately **not** here; it is a separate, lands-green
change.
144 changes: 144 additions & 0 deletions packages/i18n/src/__tests__/objectView-value-language-3582.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* `console.objectView.systemViewReadonly` / `.expandToPage` hold real
* translations in all ten packs — not English (objectui#3582).
*
* This is a different defect class from objectui#3546's ledger. There the key
* was MISSING from a pack; here the key was PRESENT in all ten and eight of
* them stored the English string as their "translation". Every existing gate
* compares key *sets* or placeholder *shape*, so all of them stayed green:
*
* - `all-locales-key-parity` — key sets are identical, and neither key
* interpolates anything, so its placeholder check has nothing to compare.
* It is green before AND after this fix, by construction; that is exactly
* why it could not see the bug.
* - `scripts/check-i18n-call-site-keys.mjs` and its baseline ratchet — both
* ask whether the key exists in `en`. It does. These two keys were never
* in the ratchet.
*
* `systemViewReadonly` carried the sharper half: the eight packs did not hold
* `en`'s current sentence, they held a STALE one `en` itself had already
* abandoned — "duplicate to customize", a workflow the product no longer
* offers. So eight locales were not merely untranslated, they described a
* path that does not exist. The `en` pin below is the guard against a repeat:
* change `en`'s copy again and this file goes red, which forces the eight to
* be revisited in the same PR instead of drifting for another release.
*/
import { describe, it, expect } from 'vitest';
import { builtInLocales } from '../locales';

const LANGS = Object.keys(builtInLocales);

const objectViewOf = (lang: string) =>
(builtInLocales[lang] as any)?.console?.objectView as Record<string, string>;

const readonlyOf = (lang: string) => objectViewOf(lang)?.systemViewReadonly;
const expandOf = (lang: string) => objectViewOf(lang)?.expandToPage;

/**
* Both English spellings that must not survive outside `en`: the CURRENT `en`
* copy, and the STALE sentence the eight packs stored before objectui#3582.
* Checking only the current one would let the original defect back in
* verbatim.
*/
const EN_CURRENT = {
systemViewReadonly: 'System view defined in code — read-only.',
expandToPage: 'Open as full page',
} as const;
const EN_STALE_READONLY = 'System view defined in code - duplicate to customize.';

/**
* Script ranges for the packs where "is this value in the pack's language?" is
* mechanically decidable. de/fr/es/pt share the Latin alphabet with `en`, so
* they get the equality checks below instead — there is no clean positive
* criterion for them, which is the reason objectui#3582 recorded a repo-wide
* ASCII gate as a SEPARATE, non-acceptance item rather than folding it in here.
*/
const NATIVE_SCRIPT: Record<string, RegExp> = {
zh: /\p{Script=Han}/u,
ja: /\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Han}/u,
ko: /\p{Script=Hangul}/u,
ru: /\p{Script=Cyrillic}/u,
ar: /\p{Script=Arabic}/u,
};

describe('console.objectView value-language coverage (objectui#3582)', () => {
it('covers all ten built-in packs', () => {
expect(LANGS).toHaveLength(10);
});

it.each(LANGS)('%s defines both keys as non-empty strings', (lang) => {
for (const key of ['systemViewReadonly', 'expandToPage'] as const) {
const value = objectViewOf(lang)?.[key];
expect(typeof value, `${lang}.console.objectView.${key}`).toBe('string');
expect(value!.trim().length, `${lang}.console.objectView.${key} is empty`).toBeGreaterThan(0);
}
});

it('the en pack still says read-only, not "duplicate to customize"', () => {
// The semantic the other nine were translated against. If this literal
// changes, the nine translations below are stale by definition — that is
// the failure this pin exists to make loud, and it is precisely what went
// unnoticed when `en` was last reworded.
expect(readonlyOf('en')).toBe(EN_CURRENT.systemViewReadonly);
expect(expandOf('en')).toBe(EN_CURRENT.expandToPage);
});

it('no pack but en serves either English spelling of systemViewReadonly', () => {
const english = LANGS.filter(
(l) =>
l !== 'en' &&
(readonlyOf(l) === EN_CURRENT.systemViewReadonly || readonlyOf(l) === EN_STALE_READONLY),
);
expect(english, `packs serving English: ${english.join(', ')}`).toEqual([]);
});

it('no pack but en serves the English spelling of expandToPage', () => {
const english = LANGS.filter((l) => l !== 'en' && expandOf(l) === EN_CURRENT.expandToPage);
expect(english, `packs serving English: ${english.join(', ')}`).toEqual([]);
});

it('the retired "duplicate to customize" workflow is named by no pack', () => {
// Scoped to these two keys on purpose. The same stale claim still lives in
// `view.readonlyTooltip`, TRANSLATED into all eight languages — objectui#3625.
// Widening this regex to the whole pack would red on that one, which is a
// separate fix; and note no equality-with-English or ASCII heuristic can
// see it, because those eight values ARE idiomatic translations. They just
// translate a sentence `en` retired.
const leaked = LANGS.filter((l) => /duplicate to customi[sz]e/i.test(readonlyOf(l)!));
expect(leaked).toEqual([]);
});

it.each(Object.keys(NATIVE_SCRIPT))('%s writes both values in its own script', (lang) => {
const re = NATIVE_SCRIPT[lang];
expect(re.test(readonlyOf(lang)!), `${lang} systemViewReadonly has no native script`).toBe(true);
expect(re.test(expandOf(lang)!), `${lang} expandToPage has no native script`).toBe(true);
});

it('all ten packs differ from one another on both keys', () => {
// The tightest statement of "nobody pasted one language into another".
// These two sentences are legitimately distinct in all ten today; a future
// translation that genuinely collides is a reason to relax this line with
// a note, not to widen it silently.
for (const [key, read] of [
['systemViewReadonly', readonlyOf],
['expandToPage', expandOf],
] as const) {
const values = LANGS.map(read);
expect(new Set(values).size, `${key} has duplicate values across packs`).toBe(LANGS.length);
}
});

it('expandToPage matches its own pack detail.openAsFullPage, in every pack', () => {
// `detail.openAsFullPage` is the SAME English sentence in a neighbouring
// namespace ("Open as full page"), and it was already translated in all
// ten while `console.objectView.expandToPage` was not. en and zh already
// held the two byte-identical to each other; this keeps the eight that way
// too, so the icon button and the expand affordance cannot drift into two
// different words for one action in a single locale.
for (const lang of LANGS) {
expect(expandOf(lang), `${lang} expandToPage vs detail.openAsFullPage`).toBe(
(builtInLocales[lang] as any).detail.openAsFullPage,
);
}
});
});
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,10 @@ const ar = {
viewNameHelp: "مفتاح برمجي (snake_case). يُملأ تلقائيًا من العنوان — عدّله إن أردت.",
viewNameRequired: "أدخل مفتاحًا (أحرف إنجليزية صغيرة وأرقام وشرطة سفلية). لا يُملأ تلقائيًا للعناوين غير اللاتينية.",
viewNameInvalid: "استخدم أحرفًا إنجليزية صغيرة وأرقامًا وشرطات سفلية؛ وابدأ بحرف أو شرطة سفلية.",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "عرض النظام معرَّف في الكود — للقراءة فقط.",
cannotEditMetaView: "هذا عرض مدمج معرَّف في الكود — لا يمكن تعديله.",
cannotDeleteMetaView: "هذا عرض مدمج معرَّف في الكود — لا يمكن حذفه.",
expandToPage: 'Open as full page',
expandToPage: "فتح كصفحة كاملة",
objectNotFound: "الكائن غير موجود",
objectNotFoundDescription: "الكائن \"{{objectName}}\" غير موجود في الإعدادات الحالية.",
objectNotFoundHint: "تحقق من إعدادات التنقل في تطبيقك أو اختر كائنًا آخر من الشريط الجانبي.",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,10 @@ const de = {
viewNameHelp: "Technischer Schlüssel (snake_case). Wird aus dem Titel automatisch gefüllt — bei Bedarf anpassen.",
viewNameRequired: "Geben Sie einen Schlüssel ein (Kleinbuchstaben, Ziffern, Unterstrich). Bei nicht-lateinischen Titeln wird er nicht automatisch gefüllt.",
viewNameInvalid: "Verwenden Sie Kleinbuchstaben, Ziffern und Unterstriche; beginnen Sie mit einem Buchstaben oder Unterstrich.",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "Systemansicht — im Code definiert, schreibgeschützt.",
cannotEditMetaView: "Diese Ansicht ist integriert und im Code definiert – sie kann nicht geändert werden.",
cannotDeleteMetaView: "Diese Ansicht ist integriert und im Code definiert – sie kann nicht gelöscht werden.",
expandToPage: 'Open as full page',
expandToPage: "Als ganze Seite öffnen",
objectNotFound: "Objekt nicht gefunden",
objectNotFoundDescription: "Das Objekt „{{objectName}}\" existiert in der aktuellen Konfiguration nicht.",
objectNotFoundHint: "Überprüfen Sie Ihre App-Navigationseinstellungen oder wählen Sie ein anderes Objekt aus der Seitenleiste.",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1532,10 +1532,10 @@ const es = {
viewNameHelp: "Clave técnica (snake_case). Se rellena automáticamente a partir del título — edítela si lo desea.",
viewNameRequired: "Introduzca una clave (minúsculas, números, guion bajo). No se rellena automáticamente con títulos no latinos.",
viewNameInvalid: "Use minúsculas, números y guiones bajos; empiece por una letra o un guion bajo.",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "Vista del sistema definida en el código: solo lectura.",
cannotEditMetaView: "Esta vista está integrada y definida en el código: no se puede modificar.",
cannotDeleteMetaView: "Esta vista está integrada y definida en el código: no se puede eliminar.",
expandToPage: 'Open as full page',
expandToPage: "Abrir como página completa",
objectNotFound: "Objeto no encontrado",
objectNotFoundDescription: "El objeto \"{{objectName}}\" no existe en la configuración actual.",
objectNotFoundHint: "Verifique la configuración de navegación de su aplicación o seleccione un objeto diferente en la barra lateral.",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,10 @@ const fr = {
viewNameHelp: "Clé technique (snake_case). Remplie automatiquement à partir du titre — modifiable si vous le souhaitez.",
viewNameRequired: "Saisissez une clé (minuscules, chiffres, tiret bas). Non remplie automatiquement pour les titres non latins.",
viewNameInvalid: "Utilisez des minuscules, des chiffres et des tirets bas ; commencez par une lettre ou un tiret bas.",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "Vue système définie dans le code — en lecture seule.",
cannotEditMetaView: "Cette vue est intégrée et définie dans le code — elle ne peut pas être modifiée.",
cannotDeleteMetaView: "Cette vue est intégrée et définie dans le code — elle ne peut pas être supprimée.",
expandToPage: 'Open as full page',
expandToPage: "Ouvrir en pleine page",
objectNotFound: "Objet introuvable",
objectNotFoundDescription: "L'objet « {{objectName}} » n'existe pas dans la configuration actuelle.",
objectNotFoundHint: "Vérifiez les paramètres de navigation de votre application ou sélectionnez un autre objet dans la barre latérale.",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,10 @@ const ja = {
viewNameHelp: "マシンキー(snake_case)。タイトルから自動入力されます — 必要に応じて編集してください。",
viewNameRequired: "キーを入力してください(英小文字、数字、アンダースコア)。ラテン文字以外のタイトルでは自動入力されません。",
viewNameInvalid: "英小文字、数字、アンダースコアを使用し、先頭は文字かアンダースコアにしてください。",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "システムビューはコードで定義されており、読み取り専用です。",
cannotEditMetaView: "このビューはコードで定義された組み込みビューのため、変更できません。",
cannotDeleteMetaView: "このビューはコードで定義された組み込みビューのため、削除できません。",
expandToPage: 'Open as full page',
expandToPage: "フルページで開く",
objectNotFound: "オブジェクトが見つかりません",
objectNotFoundDescription: "オブジェクト「{{objectName}}」は現在の設定に存在しません。",
objectNotFoundHint: "アプリのナビゲーション設定を確認するか、サイドバーから別のオブジェクトを選択してください。",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,10 @@ const ko = {
viewNameHelp: "머신 키(snake_case)입니다. 제목에서 자동으로 채워집니다 — 원하면 수정하세요.",
viewNameRequired: "키를 입력하세요(소문자, 숫자, 밑줄). 비라틴 문자 제목에서는 자동으로 채워지지 않습니다.",
viewNameInvalid: "소문자, 숫자, 밑줄을 사용하고 문자 또는 밑줄로 시작하세요.",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "시스템 보기는 코드에 정의되어 있어 읽기 전용입니다.",
cannotEditMetaView: "이 보기는 코드에 정의된 기본 제공 보기이므로 변경할 수 없습니다.",
cannotDeleteMetaView: "이 보기는 코드에 정의된 기본 제공 보기이므로 삭제할 수 없습니다.",
expandToPage: 'Open as full page',
expandToPage: "전체 페이지로 열기",
objectNotFound: "객체를 찾을 수 없음",
objectNotFoundDescription: "객체 \"{{objectName}}\"이(가) 현재 구성에 존재하지 않습니다.",
objectNotFoundHint: "앱 탐색 설정을 확인하거나 사이드바에서 다른 객체를 선택하세요.",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,10 @@ const pt = {
viewNameHelp: "Chave técnica (snake_case). Preenchida automaticamente a partir do título — edite se quiser.",
viewNameRequired: "Informe uma chave (letras minúsculas, números, sublinhado). Não é preenchida automaticamente para títulos não latinos.",
viewNameInvalid: "Use letras minúsculas, números e sublinhados; comece com uma letra ou sublinhado.",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "Exibição do sistema definida no código — somente leitura.",
cannotEditMetaView: "Esta exibição é integrada e definida no código — não é possível alterá-la.",
cannotDeleteMetaView: "Esta exibição é integrada e definida no código — não é possível excluí-la.",
expandToPage: 'Open as full page',
expandToPage: "Abrir como página inteira",
objectNotFound: "Objeto não encontrado",
objectNotFoundDescription: "O objeto \"{{objectName}}\" não existe na configuração atual.",
objectNotFoundHint: "Verifique as configurações de navegação do seu aplicativo ou selecione um objeto diferente na barra lateral.",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,10 @@ const ru = {
viewNameHelp: "Машинный ключ (snake_case). Заполняется автоматически из заголовка — при желании измените.",
viewNameRequired: "Введите ключ (строчные буквы, цифры, подчёркивание). Для нелатинских заголовков автозаполнение не работает.",
viewNameInvalid: "Используйте строчные латинские буквы, цифры и подчёркивания; начните с буквы или подчёркивания.",
systemViewReadonly: 'System view defined in code - duplicate to customize.',
systemViewReadonly: "Системное представление определено в коде — только для чтения.",
cannotEditMetaView: "Это встроенное представление, определённое в коде, — изменить его нельзя.",
cannotDeleteMetaView: "Это встроенное представление, определённое в коде, — удалить его нельзя.",
expandToPage: 'Open as full page',
expandToPage: "Открыть на всю страницу",
objectNotFound: "Объект не найден",
objectNotFoundDescription: "Объект «{{objectName}}» не существует в текущей конфигурации.",
objectNotFoundHint: "Проверьте настройки навигации приложения или выберите другой объект на боковой панели.",
Expand Down
Loading