Summary
For an app whose metadata labels are authored in the workspace default locale (zh-CN, inline label: '填报单') and which ships only an en translation bundle as a courtesy for English users, the metadata API serves the English bundle labels to a zh-CN request. The authored Chinese label never wins because the label lookup's fallback chain is hard-wired to ['en'], independent of the stack's i18n.fallbackLocale.
The effect on screen: object title Entry Sheet, columns Sheet / Plan / Subject / Status / Final Score, app name KPI Assessment — sitting next to fields that have no en entry and therefore correctly show 主体类型 / 填报说明, and next to platform chrome that is fully Chinese. A zh-CN deployment cannot ship an en bundle without breaking its own default language.
Minimal reproduction
Platform 17.2.0, objectstack dev, SQLite, single tenancy, fresh DB.
objectstack.config.ts:
i18n: { defaultLocale: 'zh-CN', supportedLocales: ['zh-CN', 'en'], fallbackLocale: 'zh-CN' },
translations: [defineTranslationBundle({
en: { objects: { kpi_entry_sheet: { label: 'Entry Sheet', pluralLabel: 'Entry Sheets',
fields: { name: { label: 'Sheet' }, status: { label: 'Status' }, total_score: { label: 'Final Score' } } } } },
})],
kpi_entry_sheet object: label: '填报单', fields name: Field.text({ label: '填报单名称' }), status: Field.select({ label: '状态' }), total_score: Field.number({ label: '最终得分' }), subject_type: Field.select({ label: '主体类型' }) (no en entry for subject_type).
GET /api/v1/i18n/locales
→ {"locales":[{"code":"zh-CN","isDefault":true},{"code":"en","isDefault":false}]}
GET /api/v1/meta/object/kpi_entry_sheet (Accept-Language: zh-CN, signed in as admin)
→ item.label = "Entry Sheet" ← expected 填报单
item.pluralLabel = "Entry Sheets"
fields.name = "Sheet" ← expected 填报单名称
fields.status = "Status" ← expected 状态
fields.total_score= "Final Score" ← expected 最终得分
fields.subject_type = "主体类型" (no en entry → authored label survives)
fields.created_at = "创建时间" (platform zh-CN pack → correct)
GET /api/v1/meta/object/kpi_entry_sheet (Accept-Language: en)
→ identical for the app fields; created_at = "Created At"
GET /api/v1/meta/app/kpi_app (Accept-Language: zh-CN)
→ label = "KPI Assessment" ← authored label is 'KPI 考核管理'
Same request with no Accept-Language header: same English result (the workspace default is zh-CN).
Control: remove the en bundle → every label is Chinese. So the presence of a bundle for a non-requested locale changes what the requested locale sees.
Where
@objectstack/spec system — the label resolvers (lookupObjectField, lookupObjectFieldAttr, lookupAppAttr, resolveViewLabel, lookupTabLabel, …) all iterate localeChain(opts):
function localeChain(opts) {
const locale = opts?.locale ?? "en";
const fallbacks = opts?.fallbackChain ?? ["en"]; // ← hard-wired
...
for (const code of [locale, ...fallbacks]) ...
}
and return the first bundle hit before the caller falls back to the authored label. The server-side caller does not pass a fallbackChain, so i18n.fallbackLocale: 'zh-CN' has no effect on the chain: zh-CN → en → authored. For any object the zh-CN bundle does not mention (i.e. every object whose labels are authored in Chinese), the en bundle is consulted before the authored label.
Expected
One of (in order of preference):
- The chain honours the stack's
i18n.fallbackLocale / defaultLocale instead of a literal ['en'], so a zh-CN workspace resolves zh-CN → (fallbackLocale) → authored label, and en is only consulted when en is requested.
- When the requested locale is the workspace default locale, the authored label wins over any other locale's bundle (the authored label is the default-locale text — that is what
defaultLocale declares).
- Failing both, the metadata API should at least not report
locale: zh-CN while serving en strings; the i18n coverage tooling should flag "label served from a locale other than the requested/default one".
Side effects seen in the app (for triage, same root cause)
- Related lists show what looks like a duplicate column pair
指标 / Indicator: indicator (lookup, no en entry → authored 指标) next to indicator_name (has en entry → Indicator). With the fix both read Chinese (指标 / 指标名称).
- Record page title suffix (
客户满意度 Indicator), breadcrumb (Indicator, Assessment Plan), related-list section headings (Entry Line 0, Indicator 0) all flip to English on a zh-CN page.
Environment
@objectstack/* 17.2.0 (runtime, spec, cli, console 17.2.0) · Node 22 · better-sqlite3 · single tenancy · objectstack dev. App: objectstack-ai/kpi (private) — the config above is a faithful excerpt.
Summary
For an app whose metadata labels are authored in the workspace default locale (
zh-CN, inlinelabel: '填报单') and which ships only anentranslation bundle as a courtesy for English users, the metadata API serves the English bundle labels to azh-CNrequest. The authored Chinese label never wins because the label lookup's fallback chain is hard-wired to['en'], independent of the stack'si18n.fallbackLocale.The effect on screen: object title
Entry Sheet, columnsSheet / Plan / Subject / Status / Final Score, app nameKPI Assessment— sitting next to fields that have noenentry and therefore correctly show主体类型 / 填报说明, and next to platform chrome that is fully Chinese. A zh-CN deployment cannot ship anenbundle without breaking its own default language.Minimal reproduction
Platform 17.2.0,
objectstack dev, SQLite, single tenancy, fresh DB.objectstack.config.ts:kpi_entry_sheetobject:label: '填报单', fieldsname: Field.text({ label: '填报单名称' }),status: Field.select({ label: '状态' }),total_score: Field.number({ label: '最终得分' }),subject_type: Field.select({ label: '主体类型' })(noenentry forsubject_type).Same request with no
Accept-Languageheader: same English result (the workspace default is zh-CN).Control: remove the
enbundle → every label is Chinese. So the presence of a bundle for a non-requested locale changes what the requested locale sees.Where
@objectstack/specsystem— the label resolvers (lookupObjectField,lookupObjectFieldAttr,lookupAppAttr,resolveViewLabel,lookupTabLabel, …) all iteratelocaleChain(opts):and return the first bundle hit before the caller falls back to the authored label. The server-side caller does not pass a
fallbackChain, soi18n.fallbackLocale: 'zh-CN'has no effect on the chain:zh-CN → en → authored. For any object the zh-CN bundle does not mention (i.e. every object whose labels are authored in Chinese), theenbundle is consulted before the authored label.Expected
One of (in order of preference):
i18n.fallbackLocale/defaultLocaleinstead of a literal['en'], so a zh-CN workspace resolveszh-CN → (fallbackLocale) → authored label, andenis only consulted whenenis requested.defaultLocaledeclares).locale: zh-CNwhile servingenstrings; the i18n coverage tooling should flag "label served from a locale other than the requested/default one".Side effects seen in the app (for triage, same root cause)
指标/Indicator:indicator(lookup, noenentry → authored指标) next toindicator_name(hasenentry →Indicator). With the fix both read Chinese (指标/指标名称).客户满意度 Indicator), breadcrumb (Indicator,Assessment Plan), related-list section headings (Entry Line 0,Indicator 0) all flip to English on a zh-CN page.Environment
@objectstack/*17.2.0 (runtime, spec, cli, console 17.2.0) · Node 22 · better-sqlite3 · single tenancy ·objectstack dev. App: objectstack-ai/kpi (private) — the config above is a faithful excerpt.