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 pointing @intlify/eslint-plugin-vue-i18n's no-missing-keys at exactly the locale catalogs each app consumes, derived from the kit's project detection when ESLint starts. Off-the-shelf i18n lint tooling models one flat catalog, which in a layered monorepo answers wrongly in both directions: a key another app owns looks fine, and cross-layer overrides look like duplicates.

Usage

The intlify peer must be installed alongside the plugin:

pnpm add -D @intlify/eslint-plugin-vue-i18n

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 repository root — detection runs from process.cwd() unless projectDir says otherwise. A missing peer throws at config load with the install command in the message.

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:

  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 how you override anything the factory emits.

Existence is checked against the reference locale only. A key defined solely in another locale is a different defect, and the-i18n-cli missing is the surface that reports it.

The lint is deliberately stricter than the scan: no-missing-keys flags call sites whose keys check classifies as uncertain. That is correct per-call-site behavior — the editor questions what CI withholds judgment on.

With @nuxt/eslint

@nuxt/eslint generates the config withNuxt() returns. The addon exported at @the-i18n-kit/eslint-plugin-vue/nuxt contributes the recommended preset and a layerAware() call to it, so the generated config carries the layer-aware lint and you write no ESLint config yourself:

nuxt.config.ts
import { i18nKitEslintAddon } from '@the-i18n-kit/eslint-plugin-vue/nuxt'

export default defineNuxtConfig({
  modules: ['@nuxt/eslint'],
  hooks: {
    'eslint:config:addons': addons => addons.push(i18nKitEslintAddon()),
  },
})

rootDir defaults to the directory Nuxt reads its config from, and the factory still runs its own detection, so a monorepo rooted at that app comes out scoped per app.

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 contributed; the addon warns to spread ...await layerAware() in the workspace root's ESLint config instead. Pass onDefer to route that message elsewhere

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 addon only decides where the call belongs.

The generated settings pin messageSyntaxVersion: '^9.0.0', and the diagnostics are intlify's: a missing key reads "missing" without naming the layers searched. Detection runs once at config load, and the intlify catalog cache expires on a timer, so a just-added key or layer can keep erroring until the ESLint server is restarted.

Copyright © 2026