fix(i18n): report the app's declared supportedLocales on GET /i18n/locales - #7812
Conversation
…cales (#7679) `GET /api/v1/i18n/locales` answered with four descriptors (`en`, `zh-CN`, `ja-JP`, `es-ES`) on the showcase, whose artifact declares `i18n.supportedLocales: ['en','zh-CN']`. The envelope was right (#3636); the set was a superset. Nothing was wrong with what had been LOADED — every platform plugin ships an `en/zh-CN/ja-JP/es-ES` bundle and pushes it at `kernel:ready`. What was wrong is that the loaded set was reported as the OFFERED set, so any picker built from this route (the platform's own Settings > Localization select included) offered locales in which only `sys_*` objects are translated. `II18nService` gains an optional `setSupportedLocales(locales)`. `AppPlugin.loadTranslations` threads the artifact's `i18n.supportedLocales` into it exactly the way it already threads `defaultLocale`, and both providers of the `i18n` slot — `createMemoryI18n` and `FileI18nAdapter` — narrow what `getLocales()` reports to it. Applied as a read-time filter, never a prune: the platform bundles arrive after the app plugin has run. Two decisions the filing left open: - Unset `supportedLocales` means NO narrowing. Every app predating this change declared nothing and keeps reporting every loaded locale. - A declared locale with no bundle is REPORTED (declared-but-unserved), not intersected away. The declaration is the app's statement of intent, a silently shortened list hides the gap from both ends, and an intersection would depend on how much had loaded when the route was called. Only the reported set narrows — `GET /i18n/translations/ja-JP` still answers on a stack that no longer advertises `ja-JP`. Verified on a live showcase boot (fresh file DB): the route now returns descriptors for `en` and `zh-CN` only, and `ja-JP` translations still serve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 117 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also reference the affected code. These are read-only:
|
Fixes #7679
The defect
GET /api/v1/i18n/localesanswered with four locale descriptors —en,zh-CN,ja-JP,es-ES— on the showcase app, whose artifact declaresi18n.supportedLocales: ['en', 'zh-CN']. The envelope half was correct (#3636); the set was a superset.Nothing was wrong with what had been loaded. Every platform plugin —
platform-objects,service-settings,service-storage,service-messaging,service-realtime,plugin-security,plugin-sharing,plugin-webhooks— ships anen/zh-CN/ja-JP/es-ESbundle and pushes it atkernel:ready, which is what a platform should do. What was wrong is that the loaded set was reported as the offered set. Those are two different facts owned by two different parties: what is loaded is decided by whichever plugins are installed, while what is offered is the app author's declaration. A picker built from this route — the platform's own Settings > Localization select included — therefore offeredja-JPandes-ES, locales in which onlysys_*objects are translated, guaranteeing a mixed-language session for everything the app itself owns.The fix
II18nServicegains an optionalsetSupportedLocales(locales).AppPlugin.loadTranslationsthreads the artifact'si18n.supportedLocalesinto it exactly the way it already threadsdefaultLocale, and both providers of thei18nslot narrow whatgetLocales()reports to that declaration:packages/spec/src/contracts/i18n-service.tspackages/spec/src/system/i18n-resolver.ts(normalizeSupportedLocales)packages/core/src/fallbacks/memory-i18n.tspackages/services/service-i18n/src/file-i18n-adapter.tspackages/runtime/src/app-plugin.tsThe runtime app-plugin layer is the only place this can originate, as the filing said:
getLocales()sees what is loaded, and the app's declaration is not visible below it.Two implementation properties worth naming, both pinned by tests:
getFieldLabelsfallback still scans the retired flato.prefix — always returns{}#3833 one route over). The normalization is shared, next totoLocaleDescriptors, for that reason.Only the reported set narrows
Explicitly out of scope, and pinned so the fix is not "completed" later by unloading bundles: undeclared locales stay loaded and stay servable. On a stack that no longer advertises
ja-JP,GET /i18n/translations/ja-JPstill answers andt()still resolves it. Thosesys_*translations cost nothing sitting in the map.The two open decisions
1. Unset
supportedLocales→ no narrowing. An app that declares nothing keeps reporting every loaded locale — the behaviour it has today. Every app written before this change declared nothing, so narrowing an undeclared app to zero, or to its default alone, would have emptied the picker on every stack whose author never opted in. Ani18nblock carrying only adefaultLocale, and asupportedLocales: []declaring no usable code, are read the same way. At the runtime layer this is a skipped call, not a call withundefined: several AppPlugins share one kernel (the config apps are AppPlugins too), and an app with noi18nblock must not clear a sibling app's declaration.2. A declared locale with no bundle → reported as declared-but-unserved. Chosen over dropping it, on three grounds.
ObjectTranslationDataSchemais built for it), so "a bundle with zero keys" and "a bundle with one key" differ by nothing a user can perceive. Dropping only the zero case is an arbitrary cliff; a declared-but-unserved locale degrades to the default/fallback exactly as a half-translated bundle's missing keys already do.kernel:ready, so an intersection's result depends on when it was computed. Declared-wins is order-independent — which is the same property that makes the read-time filter correct.Reported locales now also follow the declared order rather than the insertion order of whichever plugin loaded first, so a picker renders the ordering the app author wrote.
setSupportedLocalesis optional on the contract, likesetDefaultLocale: a third-partyII18nServicethat does not implement it keeps its current behaviour instead of failing to boot.Verification
Live showcase boot, fresh file DB (
objectstack dev --seed-admin, 47 plugins, showcase seeded):The built artifact was confirmed to carry the declaration the fix reads:
artifact.i18n = {"defaultLocale":"en","supportedLocales":["en","zh-CN"],"fallbackLocale":"en"}.Reverse verification. Reverting
memory-i18n.tstoorigin/mainand rebuilding turns 5 of the 8 cases in the new end-to-end suite red, with the reported symptom verbatim:The 3 that stay green are the DECISION-1 no-narrowing cases and the stays-servable case — correct, since those assert behaviour this PR deliberately does not change.
Suites (all green):
@objectstack/spec9955,@objectstack/runtime2042,@objectstack/core772,@objectstack/service-i18n70. Typecheck green on all four. ESLint clean on the changed files.check-nul-bytesOK,check:export-originsandcheck:generatedOK after regeneratingexport-origins/system.jsonandapi-surface/system.jsonfor the new export.New coverage:
packages/runtime/src/i18n-supported-locales.test.ts(8 cases wiring the realAppPlugin, the real provider and the real dispatcher domain together, in the real lifecycle order, asserting the body againstGetLocalesResponseSchema), plus targeted cases infallbacks.test.ts,file-i18n-adapter.test.ts,app-plugin.test.tsandi18n-resolver.test.ts.Generated by Claude Code