Layer-Aware Missing Keys
layerAware() makes "does this key exist?" mean "does it exist for this
app" in the editor. It generates flat-config blocks that point
@intlify/eslint-plugin-vue-i18n's no-missing-keys rule at exactly the
locale catalogs each app consumes — derived from the kit's project detection
when ESLint starts, so the blocks cannot go stale when a layer is added. A
layer here is a scoped locale directory — a Nuxt layer, a workspace
package, a shared UI library, or one app in a monorepo; see
Layers.
Off-the-shelf i18n lint tooling models one flat catalog. In a layered monorepo that answers wrongly in both directions: a key another app owns looks fine, and cross-layer overrides look like duplicates — measured at 2,620 false duplicate warnings on one repo. Scoping the settings per app is what removes both.
Requirements
The intlify peer must be installed alongside the plugin:
pnpm add -D @intlify/eslint-plugin-vue-i18n
layerAware() configures the intlify plugin rather than reimplementing it,
and it runs the-i18n-cli's project detection (a dependency of the plugin,
installed with it) to learn the layer graph — one definition of scope for
lint and scan. A missing peer throws at config load with the install command
in the message.
Usage
The factory is async; flat config supports top-level await:
import i18nKit, { layerAware } from '@the-i18n-kit/eslint-plugin-vue'
export default [
...i18nKit.configs.recommended,
...await layerAware(),
]
Run ESLint from the repo root — detection runs from process.cwd() unless
projectDir says otherwise.
| Option | Default | What it does |
|---|---|---|
projectDir | process.cwd() | Where project detection runs, and the base the generated globs are relative to. |
referenceLocaleFile | <defaultLocale>*.json | Glob for the reference-locale file inside each locale directory. |
What It Generates
Three kinds of ordinary flat-config entries, no override machinery:
- A rules block enabling
@intlify/vue-i18n/no-missing-keysas an error over**/*.vue,**/*.ts,**/*.js,**/*.mjs,**/*.tsx,**/*.jsx. - A root settings block pointing
settings['vue-i18n'].localeDirat the root layers' catalogs — what code outside any app subtree sees. - One settings block per app, scoped to that app's directory, listing the root catalogs plus the app's own.
So a key that exists only in app-admin's catalog errors when used from
app-portal, and a root-layer key passes everywhere. Later entries win in
flat config, which is also how you override anything the factory emits — add
your own block after the spread.
Existence is checked against the reference locale only. A key defined
solely in another locale is itself a defect, and the-i18n-cli missing is the
surface that reports it with layer semantics — the lint does not restate it.
no-missing-keys flags call
sites whose keys check classifies as uncertain — overlapping dynamic
patterns, $te existence checks. That is correct per-call-site behavior: the
editor questions what CI merely withholds judgment on.Zero Config on Nuxt
With @the-i18n-kit/nuxt and @nuxt/eslint both registered, and the plugin
plus its intlify peer installed, there is nothing to write: when @nuxt/eslint
generates its config, the kit's module contributes the recommended preset
and a layerAware() call into the generated withNuxt() config. The factory
still runs its own detection, so a monorepo rooted at that app comes out
scoped per app.
The module decides per build what to do:
| Decision | When | Effect |
|---|---|---|
| Inject | The kit config lives at this app (or nowhere) | Preset and factory land in the generated config |
| Defer | A kit config exists above the app | Nothing is injected; the module logs a hint to spread ...await layerAware() in the workspace root's ESLint config instead |
| Skip | The plugin or the intlify peer is not installed | Nothing is injected, silently — not adopting the lint surface is not an error |
The defer case exists because a kit config above the app means a larger
workspace owns linting: injecting into a sub-app's generated config would
produce blocks nobody imports at best, wrong scopes at worst. The logic stays
in layerAware() either way — the module only decides where the call belongs.
Limits
- The generated settings pin
messageSyntaxVersion: '^9.0.0'— the vue-i18n v9+ message format. - Diagnostics are intlify's: a missing key reads "missing" without naming the layers that were searched or the app that owns it.
- The intlify plugin's catalog cache expires on a timer rather than on file mtime, so a just-added key can keep erroring in the editor until the cache turns over.
- Detection runs once at config load. A layer added while the editor is open needs an ESLint server restart to be seen.