Monorepos

The Shared Library Scenario

The Repository

monorepo/
├── packages/ui/          # shared library: buttons, dialogs, form errors
│   ├── locales/en.json
│   └── src/
├── apps/checkout/        # consumes ui
│   ├── locales/en.json
│   └── src/
├── apps/admin/           # consumes ui
│   ├── locales/en.json
│   └── src/
└── scripts/              # release tooling, no app

Three layers, three locale directories, two apps. ui is a shared layer: consumed by more than one app. checkout and admin are private to one app each.

What Each Layer Is Checked Against

Each layer's scope is its own root, plus the roots of the apps that consume it, plus the project root — which claims everything outside the three, including scripts/:

LayerScope
uipackages/ui, apps/checkout, apps/admin, project root
checkoutapps/checkout, project root
adminapps/admin, project root

What changes against a repository-wide scan is who is allowed to vouch for what.

Four Keys, Four Outcomes

Take four keys and follow each one through the plan.

ui.button.save, defined in ui, used in packages/ui/src/Button.vue. The library's own root is in ui's scope, so the key is used. A shared library's keys are protected by the library's own code, which is what makes it safe to delete-check a library nobody has finished migrating to.

ui.dialog.confirm, defined in ui, used only in apps/checkout.apps/checkout is in ui's scope because checkout consumes ui. The key is used. One consumer is enough; the admin app never referencing it is not a finding.

ui.legacy.banner, defined in ui, referenced nowhere. Nothing in scope vouches for it, and nothing out of scope does either. It is an orphan key — the only bucket remove-orphans ever deletes from.

checkout.cart.title, defined in checkout, referenced only in apps/admin.apps/admin is not in checkout's scope, so the reference is not usage evidence. But the key is not unreferenced either. It is reported as a misplaced usage and left alone: deleting it would break the admin call site, and counting it as used would hide the fact that the call site cannot resolve it at runtime.

That fourth case is the one an unscoped scan gets wrong. A repository-wide grep finds the string in apps/admin, calls the key used, and the broken screen ships.

Running It

the-i18n-cli remove-orphans

Dry run is the default — nothing is written until you pass --no-dry-run. The result carries the buckets separately:

{
  "orphanKeys": {
    "ui": ["ui.legacy.banner"]
  },
  "misplacedUsages": [
    { "key": "checkout.cart.title", "layer": "checkout", "usingApps": ["admin"] }
  ],
  "summary": {
    "orphanCount": 1,
    "uncertainCount": 0,
    "misplacedCount": 1,
    "scanScope": {
      "ui": [".", "packages/ui", "apps/checkout", "apps/admin"],
      "checkout": [".", "apps/checkout"],
      "admin": [".", "apps/admin"]
    }
  }
}

To check the library alone, narrow by layer — the scope stays the same, because scope comes from the graph rather than from the flag:

the-i18n-cli remove-orphans --layer ui

On a catalog large enough that the full report is inconvenient, --output-file writes it to disk and returns only the summary, which is also how an agent keeps a thousand-key result out of its context window.

When the Library Has One Consumer

Drop apps/admin and ui is no longer a shared layer. The remaining app's scope is the whole project, so per-layer scope collapses to global scope and misplaced usages cannot occur — there is nothing left out of scope. The scan behaves exactly like an unscoped one.

What This Setup Requires

Reaching the table above needs app-to-layer edges in the resolved config, and those are derived today only for Nuxt monorepos. On a React, Vue or Laravel monorepo you can declare the three layers by hand (see Layers) and you get per-layer reports, per-layer ignore patterns and duplicate detection — but one app consuming everything, so usage evidence stays repository-wide. The consumer graph page states the boundary precisely.

Before the first real removal, run the dry run and read uncertainKeys and dynamicKeys. A shared library is exactly where keys get built at runtime from component props, and those are the keys a static scan is least sure about.

Next: duplicates and divergence — what happens when a consuming layer redefines a key the shared library already owns.

Copyright © 2026