Skip to content

_views translation keys have three producers that disagree on the spelling — a default-only list container can never resolve #5164

Description

@xuyushun441-sys

Found while fixing #4854 (the resolver read fields the served document does not have). That fix makes objects.<object>._views.<bare key> resolve for listViews / formViews entries. This issue is the residual: for one common authoring shape the key the extractor writes and the key the runtime identity yields are still different strings, so the translation can never be found no matter which side you author against.

Filed unassigned — recording the finding, not claiming it.

The divergence, measured

Three surfaces derive a _views key, and they do not agree:

surface file key it produces for a container's default list
view-document composer (runtime identity) packages/spec/src/ui/view.zod.ts expandViewContainerWithDiagnostics <object>.default → bare key default
i18n extractor (writes the bundle skeleton) packages/cli/src/utils/i18n-extract.ts:453 list
translation-reference lint (validates keys) packages/lint/src/validate-translation-references.ts collectViewRecord accepts both the map key and the inner name

Composer:

const key = typeof defaultList.name === 'string' && defaultList.name ? defaultList.name : 'default';

Extractor:

pushViewEntries(out, viewObjectName(view.list) ?? containerObject, view.list.name ?? 'list', view.list);

Measured against the real composer:

container { list: { label: 'All Tasks', type: 'grid', data: { provider: 'object', object: 'task' }, columns: ['title'] } }
composer  -> ["task.default"]     // bare key `default`
extractor -> objects.task._views.list.label

So an app whose object declares only a default list — no listViews — gets a skeleton keyed list, a runtime view keyed default, and an English label on screen forever. It is invisible in examples/app-crm only by luck: its default list is structurally identical to listViews.all, so the composer's signature dedupe collapses the two and the live key becomes all. The _views.list entry in examples/app-crm/src/translations/crm.translation.ts is dead weight that looks correct.

Second, related spelling question

collectViewRecord deliberately accepts a listViews.<key> entry's inner name as a legal _views key ("authors write either"), but the runtime identity for such an entry is derived from the map key only — expandViewContainer never reads the inner name except for the default list/form. So a bundle keyed by the inner name passes lint and resolves to nothing. No view in this repo's own apps carries an inner name, so this is currently theoretical; it is the same contract question one layer over.

Why this needs a decision rather than a patch

Per Prime Directive #12 the cure belongs at the producer, not in resolveViewLabel — teaching the resolver to try default, then list, then config.name would fossilize three dialects for one key and is exactly the consumer-side tolerance the directive bans. The decision is which spelling is canonical:

  • A. Canonical = the runtime identity's bare key (default). Extractor changes ?? 'list'?? 'default'; lint narrows to the map key / default key; existing bundles with _views.list need a conversion entry or a release note. Pro: one key, derived from the identity the registry actually assigned, and the lint can then enforce it — an AI-authored bundle either matches or is rejected loudly. Con: touches shipped bundles.
  • B. Canonical = list. Composer names the default list <object>.list instead of <object>.default. Pro: no bundle churn. Con: default is the registry identity that appears in URLs, overlay rows (sys_metadata), and normalizeViewMetadata baselines — renaming it is a far wider blast radius than renaming a translation key.

Recommendation: A, and tighten collectViewRecord to the same single spelling in the same change, so "declared = enforced" holds for view translation keys.

Repro

pnpm --filter @objectstack/spec build
# then, against src:
node -e "0" # see the measurement snippet above; expandViewContainer('task', { list: {...} }) -> ['task.default']

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions