Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
34c9ccb
feat(i18n)!: read the platform locale table, add a generated zh-Hant …
claude Nov 5, 2026
c857810
feat(i18n)!: read the platform locale table, add a generated zh-Hant …
claude Nov 5, 2026
bf4fb90
fix(ui): read every pack by its platform locale tag outside the i18n …
claude Nov 5, 2026
13abe0e
Merge origin/main into claude/project-thread-xplvvb
claude Nov 5, 2026
9070a29
test(fields): boot the read-only boolean word cases in zh-Hans
claude Nov 5, 2026
9571f5e
test(ui): the ui script suites read the renamed packs and the convert…
claude Nov 5, 2026
9b15e9a
Merge the branch's earlier push of the first commit
claude Nov 5, 2026
248747d
Merge origin/main: the marketplace namespace's catalogues on table tags
claude Nov 6, 2026
db9b349
test(i18n): read the marketplace namespace's catalogues by table tag
claude Nov 6, 2026
f198d32
fix(app-shell): pick a stored locale on the platform's one locale ladder
claude Nov 6, 2026
65bded6
docs(changeset): the marketplace namespace and the shared locale ladder
claude Nov 6, 2026
d063248
test: read main's new locale cases by table tag
claude Nov 6, 2026
c4a311d
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 6, 2026
7e8ce6e
chore(i18n): regenerate zh-Hant after merging main
claude Nov 6, 2026
e94c13c
fix(i18n): narrow a marketplace catalogue code to a table tag before …
claude Nov 6, 2026
353c492
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 6, 2026
f7e9df2
chore(i18n): regenerate zh-Hant after merging main
claude Nov 6, 2026
143ef31
fix(ui-scripts): the en-drift gate's en-first assertion runs inside a…
claude Nov 6, 2026
c1f166c
chore(regen): record the marketplace namespace's generated zh-Hant ca…
claude Nov 6, 2026
6c8cf9b
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 6, 2026
82771dc
chore(i18n): regenerate zh-Hant after merging main
claude Nov 6, 2026
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
31 changes: 31 additions & 0 deletions .changeset/30511-console-packs-platform-locale-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@object-ui/i18n': patch
'@object-ui/app-shell': patch
'@object-ui/components': patch
'@object-ui/console': patch
---

feat(i18n)!: the Console's language packs are the platform locale table's, keyed by its tags, and Traditional Chinese (`zh-Hant`) ships, generated from Simplified (#30511)

Clause-②: yes (narrowing) — widens: a `zh-Hant` pack (the `zhHant` export of `@object-ui/i18n/locales`, the `@object-ui/i18n/locales/zh-Hant` module), the `resolveBuiltInLanguage` export of `@object-ui/i18n`, and every stored or browser code the platform locale table reads as one of its tags (`zh-TW`, `zh-HK`, `zh-MO` → `zh-Hant`; `zh`, `zh-CN`, `zh-SG` → `zh-Hans`; `pt` → `pt-BR`). Narrows: the Simplified Chinese and Portuguese packs are keyed `zh-Hans` and `pt-BR`, no longer `zh` and `pt`, in `builtInLocales`, `BUILT_IN_LANGUAGE_CODES`, `getLoadedBuiltInLocales()` and `getAvailableLanguages()`, and their modules `@object-ui/i18n/locales/zh` and `@object-ui/i18n/locales/pt` are `@object-ui/i18n/locales/zh-Hans` and `@object-ui/i18n/locales/pt-BR`.
Metadata-protocol: none
Skills-docs: follow-up owed — `skills/objectui/guides/i18n.md` (its "Built-in locales" list names ten packs, with `zh` and `pt`)

<!-- adr-0087: not-required (no-migration-prescription) No metadata changes shape: no spec key, object definition, stored row or authorable spelling is added, removed or renamed, so `objectstack migrate meta` has nothing to rewrite and no ledger entry could project. The FROM/TO lines below are source rewrites for TypeScript and JavaScript consumers of `@object-ui/i18n`'s `./locales` subpath (two members of `builtInLocales`, two modules). That is the class `runtime-export-retired` exists for, but it reads a package's root entry only and refuses a subpath operand, and the root entry retires nothing here; so this category is held, and the gap is named here rather than left to the detector. -->

**BREAKING** for code that reads a built-in pack by its old key or module path.

- **Why.** The Console's packs follow the platform locale table (`PLATFORM_LOCALES` in `@objectstack/spec/system`, ruling F of #29349) instead of a list of their own: one pack per table tag, keyed by the tag, in the table's order. A language the table gains is a pack the parity and drift suites demand on the run that first sees it.
- **Traditional Chinese.** `src/locales/zh-Hant.ts` is generated from `zh-Hans.ts` by `pnpm --filter @object-ui/i18n gen:zh-hant` (OpenCC `s2twp`, pinned, with the phrase rules of the www.objectos.ai converter) and is never edited by hand; a package test fails when it is not what the generator writes. It carries no `console.ai.*` gate-only outbound keys, exactly as the other non-gate packs.
- **The marketplace namespace follows.** The lazy `marketplace` namespace keys its catalogues by the same tags (`@object-ui/i18n/namespaces/marketplace`'s `MARKETPLACE_LANGUAGE_CODES` and `resolveMarketplaceLanguage` answer `zh-Hans`, `zh-Hant`, `pt-BR`), ships a `zh-Hant` catalogue generated by the same command, and resolves a stored code through the table as the packs do, so a `zh-TW` reader's marketplace pages are Traditional too.
- **One locale ladder.** The Console picks which offered language renders a user's stored `sys_user.locale` with `resolveLocaleAmong` from `@objectstack/spec/system`, the order the server's `resolveLocale` uses, instead of a copy of it: a deployment offering both scripts renders a `zh-TW` row in its Traditional spelling, and a variant the table reads as the language itself (`es-ES` for `es`) is taken ahead of the first offered variant, as the server takes it.
- **Stored codes keep working.** A `sys_user.locale` value, a saved device language or an `Accept-Language` tag written before the table is read through the table's aliases: `zh-CN` still renders Simplified Chinese, `pt` Portuguese (Brazil), and `zh-TW` / `zh-HK` / `zh-MO` now render Traditional Chinese instead of Simplified. `loadBuiltInLocale`, `isBuiltInLanguage` and i18next's fallback chain accept every alias.
- **Studio.** The Studio namespace still ships `en` and `zh` only; a `zh-Hant` reader sees its Simplified catalogue there.

The fix, for code that names a pack:

- FROM `builtInLocales.zh` TO `builtInLocales['zh-Hans']`
- FROM `builtInLocales.pt` TO `builtInLocales['pt-BR']`
- FROM `import zh from '@object-ui/i18n/locales/zh'` TO `import zh from '@object-ui/i18n/locales/zh-Hans'`
- FROM `import pt from '@object-ui/i18n/locales/pt'` TO `import pt from '@object-ui/i18n/locales/pt-BR'`
- FROM comparing a code from `BUILT_IN_LANGUAGE_CODES`, `getAvailableLanguages()` or the keys of `getLoadedBuiltInLocales()` with `'zh'` or `'pt'` TO comparing `resolveBuiltInLanguage(code)` with `'zh-Hans'` or `'pt-BR'`
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions scripts/regen-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,33 @@ export const NOT_DRIVER_MANAGED = Object.freeze([
+ 'serves that day and drop every edit the patch list does not carry, and it needs the network, '
+ 'which a merge must not. A conflict in a vendored component stays a human\'s; never routed.',
},
// objectstack#30511 (ruling F of #29349): the Console's Traditional Chinese pack.
{
path: 'ui/packages/i18n/src/locales/zh-Hant.ts',
gen: 'gen:zh-hant',
owner: '@object-ui/i18n',
why:
'generated WHOLE from `zh-Hans.ts` by `gen:zh-hant`, so "discard both sides and re-run" '
+ 'loses no human decision. Still not routed: a `REGEN_ARTIFACTS` row needs a `check:` script '
+ 'as its currency proof, and ruling F of #29349 adds no gate to this phase. The currency proof '
+ 'is the package test `src/__tests__/zh-hant-generated.test.ts`, which reds a pack that is not '
+ 'byte-for-byte the generator\'s output wherever the ui unit tests run, and the driver is local '
+ 'anyway (never the queue\'s merge). The pack is line-aligned with `zh-Hans.ts`, one line per '
+ 'line, so two branches editing different keys text-merge apart; a conflict is resolved by '
+ 'taking either side and running `pnpm --filter @object-ui/i18n gen:zh-hant`.',
},
// objectstack#30511: the marketplace namespace's Traditional Chinese catalogue, same command.
{
path: 'ui/packages/i18n/src/namespaces/marketplace/zh-Hant.ts',
gen: 'gen:zh-hant',
owner: '@object-ui/i18n',
why:
'generated WHOLE from the namespace\'s `zh-Hans.ts` by the same `gen:zh-hant` run that writes '
+ 'the pack above, and not routed for the same reason: no `check:` script proves its currency. '
+ 'The proof is the same package test, which walks every generated target. Line-aligned with '
+ 'its source, so a conflict is resolved by taking either side and running '
+ '`pnpm --filter @object-ui/i18n gen:zh-hant`.',
},
]);

/** Marker file (inside the git dir, never the worktree) listing paths the driver deferred. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ import { App } from '../App';
import { loadLanguage } from '../loadLanguage';
import { loadLocales } from '../loadLocales';

const zh = builtInLocales.zh;
const zh = builtInLocales['zh-Hans'];
const TOKEN = 'tok-12034';
const USER = { id: 'u_12034', email: 'ada@example.com', name: 'Ada' } as unknown as AuthUser;
const SESSION = { token: TOKEN, expiresAt: new Date(Date.now() + 3_600_000) };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ describe('/studio refused: the sentence arrives in the caller language (objectui
});

describe('CONTROL: a caller with Studio access enters Studio as today', () => {
it.each(['en', 'zh'] as const)('%s', async (lang) => {
it.each(['en', 'zh-Hans'] as const)('%s', async (lang) => {
inLanguage(lang);
systemPermissions = [STUDIO_ENTRY_CAPABILITY];
renderStudio('/studio/');
Expand Down
2 changes: 1 addition & 1 deletion ui/apps/console/src/pages/SharedRecordPage-26767.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('SharedRecordPage — a shared non-chat record renders as read-only row
});

it('under zh the value words come from the zh pack', async () => {
const { zh, en } = builtInLocales;
const { 'zh-Hans': zh, en } = builtInLocales;
// Guard: these are real zh rows, not English copies.
expect(zh.common.yes).not.toBe(en.common.yes);
expect(zh.sharedRecord.redactedFields).not.toBe(en.sharedRecord.redactedFields);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,12 @@ describe('objectui#3811 — console routes DefaultAcceptInvitationPage', () => {
renderRoute({ lang: 'zh' });
await screen.findByTestId('accept-invitation-page');

const zh = builtInLocales.zh.organization.accept as Record<string, string>;
const zh = builtInLocales['zh-Hans'].organization.accept as Record<string, string>;
expect(zh.accept).toBe('接受邀请');
expect(screen.getByRole('button', { name: zh.accept })).toBeInTheDocument();
expect(screen.getByText(zh.organization)).toBeInTheDocument();
expect(screen.getByText(zh.expiresAt)).toBeInTheDocument();
// The pack that used to serve this screen's other half is gone entirely.
expect((builtInLocales.zh as Record<string, unknown>).acceptInvitation).toBeUndefined();
expect((builtInLocales['zh-Hans'] as Record<string, unknown>).acceptInvitation).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async function expectSsoCopy(expected: Record<keyof typeof ENGLISH, string>) {

describe('console LoginPage — SSO copy under zh (objectui#12762)', () => {
it('draws the zh bundle value of each SSO label, and no English default', async () => {
const zh = builtInLocales.zh.auth.login;
const zh = builtInLocales['zh-Hans'].auth.login;
for (const key of SSO_KEYS) {
expect(typeof zh[key], `zh auth.login.${key}`).toBe('string');
expect(zh[key], `zh auth.login.${key}`).not.toBe(ENGLISH[key]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function labelMembers(): string[] {
}

const MEMBERS = labelMembers();
const ZH_LOGIN = builtInLocales.zh.auth.login as Record<string, unknown>;
const ZH_LOGIN = builtInLocales['zh-Hans'].auth.login as Record<string, unknown>;

function createMockClient(): AuthClient {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function refusingServer(code: string, message: string): typeof fetch {
}) as typeof fetch;
}

type Lang = 'zh' | 'en';
type Lang = 'zh-Hans' | 'en';

/** Labels as the page renders them in `lang`, read from that pack. */
function registerLabels(lang: Lang) {
Expand Down Expand Up @@ -114,7 +114,7 @@ afterEach(() => {
vi.restoreAllMocks();
});

const zhErrors = builtInLocales.zh.auth.register.errors;
const zhErrors = builtInLocales['zh-Hans'].auth.register.errors;
const enErrors = builtInLocales.en.auth.register.errors;

describe('console RegisterPage — audience refusals under zh-CN (objectui#10998)', () => {
Expand All @@ -125,7 +125,7 @@ describe('console RegisterPage — audience refusals under zh-CN (objectui#10998

it('SELF_REGISTRATION_CLOSED shows the localized text, not the server message', async () => {
const text = await submitRefused(
'zh',
'zh-Hans',
'SELF_REGISTRATION_CLOSED',
SERVER_MESSAGE.SELF_REGISTRATION_CLOSED,
);
Expand All @@ -136,7 +136,7 @@ describe('console RegisterPage — audience refusals under zh-CN (objectui#10998

it('EMAIL_DOMAIN_NOT_ALLOWED shows the localized text, not the server message', async () => {
const text = await submitRefused(
'zh',
'zh-Hans',
'EMAIL_DOMAIN_NOT_ALLOWED',
SERVER_MESSAGE.EMAIL_DOMAIN_NOT_ALLOWED,
);
Expand All @@ -146,7 +146,7 @@ describe('console RegisterPage — audience refusals under zh-CN (objectui#10998

it('a code the page does not map still shows the raw server message', async () => {
const raw = 'The server refused this sign-up for a reason the console has no text for.';
const text = await submitRefused('zh', 'UNMAPPED_REFUSAL_FOR_TEST', raw);
const text = await submitRefused('zh-Hans', 'UNMAPPED_REFUSAL_FOR_TEST', raw);
expect(text).toBe(raw);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ afterEach(() => {
vi.unstubAllGlobals();
});

const { zh, en } = builtInLocales;
const { 'zh-Hans': zh, en } = builtInLocales;
const zhAudit = zh.console.system.auditLog;
const zhFlow = zh.console.developer.flowRuns;
const zhAgent = zh.console.developer.agentConnect;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ afterEach(() => {
vi.unstubAllGlobals();
});

const { zh, en } = builtInLocales;
const { 'zh-Hans': zh, en } = builtInLocales;
const zhDev = zh.console.developer;
const enDev = en.console.developer;
const zhSys = zh.console.system;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { FlowRunsPage } from './FlowRunsPage';

afterEach(() => cleanup());

const { zh } = builtInLocales;
const { 'zh-Hans': zh } = builtInLocales;
const zhFlow = zh.console.developer.flowRuns;

/** A client that serves one flow but has no automation surface at all. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe('objectstack#26690 — the docs sidebar serves the request locale', ()
describe("objectstack#30554 — the docs chrome's own words follow the session language", () => {
const ENTRY = '/apps/com.example.crm/docs/manual/crm_start_login';

for (const language of ['zh', 'ja', 'es'] as const) {
for (const language of ['zh-Hans', 'zh-Hant', 'ja', 'es'] as const) {
it(`a ${language} reader sees the home link and the Uncategorized title in ${language}`, async () => {
const pack = builtInLocales[language].help;
// Guard: the expectations are real translations, not the English words.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function renderRow(language: Lang, spec: Specifier, resolved?: ResolvedSettingVa
);
}

const view = builtInLocales.zh.console.settingsView as unknown as Record<string, string>;
const view = builtInLocales['zh-Hans'].console.settingsView as unknown as Record<string, string>;
const viewEn = builtInLocales.en.console.settingsView as unknown as Record<string, string>;
const LATIN = /\p{Script=Latin}/u;

Expand Down
Loading
Loading