Shared Layers and Duplicates
A Shared Library, Two Apps
monorepo/
├── packages/ui/ # shared: buttons, dialogs, form errors
├── apps/checkout/ # consumes ui
├── apps/admin/ # consumes ui
└── scripts/ # release tooling, no app
Three layers, two apps. ui is a shared layer: consumed by more than one
app. Each layer's scope is its own root, the roots of the apps that consume it,
and the project root — which claims scripts/:
| Layer | Scope |
|---|---|
ui | packages/ui, apps/checkout, apps/admin, project root |
checkout | apps/checkout, project root |
admin | apps/admin, project root |
What changes against a repository-wide scan is who may vouch for what:
| Key, defined in | Referenced in | Outcome |
|---|---|---|
ui | packages/ui | Used. A library's own code protects its keys |
ui | apps/checkout only | Used. One consumer is enough |
ui | nowhere | Orphan — the only bucket orphans --remove deletes from |
checkout | apps/admin only | Misplaced usage — reported, never removed |
The last row is what an unscoped scan gets wrong: a repository-wide grep finds
the string in apps/admin, calls the key used, and the broken screen ships. See
misplaced usages and
orphan detection.
Drop apps/admin and ui has one consumer, whose scope is the whole project.
Per-layer scope collapses to global scope and the scan behaves like an unscoped
one.
Collisions
A collision is one key defined in two layers the same app consumes: a shared layer and a layer that overrides it. The app resolves the overriding layer, so the shared value is dead for that app, with no warning at build time.
| Kind | Means | Why it matters |
|---|---|---|
| Same value on both sides | A copy | Harmless today, drift tomorrow |
| Different values — divergent | An override | The shared value is never seen by this app |
the-i18n-cli find-duplicates --locale de
{
"collisions": [
{
"key": "common.cancel",
"sharedLayer": "ui",
"childLayer": "checkout",
"sharedValue": "Cancel",
"childValue": "Discard",
"divergent": true
}
],
"summary": { "totalCollisions": 1, "divergentCount": 1, "pairsChecked": 2, "locale": "en" }
}
pairsChecked tells you whether a clean report means "no collisions" or "nothing
to compare". Pairs come from each app's layer precedence order: of two
locale-backed layers an app consumes, the earlier is the child and the later is
the shared base. Comparison is leaf by leaf in one reference locale, structural
for values that are not strings, so two equal arrays of plural forms are a copy
rather than a divergence.
Fixing a Collision
Delete one side. Never move the key — both layers already define it, so a move is a delete plus a no-op and the wrong order loses a value.
- The child value is authoritative → delete the shared copy, if no other consumer relies on it.
- The child copy was accidental → delete it and let the app fall through.
- The override is deliberate → keep both. There is no suppression list; the report shows it every run.
The same guidance ships in the result's guidance field.
--locale <code> once per locale
you ship.Two more things to expect. With no app information in the config no pairs can be
derived, and the summary says so in its message field rather than guessing at a
hierarchy; the same happens when no app consumes more than one locale-backed
layer. And duplicate detection has no gate flag: it reports and exits 0, so
failing a pipeline on collisions means reading summary.totalCollisions or
summary.divergentCount yourself.