Repository navigation
Commit 9bf5e67
fix(platform-objects): translate the zh-CN object labels, options and help that shipped as English copies (#20490)
Fixes #20462
Clause-②: no
## What changed
A zh-CN console showed English on Setup surfaces: the Invite user dialog
listed the membership roles as 所有者 / 管理员 / **Delegated Admin** / 成员, and
a team record read **MEMBER COUNT**. The keys were present in
`packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts`,
but their values were byte copies of the `en` source, as `os i18n
extract --fill=default` seeds them.
- **320 copied leaves translated** in the zh-CN objects bundle. Only
leaf values changed; the structure is untouched. The two reproduced
strings now read 受托管理员 (on both `sys_member` and `sys_invitation`) and
成员数.
- **42 leaves kept in English by design**, each declared with its reason
in a decision ledger, `objects-zh-cn-echo-decisions.test.ts`. The list
is below.
- **The provenance companion** `zh-CN.source-hashes.generated.ts` was
regenerated by `pnpm i18n:extract`, never by hand. It records a leaf
only while the leaf is a byte copy of its source, so 320 rows dropped
and 42 remain. The 42 are set-equal to the declared echoes.
- **Pin:** the reproduced leaves
`sys_member.fields.role.options.delegated_admin`,
`sys_invitation.fields.role.options.delegated_admin` and
`sys_team.fields.member_count.label` are pinned as translated under
zh-CN. The role options must read one word per role on both objects.
- `ja-JP` and `es-ES` are not touched. They carry the same copy class;
their counts are under *Out of scope*.
**The coverage-ratchet half is out of scope, per triage (5873680175).**
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. The new ledger deliberately does
not assert that no other zh-CN leaf is a copy, for the same reason.
## Copy count, before and after, by key class
A zh-CN 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` /
`zh-CN.objects.generated.ts`. Both bundles have 1529 leaves.
| key class | before (b810ddb) | after |
|---|---:|---:|
| `label` (object, field, view, action param) | 196 | 27 |
| `options.*` | 20 | 9 |
| `pluralLabel` | 11 | 0 |
| `help` | 114 | 0 |
| `description` | 11 | 0 |
| `placeholder` | 4 | 4 |
| `emptyState.title` / `.message` | 4 | 0 |
| result-dialog field (`client.client_id`, `client.client_secret`) | 2 |
2 |
| **total** | **362** | **42** |
| `label` + `options.*` | 216 | 36 |
The card reads 338 in total, 192 of them `label` / `options.*`, at
3cf6449. The zh-CN and en object bundles are byte-identical between
3cf6449 and b810ddb, and this walk reads 362 / 216 at 3cf6449
too. So the difference of 24 comes from the counting method, and the
card does not state its method.
`help` / `description` / `placeholder` / `emptyState` are decided as the
same class as `label`. The extractor files each of them as an
`ExpectedEntry` under the same `source` kind as its element's label
(`field`, `object`, `view`, `action`;
`packages/cli/src/utils/i18n-extract.ts`), and the coverage detector
consumes them identically. So they were decided as the same class:
`help`, `description` and `emptyState` were translated, and all four
placeholders are declared echoes, because each shows a value the admin
types (see the list below).
## Kept in English by design (42), with reasons
| 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, shown on the link button as the name a user recognises. The
bundle renders no brand name: the authored SSO empty state keeps Okta,
Entra and Auth0 verbatim. |
| `fields.id.label` on `sys_oauth_application`,
`sys_oauth_access_token`, `sys_oauth_refresh_token`,
`sys_oauth_consent`, `sys_oauth_resource`, `sys_oauth_client_resource`,
`sys_oauth_client_assertion`, `sys_sso_provider`, the eight `sys_scim_*`
objects, `sys_email_template`, `sys_metadata`, `sys_metadata_history`,
`sys_view_definition`, `sys_metadata_audit`, `sys_secret`,
`sys_setting_audit` (23) | ID | The bundle keeps the initialism verbatim
in every label that carries it (用户 ID, 团队 ID, 客户端 ID). A bare `ID` has
no noun to render, and adding one would invent content. |
| `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) |
JWKS, JWKS URI | Protocol names the bundle keeps verbatim: `sys_jwks` is
authored 签名密钥 (JWKS), and URIs stay URI (重定向 URI). Their `help` leaves
are translated. |
|
`sys_oauth_application._actions.create_oauth_application.resultDialog.fields.client.client_id`
/ `.client_secret` (2) | Client ID, Client Secret | The credential names
an admin copies out of the one-time dialog. The same dialog family
authored its zh-CN text with the names verbatim (我已保存 Client Secret, 新的
Client Secret). |
|
`sys_sso_provider._actions.register_saml_provider.params.entryPoint.label`
(1) | IdP SSO URL | Nothing but protocol initialisms the bundle keeps
verbatim. 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
| A placeholder shows the literal value the admin types: OIDC scope and
claim names the IdP matches byte for byte. The authored help on the same
params keeps them verbatim (默认为 “email”). |
|
`sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder`
(1) | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress | The SAML
NameID format URN the admin types: an identifier, not prose. |
| `sys_email.fields.message_id.label` (1) | Message-ID | The RFC 5322
header name. The authored help on the same field keeps it (传输层分配的
RFC-5322 Message-ID). |
| `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API |
Initialisms the bundle keeps wherever they appear (API 密钥, 在 UI 中暴露). |
**Where the declaration lives.** Neither the extract config nor `os i18n
extract` / `os i18n check` has a keep-source mechanism: no allowlist, no
flag, no provenance mark. `--filter` only narrows emission. The existing
mechanism that declares a leaf "English by design" is this package's
echo-decision ledger. The shape is `type Verdict = 'translate' |
'echo'`, with a reason on every row and a departure reason required for
every `echo` (`undeclaredEchoes`). It lives in
`packages/platform-objects/src/apps/translations/*-echo-decisions.test.ts`,
one file per family. `report-dataset-panel-echo-decisions.test.ts` is
the first: it has `Verdict`, a reason per row and the per-locale
departure check written inline. The named `undeclaredEchoes` predicate
arrives in the later rounds, first in
`object-field-editor-panel-echo-decisions.test.ts`.
`objects-zh-cn-echo-decisions.test.ts` is that shape applied to the
objects bundle. No new mechanism was added.
## Terminology
The terms follow the zh-CN leaves that were already authored, so one
term is not translated two ways:
- `delegated_admin` is 受托管理员. That is the console's own word for this
role value (objectui `organization.roles.delegatedAdmin`). The other
three roles already read the same in both places (所有者 / 管理员 / 成员). The
alternative is 委派管理员, the in-repo word for the ADR-0090 concept
(委派管理范围). One line changes it if the maintainer prefers it.
- SCIM provisioning is 预配, following
`sys_scim_group_member.display_title` (预配的用户及其所在组). Decommission is 下线.
- The notification leaves take the service-messaging bundle's words: 主题,
严重程度 (信息 / 警告 / 严重), 负载, 去重键 and 通道.
- `managed_by` options take `sys_metadata`'s words: 管理方, 平台 / 包 / 管理员.
- OIDC claims stay `claim`, and help text keeps `scopes`, both as the
bundle already writes them (映射:邮箱 claim; 客户端可申请的 scopes …).
- "My Memberships" is 我的组织, the account nav's label for the same screen.
`Web` is Web 应用, like the sibling
`sys_oauth_application.fields.type.options.web`.
- `sys_migration` keeps its section's ASCII punctuation.
## Collateral: nine existing ledgers encoded the old copy debt
Nine panel ledgers in the same directory asserted floors on the zh-CN
provenance table: more than 100 rows, more than 300, and more than 50
echoing objects leaves "to sample". Those floors held only while about
360 zh-CN objects copies existed, so any card that translated them would
go red. Each floor's own message states its intent, "provenance table is
empty" or "has no echoing objects leaf to sample". Each now asks for
greater than 0, with a one-line comment saying why.
One more test pinned `sys_oauth_resource.fields.access_token_ttl.label`
as an unauthored fill in all three locales. Its own message says "if a
translator authored it, this ledger note is stale". It now records that
zh-CN is authored (访问令牌 TTL keeps the `TTL` token, the treatment the
lifecycle rows give it), while ja-JP and es-ES stay fills.
Files: `action-body-panel-`, `bare-type-display-`, `dataset-panel-`,
`field-panel-`, `hook-execution-panel-`, `object-collapsed-sections-`,
`object-lifecycle-panel-`, `page-interface-panel-` and
`report-form-echo-decisions.test.ts`. PR #20485 edits three of these
files (`field-panel-`, `object-collapsed-sections-`,
`object-lifecycle-panel-`) in different hunks. A local merge-tree run
was not taken, so whether the two merge cleanly is not measured.
## Verification
Measured at head `08ebcd01d`. That is a true merge of `origin/main`
851af0c, and the CLI closure was rebuilt after the merge.
- **`pnpm check:i18n` (H3):**
- exit 0 at base;
- exit 1 after the leaf edits alone, reported as `platform-objects
DRIFTED (1)`. The drifted file was the provenance companion;
- exit 0 after `pnpm i18n:extract`. The extract changed only
`zh-CN.source-hashes.generated.ts`: −320 rows, 0 added, and the bundle
file stayed byte-identical.
- **`pnpm --filter @objectstack/platform-objects exec vitest run
--maxWorkers=2`:** 56 files and 921 tests passed. Before the collateral
edit, the same run had 15 failures across 9 files, all of them the
floors above.
- **`pnpm --filter @objectstack/platform-objects typecheck`:** exit 0.
`tsconfig.test.json` includes the new ledger (`--listFilesOnly`: 1 hit).
- **Reverse verification.** Three mutation legs, each through
`scripts/ablation-replace.mjs` on `zh-CN.objects.generated.ts`, each red
as predicted, and each restored to blob `24cfb9355b89` with `git diff
HEAD` empty:
1. `label: "成员数"` changed back to `"Member Count"`: 1 failed / 9 passed,
"sys_team.fields.member_count.label reads its en source again".
2. Both `delegated_admin: "受托管理员"` changed back to `"Delegated Admin"`:
2 failed / 8 passed. The verdict row failed, and so did the role-word
test ("expected 'Delegated Admin' to match /\p{Script=Han}/u").
3. The declared echo `google: "Google"` changed to `"谷歌"`: 1 failed / 9
passed, "is a declared echo and must stay the en source".
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `08ebcd01d` printed 61 commands.
All 61 were run, and each exit code was 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: workspace `dist/` missing for packages outside the CLI
closure). That is not a measurement. Rerun once the `dist/` existed:
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.
- NOT MEASURED locally, declared to CI: the 6 workflow-valued families,
the 5 path-scheduled CI jobs and the 4 type-check lanes the derivation
names.
## Patch round 1
The contract review 5876239898 (PASS at `08ebcd01d`) named one false
word and two comment slips. All three are corrected here, as text only;
no translated value, assertion or floor changed.
- **Changeset:** the translated-class list no longer names placeholders.
All four placeholders were decided as echoes, and none was translated.
- **Ledger header** (`objects-zh-cn-echo-decisions.test.ts`, comment
only): it no longer says the provenance table "kept exactly the 42" as
an assertion. It now names what is asserted: every declared echo still
has its provenance row, and no pinned translation has one. That the
table holds nothing beyond the echoes is measured, and deliberately not
asserted.
- **This body:** the H4 sentence now says placeholders were decided as
the same class and are all declared echoes. The ledger-history sentence
now credits `undeclaredEchoes` to the later rounds rather than to
`report-dataset-panel-`.
- **Merge and readings:** `origin/main` fc0db22 was merged with a true
merge commit. At head `927957df4`, `pnpm check:i18n` exited 0 and `pnpm
--filter @objectstack/platform-objects exec vitest run
src/apps/translations/objects-zh-cn-echo-decisions.test.ts` passed 10 of
10 (lock verdict: command-exit 0). The round-0 gate readings above were
taken at `08ebcd01d` and were not re-run.
## Out of scope
- The coverage ratchet (see above).
- `ja-JP` and `es-ES` carry the same copy class, measured at b810ddb
and not edited:
- `ja-JP`: 383 copies (`label` 210 · `options.*` 20 · `pluralLabel` 12 ·
`help` 121 · `description` 12 · `placeholder` 4 · `emptyState` 4);
`label` + `options.*` = 230.
- `es-ES`: 392 copies (`label` 217 · `options.*` 22 · `pluralLabel` 12 ·
`help` 121 · `description` 12 · `placeholder` 4 · `emptyState` 4);
`label` + `options.*` = 239.
## Acceptance notes
- The existing zh-CN bundle renders `Locked Until` two ways, 锁定至 on
`sys_user` and 锁定到 on `sys_job_queue`. This card uses 锁定至 for
`sys_two_factor` and leaves the pre-existing pair alone.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent de8c973 commit 9bf5e67
13 files changed
Lines changed: 728 additions & 659 deletions
File tree
- .changeset
- packages/platform-objects/src/apps/translations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
Lines changed: 3 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
528 | 528 | | |
529 | 529 | | |
530 | 530 | | |
531 | | - | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
532 | 534 | | |
533 | 535 | | |
534 | 536 | | |
| |||
Lines changed: 6 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
703 | 703 | | |
704 | 704 | | |
705 | 705 | | |
706 | | - | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
707 | 709 | | |
708 | 710 | | |
709 | 711 | | |
| |||
754 | 756 | | |
755 | 757 | | |
756 | 758 | | |
757 | | - | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
758 | 762 | | |
759 | 763 | | |
760 | 764 | | |
| |||
Lines changed: 3 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
556 | 556 | | |
557 | 557 | | |
558 | 558 | | |
559 | | - | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
560 | 562 | | |
561 | 563 | | |
562 | 564 | | |
| |||
Lines changed: 3 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
557 | 557 | | |
558 | 558 | | |
559 | 559 | | |
560 | | - | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
561 | 563 | | |
562 | 564 | | |
563 | 565 | | |
| |||
Lines changed: 3 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
669 | 669 | | |
670 | 670 | | |
671 | 671 | | |
672 | | - | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
673 | 675 | | |
674 | 676 | | |
675 | 677 | | |
| |||
Lines changed: 9 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
770 | 770 | | |
771 | 771 | | |
772 | 772 | | |
773 | | - | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
774 | 776 | | |
775 | 777 | | |
776 | 778 | | |
| |||
807 | 809 | | |
808 | 810 | | |
809 | 811 | | |
810 | | - | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
811 | 815 | | |
812 | 816 | | |
813 | 817 | | |
| |||
848 | 852 | | |
849 | 853 | | |
850 | 854 | | |
851 | | - | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
852 | 858 | | |
853 | 859 | | |
854 | 860 | | |
| |||
Lines changed: 20 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1292 | 1292 | | |
1293 | 1293 | | |
1294 | 1294 | | |
1295 | | - | |
1296 | | - | |
1297 | | - | |
1298 | | - | |
| 1295 | + | |
| 1296 | + | |
| 1297 | + | |
| 1298 | + | |
| 1299 | + | |
| 1300 | + | |
1299 | 1301 | | |
1300 | 1302 | | |
1301 | 1303 | | |
1302 | 1304 | | |
1303 | | - | |
| 1305 | + | |
| 1306 | + | |
| 1307 | + | |
| 1308 | + | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
1304 | 1313 | | |
1305 | 1314 | | |
1306 | 1315 | | |
| |||
1317 | 1326 | | |
1318 | 1327 | | |
1319 | 1328 | | |
1320 | | - | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
1321 | 1332 | | |
1322 | 1333 | | |
1323 | 1334 | | |
| |||
1342 | 1353 | | |
1343 | 1354 | | |
1344 | 1355 | | |
1345 | | - | |
| 1356 | + | |
| 1357 | + | |
| 1358 | + | |
1346 | 1359 | | |
1347 | 1360 | | |
1348 | 1361 | | |
| |||
0 commit comments