Monorepos

Layers

What a Layer Is

A layer is a scoped locale directory together with the code that owns it. It is the unit everything else in this section is built on, and it is not a framework-specific idea:

  • a workspace package that ships its own locales/ folder
  • a shared UI library with translations for its own components
  • an app in a monorepo, with translations only that app uses
  • a Nuxt layer that other Nuxt apps extend
  • a Laravel lang/ directory, or a single messages/ folder in a one-app React or Vue project — one layer, which is the common case

Every layer the tool resolves has three parts:

PartMeans
pathThe locale directory itself — where the JSON or PHP files live
layerThe layer's 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

layerRootDir is what turns a folder of JSON into a layer. It is the reason a key defined in a shared library can be checked against the library's own code plus the apps that consume it, rather than against the whole repository.

Where Layer Names Come From

Layer names are derived config: read out of the framework's own setup on every run rather than written down twice. What each adapter derives:

FrameworkLayers
NuxtOne layer per entry in Nuxt's resolved layer list that has a locale directory, for every app found. Names come from the directory path relative to the project root
LaravelOne layer, named root, rooted at the project directory
VueOne layer, named root
React / Next.jsOne layer, named root
GenericOne layer per entry in localeDirs. A string entry is named default; an object entry { path, layer } carries the name you give it

So a multi-layer project is derived automatically on Nuxt, and declared by hand elsewhere. To split a non-Nuxt monorepo into named layers, list the directories yourself:

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' },
  ],
})

Every path must exist when the tool runs; a missing one is a configuration error rather than an empty layer.

Inspecting the Layers You Have

Two commands answer "what did it actually find":

the-i18n-cli list-dirs

Lists each locale directory with its layer name, file count and top-level key namespaces — and marks alias entries with the layer they belong to.

the-i18n-cli detect

Prints the whole resolved configuration: locales, default locale, localeDirs, apps and the project config that was loaded. This is the input to everything on the following pages, so when a report surprises you, start here.

Aliases: Two Names, One Directory

In a monorepo the same locale directory is often reachable under more than one name — an app extends a package that another app also extends, and each app reports the directory in its own terms. The tool deduplicates by real path: one entry becomes the owner, the others become aliases carrying aliasOf.

Aliases are excluded from operations that run over all layers, so a directory is never scanned, translated or written twice. Reads resolve through an alias to the owner's files, so get and list-dirs still answer for the alias name. Anything that writes — or that could delete, like orphan removal — rejects an alias name with an error naming the source layer to use instead.

Telling an Agent Where Keys Belong

Layer names are only half the problem: an agent adding a feature has to pick one. layerRules in the project config is where you write that decision down once, in prose, instead of restating it in every prompt:

i18n-kit.config.ts
import { defineI18nKitConfig } from '@the-i18n-kit/cli/config'

export default defineI18nKitConfig({
  layerRules: [
    {
      layer: 'ui',
      description: 'Design-system strings shared by every app',
      when: 'The string belongs to a component in packages/ui',
    },
    {
      layer: 'checkout',
      description: 'Checkout-only copy',
      when: 'The string appears only in apps/checkout',
    },
  ],
})

Each rule is a layer, a description and a when. The rules are handed to the agent in two places: the resolved project config that discover returns, and the add-feature-translations prompt, which renders them as a LAYER RULES block and tells the agent to pick a layer from them when the caller did not name one.

layerRules is guidance an agent reads, not routing the tool performs. Writes still name their target layer explicitly — write_translations requires layer, and so does the-i18n-cli write. A rule for a layer name that does not exist is not an error; it never matches anything.

Limits

  • One layer is one locale directory. A package with two locale directories is two layers, and there is no grouping above the layer.
  • Layer names are derived from directory names on every framework except generic, so renaming a directory renames the layer — and any layerRules or orphanScan entry keyed by the old name stops matching. orphanScan warns on stderr when a key matches no detected layer; layerRules does not.
  • A layer with no locale directory (a Nuxt layer that ships code but no translations) never appears in localeDirs. It still contributes to the consumer graph, which the next page covers.

Next: the consumer graph — which apps consume which layers, and why that decides whether a key counts as used.

Copyright © 2026