Uh oh!
There was an error while loading. Please reload this page.
feat(console): seed the UI language from the tenant's server-side locale - #4342
Merged
Conversation
…ale (#4035) `/auth/me/localization` was fetched on every boot but its `locale` only fed currency/date formatting, so the UI language was decided entirely client-side and a tenant configured zh-CN handed every new device an English console. Precedence is now: explicit choice > tenant seed > browser language > en. The server value is cached in its own `objectui-locale-seed` slot and never written into the explicit-choice slot, so it cannot masquerade as a preference the user expressed; only a manual switch promotes a language to a choice. Implements the maintainer ruling on objectstack#5419 (2026-08-06). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 11, 2026 18:01
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#4035
GET /auth/me/localization(ADR-0053) has always been fetched on every consoleboot, but its
localeonly ever fed currency/date formatting. The UIlanguage was decided entirely client-side, so a tenant configured
zh-CNstillhanded every new device an English console until each user switched by hand.
This puts the tenant locale into the language precedence chain, implementing
option B of the maintainer's final ruling on objectstack#5419.
The ruling of record
objectstack#5419, final maintainer ruling of 2026-08-06 04:14Z (comment),
which supersedes the two interim comments above it. Quoted verbatim:
How each ruling point is met
1 — Precedence.
resolveBootstrapConfignow consults the tenant tier whenthere is no explicit choice, applying the seed with
detectBrowserLanguage: falseso it outranks the environment. The chain is: explicit choice (
objectui-locale)then tenant seed (
objectui-locale-seed) then browser language thenen. Astored choice that gets purged as unshippable falls to the tenant tier too,
rather than skipping it and landing on the browser.
2 — Seed provenance. The seed lives in its own
LOCALE_SEED_STORAGE_KEYslot and is never written into the explicit-choice slot. Applying a seed at
bootstrap cannot promote it: i18next does not fire
languageChangedfor thebootstrap language, and that event is the single choke point where an explicit
choice is persisted, so only a real manual switch promotes. Stale-while-revalidate
is split across the two halves the ruling names — a cached seed applies
synchronously at boot, and
LocalizationFetchProviderrefreshes the cache fromevery successful answer, so a tenant that changes its locale reaches choice-less
devices on their next boot without being pinned by an old seed. A
nulllocalefrom an authenticated answer clears the cache rather than leaving the stale value.
3 — Bounded race, no first-paint block. On a true first visit only (no
explicit choice and no cached seed),
seedTenantLanguageraces the fetch againsta 500ms timeout and fails open to browser/
en. It joins thePromise.alltheconsole already awaits before
createRoot().render(), so it runs concurrentlywith the runtime-config and auth-preflight round-trips rather than adding a
serial delay, and it short-circuits to a no-op on every later boot. A seed that
arrives after the bound is cached for the next boot rather than re-languaging a
live session. The existing fetch keeps its cosmetic / never-fail-closed /
retried-in-the-background contract untouched: the added line is a cache write.
4 — Seed validation reuses
canResolveLanguage. The predicate from #4039 isthe verdict, deliberately not a second "do we ship this locale" check. Two
things are layered on top, both about which question to ask it rather than
second-guessing the answer: a region-qualified tag falls back to its base
language (the platform answers
zh-CN, the packs are keyedzh), exact tagfirst so a genuine
pt-BRpack still wins overpt; and the seed does not getthe dynamic loader's optimistic credit, because that credit exists for a
menu-picked value backed by the
provisionalself-heal, and a seed has neither.An unshippable or malformed tenant locale falls through to the next tier.
5 — Zero platform additions. objectui-only. No new endpoint, no client
read/write API,
sys_user_preferenceuntouched.Tests
New:
packages/i18n/src/__tests__/provider-tenant-locale-seed.test.tsx(14cases),
apps/console/src/languageSeed.test.ts(8 cases), plus atenant language seed cacheblock inapps/console/src/LocalizationFetchProvider.test.tsx(3 cases).npx vitest run apps/console/src/languageSeed.test.ts apps/console/src/LocalizationFetchProvider.test.tsx packages/i18n/src/__tests__/— 42 files, 710 tests passed.pnpm --filter @object-ui/i18n --filter @object-ui/console type-check— green (both tsc commands per package, after building the dependency closure).pnpm --filter @object-ui/i18n --filter @object-ui/console lint— 0 errors.changeset:check,check:i18n-keys,check:i18n-drift,check:control-bytes,lint:coverage,type-check:coverage— all green.Reverse verification
Five revert experiments, each with its expected direction written down before
running, then reverted:
canResolveLanguagevalidationcacheLanguageSeedat the explicit-choice slotThe second experiment is the load-bearing one: the fall-through cases stay green
when the tier is removed (nothing is produced, so a negative assertion holds
vacuously) and only go red when the validation itself is dropped. Reported in
full rather than collapsed into "before green, after red", because two of the
five do not have that shape.
One honest note on coverage:
keeps the seed in a slot of its ownasserts onlythat the two key constants differ, so no realistic regression in their usage
can turn it red. It is a documentation-grade guard; the provenance contract is
actually pinned by the other cases above.
Provenance of this branch
The first agent on this card was killed by a host restart after committing and
before running any verification. This continuation inherited worktree and commit
fbc46e7abunchanged, audited it against the five ruling points, merged currentmain, and ran the whole verification ladder above for the first time. Theinherited commit was not amended, so the ratchet stays visible in the history.
Generated by Claude Code