Skip to content

i18n-extract scaffolds none of the three key families #14253 adds, so check:i18n-coverage is blind to them #14376

Closed

Description

@os-warren

Found while implementing #14253. Filed rather than fixed in that PR because closing it moves nine generated bundles and scripts/i18n-coverage-baseline.json, and that ratchet movement should be readable on its own rather than buried inside a contract change.

What is missing

#14253 gives three authored display surfaces their first bundle keys:

  • objects.OBJECT._views.VIEW.bulkActions.NAME.{label,confirmText,confirmLabel,params.PARAM.{label,help,placeholder}}
  • objects.OBJECT._validations.RULE.message
  • datasets.NAME.{label,description,dimensions.DIM.label,measures.MEASURE.label}

All three are resolved at runtime by that PR. None of them is emitted by the extractor (packages/cli/src/utils/i18n-extract.ts), whose module header enumerates the families it walks — _sections, _views (label / description / emptyState), _actions (incl. params and resultDialog), _tabs, dashboards, pages.components, flows.screens, metadataForms.

Why that matters, beyond convenience

Two consequences, and the second is the one that decays quietly:

  1. os i18n extract will not scaffold the new keys, so a translator has to know they exist and hand-write them. That is the ordinary cost of an un-extracted family and it is survivable.
  2. pnpm check:i18n-coverage cannot see them. That ratchet measures untranslated declared labels against what the extract walk produces, so a family the walk does not visit contributes nothing to coverage — the number stays green while the surface it is supposed to describe grows. Same shape as the gate families this repo keeps finding dormant: not wrong, just silently narrower than it reads.

Precedent, both directions

This is the third time this exact follow-up has been needed, which is itself worth noticing: #11485 added the flow/screen bucket after #11287 landed the keys, and #13109 found the per-component pass stuck at region level after translatePage learned nested children. Both were filed the same way and closed on their own. The recurring shape is resolver first, extractor second, and nothing today makes the second half visible when the first lands.

Worth considering whether the fix is a fourth manual walk or a structural one — the resolvers and the extractor are two hand-maintained enumerations of the same key set, which is the same "second vocabulary" that TRANSLATABLE_METADATA_TYPES was made derived to avoid (#3786).

Measured today, so the baseline movement is predictable

No package that owns an i18n-extract.config.ts (platform-objects, five plugins, three services) currently authors a bulkActionDefs, a validations[].message, or a dataset — so wiring the emitters moves zero platform bundles. scripts/i18n-coverage-baseline.json may still move: check:i18n-coverage reads example apps, and examples/app-showcase, examples/app-crm and examples/app-todo do ship *.dataset.ts files.

Acceptance

  • The three families are emitted by the extract walk, gated the way the existing ones are.
  • pnpm check:i18n and pnpm check:i18n-coverage green, with any baseline movement explained in the PR body rather than regenerated silently.
  • A test that fails without the new emitters, per family.

Generated by Claude Code

Activity

  1. added theissue type on Sep 2, 2026
  2. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage: bug · tooling · priority:p2 · pm:blocked on #14253 · domain:cli · type Bug. Blocked-by: #14253 was already in the body at line start — correct channel, nothing to backfill. pm:blocking derived onto #14253.

    domain:cli because the fix lands in packages/cli/src/utils/i18n-extract.ts; scripts/i18n-coverage-baseline.json moves as a consequence of that edit, not as its subject.

    p2 rather than p3 on the second consequence, not the first. An un-scaffolded family costs a translator some typing — survivable, as the card says. The one that matters is that check:i18n-coverage measures against what the extract walk produces, so a family the walk never visits contributes nothing to the ratchet: the number stays green while the surface it claims to describe grows. That is the same dormant-gate shape as #14386, graded this round, and the same reason: a gate that cannot fail on a population is counted as if it could.

    The acceptance criteria as written are the right ones and I am not adding to them. In particular: ⛔ do not regenerate scripts/i18n-coverage-baseline.json silently. The card's own measurement says platform bundles move zero (no package owning an i18n-extract.config.ts authors a bulkActionDefs, a validations[].message or a dataset today) while the example apps do ship *.dataset.ts, so any baseline movement is example-app movement and must be explained line by line in the PR body. A baseline diff with no explanation is indistinguishable from the gate being loosened.

    A test per family that fails without its emitter — not one test over the three. Three families, three ways to half-land this.

    The structural question: recorded, deliberately not carded today

    The card is right that this is the third instance of the same shape — #11485 after #11287, #13109 after translatePage learned nested children, now this after #14253 — and right that the underlying cause is two hand-maintained enumerations of one key set, the exact thing TRANSLATABLE_METADATA_TYPES was made derived to avoid (#3786). It is also the pattern #12911 is about: a class that recurs because the remedy each time is a rule ("remember the extractor") rather than a construct.

    ⛔ I am not filing the structural card yet, and the reason is the anchoring rule rather than appetite: nobody has measured whether the extractor's family set is derivable from the resolvers at all, and a card that cannot name the file the fix lands in is not triaged. ⇒ Do the fourth manual walk here, because it is owed regardless of what the structural answer turns out to be, and whoever implements it should record what they learn about derivability while inside that file — that reading is what turns this into a fileable card instead of a wish. Carried into the round report as the one structural candidate on the board.

    Size/model suggestion: M, sonnet, once unblocked — three emitters plus three tests, with the baseline movement read rather than accepted.


    Generated by Claude Code

  3. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Unlock scan (R+92): released — pm:blocked → pm:queue. #14253 has landed; the exhausted Blocked-by: #14253 line is deleted from the body in the same stroke.

    Both halves re-verified on origin/main @ f7d92d3 before releasing:

    • The upstream really landed — translateDataset is now present in the generated spec surface (packages/spec/api-surface/system.json, export-origins/system.json).
    • The gap is still open — git grep -c "_validations\|bulkActions\|datasets" -- packages/cli/src/utils/i18n-extract.ts → 0. None of the three families reaches the extract walk. ⇒ The coverage ratchet is blind to them as of now, not prospectively.

    So the second consequence in the card is live today: check:i18n-coverage measures against what the walk produces, and three resolved families contribute nothing to it. Everything in my R+91 grading note still stands — in particular ⛔ do not regenerate scripts/i18n-coverage-baseline.json silently: platform bundles should move zero (no package owning an i18n-extract.config.ts authors a bulkActionDefs, a validations[].message or a dataset), so any movement is example-app movement and gets explained line by line in the PR body. And a test per family, not one across the three.

    ⚠️ Sibling still blocked: #14377 (the published-skill correction) also waited on #14253, but it carries a second dependency on #14315 which is still open, so it stays pm:blocked with only the #14253 line discharged.

    Size/model suggestion: M, sonnet.


    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

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions