You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
i18n phase 4 (locale menu): the Console language menu offers the PLATFORM_LOCALES languages, intersected with an app’s supportedLocales #30514
Filing gate: ③ a split of tracking card #29428 (ruling F of #29349), which domain:spec#2 asked for (6659592659). Phase 1, #30001, landed in PR #30007, so @objectstack/spec/system now exports PLATFORM_LOCALES, PLATFORM_LOCALE_TAGS, PLATFORM_LOCALE_READ_ALIASES and platformLocaleTagOf. Nothing reads them yet. ⛔ Not a claim.
The Console language menu offers the table's languages, intersected with an app's declared supportedLocales as today. Each language it offers is then complete on every surface (ruling F).
Reader: domain:ui. It starts after phases 2 and 3 land, because a menu entry for a language the packs and bundles do not yet carry would offer an incomplete language.
Claim: PM loop round 1 · 2026-11-09T03:14Z
Session: session_01QsR2jRuaxwwjABz1AXpCeM (card thread, dev) · seat session_01QsR2jVEjiYrK7onirxUGyo
Account: zhuangjianguo
Branch: claude/project-thread-wqp71j
Worktree: objectstack-issue-30514
Domain: domain:ui
Seat: domain:ui#3
File surface: ui/packages/i18n/src/provider.tsx (and its locale helpers), ui/packages/app-shell/src/layout/LocaleSwitcher.tsx, their tests, root .changeset/; ⛔ no server, packages/spec/** or cloud change
Container & model: S–M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
Clause-②: no expected (step 1 confirms)
Metadata-protocol: none
Narrowing: to be measured in step 1. If an app locale outside PLATFORM_LOCALES stops being offered, that is a narrowing, and the seat owes a contract-review-tier review before the queue.
Responsibility: the Console language menu in objectstack offers the app's loadLocales() answer filtered by canResolveLanguage | the platform path is ruling F (#29428): offer PLATFORM_LOCALES ∩ the app's supportedLocales, each complete on every surface | every Console user choosing a language, who must never be offered an incomplete one
Thread-read: 6730644685
Premise: partly met, to be measured. On origin/main9e1f1f90d, LocaleSwitcher.tsx already names entries from PLATFORM_LOCALES (#31053). offerableLanguages (provider.tsx ~L793) filters the app's list by canResolveLanguage, so a language outside the table may still be offered on the loader's credit.
Serial constraints cleared: the seat's other cards in build are #30239 (delete action, Setup page, locale packs) and #31759 (plugin-form). Neither touches provider.tsx or LocaleSwitcher.tsx.
Scope: the card's "Wanted".
Step 1: today's menu measured in four cases, the gap against ruling F, the change and its narrowing count, and the declarations. If the gap is nothing, closing on the census is a legal outcome.
Then build per the seat's amendment. The PR says Fixes #30514. Brief: /mnt/project-files/issue-30514/brief.md.
Status reading: no commit, no PR. premise_still_valid: true. The gap is real, and the change is a narrowing with 0 first-party apps affected.
1. What the menu offers today, measured
Probe: a throwaway vitest file mounting the real I18nProvider with a mocked loadLocales, run under the verify lock from ui/ (exit 0, 1/1), then deleted. Labels are localeLabel's rule (table row label, else Intl.DisplayNames). "pack" is resolveBuiltInLanguage, "studio" is resolveStudioLanguage.
Case
loadLocales() answer
Menu offers (label)
1 · no loader
none
the 11 table tags, table labels
2a · subset, table tags
en, zh-Hans
English, 简体中文
2b · app-crm, app-showcase
en, zh-CN
English, 中文(中国)
2c · app-todo
en, zh-CN, ja-JP
English, 中文(中国), 日本語 (日本)
2d · HotCRM sales
en, zh-CN, ja-JP, es-ES
…, Español de España
3 · out-of-table locale
en, zh-CN, th
…, ไทย (no Console pack: resolveBuiltInLanguage('th') = null)
4a · read alias beside its tag
en, zh-CN, zh-Hans
two entries for one pack: 中文(中国) and 简体中文
4b · region variants
de-AT, pt-PT, zh-TW, en-GB
Österreichisches Deutsch, Português europeu (served by the pt-BR pack), 中文(台灣), British English
5 · app declares nothing
the loaded bundle set
11 entries in mixed spelling: zh-CN, ja-JP, es-ES beside table tags, with CLDR labels
Where loadLocales gets its list.ui/apps/console/src/loadLocales.ts reads GET /api/v1/i18n/locales and keeps each descriptor's code. The server answers II18nService.getLocales(): when the app declared i18n.supportedLocales, that declaration verbatim (trimmed, de-duplicated, declared order, declared-but-unserved kept: normalizeSupportedLocales in packages/spec/src/system/i18n-resolver.ts, set by AppPlugin in packages/runtime/src/app-plugin.ts). When the app declared nothing, it is every loaded bundle key. So yes, in the declared case the list is the app's supportedLocales, as authored. The provider then keeps every well-formed tag on the loader's credit (canResolveLanguage(code, config, hasLoader)), so nothing the app lists is ever dropped and nothing is canonicalized.
First-party census.supportedLocales declarations: examples/app-crm ['en', 'zh-CN'], examples/app-showcase ['en', 'zh-CN'], examples/app-todo ['en', 'zh-CN', 'ja-JP'], HotCRM src/sales/index.ts['en', 'zh-CN', 'ja-JP', 'es-ES']; gallery declares none. Every first-party declaration uses read-alias spellings, not table tags, and none names a locale outside the table. No example or HotCRM translations file is keyed outside the table.
2. The gap against ruling F
Per surface, for the 11 tags (all counts on origin/mainc2bb879):
Console packs (ui/packages/i18n/src/locales): 11 of 11, held by all-locales-key-parity.test.ts. Complete.
Marketplace namespace: 11 of 11 files. Complete.
Studio namespace (namespaces/studio): 2 catalogues, en and zh. Measured: es, ar, fr, pt-BR, ru, de, ja, ko resolve to en, and zh-Hant resolves to zh (Simplified), because resolveStudioLanguage lower-cases and drops to the base subtag. Incomplete for 9 of 11.
Server-translated labels (translations/*.objects.generated.ts): all 10 bundle packages carry all 11 locales (en, zh-CN, ja-JP, es-ES + the seven table tags), and service-settings/src/translations carries the same 11. Complete. A table tag finds its alias-keyed bundle: resolveLocaleAmong('zh-Hans', ['en','zh-CN']) = zh-CN, ja → ja-JP, es → es-ES (measured).
Built-in emails (plugin-email templates): 4 locales, en-US (14 rows), zh-CN, ja-JP, es-ES (8 each). zh-Hant, ar, fr, pt-BR, ru, de, ko fall to the en-US floor. The phase-3 card's last two boxes (templates, EMAIL_TEMPLATE_FLOOR_LOCALE) are unticked; no open card carries them (searched EMAIL_TEMPLATE_FLOOR_LOCALE, built-in email templates PLATFORM_LOCALES).
Per case:
Cases 1, 2a, 5: every entry is a table language, so each one's gap is exactly the Studio and email rows above (9 and 7 languages). Phase 4 cannot close those: they are outside its surface.
Cases 2b–2d, 4: the languages are table languages, but the entries are not the table's. They carry CLDR names instead of the table's, a stored-alias pick writes zh-CN back to sys_user.locale where new writes should use table tags, 4a lists one pack twice, and 4b names pt-PT while serving pt-BR.
Case 3: th is offered with no Console pack, no Studio catalogue, no platform bundle and no email row. That is the direct breach: ruling F has one list and no tiers.
3. The change (recommended, option A)
offerableLanguages = the app's list read through resolveBuiltInLanguage (the table's read aliases, then RFC 4647 prefixes), kept only where it lands on a table tag, de-duplicated, in the app's declared order, as table tags. So every entry gets its table label, a pick writes the table tag (the server reads it back through resolveLocaleAmong, measured above), and zh-CN + zh-Hans is one entry.
Dropped: a code with no table tag (th). This is the narrowing. First-party apps affected: 0 (census above). The same rule applies to config.resources keys outside the table in the offline fallback: 0 in-repo producers pass config.resources to I18nProvider (git grep over ui/apps, ui/packages, ui/examples).
The provisional self-heal adjudicates against the offered set, not the raw app list, so a stored th is purged once the list lands.
The loader's credit in canResolveLanguage exists only for out-of-table app locales. After A it can admit nothing the menu offers, so A retires it in the same PR instead of leaving it to boot into a locale and retract it. The th restore cases in provider-app-locale-restore.test.tsx are rewritten, not deleted.
The checkmark compares through resolveBuiltInLanguage, so a session in zh-CN ticks 简体中文.
An empty intersection keeps today's rule: the offline fallback wins.
Surface amendment needed (ask before touching):ui/apps/console/src/pages/system/ProfilePage.tsx L639–646 adds the stored value when !offered.includes(stored). Exact match, so after A every first-party account with zh-CN stored would see a stray 中文(中国) option beside 简体中文. The fix is to match it through the same resolution. useUserLocale.tspickRenderableLocale already uses resolveLocaleAmong and needs no change.
Options weighed on the four axes:
A (recommended). Business need: measured, every first-party app hits the alias labels today, and th is the case the ruling forbids. Long-term: one list read one way, no tier of app-only languages. AI-error: the menu can no longer advertise a language the platform does not complete. Startup scope: retires the loader credit at once and adds no gate.
B, canonicalize only and keep out-of-table locales. No narrowing, but it keeps a second tier ruling F rules out. Not recommended.
C, close on the census. Not legal: cases 2–4 measure a real gap.
4. Declarations
Clause-②: no (narrowing). No published type or export changes (offerableLanguages stays readonly string[] | null). The arm is (narrowing) because an app locale outside the table, offered today, stops being offered, so the seat owes the contract-review-tier review. This differs from the claim's no expected; the claim decides.
Metadata-protocol: none. What an author can write is unchanged: i18n.supportedLocales accepts the same values and GET /i18n/locales reports the same list. Only the Console's reading narrows.
Narrowing: yes, 0 first-party apps, as counted in section 1.
5. Out of scope (for the seat to file or route)
class: a · reach: Studio opened in a 繁體中文 session renders Simplified strings, resolveStudioLanguage('zh-Hant') = zh (measured) · Studio namespace ships 2 of 11 languages against ruling F · dedupe words: studio namespace, zh-Hant, resolveStudioLanguage, STUDIO_LANGUAGE_CODES
class: b · reach: a zh-Hant/ar/fr/pt-BR/ru/de/ko recipient gets the en-US built-in email · Seam: spec:PLATFORM_LOCALES → runtime:plugin-email template resolver · the phase-3 card's two unticked boxes · dedupe words: built-in email templates, EMAIL_TEMPLATE_FLOOR_LOCALE, seven locales
class: c · reach: TranslationConfigSchema.safeParse({ defaultLocale: 'en', supportedLocales: ['en', 'th'] }) succeeds (measured on the built spec dist), and after A the Console will not offer th · Seam: spec:TranslationConfigSchema.supportedLocales → renderer:LocaleSwitcher · a loud refusal at authoring is a spec card, outside this one · dedupe words: supportedLocales, PLATFORM_LOCALES, out-of-table locale
{
"issue": 30514,
"status": "reading",
"branch": "claude/project-thread-wqp71j",
"pr": null,
"session": "session_01QsR2jRuaxwwjABz1AXpCeM",
"premise_still_valid": true,
"tree": "objectstack-ai/objectstack@c2bb879f",
"summary": "Measured the menu in 9 cases: aliases show CLDR labels and can duplicate a pack, th is offered with nothing behind it. Recommended change A canonicalizes to table tags and drops out-of-table codes: a narrowing, 0 first-party apps affected. Studio namespace (2 of 11) and built-in emails (4 of 11) stay incomplete and are outside this card.",
"tests": "throwaway probe ui/packages/i18n/src/__tests__ (deleted after): os-verify-lock VERDICT command-exit 0, Test Files 1/1, Tests 1/1; spec build: VERDICT command-exit 0",
"gates": "none run: no diff",
"line_budget": "n/a, no diff",
"deviations": ["Clause-② measured as no (narrowing) against the claim's no expected", "surface amendment asked: ui/apps/console/src/pages/system/ProfilePage.tsx"],
"files_changed": [],
"dogfood_battery": "n/a, no diff",
"mcp_calls": "7, all reads: issue_read x7",
"api_writes": "1, this comment via post-stamped",
"open_questions": [
{ "question": "Land the menu change now, or hold it until the Studio namespace and the built-in emails carry all 11?", "options": ["A land now and file the two backfills", "B add Blocked-by on both backfills"], "recommendation": "A, because the menu already offers all 11 today when the app declares nothing; this change adds no language anywhere and only removes entries" },
{ "question": "Amend the surface with ProfilePage.tsx?", "options": ["A yes", "B no, leave a stray alias option on the profile page"], "recommendation": "A, because every first-party account storing zh-CN would otherwise see two Simplified options" }
],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: Studio in a zh-Hant session renders Simplified strings · resolveStudioLanguage('zh-Hant') = zh, measured · studio namespace, zh-Hant, STUDIO_LANGUAGE_CODES",
"class: b · reach: zh-Hant/ar/fr/pt-BR/ru/de/ko recipients get en-US built-in emails · Seam: spec:PLATFORM_LOCALES → runtime:plugin-email template resolver · EMAIL_TEMPLATE_FLOOR_LOCALE, built-in email templates",
"class: c · reach: TranslationConfigSchema accepts supportedLocales [en, th] (measured safeParse) that the Console will not offer after A · Seam: spec:TranslationConfigSchema.supportedLocales → renderer:LocaleSwitcher · supportedLocales, out-of-table locale"
]
}
This amends the claim on #30514, after the dev's step-1 report 6731426750 (premise_still_valid: true).
Option A, land now. The menu offers the app's list read through resolveBuiltInLanguage:
kept only where it lands on a table tag;
de-duplicated;
in the app's declared order;
as table tags.
The provisional self-heal is judged against the offered set. The loader's credit in canResolveLanguage is retired in the same PR, and the checkmark compares through the same resolution. An empty intersection keeps today's offline fallback.
Why now, not Blocked-by the backfills: the Studio namespace (2 of 11) and built-in emails (4 of 11) are incomplete today, whatever this card does. A reads the same list and only removes entries, so it adds no language anywhere. Ruling F is already met for what A offers on the Console packs, marketplace and server bundles, as measured.
Surface widened by one file:ui/apps/console/src/pages/system/ProfilePage.tsx (~L639–646). The stored-value check matches through the same resolution, so an account storing zh-CN sees one 简体中文 option, not a stray 中文(中国).
Declarations, replacing the claim's:Clause-②: no (narrowing), Metadata-protocol: none.
Narrowing: an app locale with no table tag (for example th), offered today, is no longer offered. First-party apps affected: 0. Every first-party declaration (app-crm, app-showcase, app-todo, HotCRM sales) uses read aliases, and all of them land on table tags.
The ruling F intersection is the maintainer's own rule, so it needs no further decision. A CONTRACT_REVIEW_TIER review is owed before the queue; the seat requests it at PR time. The PR body carries the measured case table and the 0-app census.
Tests at build:
the four alias cases (2b–2d, 4a) read as table entries with table labels, and 4a is one entry;
th is dropped, and a stored th is purged once the list lands;
CONTROL: case 1 (no loader) offers the 11 table tags as today;
the ProfilePage case;
the rewritten provider-app-locale-restoreth cases;
ablation of the canonicalizing step.
Out-of-scope findings: a (the Studio namespace, 2 of 11, with zh-Hant resolving to Simplified), b (built-in emails, 4 of 11) and c (TranslationConfigSchema accepts an out-of-table supportedLocales entry). All three are carried on the ruling F tracking card Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428, which owns "complete on every surface". The seat files no separate cards (one close-out card per family).
Claim amendment 2 · the loader credit · 2026-11-09T03:23Z
This amends the first amendment on #30514, after the dev found that provisional is set only by the loader's credit (resolveBootstrapConfig), and that hasLoader is a member of the published BootstrapLocaleOptions (@object-ui/i18n's . entry).
Ruling: option C.Keep the loader's credit and provisional, and point the self-heal at the offered set: the canonicalized table-tag list, not the raw app list. A stored th still boots on the credit, then reverts once the list lands and does not offer it. That is today's mechanism, judged against the new list.
The first amendment's "retire the credit in the same PR" is withdrawn.
Option A removes BootstrapLocaleOptions.hasLoader from a published type. That narrows a published export beyond what ruling F needs, and pulls ui/apps/console/src/main.tsx, two tests and the README into the surface.
Under C, hasLoader still does something (the provisional boot), so it is not an inert option. Option B, a deprecated option that is ignored, is refused for that reason.
Declarations are unchanged:Clause-②: no (narrowing) (the menu only), Metadata-protocol: none. No published export changes, so the changeset level stays patch. The CONTRACT_REVIEW_TIER review is still owed before the queue.
Pin: a stored th boots, the list lands without it, and the language reverts to the default and is purged. A stored zh-CN (a read alias) stays, and ticks 简体中文.
Build report · PR #31793 · head 96d210d · 2026-11-09T04:44Z
Per the claim amendments: option A, with C on the restore path, landing now and ProfilePage.tsx added to the surface. The menu offers the platform locale table ∩ the app's list, as table tags, de-duplicated and in the app's order; out-of-table codes are dropped. The self-heal is judged against the offered set. The loader credit and BootstrapLocaleOptions.hasLoader are unchanged. The checkmark and ProfilePage compare through resolveBuiltInLanguage. The edited pin userLocale-10059 was approved by the seat. Downstream readers (html lang, Accept-Language, sys_user.locale) accept zh-Hans, as measured in the PR body.
{
"issue": 30514,
"status": "done",
"branch": "claude/project-thread-wqp71j",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31793",
"head": "96d210d4",
"session": "session_01QsR2jRuaxwwjABz1AXpCeM",
"premise_still_valid": true,
"summary": "offerableLanguages now reads the app's locale list through resolveBuiltInLanguage and offers table tags only (zh-CN shows as 简体中文, one entry per language, th dropped). The offline fallback is the 11 table tags. The provisional self-heal judges against the offered set, so a stored th boots, then reverts and is purged. LocaleSwitcher ticks through the same resolution. ProfilePage reads a stored alias as its table tag. Narrowing with 0 first-party apps affected.",
"tests": "from ui/ under os-verify-lock: packages/i18n/src/__tests__ + LocaleSwitcher* + ProfilePage*: Test Files 107/107, Tests 1873 passed 13 skipped; consumer set (userLocale*, AppHeader*, studio-locale.i18n, i18nAfterSignIn-12034, loadLocales*, FirstRunChecklist, HomeLayout-12601, sharedInboxFeed.twoSurfaces, affordanceGrantMap-12082): 46 files, 45 green plus userLocale-10059 red as expected, then 24/24 after the approved pin edit; ablation via scripts/ablation-replace.mjs (offeredTableTags pushes the raw code): anchor x1->x0, blob c7299ca5->2551b42e, Tests 11 failed 22 passed, restore blob == HEAD and git diff HEAD empty; @object-ui/i18n type-check exit 0; eslint on the 7 touched ui files: 0 errors",
"gates": "dispatch-gates --commands derived 36; all 36 run, each exit 0 (exit codes captured before any pipe); dispatch-gates --ran: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN; PR-body gates against the posted body: check-changeset-no-major 0, check-adr-0087-registration 0 (not-required, no-migration-prescription), check-closing-keyword-parity 0, check-partof-closing-keyword 0; CI check-runs on the PR: in_progress at report time",
"line_budget": "8 files, +267 / -84 against origin/main",
"files_changed": [
".changeset/30514-locale-menu-platform-table.md",
"ui/apps/console/src/pages/system/ProfilePage.tsx",
"ui/apps/console/src/pages/system/__tests__/ProfilePage.language.test.tsx",
"ui/packages/app-shell/src/hooks/__tests__/userLocale-10059.test.tsx",
"ui/packages/app-shell/src/layout/LocaleSwitcher.tsx",
"ui/packages/app-shell/src/layout/__tests__/LocaleSwitcher.appLocales.test.tsx",
"ui/packages/i18n/src/__tests__/provider-app-locale-restore.test.tsx",
"ui/packages/i18n/src/provider.tsx"
],
"deviations": [
"config.resources keys outside the table no longer add an entry to the offline fallback (step-1 report section 3, 0 in-repo producers); named in the changeset and the PR body",
"app-shell and console type-checks not run locally (they need the whole ui closure built); left to CI's Type Check UI",
"no assignees and no reviewers on the PR, per seat rule 10; the harness's MCP assign and review-request step was skipped because rule 3 forbids MCP writes"
],
"dogfood_battery": "head 96d210d4, under os-verify-lock: the dogfood dependency build, then pnpm --filter @objectstack/dogfood test; VERDICT command-exit 0 (33m40s); Test Files 440 passed, 3 skipped (443); Tests 3653 passed, 41 skipped (3694); 0 failures",
"mcp_calls": "8, all reads: issue_read x8",
"api_writes": "3: issue comment step-1 6731426750 (post-stamped), pr_create #31793 (fleet-write relay), this comment (post-stamped)",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: #29428 (seat) · the Studio namespace ships 2 of 11 languages, and zh-Hant resolves to Simplified",
"carrier: #29428 (seat) · built-in emails ship 4 of 11 locales",
"carrier: #29428 (seat) · TranslationConfigSchema accepts an out-of-table supportedLocales entry"
]
}
ACCEPT → PR #31793 comment 6732476843 · 2026-11-09T04:48Z · head 96d210d4. The Console language menu now offers the platform locale table ∩ the app's list, as table tags. zh-CN shows as 简体中文, once, and ticked, and the out-of-table th is no longer offered. The profile page reads stored spellings the same way. Clause-②: no (narrowing), with a CONTRACT_REVIEW_TIER PASS (6732179078) and 0 first-party apps affected. Dogfood is green. 351 lines, not governed, Fixes #30514. Landing through the merge queue.
closed (completed) by Fixes #30514; pm:dispatched removed
Delivered: the Console language menu offers the platform locale table ∩ the app's locale list, as table tags. zh-CN shows once as 简体中文, ticked, and an out-of-table th is not offered. The profile page reads stored spellings the same way. The narrowing was reviewed at CONTRACT_REVIEW_TIER (6732179078), with 0 first-party apps affected.
Remaining: ruling F gaps a/b/c are carried on #29428. One doc nit rides a later edit of ProfilePage.tsx: the LanguageCard docblock still says the globe menu does not write sys_user.locale.
Activity
objectstack-fleet commentedon Nov 9, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblocked ·
pm:blocked→pm:queue· 2026-11-09T02:16Z⛔ Not a claim.
Blocked-by:target is done: i18n phase 2 (Console UI): ui/packages/i18n reads PLATFORM_LOCALES, adds zh-Hant, and moves pack keys to table tags #30511 and i18n phase 3 (platform metadata and built-in emails): extraction and bundle checks read PLATFORM_LOCALES; backfill the seven missing locales #30512 (closed; PR i18n(platform-objects): ship the platform objects, Setup, Studio and Account in Korean #31588 merged).Generated by Claude Code
objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Claim: PM loop round 1 · 2026-11-09T03:14Z
Session:
session_01QsR2jRuaxwwjABz1AXpCeM(card thread, dev) · seatsession_01QsR2jVEjiYrK7onirxUGyoAccount:
zhuangjianguoBranch:
claude/project-thread-wqp71jWorktree:
objectstack-issue-30514Domain:
domain:uiSeat:
domain:ui#3File surface:
ui/packages/i18n/src/provider.tsx(and its locale helpers),ui/packages/app-shell/src/layout/LocaleSwitcher.tsx, their tests, root.changeset/; ⛔ no server,packages/spec/**or cloud changeContainer & model: S–M,
mode:thread,model: default tier(dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no expected (step 1 confirms)
Metadata-protocol: none
Narrowing: to be measured in step 1. If an app locale outside
PLATFORM_LOCALESstops being offered, that is a narrowing, and the seat owes a contract-review-tier review before the queue.Responsibility: the Console language menu in objectstack offers the app's
loadLocales()answer filtered bycanResolveLanguage| the platform path is ruling F (#29428): offerPLATFORM_LOCALES∩ the app'ssupportedLocales, each complete on every surface | every Console user choosing a language, who must never be offered an incomplete oneThread-read: 6730644685
Premise: partly met, to be measured. On
origin/main9e1f1f90d,LocaleSwitcher.tsxalready names entries fromPLATFORM_LOCALES(#31053).offerableLanguages(provider.tsx~L793) filters the app's list bycanResolveLanguage, so a language outside the table may still be offered on the loader's credit.Serial constraints cleared: the seat's other cards in build are #30239 (delete action, Setup page, locale packs) and #31759 (plugin-form). Neither touches
provider.tsxorLocaleSwitcher.tsx.Scope: the card's "Wanted".
Fixes #30514. Brief:/mnt/project-files/issue-30514/brief.md.objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jRuaxwwjABz1AXpCeM
os-dev-report
Step 1 reading · objectstack
origin/mainc2bb879 · 2026-11-09T03:19ZStatus
reading: no commit, no PR.premise_still_valid: true. The gap is real, and the change is a narrowing with 0 first-party apps affected.1. What the menu offers today, measured
Probe: a throwaway vitest file mounting the real
I18nProviderwith a mockedloadLocales, run under the verify lock fromui/(exit 0, 1/1), then deleted. Labels arelocaleLabel's rule (table rowlabel, elseIntl.DisplayNames). "pack" isresolveBuiltInLanguage, "studio" isresolveStudioLanguage.loadLocales()answeren, zh-Hansen, zh-CNen, zh-CN, ja-JPen, zh-CN, ja-JP, es-ESen, zh-CN, thresolveBuiltInLanguage('th')= null)en, zh-CN, zh-Hansde-AT, pt-PT, zh-TW, en-GBpt-BRpack), 中文(台灣), British Englishzh-CN,ja-JP,es-ESbeside table tags, with CLDR labelsWhere
loadLocalesgets its list.ui/apps/console/src/loadLocales.tsreadsGET /api/v1/i18n/localesand keeps each descriptor'scode. The server answersII18nService.getLocales(): when the app declaredi18n.supportedLocales, that declaration verbatim (trimmed, de-duplicated, declared order, declared-but-unserved kept:normalizeSupportedLocalesinpackages/spec/src/system/i18n-resolver.ts, set byAppPlugininpackages/runtime/src/app-plugin.ts). When the app declared nothing, it is every loaded bundle key. So yes, in the declared case the list is the app'ssupportedLocales, as authored. The provider then keeps every well-formed tag on the loader's credit (canResolveLanguage(code, config, hasLoader)), so nothing the app lists is ever dropped and nothing is canonicalized.First-party census.
supportedLocalesdeclarations: examples/app-crm['en', 'zh-CN'], examples/app-showcase['en', 'zh-CN'], examples/app-todo['en', 'zh-CN', 'ja-JP'], HotCRMsrc/sales/index.ts['en', 'zh-CN', 'ja-JP', 'es-ES']; gallery declares none. Every first-party declaration uses read-alias spellings, not table tags, and none names a locale outside the table. No example or HotCRM translations file is keyed outside the table.2. The gap against ruling F
Per surface, for the 11 tags (all counts on
origin/mainc2bb879):ui/packages/i18n/src/locales): 11 of 11, held byall-locales-key-parity.test.ts. Complete.namespaces/studio): 2 catalogues,enandzh. Measured:es, ar, fr, pt-BR, ru, de, ja, koresolve toen, andzh-Hantresolves tozh(Simplified), becauseresolveStudioLanguagelower-cases and drops to the base subtag. Incomplete for 9 of 11.translations/*.objects.generated.ts): all 10 bundle packages carry all 11 locales (en,zh-CN,ja-JP,es-ES+ the seven table tags), andservice-settings/src/translationscarries the same 11. Complete. A table tag finds its alias-keyed bundle:resolveLocaleAmong('zh-Hans', ['en','zh-CN'])=zh-CN,ja→ja-JP,es→es-ES(measured).en-US(14 rows),zh-CN,ja-JP,es-ES(8 each).zh-Hant, ar, fr, pt-BR, ru, de, kofall to theen-USfloor. The phase-3 card's last two boxes (templates,EMAIL_TEMPLATE_FLOOR_LOCALE) are unticked; no open card carries them (searchedEMAIL_TEMPLATE_FLOOR_LOCALE,built-in email templates PLATFORM_LOCALES).Per case:
zh-CNback tosys_user.localewhere new writes should use table tags, 4a lists one pack twice, and 4b namespt-PTwhile servingpt-BR.this offered with no Console pack, no Studio catalogue, no platform bundle and no email row. That is the direct breach: ruling F has one list and no tiers.3. The change (recommended, option A)
offerableLanguages= the app's list read throughresolveBuiltInLanguage(the table's read aliases, then RFC 4647 prefixes), kept only where it lands on a table tag, de-duplicated, in the app's declared order, as table tags. So every entry gets its table label, a pick writes the table tag (the server reads it back throughresolveLocaleAmong, measured above), andzh-CN+zh-Hansis one entry.th). This is the narrowing. First-party apps affected: 0 (census above). The same rule applies toconfig.resourceskeys outside the table in the offline fallback: 0 in-repo producers passconfig.resourcestoI18nProvider(git grepoverui/apps,ui/packages,ui/examples).provisionalself-heal adjudicates against the offered set, not the raw app list, so a storedthis purged once the list lands.canResolveLanguageexists only for out-of-table app locales. After A it can admit nothing the menu offers, so A retires it in the same PR instead of leaving it to boot into a locale and retract it. Thethrestore cases inprovider-app-locale-restore.test.tsxare rewritten, not deleted.resolveBuiltInLanguage, so a session inzh-CNticks 简体中文.Surface amendment needed (ask before touching):
ui/apps/console/src/pages/system/ProfilePage.tsxL639–646 adds the stored value when!offered.includes(stored). Exact match, so after A every first-party account withzh-CNstored would see a stray 中文(中国) option beside 简体中文. The fix is to match it through the same resolution.useUserLocale.tspickRenderableLocalealready usesresolveLocaleAmongand needs no change.Options weighed on the four axes:
this the case the ruling forbids. Long-term: one list read one way, no tier of app-only languages. AI-error: the menu can no longer advertise a language the platform does not complete. Startup scope: retires the loader credit at once and adds no gate.4. Declarations
Clause-②: no (narrowing). No published type or export changes (offerableLanguagesstaysreadonly string[] | null). The arm is(narrowing)because an app locale outside the table, offered today, stops being offered, so the seat owes the contract-review-tier review. This differs from the claim'sno expected; the claim decides.Metadata-protocol: none. What an author can write is unchanged:i18n.supportedLocalesaccepts the same values andGET /i18n/localesreports the same list. Only the Console's reading narrows.5. Out of scope (for the seat to file or route)
resolveStudioLanguage('zh-Hant')=zh(measured) · Studio namespace ships 2 of 11 languages against ruling F · dedupe words: studio namespace, zh-Hant, resolveStudioLanguage, STUDIO_LANGUAGE_CODESzh-Hant/ar/fr/pt-BR/ru/de/korecipient gets theen-USbuilt-in email · Seam: spec:PLATFORM_LOCALES → runtime:plugin-email template resolver · the phase-3 card's two unticked boxes · dedupe words: built-in email templates, EMAIL_TEMPLATE_FLOOR_LOCALE, seven localesTranslationConfigSchema.safeParse({ defaultLocale: 'en', supportedLocales: ['en', 'th'] })succeeds (measured on the built spec dist), and after A the Console will not offerth· Seam: spec:TranslationConfigSchema.supportedLocales → renderer:LocaleSwitcher · a loud refusal at authoring is a spec card, outside this one · dedupe words: supportedLocales, PLATFORM_LOCALES, out-of-table locale{ "issue": 30514, "status": "reading", "branch": "claude/project-thread-wqp71j", "pr": null, "session": "session_01QsR2jRuaxwwjABz1AXpCeM", "premise_still_valid": true, "tree": "objectstack-ai/objectstack@c2bb879f", "summary": "Measured the menu in 9 cases: aliases show CLDR labels and can duplicate a pack, th is offered with nothing behind it. Recommended change A canonicalizes to table tags and drops out-of-table codes: a narrowing, 0 first-party apps affected. Studio namespace (2 of 11) and built-in emails (4 of 11) stay incomplete and are outside this card.", "tests": "throwaway probe ui/packages/i18n/src/__tests__ (deleted after): os-verify-lock VERDICT command-exit 0, Test Files 1/1, Tests 1/1; spec build: VERDICT command-exit 0", "gates": "none run: no diff", "line_budget": "n/a, no diff", "deviations": ["Clause-② measured as no (narrowing) against the claim's no expected", "surface amendment asked: ui/apps/console/src/pages/system/ProfilePage.tsx"], "files_changed": [], "dogfood_battery": "n/a, no diff", "mcp_calls": "7, all reads: issue_read x7", "api_writes": "1, this comment via post-stamped", "open_questions": [ { "question": "Land the menu change now, or hold it until the Studio namespace and the built-in emails carry all 11?", "options": ["A land now and file the two backfills", "B add Blocked-by on both backfills"], "recommendation": "A, because the menu already offers all 11 today when the app declares nothing; this change adds no language anywhere and only removes entries" }, { "question": "Amend the surface with ProfilePage.tsx?", "options": ["A yes", "B no, leave a stray alias option on the profile page"], "recommendation": "A, because every first-party account storing zh-CN would otherwise see two Simplified options" } ], "security_findings": "none", "out_of_scope_findings": [ "class: a · reach: Studio in a zh-Hant session renders Simplified strings · resolveStudioLanguage('zh-Hant') = zh, measured · studio namespace, zh-Hant, STUDIO_LANGUAGE_CODES", "class: b · reach: zh-Hant/ar/fr/pt-BR/ru/de/ko recipients get en-US built-in emails · Seam: spec:PLATFORM_LOCALES → runtime:plugin-email template resolver · EMAIL_TEMPLATE_FLOOR_LOCALE, built-in email templates", "class: c · reach: TranslationConfigSchema accepts supportedLocales [en, th] (measured safeParse) that the Console will not offer after A · Seam: spec:TranslationConfigSchema.supportedLocales → renderer:LocaleSwitcher · supportedLocales, out-of-table locale" ] }objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Claim amendment · step 1 accepted · 2026-11-09T03:21Z
This amends the claim on #30514, after the dev's step-1 report 6731426750 (
premise_still_valid: true).resolveBuiltInLanguage:The
provisionalself-heal is judged against the offered set. The loader's credit incanResolveLanguageis retired in the same PR, and the checkmark compares through the same resolution. An empty intersection keeps today's offline fallback.Blocked-bythe backfills: the Studio namespace (2 of 11) and built-in emails (4 of 11) are incomplete today, whatever this card does. A reads the same list and only removes entries, so it adds no language anywhere. Ruling F is already met for what A offers on the Console packs, marketplace and server bundles, as measured.ui/apps/console/src/pages/system/ProfilePage.tsx(~L639–646). The stored-value check matches through the same resolution, so an account storingzh-CNsees one 简体中文 option, not a stray 中文(中国).Clause-②: no (narrowing),Metadata-protocol: none.th), offered today, is no longer offered. First-party apps affected: 0. Every first-party declaration (app-crm, app-showcase, app-todo, HotCRM sales) uses read aliases, and all of them land on table tags.CONTRACT_REVIEW_TIERreview is owed before the queue; the seat requests it at PR time. The PR body carries the measured case table and the 0-app census.this dropped, and a storedthis purged once the list lands;provider-app-locale-restorethcases;TranslationConfigSchemaaccepts an out-of-tablesupportedLocalesentry). All three are carried on the ruling F tracking card Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428, which owns "complete on every surface". The seat files no separate cards (one close-out card per family).objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Claim amendment 2 · the loader credit · 2026-11-09T03:23Z
This amends the first amendment on #30514, after the dev found that
provisionalis set only by the loader's credit (resolveBootstrapConfig), and thathasLoaderis a member of the publishedBootstrapLocaleOptions(@object-ui/i18n's.entry).provisional, and point the self-heal at the offered set: the canonicalized table-tag list, not the raw app list. A storedthstill boots on the credit, then reverts once the list lands and does not offer it. That is today's mechanism, judged against the new list.BootstrapLocaleOptions.hasLoaderfrom a published type. That narrows a published export beyond what ruling F needs, and pullsui/apps/console/src/main.tsx, two tests and the README into the surface.hasLoaderstill does something (the provisional boot), so it is not an inert option. Option B, a deprecated option that is ignored, is refused for that reason.Clause-②: no (narrowing)(the menu only),Metadata-protocol: none. No published export changes, so the changeset level stayspatch. TheCONTRACT_REVIEW_TIERreview is still owed before the queue.thboots, the list lands without it, and the language reverts to the default and is purged. A storedzh-CN(a read alias) stays, and ticks 简体中文.objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jRuaxwwjABz1AXpCeM
os-dev-report
Build report · PR #31793 · head 96d210d · 2026-11-09T04:44Z
Per the claim amendments: option A, with C on the restore path, landing now and ProfilePage.tsx added to the surface. The menu offers the platform locale table ∩ the app's list, as table tags, de-duplicated and in the app's order; out-of-table codes are dropped. The self-heal is judged against the offered set. The loader credit and
BootstrapLocaleOptions.hasLoaderare unchanged. The checkmark and ProfilePage compare throughresolveBuiltInLanguage. The edited pinuserLocale-10059was approved by the seat. Downstream readers (html lang,Accept-Language,sys_user.locale) acceptzh-Hans, as measured in the PR body.{ "issue": 30514, "status": "done", "branch": "claude/project-thread-wqp71j", "pr": "https://github.com/objectstack-ai/objectstack/pull/31793", "head": "96d210d4", "session": "session_01QsR2jRuaxwwjABz1AXpCeM", "premise_still_valid": true, "summary": "offerableLanguages now reads the app's locale list through resolveBuiltInLanguage and offers table tags only (zh-CN shows as 简体中文, one entry per language, th dropped). The offline fallback is the 11 table tags. The provisional self-heal judges against the offered set, so a stored th boots, then reverts and is purged. LocaleSwitcher ticks through the same resolution. ProfilePage reads a stored alias as its table tag. Narrowing with 0 first-party apps affected.", "tests": "from ui/ under os-verify-lock: packages/i18n/src/__tests__ + LocaleSwitcher* + ProfilePage*: Test Files 107/107, Tests 1873 passed 13 skipped; consumer set (userLocale*, AppHeader*, studio-locale.i18n, i18nAfterSignIn-12034, loadLocales*, FirstRunChecklist, HomeLayout-12601, sharedInboxFeed.twoSurfaces, affordanceGrantMap-12082): 46 files, 45 green plus userLocale-10059 red as expected, then 24/24 after the approved pin edit; ablation via scripts/ablation-replace.mjs (offeredTableTags pushes the raw code): anchor x1->x0, blob c7299ca5->2551b42e, Tests 11 failed 22 passed, restore blob == HEAD and git diff HEAD empty; @object-ui/i18n type-check exit 0; eslint on the 7 touched ui files: 0 errors", "gates": "dispatch-gates --commands derived 36; all 36 run, each exit 0 (exit codes captured before any pipe); dispatch-gates --ran: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN; PR-body gates against the posted body: check-changeset-no-major 0, check-adr-0087-registration 0 (not-required, no-migration-prescription), check-closing-keyword-parity 0, check-partof-closing-keyword 0; CI check-runs on the PR: in_progress at report time", "line_budget": "8 files, +267 / -84 against origin/main", "files_changed": [ ".changeset/30514-locale-menu-platform-table.md", "ui/apps/console/src/pages/system/ProfilePage.tsx", "ui/apps/console/src/pages/system/__tests__/ProfilePage.language.test.tsx", "ui/packages/app-shell/src/hooks/__tests__/userLocale-10059.test.tsx", "ui/packages/app-shell/src/layout/LocaleSwitcher.tsx", "ui/packages/app-shell/src/layout/__tests__/LocaleSwitcher.appLocales.test.tsx", "ui/packages/i18n/src/__tests__/provider-app-locale-restore.test.tsx", "ui/packages/i18n/src/provider.tsx" ], "deviations": [ "config.resources keys outside the table no longer add an entry to the offline fallback (step-1 report section 3, 0 in-repo producers); named in the changeset and the PR body", "app-shell and console type-checks not run locally (they need the whole ui closure built); left to CI's Type Check UI", "no assignees and no reviewers on the PR, per seat rule 10; the harness's MCP assign and review-request step was skipped because rule 3 forbids MCP writes" ], "dogfood_battery": "head 96d210d4, under os-verify-lock: the dogfood dependency build, then pnpm --filter @objectstack/dogfood test; VERDICT command-exit 0 (33m40s); Test Files 440 passed, 3 skipped (443); Tests 3653 passed, 41 skipped (3694); 0 failures", "mcp_calls": "8, all reads: issue_read x8", "api_writes": "3: issue comment step-1 6731426750 (post-stamped), pr_create #31793 (fleet-write relay), this comment (post-stamped)", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: #29428 (seat) · the Studio namespace ships 2 of 11 languages, and zh-Hant resolves to Simplified", "carrier: #29428 (seat) · built-in emails ship 4 of 11 locales", "carrier: #29428 (seat) · TranslationConfigSchema accepts an out-of-table supportedLocales entry" ] }objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
ACCEPT → PR #31793 comment 6732476843 · 2026-11-09T04:48Z · head
96d210d4. The Console language menu now offers the platform locale table ∩ the app's list, as table tags.zh-CNshows as 简体中文, once, and ticked, and the out-of-tablethis no longer offered. The profile page reads stored spellings the same way.Clause-②: no (narrowing), with aCONTRACT_REVIEW_TIERPASS (6732179078) and 0 first-party apps affected. Dogfood is green. 351 lines, not governed,Fixes #30514. Landing through the merge queue.objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Landed · 2026-11-09T05:17Z
a05841367ea8467f1f8a6481c7ce4ac0730fa089onmainFixes #30514;pm:dispatchedremovedDelivered: the Console language menu offers the platform locale table ∩ the app's locale list, as table tags.
zh-CNshows once as 简体中文, ticked, and an out-of-tablethis not offered. The profile page reads stored spellings the same way. The narrowing was reviewed atCONTRACT_REVIEW_TIER(6732179078), with 0 first-party apps affected.Remaining: ruling F gaps a/b/c are carried on #29428. One doc nit rides a later edit of
ProfilePage.tsx: theLanguageCarddocblock still says the globe menu does not writesys_user.locale.feat(app-shell): the marketplace catalog filters by the languages an …