Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
Version History
- "Init history"v9.3.112/08/2026
The content of this page was translated using an AI.
See the last version of the original content in EnglishIf you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
ESLint x OXLint Plugin
eslint-plugin-intlayer catches the kinds of i18n mistake TypeScript cannot:
- Hardcoded text that never made it into a dictionary.
- Dynamic calls that type-check and run, but that the Intlayer compiler cannot optimize.
- Dead content — dictionaries and fields nothing in the project reads (opt-in).
Unknown dictionary keys, unknown field paths and missing locales are already compile errors, so the plugin does not repeat them.
Installation
Copy the code to the clipboard
npm install --save-dev eslint-plugin-intlayerRequires ESLint 9 or later (flat config). ESLint 10 is supported.
Usage
The plugin runs in both ESLint and oxlint — the same rules, the same options.
Or spread a config and set the severities yourself:
Configs
Open the table in a modal to view all data content clearly
| Config | no-raw-text | static-dictionary-key | no-dynamic-field-access | enforce-adapter-import | no-unused-content |
|---|---|---|---|---|---|
recommended | warn | error | error | off | off |
strict | error (+ non-JSX literals) | error | error | error | off |
contract-only | off | error | error | off | off |
recommended keeps no-raw-text at warn on purpose: pointing it at an existing codebase surfaces every untranslated string at once, which should not break your build on day one.
enforce-adapter-import is off by default — enable it explicitly if you want it.
no-unused-content is off in every config, strict included. It is the one rule that reads your Intlayer configuration and walks your source files from disk, so turning it on should be a deliberate choice rather than something a preset does for you.
Rules
no-raw-text
Reports user-facing text that is not declared in a dictionary. It uses the same detection as intlayer extract, so brand names, CSS classes and technical identifiers are ignored.
Copy the code to the clipboard
// ✗ Reported<h1>Welcome to our documentation</h1><input placeholder="Enter your email address" />// ✓ Fineconst { title } = useIntlayer("home");<h1>{title}</h1>Content declaration files (*.content.ts, …) are skipped.
To fix a whole file at once, run npx intlayer extract and let the compiler move the strings into a dictionary for you.
Options
static-dictionary-key
Requires the dictionary key to be a string literal.
The compiler can only pre-load a dictionary when it can read the key directly at the call site. With a computed key it silently skips the optimization and bundles every dictionary instead.
Copy the code to the clipboard
// ✗ ReporteduseIntlayer(dictionaryKey);useIntlayer(`home-${suffix}`);getTranslations({ namespace: page });// ✗ A variable is still not a literalconst key = "home";useIntlayer(key);// ✓ FineuseIntlayer("home");getTranslations({ namespace: "home" });This applies to useIntlayer, getIntlayer and every compat adapter (useTranslation, useTranslations, formatMessage, <FormattedMessage id>, <Trans i18nKey>, …).
no-dynamic-field-access
Requires the field you read from a dictionary to be statically known.
The compiler removes fields it does not see used. A computed access is invisible to it, so the read can return undefined at runtime.
Copy the code to the clipboard
// ✗ Reportedconst content = useIntlayer("home");content[fieldName];const t = useTranslations("home");t(messageKey);// ✓ Finecontent.title;content["title"];content.items[0];t("hero.title");enforce-adapter-import
Prefers the @intlayer/* compat adapter over the original package. The original only resolves to Intlayer when the bundler alias is configured; the adapter always does. Autofixable with --fix.
Copy the code to the clipboard
// ✗ Reportedimport { useTranslation } from "react-i18next";import { getTranslations } from "next-intl/server";// ✓ Fineimport { useTranslation } from "@intlayer/react-i18next";import { getTranslations } from "@intlayer/next-intl/server";no-unused-content
Off by default. Reports content nothing in your project reads, plus dictionary keys declared in more than one place.
Copy the code to the clipboard
export default { key: "home", // ✗ Reported when no caller anywhere asks for "home" content: { title: t({ "en-GB": "Title", en: "Title" }), // ✗ Reported when nothing reads `hero` hero: { subtitle: t({ "en-GB": "Subtitle", en: "Subtitle" }), }, },};Unlike the other rules, this one cannot answer from the file in front of it — a field is unused only relative to the whole project. On the first content declaration of a lint run it loads your Intlayer configuration, globs the source files that configuration declares (build.traversePattern, compiler.transformPattern) and runs the same usage analyser that powers @intlayer/lsp and the "unused" strikethrough in the VS Code extension. The result is cached for cacheTtl milliseconds, so the scan happens once per run rather than once per file.
Options
Lower cacheTtl when you lint from a long-lived editor server and want your edits reflected sooner; set baseDir when a single lint run spans several Intlayer projects in a monorepo.
It errs towards silence. A false positive here deletes a translation, so nothing is reported when the dictionary is consumed in a way the analysis cannot follow: the content object passed on as a whole, a translator function bound from it (const t = useTranslations("home")), a declaration reached through a direct import (useDictionary(myDictionary)), anest()from another dictionary, or a field list made non-exhaustive by a spread. Single-file components (.vue,.svelte,.astro) count as using every field of the dictionaries they mention, because their script blocks are not parsed here.
reportDuplicateKeys reads the unmerged dictionaries the build writes under .intlayer/, so it stays quiet until the project has been built at least once. Two declarations sharing a key are merged, which is a legitimate pattern — the report exists because a field defined on both sides silently keeps only one of the two values.
The analyser is loaded from @intlayer/lsp, which ships as ESM. The rule therefore needs a Node version that can require() an ES module — Node 20.19+ or 22.12+. On anything older it reports nothing rather than failing the lint run.
Frameworks
Every rule works across all Intlayer integrations, including inside Vue, Svelte and Angular templates. You only need to tell ESLint which parser reads each file type.
Open the table in a modal to view all data content clearly
| Framework | Files | Parser |
|---|---|---|
| React, Preact, Solid, Lit | .jsx .tsx | typescript-eslint |
| Next.js | .jsx .tsx | typescript-eslint |
| Vue, Nuxt | .vue | vue-eslint-parser |
| Svelte, SvelteKit | .svelte | svelte-eslint-parser |
| Angular | .ts | typescript-eslint |
| Angular templates | .component.html | @angular-eslint/template-parser |
| Astro | .astro | astro-eslint-parser |
Install only the parsers your project needs.
Known limitation. In Vue and Angular templates, an expression such as{{ content[key] }}is not checked byno-dynamic-field-access. Dynamic reads written in the script block are caught normally.