The Consumer Graph
What the Graph Is
The consumer graph is the set of edges from apps to the layers they consume. An app contributes three things to it:
| Field | Means |
|---|---|
name | The app's name, as it appears in reports |
rootDir | The app's code root |
layers | The layers it consumes, in precedence order — earlier entries override later ones at runtime |
Two terms come out of those edges and are used in every report:
- canonical layers — every layer that owns its locale directory, aliases excluded
- shared layers — canonical layers consumed by more than one app
The whole graph derives from configuration you can print with
the-i18n-cli detect.
Where the Edges Come From
What Counts as Usage
Usage counts only from the code of the apps that consume the layer. A key
lives in one layer, and at runtime only apps that consume that layer can resolve
it — so a reference to checkout.cart.title inside an admin app that does not
consume the checkout layer is evidence of a bug, not evidence that the key is
needed.
A layer's scope is therefore its own root directory, plus the root directory of
every app that consumes it, plus any part of the repository that lies outside
every app — a monorepo root holding scripts/ or tools/ counts for every
layer, so code outside all apps never turns into a false orphan.
Reading scanScope
remove-orphans and find_orphan_keys report the effective scope per layer,
relative to the project directory, so the scoping is auditable rather than
implied:
{
"scanScope": {
"root": [".", "app-admin", "app-shop"],
"app-admin": ["app-admin"],
"app-shop": ["app-shop"]
}
}
Read it as: the shared root layer was checked against both apps and the
repository root, while each app-private layer was checked against its own app
only. A key defined in app-admin and referenced only from app-shop is
outside app-admin's scope — that is a
misplaced usage, reported separately and never
removed.
When Scope Is the Whole Project
Scoping widens a layer's scope rather than narrowing it wrongly. With no app information in the config, with a layer no app consumes, or in a single-app project, every layer is checked against the whole project — the same behavior as an unscoped scan.
scanDirs is the manual override, on the find_orphan_keys and
remove_orphan_keys MCP tools and on the same functions in the programmatic
API: all layers are then checked against one combined usage set from those
directories, and misplaced-usage detection is off. It is not a CLI flag — from
the terminal, scoping is derived, and the way to narrow a run is --layer.
Per-Layer Scanning
Layers are the unit of work everywhere:
the-i18n-cli remove-orphans --layer uichecks one layer, against that layer's scope. Without--layer, every non-alias layer is checked, each against its own scope, in one pass.- Reports are keyed by layer:
orphanKeys,uncertainKeysandremovedare all maps from layer name to keys, andfind_orphan_keysaddssummary.layersCheckednaming the layers that ran. orphanScanin the project config is keyed by layer name too, so a family of runtime-built keys can be declared dynamic in the layer that owns it:
import { defineI18nKitConfig } from '@the-i18n-kit/cli/config'
export default defineI18nKitConfig({
orphanScan: {
ui: { ignorePatterns: ['ui.icons.*'] },
},
})
A key in orphanScan that matches no detected layer silently applies to
nothing, so the CLI warns on stderr and lists the layer names it did detect.
Limits
- The graph is only as good as what the project declares. An app that consumes a layer through a mechanism the adapter cannot see is missing an edge, and its usages will read as misplaced.
- Scope is directory-based. Two apps sharing one root directory cannot be told apart.
- Layer precedence comes from the order an app lists its layers. Where two apps disagree about the order of the same two layers, the first one seen wins — which matters for duplicate detection, not for scoping.
Next: the shared library scenario, where all of this earns its keep.