validate-translation-references reports an orphaned locale key correctly and
usefully — and no consumer can make that report fail anything. Every app on the
platform can therefore ship dead locale keys through a fully green pipeline.
This is not a request to build the rule. It exists and it works. The ask is
about severity and gateability.
Measured (hotcrm @ @objectstack/* 17.3.0)
Planting one orphan key in apps.crm_enterprise.navigation and one in
objects.crm_lead._sections, in all four locale bundles, then running the real
objectstack lint --json:
| tree |
total |
warnings |
passed |
exit |
| baseline |
13 |
1 |
true |
0 |
| 8 orphan keys planted |
21 |
9 |
true |
0 |
| restored |
13 |
1 |
true |
0 |
Each orphan is reported once per locale, with the id named and the remedy
printed. The diagnosis is excellent. The exit code is 0.
Why nothing downstream can gate it
Two independent properties, either of which alone would be enough:
- Severity is hard-coded
warning. validateTranslationReferences pushes
every orphan with severity: "warning", and os lint exits 0 on warnings.
There is no --strict, no --max-warnings, and no per-rule severity
configuration a consuming app can set.
- The rule id carries no namespace. Registry findings reach the JSON report
as rule: f.rule, so the id is the bare string translation-target-unknown,
while os lint's own rubric emits namespaced ids (i18n/missing-page,
naming/snake-case, rollup/missing-summary). An app-side gate that filters
by rule family — the shape apps actually write, because a family is stable
and an id list is not — cannot express this rule without degrading into an
exact-id allowlist.
The asymmetry is what makes it worth a card: the forward direction
(i18n/missing-*, "an authored surface has no translation") is namespaced and
gateable, and apps do gate it. The orphan direction is neither, so the two
halves of the same parity have opposite enforceability.
Consequence, in one sentence
A PR that deletes a navigation entry, a form section or a view and leaves its
locale keys behind is green everywhere, and the dead keys are actively
misleading afterwards: grepping the id returns a confident-looking hit in every
locale, which reads as "this exists and is translated".
Possible shapes, for the platform to choose between
- A per-rule severity override consumers can set (the general fix; also closes
the equivalent question for every other advisory rule).
- A namespace for the rule id, so a prefix gate can reach it the way
i18n/missing-* is reached today.
- A
--max-warnings style flag on os lint.
- Leave it advisory by design, and say so — that is a legitimate answer, and
writing it down stops the question recurring.
Back-link
Filed from hotcrm#1262, which measured this while deciding its own fix. HotCRM
closed its own case with a repo-side assertion over apps.*.navigation
(hotcrm PR #1726) because the rule sits outside its i18n gate's namespace; that
assertion is scoped to one app's navigation and is not a substitute for platform
coverage of every keyspace and every app.
Generated by Claude Code
validate-translation-referencesreports an orphaned locale key correctly andusefully — and no consumer can make that report fail anything. Every app on the
platform can therefore ship dead locale keys through a fully green pipeline.
This is not a request to build the rule. It exists and it works. The ask is
about severity and gateability.
Measured (hotcrm @
@objectstack/*17.3.0)Planting one orphan key in
apps.crm_enterprise.navigationand one inobjects.crm_lead._sections, in all four locale bundles, then running the realobjectstack lint --json:passedtruetruetrueEach orphan is reported once per locale, with the id named and the remedy
printed. The diagnosis is excellent. The exit code is 0.
Why nothing downstream can gate it
Two independent properties, either of which alone would be enough:
warning.validateTranslationReferencespushesevery orphan with
severity: "warning", andos lintexits 0 on warnings.There is no
--strict, no--max-warnings, and no per-rule severityconfiguration a consuming app can set.
as
rule: f.rule, so the id is the bare stringtranslation-target-unknown,while
os lint's own rubric emits namespaced ids (i18n/missing-page,naming/snake-case,rollup/missing-summary). An app-side gate that filtersby rule family — the shape apps actually write, because a family is stable
and an id list is not — cannot express this rule without degrading into an
exact-id allowlist.
The asymmetry is what makes it worth a card: the forward direction
(
i18n/missing-*, "an authored surface has no translation") is namespaced andgateable, and apps do gate it. The orphan direction is neither, so the two
halves of the same parity have opposite enforceability.
Consequence, in one sentence
A PR that deletes a navigation entry, a form section or a view and leaves its
locale keys behind is green everywhere, and the dead keys are actively
misleading afterwards: grepping the id returns a confident-looking hit in every
locale, which reads as "this exists and is translated".
Possible shapes, for the platform to choose between
the equivalent question for every other advisory rule).
i18n/missing-*is reached today.--max-warningsstyle flag onos lint.writing it down stops the question recurring.
Back-link
Filed from hotcrm#1262, which measured this while deciding its own fix. HotCRM
closed its own case with a repo-side assertion over
apps.*.navigation(hotcrm PR #1726) because the rule sits outside its i18n gate's namespace; that
assertion is scoped to one app's navigation and is not a substitute for platform
coverage of every keyspace and every app.
Generated by Claude Code