Uh oh!
There was an error while loading. Please reload this page.
feat(app-shell): build the language menu from the app's locale list - #4260
Merged
Conversation
…4039) The console's LocaleSwitcher built its items from a module-level LANGUAGES constant — exactly the ten codes @object-ui/i18n ships packs for — and never asked the app which locales it actually ships, even though GET /api/v1/i18n/locales has been serving that list all along. The menu is now the intersection: the app's own locale list against what the renderer can resolve (built-in packs, config.resources, and the locales a wired dynamic loader can fetch). Both failure directions close at once — an app-shipped locale outside the ten becomes offerable, and locales the app does not ship stop being offered. The restore validation from PR #3376 widens in lockstep, because otherwise this change would have minted the next bug: a locale the user can now pick was a locale that bound rejected, so the preference would be purged on the next reload. The bound stays honest — only well-formed BCP-47 tags, only for apps that wired a loader — and the app's own list adjudicates it once it lands. 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 09:38
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#4039
Implements the binding INTERSECTION ruling on the card: the switcher offers (the app's
GET /api/v1/i18n/localesresponse) ∩ (what the renderer can resolve). The hardcoded ten survive only as the offline/no-backend fallback, and PR #3376's restore validation widens in this same PR, as the card's own warning required.Premise check (verified on tip, before implementing)
All three parts of the premise held:
LocaleSwitcher.tsxdid build its menu from a module-levelLANGUAGESconstant — the ten built-in codes, never consulting the app.isKnownLanguage= built-in packs ∪config.resources./api/v1/i18n/locales(only/i18n/translations/:locale, viaapps/console/src/loadLanguage.ts), so adding a minimal one was in scope per the dispatch. It mirrors that sibling exactly.The two sides of the intersection, as implemented
App side — a new
loadLocalesprop onI18nProvider, wired the same way as the existingloadLanguage: the app owns the transport, the provider owns what is done with the answer.apps/console/src/loadLocales.tsreads the real producer contract —GetLocalesResponseSchemadeclares{ data: { locales: [{ code, label, isDefault }] } }, emitted through the sharedtoLocaleDescriptorsby both serving surfaces.Renderer side — built-in packs,
config.resources, and the locales a wired dynamic loader can fetch. That last clause is load-bearing rather than generous: the console wiresloadLanguageand noconfig.resourcesat all, so its app packs live behindGET /api/v1/i18n/translations/:locale. Without it the intersection in the real product would collapse toappLocales ∩ the built-in ten, and the card's first failure direction (an app shippingth) could never close.One predicate serves both sides.
canResolveLanguageis simultaneously the menu's right-hand side and the restore bound, so the two cannot drift apart — the lockstep is structural, not a convention someone has to remember. Reverse verification 2 below demonstrates this by construction.Lockstep: the restore validation
A locale the user can now pick was a locale the old bound rejected, so the preference would have been purged on the next reload — the exact next bug objectstack#5418 predicted this fix would mint. The bound is widened, not removed:
Intl.getCanonicalLocalesrejectsconstructor,__proto__,en_US) — the prototype-pollution guard fix(i18n): 语言选择写入 localStorage 并在启动时恢复,刷新不再退回 en #3376 wrote still holds;enpreference the user never expressed and suppressing browser detection from then on.Labels, loading, fallback
Built-in native names are kept where they exist (
中文,日本語, … unchanged); everything else is named byIntl.DisplayNamesin its own locale. The endpoint's ownlabelis deliberately not used for display —toLocaleDescriptorssetslabel: code, so a server label would putthin the menu whereไทยbelongs. No new i18n keys are added:check:i18n-driftreports 0 en values changed, 0 keys added.While the list is in flight the switcher renders nothing, following the closest existing idiom in the same folder (
AppSwitcher,WorkspaceSwitcherbothreturn nullfor data they do not have yet) — so the ten never flash past on an app that ships two. No backend, a failed endpoint, or an answer with nothing this renderer can produce all fall back to the built-in ten: the menu is never empty.Reverse verification
1 — revert the switcher alone (
git checkout origin/main -- LocaleSwitcher.tsx): predicted RED on the intersection and loading pins. Confirmed, 4 failed / 5 passed:The 5 that stayed green are expected to stay green and were predicted to: the two fallback pins and the built-in-native-name control cannot go red by reverting the switcher, because the offline fallback is the old behaviour. Reported rather than manufactured into a red.
2 — revert the validation widening alone (
canResolveLanguage's loader clause only, switcher intact): predicted RED on the restore pins. Confirmed, 4 failed / 8 passed:I predicted three; the fourth is the informative one.
offerableLanguageswent red too because the same predicate feeds the menu — which is the lockstep proven by construction: you cannot revert the restore half without also shrinking the menu half.One honest non-red:
still drops a stale app locale when the app no longer ships itstays green under the narrow bound, because it reaches the same end state through the bootstrap purge instead of the self-heal.Tests
New:
LocaleSwitcher.appLocales.test.tsx(9),provider-app-locale-restore.test.tsx(12, incl. a StrictMode single-request pin —main.tsxmounts underReact.StrictModeand an inline prop identity changes every render),loadLocales.test.ts(7).Changeset:
.changeset/locale-menu-from-app-4039.md(@object-ui/i18nminor,@object-ui/app-shell+@object-ui/consolepatch). Noskip-changeset.Generated by Claude Code