Skip to content

Commit 6427e2c

Browse files
fix(platform-objects): translate the ja-JP and es-ES object labels, options and help that shipped as English copies (#20530)
Fixes #20493 Clause-②: no ## What changed A ja-JP or es-ES console read English on Setup surfaces: the Invite user dialog's role list carried **Delegated Admin** beside the translated roles, and every Setup record page read the same copied leaves. The keys were present in `packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts` and `es-ES.objects.generated.ts`, but their values were byte copies of the `en` source that `os i18n extract --fill=default` seeds. This PR applies the zh-CN mechanism of PR #20490 (squash `9bf5e67af`) to both locales, one PR, one ledger per locale. - **ja-JP: 340 copied leaves translated**, 43 kept as written by design. - **es-ES: 338 copied leaves translated**, 54 kept as written by design. - Leaf values only; the structure of both bundles is untouched. `pnpm i18n:extract` re-read both bundles byte-identical. - **Two ledgers**, one per locale, of the shape `objects-zh-cn-echo-decisions.test.ts` built (`type Verdict = 'translate' | 'echo'`, a reason on every row, a departure on every echo, `undeclaredEchoes` proved on a synthetic row): `objects-ja-jp-echo-decisions.test.ts` and `objects-es-es-echo-decisions.test.ts`. No new mechanism. - **Provenance companions** `ja-JP.source-hashes.generated.ts` and `es-ES.source-hashes.generated.ts` regenerated by `pnpm i18n:extract`, never by hand: −340 and −338 rows, 0 added. The rows that remain are set-equal to each ledger's declared echoes (43 and 54). - **One sibling ledger followed:** `object-lifecycle-panel-echo-decisions.test.ts` pinned `sys_oauth_resource.fields.access_token_ttl.label` as an unauthored fill in ja-JP and es-ES. Both are now authored (アクセストークン TTL, TTL del token de acceso), so the test now asserts all three locales authored with the `TTL` token kept and no provenance row. The other eight sibling ledgers already asked for a non-empty table (`> 0`) since PR #20490 and needed no edit. - zh-CN, `*.metadata-forms.generated.ts`, the hand-authored `ja-JP.ts` / `es-ES.ts` and objectui are not touched. **The coverage-ratchet half is out of scope, per triage (5877552039, 「新增门禁默认否」).** Teaching `i18n-coverage-baseline` to count a copied source string as untranslated widens an existing gate; if the maintainer wants that ratchet, it is a separate gate card. For the same reason neither ledger asserts that no other leaf of its locale is a copy. ## Pinned strings | leaf | ja-JP | es-ES | pin | |---|---|---|---| | `sys_member.fields.role.options.delegated_admin` | 委任管理者 | Administrador delegado | exact word, equal to the console's | | `sys_invitation.fields.role.options.delegated_admin` | 委任管理者 | Administrador delegado | equal to the `sys_member` options, one word per role | | `sys_team.fields.member_count.label` | メンバー数 | Número de miembros | not the `en` source, no provenance row | The console's words were read at objectui `main` `797a30f`, read-only: `packages/i18n/src/locales/ja.ts`, key `organization.roles.delegatedAdmin` = 委任管理者; `packages/i18n/src/locales/es.ts`, same key = Administrador delegado. The role test also asserts the four role words are distinct, identical on both objects, Japanese-script in ja-JP and none equal to its `en` source in es-ES. ## Copy count, before and after, by key class A string leaf counts as a copy when it is byte-equal to the `en` leaf at the same path, over every leaf of `en.objects.generated.ts` and the locale's bundle. All three bundles have 1529 leaves. "Before" is measured at base `397572ed5`; the card's `b810ddb6f` counts are reproduced exactly (H1 holds). | key class | ja-JP before | ja-JP after | es-ES before | es-ES after | |---|---:|---:|---:|---:| | `label` (object, field, view, action param) | 210 | 29 | 217 | 38 | | `options.*` | 20 | 10 | 22 | 12 | | `pluralLabel` | 12 | 0 | 12 | 0 | | `help` | 121 | 0 | 121 | 0 | | `description` | 12 | 0 | 12 | 0 | | `placeholder` | 4 | 4 | 4 | 4 | | `emptyState.title` / `.message` | 4 | 0 | 4 | 0 | | **total** | **383** | **43** | **392** | **54** | | `label` + `options.*` | 230 | 39 | 239 | 50 | `help` / `description` / `placeholder` / `emptyState` were decided as the same class as `label` (H5), as zh-CN did: the extractor files each under the same `source` kind as its element's label (`packages/cli/src/utils/i18n-extract.ts`). All four placeholders in both locales are typed literals and stay as written. ## Kept as written by design — ja-JP (43) | leaves | value(s) | reason | |---|---|---| | `sys_account._actions.link_social.params.provider.options.{google, github, microsoft, apple, facebook, gitlab, discord}` (7) | Google, GitHub, Microsoft, Apple, Facebook, GitLab, Discord | Sign-in provider brands. The bundle renders no brand name in kana: the authored SSO empty state keeps Okta, Entra and Auth0 verbatim. | | `fields.id.label` on the 23 objects zh-CN lists (`sys_oauth_*` ×7, `sys_sso_provider`, the eight `sys_scim_*`, `sys_email_template`, `sys_metadata`, `sys_metadata_history`, `sys_view_definition`, `sys_metadata_audit`, `sys_secret`, `sys_setting_audit`) (23) | ID | The bundle keeps ID verbatim in every label that carries it (ユーザー ID, 組織 ID, クライアント ID); a bare ID has no noun to render. | | `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) | JWKS, JWKS URI | Protocol names: `sys_jwks` is authored 署名キー (JWKS), and URIs stay URI (リダイレクト URI). Their `help` leaves are translated. | | `sys_oauth_application._actions.create_oauth_application.params.type.options.web` (1) | Web | The bundle already renders the sibling `sys_oauth_application.fields.type.options.web` as Web; the dialog and the record field read one word. | | `sys_sso_provider._actions.register_saml_provider.params.entryPoint.label` (1) | IdP SSO URL | Protocol initialisms only; the sibling SAML params are authored IdP エンティティ ID and IdP 署名証明書. | | `sys_sso_provider._actions.register_sso_provider.params.{scopes, mapEmail, mapName}.placeholder` (3) | openid email profile, email, name | Typed literals: OIDC scope and claim names; the authored help on the same params keeps them (既定値は「email」). | | `sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder` (1) | the SAML NameID emailAddress URN | Typed literal: an identifier, not prose. | | `sys_email.fields.message_id.label` (1) | Message-ID | RFC 5322 header name; the authored help keeps it (トランスポートが割り当てる RFC-5322 Message-ID). | | `sys_email.fields.cc_addresses.label`, `.bcc_addresses.label` (2) | Cc, Bcc | Header abbreviations Japanese mail clients print in Latin letters, with no kana or kanji form in use. The prose labels on the same record are authored (送信元, 宛先, and 返信先 for Reply-To in this PR). | | `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API | Initialisms the bundle keeps (API キー, UI には表示されません; `sys_metadata.fields.source.options.api` is authored API). | Where ja-JP departs from zh-CN: the result dialog's `Client ID` / `Client Secret` were never copies here (authored クライアント ID / クライアントシークレット); `Web` and `Cc` / `Bcc` are ja-JP decisions of their own. ## Kept as written by design — es-ES (54) | leaves | value(s) | reason | |---|---|---| | the 7 sign-in provider options | Google … Discord | Brands; the authored SSO empty state keeps Okta, Entra and Auth0 verbatim. | | `fields.id.label` on the same 23 objects (23) | ID | The bundle keeps ID verbatim in every label that carries it (ID de usuario, ID de organización, ID de cliente). | | `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) | JWKS, JWKS URI | `sys_jwks` is authored Clave de firma (JWKS); URIs stay URI (URI de redirección). | | `...create_oauth_application.params.type.options.web` (1) | Web | The sibling `fields.type.options.web` is authored Web. | | the 3 OIDC placeholders + the SAML NameID URN placeholder (4) | openid email profile, email, name, URN | Typed literals; the authored help keeps them (Valor predeterminado: «email»). | | `sys_email.fields.message_id.label` (1) | Message-ID | Authored help: Message-ID RFC-5322 asignado por el transporte. | | `sys_email.fields.cc_addresses.label` (1) | Cc | The Spanish copy abbreviation (con copia) is Cc too; the blind-copy sibling is authored Cco. | | `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API | Initialisms the bundle keeps (nunca se muestra en la UI; `sys_metadata` source option `api` is authored API). | | `sys_email.fields.error.label`, `sys_job_run.fields.error.label` (2) | Error | Spanish spells it the same (el error); the bundle writes Último error on `sys_job_queue`. | | `sys_notification.fields.actor_id.label`, `sys_metadata_audit.fields.actor.label`, `sys_setting_audit.fields.actor_id.label` (3) | Actor | Spanish spells it the same (el actor); authored help reads Último actor que escribió esta fila, and the messaging bundle authors its actor label Actor. | | `sys_setting.fields.scope.options.global`, `sys_setting_audit.fields.scope.options.global` (2) | Global | Spanish spells it the same, beside the authored sibling options Inquilino and Usuario. | | `sys_email_template.fields.variables_json.label` (1) | Variables (JSON) | Spanish spells it the same; the authored description of the same object says (asunto + contenido + variables). | | `sys_oauth_access_token.fields.token.label`, `sys_oauth_refresh_token.fields.token.label` (2) | Token | The bundle's own word for the concept (Token de sesión, Token de acceso). | | `sys_metadata.fields.checksum.label`, `sys_metadata_history.fields.checksum.label` (2) | Checksum | The bundle's own word (Checksum anterior). | | `sys_organization.fields.slug.label` (1) | Slug | The bundle's own word (Cambiar slug); the console says the same (Ese slug ya está en uso). | Where es-ES departs from zh-CN: `Client ID` / `Client Secret` in the result dialog and `IdP SSO URL` were never copies here (authored); the 14 Spanish-as-written rows above are es-ES's own class. ## Terminology (H4: one term, one rendering) - **ja-JP** takes the bundle's already-authored words: 作成日時 / 更新日時 / 有効期限 / 失効日時 / ローテーション日時, クライアント ID, スコープ, クレーム (claim, as in マッピング: メールクレーム), プロビジョニング (as in IdP プロビジョニング), 投影 and 付与 (as `sys_scim_projection_grant.display_title` authors them), リース and 冪等キー (`sys_job_queue`), トピック / 重大度 / 情報・警告・重大 / 操作者 (the messaging bundle), ポジション (the security bundle), 管理者 for `Managed By` (as `sys_metadata`), 所属組織 for My Memberships (the account nav's label, `ja-JP.ts` `nav_account_memberships`), 配属 for Placement (objectui's word). Decommission is 廃止; tombstone is トゥームストーン, the service-storage ja-JP objects bundle's existing rendering. - **es-ES** likewise: Creado el / Actualizado el / Caduca el / Revocado el / Rotado el, ID de cliente, Ámbitos, reclamo (claim, as in Mapeo: reclamo de correo), aprovisionamiento (as in Aprovisionada por IdP), concesión (as in Tipos de concesión), arrendamiento (`sys_job_queue`'s lease), Tema / Severidad / Información · Advertencia · Crítica (the messaging bundle), Puesto (the security bundle), principal for Parent (the bundle's word throughout: Unidad de negocio principal, Objeto principal), Mis organizaciones (`es-ES.ts` `nav_account_memberships`), Asignación for Placement (objectui's word). Decommission is retirada; tombstone is lápida. ## Verification Measured at head `d616c8cc3`, a true merge of `origin/main` `fb386074f` (parents `8f3e7cae3`, `fb386074f`). The CLI closure was rebuilt after the merge. - **H2, `pnpm check:i18n`:** - exit 0 with both bundles restored to their `397572ed5` bytes; - exit 1 after the leaf edits alone, `platform-objects DRIFTED (2)`: the two provenance companions (zh-CN's run read `DRIFTED (1)`, one locale); - exit 0 after `pnpm i18n:extract`, which changed only the two companions (−340 / −338 rows, 0 added). Remaining rows vs remaining copies: 43 = 43 and 54 = 54, set difference 0. The mechanism carried over unchanged; the only per-locale difference is which leaves each locale keeps. - exit 0 again at `d616c8cc3` (derived gate run below). - **`pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2`:** 58 files, 942 tests passed (lock verdict command-exit 0), at `d616c8cc3` and before the merge. - **`pnpm --filter @objectstack/platform-objects typecheck`:** exit 0. `tsc -p tsconfig.test.json --listFilesOnly` lists both new ledgers (2 hits). - **Reverse verification.** Five mutation legs through `scripts/ablation-replace.mjs` (anchor hit counts declared, blob change proved, restore proved blob == HEAD and `git diff HEAD` empty each time), each red as predicted: 1. ja-JP both `delegated_admin: "委任管理者"` → `"Delegated Admin"`: 2 failed / 9 passed (verdict row; role test "expected 'Delegated Admin' to match" the Japanese-script pattern). 2. es-ES both `delegated_admin: "Administrador delegado"` → `"Delegated Admin"`: 2 failed / 8 passed (verdict row; "es-ES role delegated_admin reads its en source"). 3. ja-JP declared echo `google: "Google"` → `"グーグル"`: 1 failed / 10 passed ("is a declared echo and must stay the en source"). 4. es-ES declared echo `global: "Global"` (×2) → `"Mundial"`: 1 failed / 9 passed. 5. ja-JP `委任管理者` (×2) → `委託管理者`, still Japanese: 1 failed / 10 passed, only the console-word pin ("expected '委託管理者' to be '委任管理者'"). 6. Sibling leg: `object-lifecycle-panel-echo-decisions.test.ts` restored to its `397572ed5` bytes against the new bundles: 1 failed / 34 passed ("ja-JP sys_oauth_resource.fields.access_token_ttl.label: expected 'アクセストークン TTL' to be 'Access Token TTL'"); restored to HEAD, blob equal. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `d616c8cc3` printed 61 commands. All 61 were run, each exit code written to a file before any pipe. - 60 exited 0 on their first run. - `pnpm check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit 3: `dist/` missing for packages outside the CLI closure). Not a measurement. Rerun after a workspace build (71 of 71 tasks cached): exit 0. - `--ran` reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). - **`pnpm check:i18n-coverage`:** exit 0, "621 baselined untranslated string(s), none new". The ratchet is unchanged, as expected. - **ESLint, narrowed and declared:** `eslint --no-inline-config --format json` over the 7 changed `.ts` files: 7 files linted (each confirmed not ignored by the repo config's own `isPathIgnored`), 0 errors, 0 warnings. `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, stated in its own comment), so this diff cannot move a verdict on any untouched file. The repo-wide `pnpm lint` is CI's. - **H6, size:** the derivation reads 2787 changed lines (+1416 / −1371) over 8 files, under the 5000 human-merge threshold. - **NOT MEASURED locally, declared to CI:** the 6 workflow-valued families, the 11 wide-population families and the path-scheduled CI jobs the derivation names. The Invite dialog was not re-driven in a browser (NOT MEASURED: browser rendering; reason: bundle leaves are read directly, as the card measured them). ## Acceptance notes - es-ES renders `Parent` as principal throughout the bundle, so the pre-existing `sys_business_unit.fields.parent_business_unit_id.label` and `sys_user.fields.primary_business_unit_id.label` both read Unidad de negocio principal. This PR follows the bundle's word for `Parent Organization` (Organización principal) and leaves the pre-existing pair alone. Carrier: none. - The ja-JP bundle renders `Locked Until` two ways already (ロック解除日時 on `sys_user`, ロック期限 on `sys_job_queue`). This PR uses ロック解除日時 for `sys_two_factor`, the same lockout concept as `sys_user`. - ja-JP `Managed By` is 管理者, `sys_metadata`'s word, so `sys_email_template` reads 管理者 as the label and as the `admin` option. 管理元 would disambiguate; one line changes it if the maintainer prefers it. - A few authored (not copied) ja-JP / es-ES leaves no longer match their moved `en` source, for example `sys_two_factor.fields.backup_codes.help` (still "JSON-serialized backup codes" where `en` now says an opaque ciphertext) and `sys_notification.description`. Authored leaves are legacy-trusted by the provenance design and outside this card's copy class. Carrier: none. - The eight sibling ledgers' comments name only zh-CN ("since #20462 zh-CN's objects slice holds only its declared English-by-design leaves"); the `> 0` floors they guard hold for all three locales now. Comment-only drift, not edited here. --- _Generated by [Claude Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c7ad16f commit 6427e2c

8 files changed

Lines changed: 1416 additions & 1371 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the ja-JP and es-ES platform-object bundles no longer ship English labels, options and help as copies of the source (#20493)
6+
7+
Clause-②: no
8+
9+
A ja-JP or es-ES console showed English on Setup surfaces: the Invite user
10+
dialog listed the membership roles with **Delegated Admin** among the
11+
translated ones. The keys were present in `ja-JP.objects.generated.ts` and
12+
`es-ES.objects.generated.ts`, but their values were byte copies of the English
13+
source that `os i18n extract --fill=default` seeds.
14+
15+
- ja-JP: 340 of the 383 string leaves that equalled their `en` source are now
16+
translated: labels, plural labels, select options, help, descriptions and
17+
empty states. `delegated_admin` reads 委任管理者 on both `sys_member` and
18+
`sys_invitation`, the word the console already uses for that role.
19+
- es-ES: 338 of the 392 are now translated the same way. `delegated_admin`
20+
reads Administrador delegado on both objects, again the console's word.
21+
- The rest stay as written by design, each recorded with its reason in a
22+
per-locale ledger (`objects-ja-jp-echo-decisions.test.ts`,
23+
`objects-es-es-echo-decisions.test.ts`). ja-JP keeps 43: sign-in provider
24+
brands, the bare `ID` initialism, protocol names (JWKS, IdP SSO URL,
25+
Message-ID, UI / API), the `Web` client type as the bundle already renders it,
26+
the Cc / Bcc header abbreviations, and placeholders that show a value the
27+
admin types. es-ES keeps 54: the same brands, `ID`, JWKS, Message-ID,
28+
UI / API, `Web` and placeholders, plus words Spanish spells as English (Error,
29+
Actor, Global, Variables), the loanwords the bundle already uses (Token,
30+
Checksum, Slug) and Cc.
31+
32+
`ja-JP.source-hashes.generated.ts` and `es-ES.source-hashes.generated.ts` were
33+
regenerated by `pnpm i18n:extract`: each records a leaf only while the leaf is
34+
a copy of its source, so they now list exactly those 43 and 54. zh-CN is
35+
unchanged.

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 338 additions & 338 deletions
Large diffs are not rendered by default.

‎packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts‎

Lines changed: 0 additions & 338 deletions
Large diffs are not rendered by default.

‎packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts‎

Lines changed: 340 additions & 340 deletions
Large diffs are not rendered by default.

‎packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts‎

Lines changed: 0 additions & 340 deletions
Large diffs are not rendered by default.

‎packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts‎

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,29 +1291,22 @@ describe('#19403 round 10 — ⚠️⚠️ the phantom-translation trap, asserte
12911291
}
12921292
});
12931293

1294-
it('⭐ the near-twin that was LOOKED UP AND REFUSED is asserted to be a fill, not a decision', () => {
1294+
it('⭐ the near-twin that was LOOKED UP AND REFUSED is authored in every locale, never a standing echo', () => {
12951295
// `Access Token TTL` was byte-identical in all three locales and read like a
12961296
// standing decision to keep TTL verbatim. It was an unauthored fill in every
12971297
// locale, and the provenance tables said so. Asserted so that no later round
1298-
// leans on it. #20462 then AUTHORED the zh-CN leaf (访问令牌 TTL): the
1299-
// concept is rendered and the initialism survives, the same treatment the
1300-
// lifecycle rows give TTL. ja-JP and es-ES are still fills.
1298+
// leans on it. #20462 then AUTHORED the zh-CN leaf (访问令牌 TTL) and #20493
1299+
// the ja-JP (アクセストークン TTL) and es-ES (TTL del token de acceso) ones:
1300+
// the concept is rendered and the initialism survives, the same treatment
1301+
// the lifecycle rows give TTL.
13011302
const key = 'sys_oauth_resource.fields.access_token_ttl.label';
13021303
const en = FLAT_OBJECTS.get('en')!.get(key);
13031304
expect(en).toBe('Access Token TTL');
13041305
for (const [locale, table] of PROVENANCE) {
13051306
const value = FLAT_OBJECTS.get(locale)!.get(key);
1306-
if (locale === 'zh-CN') {
1307-
expect(value, `${locale} ${key} reads its en source again`).not.toBe(en);
1308-
expect(carriesToken(value!, 'TTL'), `${locale} dropped the machine token`).toBe(true);
1309-
expect(table[`objects.${key}`], `${locale} still records the authored ${key} as a fill`).toBeUndefined();
1310-
continue;
1311-
}
1312-
expect(value, `${locale} ${key}`).toBe(en);
1313-
expect(
1314-
table[`objects.${key}`],
1315-
`${locale} no longer records ${key} as a fill — if a translator authored it, this ledger note is stale`,
1316-
).toBeTypeOf('string');
1307+
expect(value, `${locale} ${key} reads its en source again`).not.toBe(en);
1308+
expect(carriesToken(value!, 'TTL'), `${locale} dropped the machine token`).toBe(true);
1309+
expect(table[`objects.${key}`], `${locale} still records the authored ${key} as a fill`).toBeUndefined();
13171310
}
13181311
});
13191312
});

‎packages/platform-objects/src/apps/translations/objects-es-es-echo-decisions.test.ts‎

Lines changed: 357 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/platform-objects/src/apps/translations/objects-ja-jp-echo-decisions.test.ts‎

Lines changed: 338 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)