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

Activity

  1. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    Correction from the reporter side: this issue's central claim is wrong on
    rc.2, and I verified it live rather than from schema source this time.

    Running HotCRM against @objectstack/* 17.0.0-rc.2 with a complete zh-CN
    bundle, the account list's tab bar renders 全部客户 / 客户卡片 / 客户地图 /
    企业客户 — strings that exist only under
    objects.crm_account._views.*.label, reachable only by following
    tabs[].view. So the console already resolves a tab's label via its referenced
    view's translated label, exactly the "resolution 1" this issue listed as
    unverified. The headline scenario — a translated app with an English tab bar —
    does not reproduce when tabs reference views. Evidence and screenshots:
    objectstack-ai/hotcrm#661 (now closable on that basis).

    Two narrower points from the report survive and are, I think, worth keeping
    this issue open for (retitled), or splitting:

    1. A tab with filter but no view has no translation path at all.
      ViewTabSchema allows exactly that shape; with no referenced view there is
      nothing to fall back to, _tabs still does not exist in
      ObjectTranslationDataSchema, and no resolver reads tab labels. The gap is
      real for that subset — it just happens that HotCRM's 59 tabs all carry
      view.
    2. I18nLabelSchema's description is inaccurate ("i18n keys are
      auto-generated by the framework" — none are, for tabs[].label). Worth
      correcting wherever that schema is reused; AriaPropsSchema.ariaLabel
      carries the same description and likely the same problem.

    The visualization-switcher strings visible in the same screenshots
    (Grid / Gallery) are a different component and already covered by #5084 —
    not a reason to keep this issue broad.


    Generated by Claude Code

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    分诊(spec 车道欠账代扫,维护者 2026-08-05 指令):挂 pm:queue,范围按报告方 08-05 更正评论收窄——正文头条场景(整条 tab 栏英文)在 rc.2 上不复现(tab 引用 view 时经 _views.*.label 已可翻译),派发词只框两个幸存点:① filter-only(无 view)的 tab 无任何翻译路径(_tabs 缺位 + 无 resolver);② I18nLabelSchema 的 describe 谎称「i18n keys are auto-generated」,所有复用处(含 AriaPropsSchema.ariaLabel)一并更正。落点 packages/spec translation schema / i18n-resolver / 文案。不构成认领。会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    打包建议(致 spec 座位):本单与 #5199(report / dataset 无 translator)同为 i18n translator 覆盖族,建议同一接力棒内派发,摊薄四步重建 + CI 圈的每单固定成本。背景:维护者 2026-08-06 拍板 spec 车道缩盘方案,见 #5837。


    Generated by Claude Code

  4. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Release-board audit (maintainer-directed re-audit of non-board domain:spec items, 2026-08-07): adding target:v17 — criterion ②: filter-only view tabs (a legal authored shape) have no _tabs translation slot or resolver, while I18nLabelSchema's describe claims keys are auto-generated — a published-contract promise the runtime doesn't honor (scope already narrowed 08-05: view-ref tabs work on rc.2). Triage seat may veto.


    Generated by Claude Code

  5. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Bundled into sweep card #6489 (untranslatable authoring surfaces, 2 members). Keeps pm:queue but is excluded from batch selection while #6489 is open — claim the sweep card, not this one; it closes via Fixes when that PR merges.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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