Skip to content

Commit 2085be2

Browse files
docs(i18n): state the shipped severity for an unresolvable translation key (#18766)
Part of #18452 `content/docs/protocol/kernel/i18n-standard.mdx` told authors that an unresolvable translation-bundle key "is a **warning**, not an error: nothing crashes". Both halves are false against the shipped linter, and have been since #16310 promoted `translation-target-unknown` to `error`. An author who read the page and then wrote a locale key against a target the linter cannot resolve got a FAILED run, having been told in writing it would be a nag. ## The two texts, re-measured on this branch Repository `objectstack-ai/objectstack`, worktree at base `f1c9bb305`. | Reading | Result | |:---|:---| | `content/docs/protocol/kernel/i18n-standard.mdx:1027` (before this PR) | `An unresolvable key is a **warning**, not an error: nothing crashes, the string` | | `packages/lint/src/validate-translation-references.ts:151` | `const TRANSLATION_TARGET_UNKNOWN_SEVERITY = 'error' as const;` | | consumed as the `severity` of every finding at | `:912` and `:1364` — the card's `:813` / `:1265` are stale, so everything here was located by CONTENT | | control that the probe reaches the page at all | `translation` matches **49** lines in that same mdx | What an `error` actually does, read from the consuming commands rather than inferred from the severity word: - `os lint` — `failing = errors.length + (strict ? warnings.length : 0)` and `if (failing > 0) process.exit(1)` (`packages/cli/src/commands/lint.ts:918-922`, `:1040`). No flag is needed; `--strict` only adds warnings on top. - `os validate` — `splitBySeverity(findings)`, then `this.exit(1)` on a non-empty `ruleErrors` (`packages/cli/src/commands/validate.ts:353-383`). - `os compile` — the same shape (`packages/cli/src/commands/compile.ts:370-397`). The rule reaches all three because its suite entry declares `commands: ALL` (`packages/lint/src/authoring-rules.ts:709-713`). ## Where the repair's boundary was drawn The old sentence promised two things — "a warning, not an error" AND "nothing crashes" — so repairing only the severity word would have left a worse half-truth than the original. The whole claim is repaired instead: - the severity now reads **error**, names the rule id so a reader can grep it, and names the three commands that exit non-zero; - the runtime half is **kept**, because it is still true and is precisely the argument for the promotion: the string really does render in its source locale while its neighbours resolve, and that invisibility is what makes an orphan key worth failing on. The rule's own module header makes the identical argument (`packages/lint/src/validate-translation-references.ts:29-48`); - the sibling rule is separated out, because the very next paragraph on the page is about it: `translation-option-key-unknown` is still a `warning` (`packages/lint/src/validate-translation-references.ts:1309`, `:1328`). Out of bounds on purpose: the two places on this page that describe the **key shape** (the card measured both as correct) are untouched, as is `content/docs/ui/translations.mdx`. `packages/lint/**` is untouched by design — #16310 was a deliberate promotion, so the page is what drifted, and downgrading the rule would be the gate-weakening the triage ruling forbids. ## The sibling sweep — the spellings and the populations, named The card's starting point ("under `content/docs`, exactly one file mentions `unresolvable key` or `translation-target-unknown`") is a reading over ONE spelling, not over the claim, so it was re-run wide. Probes, all on `f1c9bb305`, all case-insensitive: | # | Spelling | Population | |:--|:---|:---| | 1 | `translation-target-unknown` | whole repo | | 2 | `unresolvable` | whole repo | | 3 | `orphan[- ](key\|translation)` / `orphaned (key\|translation)` | whole repo | | 4 | `nothing crashes` | whole repo | | 5 | `warning,? not an error` / `not an error,? (a\|just a) warning` | whole repo | | 6 | `inert` | `content/docs`, `skills`, `examples`, `apps`, `README.md` | | 7 | a severity word (`warning\|warn\|error\|severity\|fails the run\|fails the build\|exit 1`) on the same line as translation-key language (`translat\|locale\|i18n\|bundle key\|orphan`) | `content/docs`, `skills`, `examples`, `apps`, `README.md`, minus `content/docs/references` and `content/docs/releases` | | 8 | `advisory\|does not fail\|doesn't fail\|won't fail\|never fails\|non-blocking\|non-gating\|safe to ignore` near translation language | `content/docs`, `skills`, `examples`, `docs`, `apps` | | 9 | `renders? (in its )?(source\|original) locale` / `renders? untranslated` / `stays english` | `content/docs`, `skills`, `examples`, `docs`, `apps` | | 10 | the rule id / claim spellings of 1 and 3 | `docs`, `.claude`, `apps`, `skills`, `content` | | 11 | every severity word inside `i18n-standard.mdx` itself | that one file | What they returned: - **One live carrier in `content/docs`** — the sentence this PR repairs. Probe 11 confirms the page states the severity claim exactly once; the other hits on that page are `Callout type="warn"` markers and an unrelated parse-failure sentence. - **A positive control, and it is the useful one.** `skills/objectstack-i18n/SKILL.md:196-197` already states the claim **correctly**: "`translation-target-unknown` is an **error** and fails the run; `translation-option-key-unknown` is a warning." The probe reaches a second document carrying this claim, and that document has been right all along — so a zero elsewhere is a reading, not a blind spot. - **Dated historical records, deliberately not edited**: `docs/audits/2026-07-app-metadata-reference-integrity-assessment.md:93` ("**warning** throughout", stamped "Landed 2026-07-28") records what shipped on that date; `.changeset/16310-orphan-locale-key-gates.md`, `packages/lint/CHANGELOG.md`, `packages/cli/CHANGELOG.md` and `content/docs/releases/**` are release-owned and correct as of their entries. AGENTS.md forbids amending them from a code PR. - **Near-misses re-read and cleared**: `content/docs/ui/translations.mdx:242-244` (default-locale COVERAGE severity) and `:346-365` (the `flows` group's liveness warning) are different rules, not this claim. - **One carrier this PR is fenced out of** — see below; it is why this body says `Part of` and not a closing keyword. ## Verification Gate families derived mechanically from the change set rather than listed by hand: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at commit `4083936f7`, then reconciled with `--ran` carrying one recorded exit code per family: ```text Run reconciliation - 39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN. dispatch-gates --ran: 39 derived famil(ies) accounted for - 39 run, 0 NOT-MEASURED (a DERIVED zero - all 39 recorded an exit code and none of them is 3). ``` Three families first answered **exit 3 = PREREQUISITE NOT MET** (`@objectstack/lint` and `@objectstack/client-react` were unbuilt in a fresh worktree), which is not a finding. They were re-run after building those packages and all three then exited 0: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:docs-transcript-drift`, plus `check:skill-examples` which had exited 1 for the same unbuilt-`dist` reason it prints by name. Notable members of the 39, all exit 0: `check:docs-section-name`, `check:doc-anchors`, `check:doc-authoring`, `check:docs-single-h1`, `check:doc-frontmatter`, `check:corpus-claim-drift`, `check:docs-audit-scope`, `check:cross-package-test-inputs`, `check:nul-bytes`, `pnpm --filter @objectstack/spec check:docs`. Also self-scanned for control bytes beyond the gate, per AGENTS.md: `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over the edited file returns nothing (exit 1). Declared scope of the local run: repository-wide scans (`pnpm lint` foremost) are CI's run, and `origin/main` moved 4 commits after this branch's base. The only gate family those commits add is `check:rerun-safety-verdict`, a self-test-only checker-health family wired whole-tree and unrelated to `content/docs`; they add no docs-path family (measured by diffing `.github/workflows/lint.yml` and `package.json` across that range). CI derives on the merge ref. ## Changeset: none, and the measurement behind that The criterion is whether anything PUBLISHED moves — what each package's `files[]` actually ships. Measured over every non-private `package.json` in the workspace: **70 published packages, 0 whose `files[]` names `content/docs`**; positive control on the same instrument, **70 of 70 name `dist`**. `apps/docs` (the Fumadocs site that renders this page) is `"private": true`. Nothing copies `content/docs` into a package at build time. Hence `skip-changeset`. ## Acceptance notes Observed while sweeping, NOT fixed here and NOT filed as cards: - `packages/lint/src/validate-translatable-sections.ts:40-43` justifies its own `warning` severity with "Same reading as its sibling rule (ADR-0072 D1): nothing crashes and nothing is dead". The sibling is `validate-translation-references`, which no longer holds that reading — this is the same falsified claim, one file over, by reference. Its own rule's warning severity is still correct; only the cross-reference is stale. `packages/lint/**` is fenced for this card by the triage ruling and by the dispatch, so it is reported rather than touched, and it is why this body reads `Part of #18452` — the docs half is complete, the card's last carrier is not. The repo already carries the correction one file over, in `packages/lint/src/reference-integrity-suite.ts:406-410`: "warning-only on its own reading - NOT on its sibling's any more". - `packages/lint/src/validate-translation-references.test.ts:26-27` narrates the superseded Severity note in the present tense inside a docblock; harmless in context (the tests below it assert `error`), noted only because it is the third text in the family. Neither is a reproducible defect, a declared-contract violation or a metadata-authoring trap, so neither is filed by this PR; both are handed to triage in the report instead. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 062f5cd commit 2085be2

1 file changed

Lines changed: 10 additions & 3 deletions

File tree

‎content/docs/protocol/kernel/i18n-standard.mdx‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,9 +1024,16 @@ every bundle against the stack it ships with:
10241024
| `dashboards.{dash}` / `.widgets.{id}` / `.actions.{actionUrl}` | the dashboard's `name` / a widget `id` / a header `actionUrl` |
10251025
| `globalActions.{action}` | an action with **no** `objectName` |
10261026

1027-
An unresolvable key is a **warning**, not an error: nothing crashes, the string
1028-
simply renders in its source locale while every neighbouring label resolves — so
1029-
the app looks translated and one field does not.
1027+
An unresolvable key is an **error**, not a warning: `os validate`, `os lint` and
1028+
`os compile` each report it as `translation-target-unknown` and exit non-zero, so
1029+
a bundle key naming a surface the metadata no longer declares fails the run (it
1030+
was a warning until #16310). Nothing *crashes* — the string simply renders in its
1031+
source locale while every neighbouring label resolves, so the app looks translated
1032+
and one field does not — but that is what earns the error rather than excusing it:
1033+
the orphan key stays a confident-looking grep hit, in every locale, for a surface
1034+
that was deleted. The mis-keyed **option** below is the deliberate exception and
1035+
stays a warning (`translation-option-key-unknown`): it names something real, so
1036+
its remedy is a rename rather than a deletion.
10301037

10311038
The option case is worth spelling out. Option maps are keyed by the option's
10321039
stored `value`, never by its display label:

0 commit comments

Comments
 (0)