Layers and the Consumer Graph
What a Layer Is
A layer is a scoped locale directory together with the code that owns it —
a workspace package that ships its own locales/, a shared UI library, one app
in a monorepo, a Nuxt layer other apps extend, or a Laravel lang/ directory.
Each resolved layer has three parts:
| Part | Means |
|---|---|
path | The locale directory — where the JSON, YAML or PHP files live |
layer | The name, used by every --layer flag and in every report |
layerRootDir | The code root that owns the directory — the subtree scanned for usages of its keys |
Where Layer Names Come From
| Framework | Layers |
|---|---|
| Nuxt | One per entry in Nuxt's resolved layer list that has a locale directory, for every app found. Named from the directory path relative to the project root |
| Laravel, React / Next.js | One layer, named root |
| Generic | One per entry in localeDirs. A string entry is named default; an object entry { path, layer } carries the name you give it. A probed directory is one layer, named default |
Outside Nuxt, declare a multi-layer project with localeDirs:
import { defineI18nKitConfig } from '@the-i18n-kit/cli/config'
export default defineI18nKitConfig({
defaultLocale: 'en',
localeDirs: [
{ path: 'packages/ui/locales', layer: 'ui' },
{ path: 'apps/checkout/locales', layer: 'checkout' },
{ path: 'apps/admin/locales', layer: 'admin' },
],
})
A path that does not exist is a configuration error. Renaming a directory renames
the layer, so any layerRules or orphanScan entry keyed by the old name stops
matching.
Where one directory is reachable under two names, the kit deduplicates by real path: one entry owns it, the others become aliases. Reads resolve through an alias; anything that writes or could delete rejects an alias name and tells you the owning layer.
Which Apps Consume Which Layers
The consumer graph is the set of edges from apps to the layers they consume.
Each app has a name and the layers it consumes in precedence order — earlier
entries override later ones at runtime. A canonical layer owns its locale
directory; a shared layer is one
more than one app consumes.
On Nuxt the edges are the layers each app extends. Everywhere else they are
inferred from the package manager workspace: packages come from packages: in
pnpm-workspace.yaml or workspaces in the root package.json, a package
holding a locale directory owns that layer, and the dependencies,
devDependencies and peerDependencies edges between those packages are the
consumption edges. A package no other one depends on becomes an app, and consumes
its own layers plus those of its transitive dependencies. Outside a workspace, or
where fewer than two apps come out, one app consumes every layer.
consumerGraph: 'off' turns the inference off.
Declaring apps overrides both:
import { defineI18nKitConfig } from '@the-i18n-kit/cli/config'
export default defineI18nKitConfig({
apps: [
{ name: 'checkout', layers: ['checkout', 'ui'] },
{ name: 'admin', layers: ['admin', 'ui'] },
],
})
status reports the graph it ran on: consumedBy names the apps declaring each
layer, and summary.unconsumedLayers the scanned layers no app consumes — filled
only where the project has more than one app.
What Counts as Usage
Usage counts only from the code of the apps that consume the layer. At
runtime only those apps can resolve the key, so a reference to
checkout.cart.title in an admin app that does not consume the checkout layer
is evidence of a bug, not of a needed key.
A layer's scope is its own root, plus the root of every app that consumes it,
plus any part of the repository outside every app — so a monorepo root holding
scripts/ counts for every layer. The orphan scan reports the effective scope
per layer:
{
"scanScope": {
"root": [".", "app-admin", "app-shop"],
"app-admin": ["app-admin"],
"app-shop": ["app-shop"]
}
}
A key defined in app-admin and referenced only from app-shop is a
misplaced usage.
Scoping only ever widens: with no app information, a layer no app consumes, or a single app, a layer is checked against the whole project.
Working Per Layer
--layer <name> narrows a run to one layer, checked against that layer's own
scope; without it, every non-alias layer is checked in one pass. Reports are
keyed by layer, and so is orphanScan, which declares the key families a layer
builds at runtime:
import { defineI18nKitConfig } from '@the-i18n-kit/cli/config'
export default defineI18nKitConfig({
orphanScan: {
ui: { ignorePatterns: ['ui.icons.*'] },
},
})
A name in apps or orphanScan matching no detected layer applies to nothing,
so the CLI warns on stderr and lists the layers it did detect.
layerRules is the other per-layer key: guidance an agent reads, never routing
the tool performs. See the field reference.