Monorepos

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:

PartMeans
pathThe locale directory — where the JSON, YAML or PHP files live
layerThe name, used by every --layer flag and in every report
layerRootDirThe code root that owns the directory — the subtree scanned for usages of its keys

Where Layer Names Come From

FrameworkLayers
NuxtOne 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.jsOne layer, named root
GenericOne 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:

i18n-kit.config.ts
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:

i18n-kit.config.ts
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:

i18n-kit.config.ts
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.

Copyright © 2026