Skip to content

fix(i18n): report the app's declared supportedLocales on GET /i18n/locales - #7812

Merged
os-help merged 2 commits into
mainfrom
claude/issue-7679-i18n-supported-locales
Aug 11, 2026
Merged

fix(i18n): report the app's declared supportedLocales on GET /i18n/locales#7812
os-help merged 2 commits into
mainfrom
claude/issue-7679-i18n-supported-locales

Conversation

@os-help

@os-help os-help commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7679

The defect

GET /api/v1/i18n/locales answered with four locale descriptors — en, zh-CN, ja-JP, es-ES — on the showcase app, whose artifact declares i18n.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 an en/zh-CN/ja-JP/es-ES bundle and pushes it at kernel: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 offered ja-JP and es-ES, locales in which only sys_* objects are translated, guaranteeing a mixed-language session for everything the app itself owns.

The fix

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 narrow what getLocales() reports to that declaration:

layer file
contract packages/spec/src/contracts/i18n-service.ts
shared normalization packages/spec/src/system/i18n-resolver.ts (normalizeSupportedLocales)
in-memory provider packages/core/src/fallbacks/memory-i18n.ts
production provider packages/services/service-i18n/src/file-i18n-adapter.ts
threading packages/runtime/src/app-plugin.ts

The 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:

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-JP still answers and t() still resolves it. Those sys_* 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. An i18n block carrying only a defaultLocale, and a supportedLocales: [] declaring no usable code, are read the same way. At the runtime layer this is a skipped call, not a call with undefined: several AppPlugins share one kernel (the config apps are AppPlugins too), and an app with no i18n block 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.

  • It is the honest answer. The declaration is the app's statement of intent and the client is entitled to see it. A silently shortened list hides the authoring gap from the author and from the client at once — which is precisely the "leave the client no signal" outcome the card ruled out.
  • There is no bright line to intersect on. Partial translation is the normal state (ObjectTranslationDataSchema is 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.
  • An intersection is not stable. Bundles are still arriving at 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.

setSupportedLocales is optional on the contract, like setDefaultLocale: a third-party II18nService that 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):

GET /api/v1/i18n/locales
{"success":true,"data":{"locales":[
  {"code":"en","label":"en","isDefault":true},
  {"code":"zh-CN","label":"zh-CN","isDefault":false}]}}

GET /api/v1/i18n/translations/ja-JP
{"success":true,"data":{"locale":"ja-JP","translations":{"objects":{"sys_position":{...}}}}}

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.ts to origin/main and rebuilding turns 5 of the 8 cases in the new end-to-end suite red, with the reported symptom verbatim:

× an app declaring two locales is not offered four
  AssertionError: expected [ …(4) ] to deeply equal [ …(2) ]
× the platform locales the app never opted into are gone from the body
  AssertionError: expected [ 'en', 'zh-CN', 'ja-JP', 'es-ES' ] to not include 'ja-JP'
× narrowing survives bundles pushed after the app plugin declared
× DECISION 2 — a declared locale with no bundle is reported, not silently dropped
× the declared order is the reported order
Tests  5 failed | 3 passed (8)

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/spec 9955, @objectstack/runtime 2042, @objectstack/core 772, @objectstack/service-i18n 70. Typecheck green on all four. ESLint clean on the changed files. check-nul-bytes OK, check:export-origins and check:generated OK after regenerating export-origins/system.json and api-surface/system.json for the new export.

New coverage: packages/runtime/src/i18n-supported-locales.test.ts (8 cases wiring the real AppPlugin, the real provider and the real dispatcher domain together, in the real lifecycle order, asserting the body against GetLocalesResponseSchema), plus targeted cases in fallbacks.test.ts, file-i18n-adapter.test.ts, app-plugin.test.ts and i18n-resolver.test.ts.


Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 6:06pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/runtime, @objectstack/service-i18n, @objectstack/spec.

117 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/runtime, @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/service-i18n, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/core, packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/runtime, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/runtime, @objectstack/service-i18n, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/service-i18n, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

8 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/runtime, @objectstack/service-i18n, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/core, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-help
os-help added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 518ca7a Aug 11, 2026
27 checks passed
@os-help
os-help deleted the claude/issue-7679-i18n-supported-locales branch August 11, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/i18n/locales advertises locales the app never opted into (returns 4, app declares 2)

2 participants