Skip to content

spec: view tab labels (tabs[].label) have no translation key and no resolver — the list-page tab bar is untranslatable in every locale #5377

Description

@yinlianghui

ViewTabSchema.label is declared as an I18nLabelSchema, whose description promises
"i18n keys are auto-generated by the framework". No such key exists. A view tab's label
is structurally unreachable by translation: there is no slot for it in the translation
schema and no resolver that would read one.

The result is a list-page tab bar that stays in the source language no matter how
complete a locale bundle is.

Verified against: 9f601e8 (main)
Downstream: objectstack-ai/hotcrm#661 — 59 tab labels across 15 objects.

Why this is now the whole remaining gap downstream

HotCRM has just finished all four of its locales (en, zh-CN, ja-JP, es-ES):
objectstack lint reports zero i18n warnings for every one of them, across fields,
options, views, sections, actions, pages, navigation, dashboards and empty states.

The tab bar is the only user-visible surface still in English — and it sits at the top
of every object's list page, directly above content that is translated. A reviewer
opening the Chinese or Japanese app sees "All / Cards / Map / Enterprise / My Accounts /
Renewals / At Risk" above a fully localized grid. There is no authoring workaround: the
bundle has no key to put the translation in.

Evidence

1. The label is declared translatable. packages/spec/src/ui/view.zod.ts:483

export const ViewTabSchema = lazySchema(() => strictObject({
  surface: 'this view tab',
  history: VIEW_HISTORY,
}, {
  name: SnakeCaseIdentifierSchema.describe('Tab identifier (snake_case)'),
  label: I18nLabelSchema.optional().describe('Display label'),
  ...
  view: z.string().optional().describe('Referenced list view name from listViews'),

packages/spec/src/ui/i18n.zod.ts:46

export const I18nLabelSchema = lazySchema(() =>
  z.string().describe('Display label (plain string; i18n keys are auto-generated by the framework)'));

2. There is no key to translate it into. ObjectTranslationDataSchema
(packages/spec/src/system/translation.zod.ts:143) is a strictObject whose only
grouped members are _views, _actions and _sections. There is no _tabs — and
because it is strictObject, an author cannot add one: it is rejected, not ignored.

3. Nothing resolves it. packages/spec/src/system/i18n-resolver.ts contains no
tab-label resolver; the string tab appears in that file only inside prose comments.
resolveViewLabel (line 149) reads objects.<object>._views.<view.name>.label and
nothing else.

Why this may be fixable without a schema change

Every tab in the HotCRM corpus points at a named list view via tabs[].view, and that
view's label is translatable through _views.<name>.label:

tabs: [
  { name: 'all',        label: 'All',        view: 'all_accounts', isDefault: true },
  { name: 'enterprise', label: 'Enterprise', view: 'enterprise_accounts' },
  { name: 'at_risk',    label: 'At Risk',    view: 'at_risk_accounts' },
],
listViews: {
  enterprise_accounts: { name: 'enterprise_accounts', label: 'Enterprise Accounts', ... },
}

tabs[].label is already .optional(). If the console fell back to
resolveViewLabel(referenced view) when label is omitted, the whole class would close
with no schema change and no new translation key — downstream apps would just delete
their inline tab labels.

I could not confirm whether that fallback exists. The console/renderer is not in this
repository (apps/ holds only docs), and ViewTabBar appears only in schema comments
and lint rules, never in an implementation. So this is a question for whoever owns the
renderer, not a claim.

If that route is taken, #5164 is a prerequisite to check. It reports that the
_views key for a default-only list container is spelled three different ways by the
composer (default), the extractor (list) and the lint rule (either) — so a tab
falling back to such a container would resolve to nothing. HotCRM's tabs all reference
explicitly-named listViews keys and so avoid that case, but a fallback design should
not assume every app does.

Possible resolutions, roughly in order of preference

  1. Console falls back to the referenced view's translated label when tabs[].label
    is absent. No schema change; the downstream fix is deleting inline labels. Needs the
    renderer to confirm, and interacts with _views translation keys have three producers that disagree on the spelling — a default-only list container can never resolve #5164.
  2. Add a _tabs group to ObjectTranslationDataSchema plus a resolveTabLabel
    helper, mirroring _views / _sections. Explicit and unambiguous, but adds a key for
    something the view reference arguably already addresses. Note that tabs may also
    carry a filter and no view at all, in which case there is no view to fall back to
    — which is an argument for this option over (1).
  3. Document tabs[].label as untranslatable and change it from I18nLabelSchema to
    a plain z.string(). Least work, but the tab bar stays monolingual — hard to defend
    for a list page's primary navigation.

Whichever way it goes, the I18nLabelSchema description on this field is inaccurate
today and worth correcting either way
: no key is auto-generated for it. The same
description is attached to AriaPropsSchema.ariaLabel, which is worth auditing for the
same problem.

Reproduction

  1. Author an object view with a tabs array where each tab has a label and a view.
  2. Provide a complete locale bundle including objects.<object>._views.<name>.label for
    every referenced view.
  3. Load the list page with that locale active. Every view's own heading resolves; the tab
    bar above it stays in the source language.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions