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 singlemessages/folder in a one-app React or Vue project — one layer, which is the common case
Every layer the tool resolves has three parts:
| Part | Means |
|---|---|
path | The locale directory itself — where the JSON or PHP files live |
layer | The layer's 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 |
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:
| Framework | Layers |
|---|---|
| Nuxt | One 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 |
| Laravel | One layer, named root, rooted at the project directory |
| Vue | One layer, named root |
| React / Next.js | One layer, named root |
| Generic | One 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:
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:
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
layerRulesororphanScanentry keyed by the old name stops matching.orphanScanwarns on stderr when a key matches no detected layer;layerRulesdoes 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.