Monorepos

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:

FieldMeans
nameThe app's name, as it appears in reports
rootDirThe app's code root
layersThe 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

Consumption edges are derived today only for Nuxt projects, where each app reports the layers it extends and the apps are merged into one project config. The Laravel, Vue, React/Next and generic adapters each resolve a single app that consumes all of their layers. On those stacks the scoping below is still applied, but every layer's scope is the whole project — the same behavior as an unscoped scan. You get per-layer reports and duplicate detection; you do not get app-scoped usage evidence, because nothing in the project declares which app consumes what.

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 ui checks 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, uncertainKeys and removed are all maps from layer name to keys, and find_orphan_keys adds summary.layersChecked naming the layers that ran.
  • orphanScan in 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:
i18n-kit.config.ts
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.

Copyright © 2026