Skip to content

Commit a11ef71

Browse files
i18n(platform-objects): ship the platform objects, Setup, Studio and Account in zh-Hant (#30951)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D7v5H7BaFmL7sUCoN7tSxhm)_ domain:server#1 · os-elon-musk · session_016WV4cya5ej5GQV86RPbRPK Part of #30512 — package 11 of 11 (`@objectstack/platform-objects`), first of its seven locales: `zh-Hant`. Per the seat's ruling B, `ar`, `fr`, `pt-BR`, `ru`, `de` and `ko` follow one PR each, and the last PR of the series makes the bundle check read the platform locale table. Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 平台内置对象(用户、组织、会话、API 密钥、OAuth、SSO、SCIM、通知、邮件、任务、导入、元数据、设置、汇率等全部 `sys_*`)、Studio 里各元数据类型的配置表单、「设置」「Studio」「账户」三个应用的导航、系统概览仪表盘与数据集、平台页面标题,以及用户 / 组织记录页上的内联文案,补上繁体中文(zh-Hant)。 - **为什么改:** 平台语言表要求每个界面带齐 11 种语言;这是第 3 期按包补齐的最后一个包(platform-objects)的第 1 种语言。用繁中的管理员和开发者打开平台不再大面积看到英文。 - **风险与回滚:** 只加翻译数据和注册行,不动逻辑、schema、接口;原有 4 种语言一字未改。新语言同样走「源文本哈希回退」,手写部分每条都记了源哈希。测试改动都是把 zh-Hant 加进各测试的语言列表(覆盖变宽,没有放宽断言),外加 cli 一个形状测试的语言码正则放宽到接受 `zh-Hant` 这种书写体子标签(否则它一个内联文案都认不出)。回滚即撤 PR。 - **先看:** `src/apps/translations/setup.translation.ts` 与 `src/metadata-translations/index.ts`(注册),`scripts/i18n-extract.config.ts`(`--locales` 加 zh-Hant),然后抽看 `zh-Hant.ts`(导航,约 140 条)和任一生成文件。 - **两分钟验证:** `node scripts/check-i18n-bundles.mjs --filter=platform-objects` 输出 in sync;`pnpm --filter @objectstack/platform-objects test` 全绿。 - **需要你做的:** 本 PR 改动超过 3000 行(含生成物),按规程需人工合并;契约复核尚未做(contract review owed),复核通过后由席位请 os-zhuang / hotlong 审批。 ## What changes - **Generated bundles** `zh-Hant.objects.generated.ts` (1,674 leaves) and `zh-Hant.metadata-forms.generated.ts` (1,263 leaves), written by the repo's own `os i18n extract --fill=default --source-hashes` from the merge baseline, plus its provenance companion `zh-Hant.source-hashes.generated.ts`. - **Hand bundle** `zh-Hant.ts` (138 leaves: the Setup, Studio and Account navigation, the System Overview dashboard, the datasets and the platform pages), with `zh-Hant.source-hashes.ts` recording the digest of the `en` source each leaf was translated from: all 138, so the first source edit under any of them falls back instead of serving a superseded translation. - **Registration:** `SetupAppTranslations` and `MetadataFormsTranslations` serve `zh-Hant` through `withSourceFallback`, exactly as the existing three; the `apps` barrel exports `zhHant`. - **Extract config:** its docstring's `--locales` (what `check:i18n` executes) and its merge baseline carry `zh-Hant`, so the gate covers the new bundles from this PR on. The root `pnpm i18n:extract` script still lists three locales; the final PR of the series moves both it and `scripts/check-i18n-bundles.mjs` to the platform locale table. - **Inline page copy:** the 42 inline locale maps on the user and organization record pages and the System Overview dataset's null label gain a `zh-Hant` entry. ## Tests - Locale lists widened to `zh-Hant`, no assertion changed: in this package `plugin.translations.test.ts`, `setup-nav-per-user-stores.test.ts`, `sys-user-details-tab.test.ts`, `system-overview-null-label.test.ts` (these four pin the shipped set and go red without it), and the vocabulary, parity, tombstone, source-hash, BU-rename, metadata-forms, repeater, dashboard-header, confirm-question and identity copy tests (these iterate a fixed list, so `zh-Hant` is now held to the same bars). - Cross-package pins held for `zh-Hant`: `plugin-security`'s People & Organization nav test (its control pins the Setup locale set and goes red without the new tag); `service-settings`' `setup-nav-hub-parity.test.ts` (each Setup sidebar entry reads as its hub card's title); `plugin-approvals`' `approval-status-vocabulary.test.ts` (the `my_pending` view label equals the account nav's `nav_account_approvals`: 待我核准). - `packages/cli/test/platform-page-i18n-parity.test.ts`: its inline-map shape test recognised a locale key as `xx` or `xx-YY` only, so with a `zh-Hant` key present it recognised no map at all (`expected 0 to be greater than or equal to 38`). The pattern now also accepts a script subtag; the 42 maps are found again and every one is complete in all five shipped locales. ## Acceptance notes - **Quality bar (#17508 / PR #19345):** written for a Taiwan reader. Each leaf starts from the reviewed `zh-CN` text, converted with OpenCC's Traditional (Taiwan, phrases) table and then a house glossary of about 170 term rules that matches the `zh-Hant` bundles nine packages already ship (權限, 類型, 中繼資料, 工作階段, 權杖, 金鑰, 檢視, 欄 / 列 for column / row, 套件, 範本, 稽核, 共用, 指派, 核准, 電子郵件, 唯讀, 逾時, 背景工作, …). 446 leaves whose English and `zh-CN` pair already has a `zh-Hant` rendering elsewhere in the repo reuse it verbatim. Every one of the 1,541 distinct short leaves was read, and the long help texts were reviewed by sample and by a targeted audit of each rule that rewrites words in context (row / column, 應用 / 應用程式, 按 / 依, 通過 / 透過). - **Mechanical checks, per leaf against `zh-CN`:** backtick spans, `{{holes}}`, `{holes}`, angle tokens and snake_case identifiers byte-identical (three intended differences, all Chinese prose inside a hole default or a JSON example: `default:'該成員'`, `"已封存"`); no Simplified-only character remains (the only s2t remarks are the Taiwan forms 台, 准, 群, 游, 里). - **Cross-package wording pins carried:** `nav_account_approvals` = plugin-approvals' `my_pending` (待我核准); `nav_assignment_rules` = #30856's object name (指派規則); the queue, sharing, approval-delegation and HTTP-delivery entries read as their objects' `zh-Hant` labels; each `nav_settings_*` entry reads as its settings hub title from #30870 (so 郵件投遞 / 簡訊投遞 there, while the HTTP delivery log says 傳送). - **Leaves equal to English, on purpose:** the 40 `objects` leaves `zh-CN` already decided as echoes (`ID`, `JWKS`, `Client ID`, the SSO placeholders, …), each recorded by the extractor in `zh-Hant.source-hashes.generated.ts`, and three hand leaves (`Webhooks`, `Studio`, `AI`). - **One cross-locale coincidence fixed:** `zh-CN`'s `action.fields.method.helpText` is the Latin `GET / POST / PUT / DELETE`, not the source; a converted copy made two locales agree on it and `check:i18n-stale-fill` reported it. `zh-Hant` now translates it (`HTTP 方法(GET、POST、PUT、DELETE)`); the stale-fill check passes with the baseline untouched. The `zh-CN` leaf itself is left as it is (existing fills are out of this card's scope). - No string opens with `os ` followed by a lowercase word (`check-cli-command-ids` passes), and the whole "Lint · docs & code guards" lane was run locally before opening. ## Verification <details><summary>Package suites, typecheck, build (verify lock)</summary> ``` pnpm install --frozen-lockfile && turbo run build --filter=!@objectstack/docs (at head e8d9055, on main 33043bd) Tasks: 118 successful, 118 total pnpm --filter @objectstack/platform-objects test Test Files 117 passed (117) Tests 1636 passed (1636) (1593 before this PR; the 43 new are the zh-Hant cases) pnpm --filter @objectstack/platform-objects typecheck (tsc, scripts tsconfig, check:test-typecheck OK) pnpm --filter @objectstack/plugin-security test 246 files, 5686 passed | 45 skipped pnpm --filter @objectstack/plugin-approvals test 115 files, 1502 passed pnpm --filter @objectstack/service-settings test 71 files, 1351 passed packages/cli: vitest run test/platform-page-i18n-parity.test.ts 27 passed os-verify-lock: VERDICT command-exit 0 (one '&&' chain) typecheck of plugin-security, plugin-approvals, service-settings and cli: 4 x exit 0 ``` </details> <details><summary>i18n gates, the derived families and the docs &amp; code guards lane</summary> ``` node scripts/check-i18n-bundles.mjs (all packages) (at e8d9055) check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys). EXIT 0 node scripts/check-i18n-stale-fill.mjs check-i18n-stale-fill: OK (11 bundle set(s) — no new stale fills, 1 baselined). EXIT 0 pnpm check:app-nav-i18n check-app-nav-i18n: OK (13 contributor(s), 5 locale(s), 2 app(s) — setup: 56 merged nav id(s), account: 12 merged nav id(s) — every id labelled in every locale). EXIT 0 pnpm check:i18n-coverage OK (14 config(s), 426 baselined untranslated string(s), none new). EXIT 0 pnpm check:i18n-walk-parity EXIT 0 node scripts/check-cli-command-ids.mjs EXIT 0 dispatch-gates --commands (at e8d9055): 64 families, each run and recorded with its exit code: 64 x exit 0 dispatch-gates --ran: 64 derived, 64 run, 0 NOT-MEASURED (a derived zero) pnpm lint (repo-wide, at e8d9055): exit 0 CI lane "Lint · docs & code guards", every command run locally (at e8d9055) 56 commands (lane-verdict excepted), 56 x exit 0, check-cli-command-ids included changed lines 7271 (+7216 / -55), 36 files: over the 3000-line human-merge threshold ``` </details> The branch is one commit on `main` `33043bd8` (after #30931 landed); every number above was measured at that head. Main's commits since the earlier draft (#30846, #30919, #30929, #30931) touch no file this PR changes. Dogfood: not run — content-only package PR, no `!` in the title and no `(narrowing)` arm. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cya5ej5GQV86RPbRPK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8ae0ab9 commit a11ef71

36 files changed

Lines changed: 7218 additions & 55 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
i18n(platform-objects): the platform objects, Setup, Studio and Account ship in Traditional Chinese
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`@objectstack/platform-objects` now carries `zh-Hant` beside `en`, `zh-CN`, `ja-JP` and `es-ES`: every `sys_*` object (labels, fields, help, options, views, actions and their dialogs), the metadata-type configuration forms Studio opens, the Setup, Studio and Account navigation, the System Overview dashboard and its datasets, the platform pages' titles, and the inline copy on the user and organization record pages. An administrator or developer reading the platform in Traditional Chinese no longer sees it in English. The new bundles pass through the same source-hash fallback as the existing ones, and every hand-authored leaf records the digest of the source it was translated from. The extract config documents `zh-Hant` in its `--locales`, so `check:i18n` now gates the new bundles. Package 11 of #30512, first of seven locales; `ar`, `fr`, `pt-BR`, `ru`, `de` and `ko` follow.

‎packages/cli/test/platform-page-i18n-parity.test.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -738,11 +738,12 @@ const RECORD_PAGES: Array<Record<string, any>> = Object.values(
738738
const SHIPPED_LOCALES = Object.keys(SetupAppTranslations as Record<string, unknown>);
739739

740740
/**
741-
* A key is a locale code (`en`, `zh-CN`). Deliberately a shape test rather than
742-
* a membership test against `SHIPPED_LOCALES`: a map carrying a locale the
743-
* bundle does not ship is still an inline map, and must still be judged.
741+
* A key is a locale code (`en`, `zh-CN`, and the platform locale table's
742+
* script-subtag tag `zh-Hant`). Deliberately a shape test rather than a
743+
* membership test against `SHIPPED_LOCALES`: a map carrying a locale the bundle
744+
* does not ship is still an inline map, and must still be judged.
744745
*/
745-
const LOCALE_KEY = /^[a-z]{2}(-[A-Z]{2})?$/;
746+
const LOCALE_KEY = /^[a-z]{2}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/;
746747

747748
/** An inline `I18nLabel` map: every key a locale code, every value a string. */
748749
const isInlineLocaleMap = (value: unknown): value is Record<string, string> => {

‎packages/platform-objects/scripts/i18n-extract.config.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* pointed at this file via `bundle-require`:
99
*
1010
* os i18n extract packages/platform-objects/scripts/i18n-extract.config.ts \
11-
* --locales=zh-CN,ja-JP,es-ES \
11+
* --locales=zh-CN,ja-JP,es-ES,zh-Hant \
1212
* --fill=default \
1313
* --source-hashes \
1414
* --out=packages/platform-objects/src/apps/translations
@@ -221,10 +221,12 @@ import { enObjects } from '../src/apps/translations/en.objects.generated.js';
221221
import { zhCNObjects } from '../src/apps/translations/zh-CN.objects.generated.js';
222222
import { jaJPObjects } from '../src/apps/translations/ja-JP.objects.generated.js';
223223
import { esESObjects } from '../src/apps/translations/es-ES.objects.generated.js';
224+
import { zhHantObjects } from '../src/apps/translations/zh-Hant.objects.generated.js';
224225
import { enMetadataForms } from '../src/apps/translations/en.metadata-forms.generated.js';
225226
import { zhCNMetadataForms } from '../src/apps/translations/zh-CN.metadata-forms.generated.js';
226227
import { jaJPMetadataForms } from '../src/apps/translations/ja-JP.metadata-forms.generated.js';
227228
import { esESMetadataForms } from '../src/apps/translations/es-ES.metadata-forms.generated.js';
229+
import { zhHantMetadataForms } from '../src/apps/translations/zh-Hant.metadata-forms.generated.js';
228230

229231
// ── Existing hand-authored app/dashboard/page translations ────────────────
230232
// The per-locale assemblers, NOT the `SetupAppTranslations` wrapper. Each one
@@ -247,6 +249,7 @@ import { en } from '../src/apps/translations/en.js';
247249
import { zhCN } from '../src/apps/translations/zh-CN.js';
248250
import { jaJP } from '../src/apps/translations/ja-JP.js';
249251
import { esES } from '../src/apps/translations/es-ES.js';
252+
import { zhHant } from '../src/apps/translations/zh-Hant.js';
250253

251254
/**
252255
* The annotation is load-bearing, not decoration (#10868). `defineStack`
@@ -347,6 +350,7 @@ const config: ObjectStackDefinition = defineStack({
347350
{ 'zh-CN': { ...zhCN, objects: zhCNObjects, metadataForms: zhCNMetadataForms } },
348351
{ 'ja-JP': { ...jaJP, objects: jaJPObjects, metadataForms: jaJPMetadataForms } },
349352
{ 'es-ES': { ...esES, objects: esESObjects, metadataForms: esESMetadataForms } },
353+
{ 'zh-Hant': { ...zhHant, objects: zhHantObjects, metadataForms: zhHantMetadataForms } },
350354
],
351355
});
352356

‎packages/platform-objects/src/apps/dashboards/system-overview-null-label.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('System Overview: the null-user bucket reads "System"', () => {
3030
'zh-CN': '系统',
3131
'ja-JP': 'システム',
3232
'es-ES': 'Sistema',
33+
'zh-Hant': '系統',
3334
});
3435
});
3536

‎packages/platform-objects/src/apps/dashboards/system.datasets.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const SysAuditLogDataset = defineDataset({
5757
label: 'User',
5858
field: 'user_id',
5959
type: 'lookup',
60-
nullLabel: { en: 'System', 'zh-CN': '系统', 'ja-JP': 'システム', 'es-ES': 'Sistema' },
60+
nullLabel: { en: 'System', 'zh-CN': '系统', 'ja-JP': 'システム', 'es-ES': 'Sistema', 'zh-Hant': '系統' },
6161
},
6262
],
6363
measures: [{ name: 'event_count', label: 'Events', aggregate: 'count' }],

‎packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,17 @@ import { en } from './translations/en.js';
4545
import { zhCN } from './translations/zh-CN.js';
4646
import { jaJP } from './translations/ja-JP.js';
4747
import { esES } from './translations/es-ES.js';
48+
import { zhHant } from './translations/zh-Hant.js';
4849
import { enObjects } from './translations/en.objects.generated.js';
4950
import { zhCNObjects } from './translations/zh-CN.objects.generated.js';
5051
import { jaJPObjects } from './translations/ja-JP.objects.generated.js';
5152
import { esESObjects } from './translations/es-ES.objects.generated.js';
53+
import { zhHantObjects } from './translations/zh-Hant.objects.generated.js';
5254
import { enMetadataForms } from './translations/en.metadata-forms.generated.js';
5355
import { zhCNMetadataForms } from './translations/zh-CN.metadata-forms.generated.js';
5456
import { jaJPMetadataForms } from './translations/ja-JP.metadata-forms.generated.js';
5557
import { esESMetadataForms } from './translations/es-ES.metadata-forms.generated.js';
58+
import { zhHantMetadataForms } from './translations/zh-Hant.metadata-forms.generated.js';
5659

5760
/** The design-record vocabulary an admin-facing string must not carry. */
5861
const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
@@ -127,10 +130,13 @@ function objectSchemas(...modules: Array<Record<string, unknown>>): Map<string,
127130
return byName;
128131
}
129132

130-
const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES } as const;
131-
const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const;
133+
const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant } as const;
134+
const OBJECT_BUNDLES = {
135+
en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects, 'zh-Hant': zhHantObjects,
136+
} as const;
132137
const FORM_BUNDLES = {
133138
en: enMetadataForms, 'zh-CN': zhCNMetadataForms, 'ja-JP': jaJPMetadataForms, 'es-ES': esESMetadataForms,
139+
'zh-Hant': zhHantMetadataForms,
134140
} as const;
135141

136142
describe('Setup copy carries no design-record vocabulary', () => {

‎packages/platform-objects/src/apps/setup-nav-distinct-labels.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import { en } from './translations/en.js';
1515
import { zhCN } from './translations/zh-CN.js';
1616
import { jaJP } from './translations/ja-JP.js';
1717
import { esES } from './translations/es-ES.js';
18+
import { zhHant } from './translations/zh-Hant.js';
1819

19-
const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES } as const;
20+
const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant } as const;
2021

2122
type NavItem = { id?: string; label?: string };
2223

‎packages/platform-objects/src/apps/setup-nav-per-user-stores.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,10 @@ describe('(2) the User page lists each per-user store, keyed to its record (#238
110110
'zh-CN': '偏好设置',
111111
'ja-JP': '設定',
112112
'es-ES': 'Preferencias',
113+
'zh-Hant': '偏好設定',
113114
});
114115
expect((tabOf('sys_account')?.label as Rec)?.en).toBe('Linked Accounts');
115-
expect(Object.keys((tabOf('sys_account')?.label as Rec) ?? {}).sort()).toEqual(['en', 'es-ES', 'ja-JP', 'zh-CN']);
116+
expect(Object.keys((tabOf('sys_account')?.label as Rec) ?? {}).sort()).toEqual(['en', 'es-ES', 'ja-JP', 'zh-CN', 'zh-Hant']);
116117
});
117118
});
118119

‎packages/platform-objects/src/apps/translations/app-nav-translation-parity.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ import { en } from './en.js';
5858
import { zhCN } from './zh-CN.js';
5959
import { jaJP } from './ja-JP.js';
6060
import { esES } from './es-ES.js';
61+
import { zhHant } from './zh-Hant.js';
6162

62-
const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES } as const;
63+
const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant } as const;
6364

6465
/** Every statically declared nav item of an app, depth-first, with its label. */
6566
function navItems(app: { navigation?: unknown[] }): Array<{ id: string; label?: string }> {

‎packages/platform-objects/src/apps/translations/bu-rename-consistency.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ import { enObjects } from './en.objects.generated.js';
1414
import { zhCNObjects } from './zh-CN.objects.generated.js';
1515
import { jaJPObjects } from './ja-JP.objects.generated.js';
1616
import { esESObjects } from './es-ES.objects.generated.js';
17+
import { zhHantObjects } from './zh-Hant.objects.generated.js';
1718

1819
const LOCALES = [
1920
{ name: 'en', objs: enObjects as Record<string, any>, old: /\bDepartment\b/ },
2021
{ name: 'zh-CN', objs: zhCNObjects as Record<string, any>, old: /部门/ },
2122
{ name: 'ja-JP', objs: jaJPObjects as Record<string, any>, old: /部門/ },
2223
{ name: 'es-ES', objs: esESObjects as Record<string, any>, old: /Departamento/ },
24+
{ name: 'zh-Hant', objs: zhHantObjects as Record<string, any>, old: /部門/ },
2325
];
2426
const OBJECTS = ['sys_business_unit', 'sys_business_unit_member'];
2527

0 commit comments

Comments
 (0)