Skip to content
27 changes: 27 additions & 0 deletions .changeset/i18n-declared-fallback-chain-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@objectstack/rest": patch
---

fix(rest): metadata label lookup honours the stack's declared `i18n.fallbackLocale` / `defaultLocale` instead of falling through to the `en` bundle (#14882)

On a workspace whose labels are authored in `zh-CN` (`defaultLocale: 'zh-CN'`,
`fallbackLocale: 'zh-CN'`) and which ships only a courtesy `en` translation bundle,
`GET /api/v1/meta/object/:name`, the `/meta/:type` list, `GET /api/v1/meta` and the
public-form schema served the ENGLISH bundle labels to a `zh-CN` request (`Entry Sheet`
for an authored `填报单`, `KPI Assessment` for `KPI 考核管理`). The document translators walk
`requested locale → fallback chain → authored label` and default the chain to a literal
`['en']`; every REST seam passed none, so the declared fallback never reached the chain
and `en` was consulted before the authored label.

Every metadata translation seam now passes `fallbackChain: [i18n.getFallbackLocale()]` —
the locale the i18n service's own `t()` falls back to, which `I18nServicePlugin` receives
from the stack config as `fallbackLocale || defaultLocale || 'en'`. For the workspace
above a `zh-CN` request now resolves `zh-CN → zh-CN → authored label` (the authored
Chinese labels), an `en` request still gets the `en` bundle, and a `zh-CN` bundle, when one
is shipped, still wins over the authored label.

Feature-detected: an i18n service that does not declare a fallback (the method is
optional on `II18nService`; the core in-memory fallback has none) gets no chain and the
resolver's own default applies exactly as before. A stack declaring `defaultLocale: 'zh-CN'`
with `fallbackLocale: 'en'` is likewise unchanged — the declared `en` is honoured as it
reads.
11 changes: 11 additions & 0 deletions .changeset/i18n-declared-fallback-chain-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@objectstack/service-i18n": minor
---

feat(service-i18n): `FileI18nAdapter.getFallbackLocale()` reports the `fallbackLocale` the adapter was constructed with (#14882)

Implements the new optional `II18nService.getFallbackLocale()`. `I18nServicePlugin`
already receives `fallbackLocale || defaultLocale || 'en'` from the stack's `i18n`
config on both boot paths (`os serve`, the dev plugin); this makes that declaration
readable, so the REST metadata reads pass the document translators the same fallback
locale `t()` itself consults. Returns `undefined` when no `fallbackLocale` was given.
26 changes: 26 additions & 0 deletions .changeset/i18n-declared-fallback-chain-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@objectstack/spec": minor
---

feat(spec): `II18nService.getFallbackLocale()` — the declared fallback locale is readable, so the metadata-document translators can be handed the chain the deployment declared (#14882)

`ResolveOptions.fallbackChain` on the `@objectstack/spec/system` label
resolvers (`translateMetadataDocument`, `translateObject`, `translateApp`,
`resolveViewLabel`, …) is the ordered list of locales consulted after the
requested one and BEFORE the authored label. Nothing on `II18nService`
exposed the deployment's declared fallback (`i18n.fallbackLocale`, else
`defaultLocale`), so no serving layer could thread it, and every caller fell
to the resolver's literal `['en']` default. A `zh-CN` workspace that shipped a
courtesy `en` bundle therefore served English bundle text to a `zh-CN`
request ahead of its own authored Chinese labels.

- New optional contract member `II18nService.getFallbackLocale?(): string | undefined`
— the locale the service's own `t()` consults second. `undefined` (or the
method absent) means nothing was declared, and a serving layer must then
leave the resolver's default in place rather than invent a chain.
- The `fallbackChain` documentation now states who supplies it (the serving
layer, from `getFallbackLocale()`) and that the `['en']` default applies
only when a caller declares no chain at all. The resolver's behaviour for
a caller that passes nothing is unchanged.

Additive: no existing implementation or caller changes shape.
2 changes: 1 addition & 1 deletion content/docs/permissions/system-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ The largest single consumer — **17 of the 106 sites**.
|:--|:---|:---|:---|:---|
| 48 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `action-execution.ts:138` |
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` |
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:5048`, `:6474`, `:6722`, `:7153`, `:7346` |
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:5084`, `:6510`, `:6758`, `:7189`, `:7382` |
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` |
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:422`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
Expand Down
Loading
Loading