Monorepos

Duplicates and Divergence

What a Collision Is

A collision is one key defined in two layers that the same app consumes: a shared layer and a layer that overrides it. The app resolves the key from the overriding layer, so the shared layer's value is dead for that app — silently, with no warning at build time.

Two flavors, and the difference is the whole point:

KindMeansWhy it matters
Same value on both sidesA copyHarmless today, drift tomorrow: the next person changes one and not the other
Different values — divergentAn overrideSomeone meant it, or nobody noticed. Either way the shared value is never seen by this app

Finding Them

the-i18n-cli find-duplicates

The agent-facing equivalent is the find_duplicate_keys MCP tool, with the same arguments and the same result. Both compare values in one reference locale — the project default unless you pass another:

the-i18n-cli find-duplicates --locale de

A result looks like this:

{
  "collisions": [
    {
      "key": "common.cancel",
      "sharedLayer": "ui",
      "childLayer": "checkout",
      "sharedValue": "Cancel",
      "childValue": "Discard",
      "divergent": true
    }
  ],
  "summary": {
    "totalCollisions": 1,
    "divergentCount": 1,
    "pairsChecked": 2,
    "locale": "en"
  }
}

pairsChecked is the number of (shared layer, overriding layer) pairs compared, which tells you whether a clean report means "no collisions" or "nothing to compare".

Which Pairs Get Compared

Pairs come from each app's layer precedence order in the consumer graph. For any two locale-backed layers an app consumes, the one listed earlier overrides the later one at runtime, so the earlier is the child and the later is the shared base. Precedence, not popularity: a layer consumed by three apps is still the child against a base only one app consumes, if that app lists it first.

Comparison is leaf-by-leaf in the reference locale. Values that are not strings — an array of plural forms, say — are compared structurally, so two equal arrays are a copy and not a divergence.

Fixing a Collision

Delete one side. Never move the key — both layers already define it, so moving is a delete plus a no-op, and doing it in the wrong order loses a value. Which side to delete follows from the values:

  • The child value is authoritative and the shared value is stale → delete the shared copy, if no other consumer relies on it.
  • The child copy was accidental → delete the child copy and let the app fall through to the shared value.
  • The override is deliberate — one app really does say "Discard" — → keep both and treat the collision as known. There is no suppression list for duplicates; the report shows it every run.

The same guidance ships in the result's guidance field, so an agent acting on the report reaches the same conclusion as you do.

Check every locale before deleting, not only the one you compared. Values can be identical in English and divergent in German, in which case deleting the child copy changes the German UI. Run find-duplicates --locale <code> once per locale you ship, or at least for the locales the divergent key appears in.

Limits

  • One locale per run. The comparison is against a single reference locale; divergence in another locale is invisible until you run it for that locale.
  • Locale files only. No source code is scanned. A collision is a statement about definitions, not about whether either definition is used — pair it with orphan detection for that.
  • Needs app-to-layer edges. 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, with a different message, when no app consumes more than one locale-backed layer — a single-layer project has nothing to collide.
  • Leaf keys only. A key defined as an object in one layer and a string in another is compared where both sides have leaves; the shape mismatch itself is not reported.
  • No CI gate. find-duplicates reports and exits zero. To fail a pipeline on collisions, read summary.totalCollisions or summary.divergentCount from the report yourself.

Next: misplaced usages — the other cross-layer finding, and the one that is never removed.

Copyright © 2026