Skip to content

fix(cli): inline label: is the default-locale source for i18n coverage (#3103)#3117

Merged
os-zhuang merged 1 commit into
mainfrom
fix/blank-template-i18n-lint
Jul 17, 2026
Merged

fix(cli): inline label: is the default-locale source for i18n coverage (#3103)#3117
os-zhuang merged 1 commit into
mainfrom
fix/blank-template-i18n-lint

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3103.

Takes option 2 from the issue — and it turned out to be more than "the better long-term semantics": the old rule was a false positive, contradicting a contract the rest of the framework already documents.

Why option 2, not option 1

A fresh scaffold's 4 errors are all for strings the template does author inline:

✗ Object "demo_note" label missing translation for locale "en"       # label: 'Note'
✗ Object "demo_note" pluralLabel missing translation for locale "en" # pluralLabel: 'Notes'
✗ Field demo_note.title label missing translation for locale "en"    # label: 'Title'
✗ Field demo_note.body label missing translation for locale "en"     # label: 'Body'

The inline label: is the default-locale text, and the framework says so everywhere except in the coverage rule:

  • The runtime resolver falls back to it. i18n-resolver.ts translateObject: lookupObjectField(bundle, objectName, 'label', opts) ?? doc.label. Its docstring states the order: requested locale → fallbackChain → literal label from the metadata. Nothing renders broken or empty.
  • os i18n extract generates bundles from itpushEntry(..., obj.label ?? objectName, ...).

So coverage was demanding an en bundle that mechanically restates data the linter already holds, for text that renders correctly. Option 1 would have shipped that duplicate into the template and frozen the ritual for every user: the errors return the moment someone adds their own object.

It's also not only about the template. An object with a label but no pluralLabel was told its pluralLabel was "missing translation" — a demand to translate a string that doesn't exist, unfixable by authoring anything except a bundle entry.

The change

An inline label satisfies the default locale; a bundle is what other locales need. A key with no source string anywhere is no longer an i18n gap — a missing label is already required/label's finding (lint.ts:155-225).

This also removes an inconsistency rather than adding a special case: collectExpectedKeys already only expected confirmText / successMessage / description when authored. label / pluralLabel were the outliers.

Platform form fields keep their key: they omit label by design and let the renderer humanize the path (nameName), so that derived text is a real source string other locales still owe a translation for. This mirrors the extractor's seed value — which the coverage walker's docstring already promises it does. (My own new test caught that I'd otherwise have silently dropped ~800 platform keys from non-default-locale coverage.)

Verification

The issue's repro, end to end — create-objectstackos lint:

Before:  4 error(s)                     After:  ✓ All checks passed   (exit 0)

And against a real multi-locale project, the non-default-locale signal is preserved exactly:

examples/app-todo before after
os lint 39 errors, 79 warnings 0 errors, 79 warnings
os i18n check 856 errors, 1712 warnings 0 errors, 1712 warnings

Warnings are byte-identical — no real gap was dropped; only the default-locale duplicates are gone. os i18n check now reports the truth: "Non-default locales have gaps but the default locale is fully covered."

The platform baseline drops 2451 → 1634 hidden issues — a delta of exactly 817, its en third (817×3 locales → 817×2) — because the platform ships English labels inline too. That arithmetic is what confirms only the en share vanished.

Full CLI suite green: 548 tests / 52 files. Two existing tests asserted the old behaviour and were rewritten to test the real intent (a genuine default-locale gap = a string in another locale with no source text for the default one), plus 7 new tests pinning the scaffold shape, the zh-CN-first case, and the bundle-only case.

Follow-ups (not in scope)

  • os lint ignores a project's declared i18n.defaultLocale and always lints against 'en' (the oclif flag's default: 'en' makes "declared" indistinguishable from "defaulted"). This change reduces the blast radius but doesn't fix it.
  • Coverage skips array-form options: [{value, label}] entirely; the extractor handles both.
  • The scaffold's AGENTS.md documents an src/i18n/ directory it never creates, while real examples use src/translations/.

🤖 Generated with Claude Code

…age (#3103)

A fresh `npm create objectstack` scaffold linted with 4 user-side i18n errors
out of the box, for its own `<ns>_note` object — every one of them for a string
the template *does* author inline (`label: 'Note'`, `pluralLabel: 'Notes'`,
`label: 'Title'`, `label: 'Body'`).

They were false positives. The inline `label:` is the default-locale source
text, and the framework already says so everywhere except here:

  - the runtime resolver falls back to it when a bundle carries no entry
    (`i18n-resolver` `translateObject`: `lookupObjectField(...) ?? doc.label`),
    and its docstring documents the order `locale → fallbackChain → literal
    label from the metadata`;
  - `os i18n extract` *generates* bundles from it (`obj.label ?? objectName`).

So coverage was demanding an `en` bundle that mechanically restates data the
linter already holds, for text the runtime renders correctly. Worse, an object
with no `pluralLabel` was told its `pluralLabel` was "missing translation" — a
demand for a translation of a string that does not exist, unfixable by
authoring anything but a bundle entry.

Honour the contract instead: an inline label satisfies the default locale, and
a bundle is what *other* locales need. A key with no source string anywhere is
no longer an i18n gap — a missing label is `required/label`'s finding, and this
file already only expected `confirmText`/`successMessage`/`description` when
authored; `label`/`pluralLabel` were the outliers.

Platform form fields keep their key: they omit `label` by design and let the
renderer humanize the path ("name" → "Name"), so that derived text is a real
source string other locales still owe a translation for. Mirrors the
extractor's seed value, which the walker's docstring already promises.

Verified on the issue's repro and against a real multi-locale project — the
non-default-locale signal is preserved exactly:

  examples/app-todo   before              after
  os lint             39 errors, 79 warn  0 errors, 79 warn
  os i18n check       856 err, 1712 warn  0 err, 1712 warn

Warnings are byte-identical; only the default-locale duplicates are gone. The
platform baseline drops 2451 → 1634 hidden issues, a delta of exactly 817 —
its `en` third (817×3 → 817×2) — because the platform ships English labels
inline too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 17, 2026 11:48am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m and removed documentation Improvements or additions to documentation tests tooling labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/cli.mdx (via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit f58db35 into main Jul 17, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the fix/blank-template-i18n-lint branch July 17, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

blank template lints with 4 user-side i18n errors out of the box

1 participant