Monorepos

Misplaced Usages

What a Misplaced Usage Is

A misplaced usage is a key referenced only from apps that do not consume its layer. It is the third outcome of orphan detection, alongside used and orphaned, and it exists because those two labels are both wrong for this case:

  • Calling it used would protect a key on the strength of a reference that cannot resolve at runtime — the app doing the referencing never loads that layer, so the screen renders the raw key.
  • Calling it an orphan would delete a key that a real call site depends on, turning a rendering bug into a missing translation in whichever layer the fix eventually lands in.

So the scan reports it as its own finding and stops there. The report carries this note verbatim next to the entries:

Keys referenced only from apps that do not consume their layer. Either the key belongs in a broader (shared) layer, or the usage is a bug. These keys are not counted as orphans and are never removed.

Reading an Entry

A key with no evidence inside its layer's scope is a candidate orphan. Where the evidence exists outside that scope — an exact reference, a bare-string occurrence, or a dynamic pattern that could produce the key — it is recorded as misplaced instead:

{
  "misplacedUsages": [
    { "key": "checkout.cart.title", "layer": "checkout", "usingApps": ["admin"] }
  ]
}

layer is where the key is defined; usingApps names the out-of-scope directories that referenced it — an app's name where an app owns the directory, and a layer's name otherwise, so an entry can name a layer rather than an app.

They Are Reported, Never Removed

Only orphanKeys is ever deleted, and that is not a default you can flip. The same holds for uncertainKeys, the keys whose evidence overlaps a dynamic pattern: reported every run, removed by nothing.

BucketMeaningRemoved
orphanKeysNo evidence of use in any app that consumes the layerYes, on --no-dry-run
uncertainKeysEvidence is ambiguous — a dynamic pattern could produce the keyNever
misplacedUsagesReferenced only from apps that do not consume the layerNever

Two consequences worth knowing:

  • summary.usedCount excludes misplaced keys, and summary.misplacedCount counts them separately, so a misplaced key never inflates your used total.
  • The --fail-on-orphans gate reads orphanCount. Misplaced usages do not trip it. If you want them to fail a pipeline, read misplacedCount from the report. The Code Quality report written by --codequality-output likewise contains orphan findings only.

Fixing One

Every entry is one of two situations, and the report cannot tell them apart — only you know which app is supposed to own the string.

The key is in the wrong layer. The referencing app is legitimate and the key belongs somewhere it can reach: a shared layer both apps consume, or that app's own layer. There is no cross-layer move — rename operates within one layer — so write the key into the target layer and remove it from the old one:

the-i18n-cli get --layer checkout --locale '*' --keys checkout.cart.title
the-i18n-cli write --layer admin --translations '{"checkout.cart.title": {"en": "Your cart", "de": "Ihr Warenkorb"}}'
the-i18n-cli remove --layer checkout --keys checkout.cart.title

Read the values first with --locale '*' so every locale moves, not only the reference one. If the key's namespace no longer fits its new home, rename it inside the target layer afterwards with the-i18n-cli rename, which updates every locale file at once.

The usage is a bug. The referencing app was never meant to render that string — a copied component, a stale import. Fix the call site to use a key its own layers define, and the finding disappears on the next run. the-i18n-cli check is the view from the other side: it reports keys referenced in code that no layer the app consumes defines, which is the same defect seen from the call site.

A third possibility, rarer but real: the app should consume that layer and the project does not say so. Then the fix is in the app's own configuration, and the graph picks it up on the next run.

When You Will Not See Any

Misplaced usages need at least one directory outside a layer's scope. They are absent whenever there is none:

  • Single-app projects — the app's scope is the whole project, so nothing is out of scope.
  • Configurations with no app information — every layer is checked against the whole project.
  • Explicit scan directories — passing scanDirs to find_orphan_keys, remove_orphan_keys or their programmatic equivalents switches to one combined usage set for all layers, and misplaced-usage detection is off. Use it when you deliberately want the unscoped behavior; an empty misplacedUsages from such a run says nothing about the project.

Since app-to-layer edges are derived today only for Nuxt monorepos, those are also the projects where this finding appears at all. Everywhere else the scan still runs, still reports per layer, and still refuses to remove anything it is unsure about — there is no second app to be misplaced from.

Limits

  • Detection is only as sharp as the evidence classes the scanner recognizes. A key built at runtime from data the scanner cannot see is neither used nor misplaced; it is a candidate orphan, which is why dry runs and orphanScan.ignorePatterns exist.
  • A dynamic pattern in an out-of-scope app matches conservatively. An expression like t(`${prefix}.title`) in a non-consuming app can mark every .title key in another layer as misplaced. Keep a literal leading segment in dynamic keys and the report stays specific.
  • The finding names apps and layers, not files. To see where the references are, run the-i18n-cli scan --keys <key> for the key in question.
Copyright © 2026