ESLint Plugin

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:

eslint.config.mjs
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.

OptionDefaultWhat it does
projectDirprocess.cwd()Where project detection runs, and the base the generated globs are relative to.
referenceLocaleFile<defaultLocale>*.jsonGlob for the reference-locale file inside each locale directory.

What It Generates

Three kinds of ordinary flat-config entries, no override machinery:

  1. A rules block enabling @intlify/vue-i18n/no-missing-keys as an error over **/*.vue, **/*.ts, **/*.js, **/*.mjs, **/*.tsx, **/*.jsx.
  2. A root settings block pointing settings['vue-i18n'].localeDir at the root layers' catalogs — what code outside any app subtree sees.
  3. 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.

The lint is deliberately stricter than the scan. 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:

DecisionWhenEffect
InjectThe kit config lives at this app (or nowhere)Preset and factory land in the generated config
DeferA kit config exists above the appNothing is injected; the module logs a hint to spread ...await layerAware() in the workspace root's ESLint config instead
SkipThe plugin or the intlify peer is not installedNothing 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.
Copyright © 2026