Commit 2085be2
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1024 | 1024 | | |
1025 | 1025 | | |
1026 | 1026 | | |
1027 | | - | |
1028 | | - | |
1029 | | - | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
1030 | 1037 | | |
1031 | 1038 | | |
1032 | 1039 | | |
| |||
0 commit comments