A unified, high-performance i18next CLI.
236
stars
937
commits
TypeScript
primary language
Sep 10, 2026
updated
A unified, high-performance i18next CLI toolchain, powered by SWC.
By default,
i18next-clionly extracts translation keys from JavaScript and TypeScript files (.js,.jsx,.ts,.tsx). To extract from other file types (such as.pug,.vue,.svelte, etc.), you must use or create a plugin. Specifying additional file extensions in theextract.inputconfig is not sufficient on its ownβplugins are required for non-JS/TS formats. See the Plugin System section for details and examples.
i18next-cli is a complete reimagining of the static analysis toolchain for the i18next ecosystem. It consolidates key extraction, type safety generation, locale syncing, linting, and cloud integrations into a single, cohesive, and blazing-fast CLI.
π Try it Now - Zero Config!
You can get an instant analysis of your existing i18next project without any configuration. Just run this command in your repository's root directory:
npx i18next-cli statusOr find hardcoded strings:
npx i18next-cli lint
If you're looking for a managed backend to pair with i18next-cli, take a look at Locize β i18next-cli already ships with locize-download, locize-sync, and locize-migrate commands. Built by the same team behind i18next, with CDN delivery, AI translation, review workflow, and no redeploys for copy changes.
i18next-cli is built from the ground up to meet the demands of modern web development.
useTranslation('ns1', { keyPrefix: '...' }), getFixedT, and aliased t functions, minimizing the need for manual workarounds.--watch modes, CLI output, and a migration from legacy tools.i18next-parser configurations.npm install --save-dev i18next-cli
Zero-to-localized in one command: starting from an app with hardcoded strings (e.g. generated with v0, Lovable, Bolt or Cursor)? Run
npx i18next-cli localizeβ it detects your setup, wraps hardcoded strings int()calls, extracts keys, connects to Locize and AI-translates your app. See thelocalizecommand. Working with an AI coding agent (Claude Code, Cursor, ...)?npx i18next-cli localize --print-agent-promptprints the same flow as a copy-paste agent runbook. The steps below are the manual path.
Create a configuration interactively:
npx i18next-cli init
Or manually create i18next.config.ts in your project root:
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{js,jsx,ts,tsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
});
Get an overview of your project's localization health:
npx i18next-cli status
npx i18next-cli extract
npx i18next-cli types
initInteractive setup wizard to create your configuration file.
npx i18next-cli init
Options:
--ci: Skip the browser launch when a backend (e.g. Locize) is selected;
the signup URL is printed instead. Useful for scripted runs. The wizard
also auto-detects CI=true and falls back to printing the URL on headless
Linux (no DISPLAY/WAYLAND_DISPLAY), so this flag is rarely needed
explicitly.--inlang: Also scaffold an inlang project
(project.inlang/settings.json) so inlang tooling β the
Sherlock VS Code
extension, the Fink web editor for translators,
and the Paraglide
compiler β works directly on your translation files. Skips the
corresponding wizard question.The wizard asks for the config file type, locales, source-file glob, output path, and finally "Translation backend?" with three options:
locize
block into the generated config so locize-sync
works out of the box. The API key prompt can be left empty (read-only
mode); add it later via a LOCIZE_API_KEY environment variable.The wizard then offers to set up inlang tooling (default: no β or pass
--inlang to skip the question). If accepted, it scaffolds a
project.inlang/settings.json that points the
inlang i18next plugin
at your existing translation files: baseLocale/locales come from your
config, and pathPattern is derived from extract.output (the namespaced
object form when your layout uses {{namespace}}, with namespaces discovered
from the primary language's files; a plain pattern otherwise). It also adds
the Sherlock extension to .vscode/extensions.json recommendations (merging
comment-aware, never clobbering existing entries). Your i18next JSON files
remain the single source of truth β inlang tools read and write them in
place, so there is no second catalog to drift. An existing
project.inlang/settings.json is never overwritten; re-running init is
safe. Requires JSON resource files. The plugin is pinned to an exact verified
version (@inlang/plugin-i18next@6.2.1) β bump the modules URL in
settings.json to pick up newer plugin releases. Only settings.json is
scaffolded by design: project.inlang/ is the
unpacked (git-friendly) project
form, and inlang tools generate and manage its remaining files (.gitignore,
README.md, cache/) on first use β so expect a few new files there after
opening the project with Sherlock or Paraglide.
extractParses source files, extracts keys, and updates your JSON translation files.
npx i18next-cli extract [options]
Options:
--watch, -w: Re-run automatically when files change--ci: Exit with non-zero status if any files are updated (for CI/CD)--dry-run: Does not change any files - useful in combination with --ci (for CI/CD)--sync-primary: Sync primary language values with default values from code--sync-all: Sync primary language values with default values from code AND clear synced keys in all other locales (implies --sync-primary)--trust-derived: When used with --sync-primary or --sync-all, also trust defaults inferred from keys such as t('Hello') or keyPrefix-derived values. This keeps the default sync behavior strict unless you opt in.--with-types: After extraction (and on every re-run in --watch mode), regenerate the TypeScript definitions whenever translation files changed. Avoids the need to run extract -w and types -w as two separate processes.--quiet: Suppress spinner and non-essential output (for CI or scripting)All commands that show progress spinners (extract, types, lint, sync) now support:
--quiet flag to silence spinner and non-essential output (for CI, scripting, or log capture)CLI Example:
npx i18next-cli extract --quiet
Programmatic Example:
import { runExtractor } from 'i18next-cli';
const logger = {
info: (msg) => myLogStream.write(msg + '\n'),
warn: (msg) => myWarnStream.write(msg + '\n'),
error: (msg) => myErrStream.write(msg + '\n'),
};
await runExtractor(config, { quiet: false, logger });
If you pass a logger, spinner output and all progress/info messages are routed to your logger instead of the interactive spinner.
Examples:
# One-time extraction
npx i18next-cli extract
# Watch mode for development
npx i18next-cli extract --watch
# CI mode (fails if files changed)
npx i18next-cli extract --ci
# Sync primary language with code defaults
npx i18next-cli extract --sync-primary
# Sync primary and clear synced keys in all other locales
npx i18next-cli extract --sync-all
# Sync using explicit defaults plus inferred key-derived defaults
npx i18next-cli extract --sync-all --trust-derived
# Combine options for optimal development workflow
npx i18next-cli extract --sync-primary --watch
# Keep TypeScript definitions in sync from a single process (no separate `types -w` needed)
npx i18next-cli extract --watch --with-types
status [locale]Displays a health check of your project's translation status. Can run without a config file. Exits with a non-zero status code when translations are missing.
The primary language is checked too: any key used in your code but absent from the primary language's translation files (a typo, or extract was never run) is reported and causes a non-zero exit code. Empty-string placeholders written by extract are considered present and do not fail the check. Running npx i18next-cli status <primaryLanguage> shows the absent keys in detail.
Options:
--namespace <ns>, -n <ns>: Filter the report by a specific namespace.--hide-translated: Hide already translated keys in the detailed view, showing only missing translations.status.ignoreKeys (config): glob patterns for keys that should not be reported (and don't fail the check), e.g. keys that are intentionally left empty in some locales. Optional ns: prefix (common:help.*-href). Only affects status.--unused: Report only unused translation keys β keys present in your translation files that are no longer used in your source code (i.e. what extract with removeUnusedKeys would delete). Never modifies any files and exits with a non-zero status code when unused keys are found, so it can serve as a dedicated CI check alongside the regular missing-translations check. Note that static analysis cannot detect dynamically constructed keys (e.g. t(`error.${code}`)); to find keys that are truly unused at runtime, see find unused translations with locize.Usage Examples:
# Get a high-level summary for all locales and namespaces
npx i18next-cli status
# Get a detailed, key-by-key report for the 'de' locale
npx i18next-cli status de
# Get a summary for only the 'common' namespace across all locales
npx i18next-cli status --namespace common
# Get a detailed report for the 'de' locale, showing only the 'common' namespace
npx i18next-cli status de --namespace common
# Show only the untranslated keys for the 'de' locale
npx i18next-cli status de --hide-translated
# Combine options to see only missing translations in a specific namespace
npx i18next-cli status de --namespace common --hide-translated
# Report only unused keys across all locales (read-only, exits 1 when any are found)
npx i18next-cli status --unused
# Report only unused keys in the 'en' files β e.g. as a separate CI check
npx i18next-cli status en --unused
The detailed view provides a rich, at-a-glance summary for each namespace, followed by a list of every key and its translation status.
Example Output (npx i18next-cli status de):
Key Status for "de":
Overall: [β β β β β β β β β β β β β β β β β β β β ] 100% (12/12)
Namespace: common
Namespace Progress: [β β β β β β β β β β β β β β β β β β β β ] 100% (4/4)
β button.save
β button.cancel
β greeting
β farewell
Namespace: translation
Namespace Progress: [β β β β β β β β β β β β β β β β β‘β‘β‘β‘] 80% (8/10)
β app.title
β app.welcome
β app.description
...
typesGenerates TypeScript definitions from your translation files for full type-safety and autocompletion.
Note: When
extract.defaultNSis set tofalse, the generateddefaultNSis derived from your resource files (i18next's type system cannot expressdefaultNS: false). Adjust the generatedi18next.d.tsif your runtime i18next config uses a different default namespace.
npx i18next-cli types [options]
Options:
--watch, -w: Re-run automatically when translation files change--ci: Exit with a non-zero status if the generated TypeScript definitions are out of date (check-only, writes nothing). Cannot be combined with --watch.--quiet, -q: Suppress spinner and non-essential output (for CI or scripting)syncSynchronizes secondary language files against your primary language file, adding missing keys and removing extraneous ones.
npx i18next-cli sync
lintAnalyzes your source code for internationalization issues. Can run without a config file.
npx i18next-cli lint
What it checks:
Hardcoded strings (error) β user-facing text in JSX elements and attributes that isn't wrapped in t()/<Trans>.
Interpolation parameters (error) β mismatches between {{placeholders}} in a translation and the params passed to t() (missing or unused). Toggle with lint.checkInterpolationParams (default: true).
String concatenation (warning by default) β translated strings glued together with +, or a sentence split across multiple adjacent translations (<Trans> components and/or {t()} expressions rendered as siblings). This breaks in languages that reorder or inflect the pieces; use a single key with placeholders instead. Configure with lint.checkConcatenation: 'warn' / true (default) reports it without failing the run, 'error' makes it fail (exit non-zero, useful for CI), and 'off' / false disables it.
// β οΈ Flagged β word order can't be translated
t('greeting') + ', ' + name
<p><Trans>Hello</Trans> and <Trans>World</Trans></p>
<p><Trans>new</Trans>{t('cat')}</p>
// β
Preferred β one key, placeholders
t('greeting', { name }) // "Hello, {{name}}"
<Trans i18nKey="greeting">Hello {{name}}</Trans>
Punctuation concatenation (off by default) β punctuation glued onto a translation, e.g. <label><Trans>Email</Trans>:</label> or <div>- <Trans>item</Trans></div>. Punctuation spacing and form differ across languages (French needs a narrow no-break space before :, CJK uses fullwidth οΌ, RTL reorders), so it belongs inside the translation or in semantic markup (a real <ul>/<li> for bullets). This is opt-in, since keeping punctuation out of a translation is often deliberate. Enable with lint.checkPunctuationConcatenation: 'warn', 'error', or 'off' / false (default).
// β οΈ Flagged when enabled
<label><Trans>Email</Trans>:</label>
<div>- <Trans>item</Trans></div>
// β
Preferred
<label><Trans i18nKey="emailLabel">Email:</Trans></label>
<ul><li><Trans>item</Trans></li></ul>
The linter exits non-zero only when it finds errors; a run with only warnings succeeds. Individual spots can be excused with the i18next-instrument-ignore directive.
To suppress warnings for code you intentionally aren't translating yet, use the i18next-instrument-ignore directive β the same comment recognized by the instrument command.
instrumentScans your source code for hardcoded user-facing strings and instruments them with i18next translation calls. This is useful for adding i18next instrumentation to an existing codebase that wasn't built with internationalization in mind. You can see this in action in this video or in this blog post.
β οΈ First-Step Tool: The
instrumentcommand uses heuristic-based detection and is designed as a first pass to identify and suggest transformation candidates. It will not catch 100% of cases, and you should expect both false positives and false negatives. Always review the suggested transformations carefully before committing them to your codebase. Think of it as an intelligent code assistant, not an automated compiler.
npx i18next-cli instrument
Options:
--dry-run: Preview changes without writing files to disk--interactive: Prompt for approval of each candidate string--namespace <ns>: Target a specific namespace for extracted keys-q, --quiet: Suppress spinner and outputWhat it transforms:
The instrument command detects four types of transformations:
Simple string β t() call:
// Before
const msg = 'Welcome back';
// After
const msg = t('welcomeBack', 'Welcome back');
Template literal (static only) β t() call:
// Before
const msg = `Welcome back`;
// After
const msg = t('welcomeBack', 'Welcome back');
Template literals with interpolation (e.g.
`Hello ${name}`) are skipped β they require manual wrapping.
JSX text β JSX expression with t():
// Before
<h1>Welcome back</h1>
// After
<h1>{t('welcomeBack', 'Welcome back')}</h1>
JSX mixed content β <Trans> component:
// Before
<p>Click <a href="/docs">here</a> to continue</p>
// After
<p><Trans i18nKey="clickHereLabel">Click <a href="/docs">here</a> to continue</Trans></p>
Namespace targeting:
Use --namespace <ns> to direct extracted keys into a specific namespace. When a non-default namespace is specified:
useTranslation('<ns>') with clean keysi18next.t('key', 'default', { ns: '<ns>' })--interactive mode you are prompted for the target namespacenpx i18next-cli instrument --namespace common
Custom scorer hook:
Override the built-in confidence heuristic via extract.instrumentScorer in your config. The function receives each candidate string and its context, and can:
null to force-skip the candidateundefined to fall back to the built-in heuristicexport default defineConfig({
// ...
extract: {
// ...
instrumentScorer: (content, { file, code, beforeContext, afterContext }) => {
// Skip strings that belong to your analytics domain
if (content.startsWith('track_')) return null;
// Boost strings in your UI layer
if (file.includes('/components/')) return 0.95;
// Fall back to built-in detection for everything else
return undefined;
}
}
});
What it skips (by design):
The instrumenter uses confidence heuristics to avoid transforming:
*.test.*, *.spec.*)ERROR_NOT_FOUND)console.log/warn/error argumentst() calls or <Trans> componentsexport const SETTINGS_SECTIONS = [
{ id: 'appearance', label: 'Appearance' }, // left untouched
]
A t() call there would be evaluated once, when the module is first imported β possibly before i18next
is initialized, and never again when the language changes. Move the text into a component, or expose the
registry as a hook that calls useTranslation() internally:
export const useSettingsSections = () => {
const { t } = useTranslation()
return [{ id: 'appearance', label: t('appearance', 'Appearance') }]
}
Auto-injection:
When transformations are applied, the command automatically:
import { useTranslation } from 'react-i18next' in React files (or import i18next from 'i18next' for non-React files)const { t } = useTranslation() into each React function component that contains transformed stringspackage.json dependencies (React, Next.js, Vue, etc.)useTranslation() hook style t() inside React components, or i18next.t() for utility / non-component codei18n.ts (or i18n.js for JS-only projects) initialization file if none exists, pre-configured with i18next-resources-to-backend to lazy-load your translation files via dynamic importsRecommended workflow:
Preview first: Always run with --dry-run to see what will change:
npx i18next-cli instrument --dry-run
Interactive mode for initial migration: Use --interactive to approve each candidate:
npx i18next-cli instrument --interactive
Review and commit: Check the changes, then commit to git before proceeding
Run extraction: After instrumentation, run extract to sync with translation files:
npx i18next-cli extract
Limitations:
The instrument command uses heuristic-based detection and has the following limitations:
t() calls without plural handling).Expected Workflow:
The intended usage pattern is:
--dry-run to preview all suggestions--interactive and carefully review each suggestion β consider using edit-key or skip liberallyextract to finalize translation filesi18next-instrument-ignoreBoth the lint and instrument commands honor an ignore comment so you can skip placeholder or intentionally-untranslated content. It works as a line or block comment, including the JSX {/* ... */} form, and comes in two variants:
| Directive | Scope |
|---|---|
i18next-instrument-ignore | The entire JSX element that begins on the next line β its opening tag, all nested children, and its closing tag. Falls back to a single line when the next line isn't a JSX element (e.g. a plain t() call). |
i18next-instrument-ignore-next-line | Only the single line immediately after the directive. |
// Suppress a whole element (including multi-line opening tags and nested children)
{/* i18next-instrument-ignore */}
<div
css={css`text-align: center;`}>
Hi, I'm Bob π
<p>This nested text is ignored too</p>
</div>
// Suppress just one line
{/* i18next-instrument-ignore-next-line */}
<p>Only this line is ignored</p>
// Also works for t() interpolation warnings in the linter
// i18next-instrument-ignore
const msg = t('Hello {{name}}!', { wrong: 'world' })
localizeOne command from hardcoded strings to a fully localized app: detect, instrument, extract, connect to Locize, AI-auto-translate, deliver. Built for taking a mono-lingual app (often AI-generated via v0/Lovable/Bolt/Cursor) to fully localized in one sitting.
npx i18next-cli localize
The command walks through six steps:
i18next.config.ts, or starts the init wizard if none exists.t() calls / <Trans> components (interactive by default β instrument is an assistant, review each change). Skipped automatically if your code can't be instrumented; a dirty git tree prompts for confirmation first.locize.projectId/locize.apiKey from your config or the LOCIZE_PROJECTID/LOCIZE_API_KEY environment variables; otherwise it opens the signup page and asks you to paste them (the one manual step). Any write-capable API key works: your target languages are created automatically on the first sync (locize-cli β₯ 12.3), and auto-translate + Quality Estimation are on by default for new Locize projects.--auto-translate, waits for the AI translations to arrive, downloads them, and prints the i18next-locize-backend CDN wiring snippet (so translation fixes go live without redeploying your app).Options:
--dry-run: Preview every step; nothing is written or pushed-y, --yes: Accept defaults; auto-approve instrumentation candidates (no per-string prompts)--ci: Non-interactive; never opens a browser or prompts. Instrumentation is skipped in CI (it rewrites source files and needs human review) unless combined with --yes--skip-instrument: Skip the code-instrumentation step (your code already calls t())--skip-translate: Sync to Locize but don't request AI auto-translation--skip-locize: Stop after extraction (local files only)--namespace <ns>: Target namespace for instrumented keys--update-values: Also update existing translation values on Locize--cdn-type <standard|pro>: Locize CDN endpoint type--print-agent-prompt: Print a copy-paste prompt for AI coding agents, then exit (see below)Behavior matrix:
| Step | interactive (default) | --yes | --ci | --dry-run |
|---|---|---|---|---|
| Instrument | per-string prompts | auto-approve | skipped (force with --yes) | candidate preview |
| Connect Locize | browser + paste credentials | same | env vars required, else exit 1 | report only |
| Sync + translate | runs | runs | runs | --dry forwarded |
| Poll + download | watches translations arrive | same | single download, no wait | skipped |
Safe to re-run: the command is idempotent. Already-wrapped strings are not re-instrumented, extraction is deterministic, and syncing never overwrites translations edited remotely (no --update-values unless you pass it; locize-cli's --reference-language-only default keeps target languages safe).
Next.js App Router: instrument injects
useTranslation(), which is client-only. Review the diff for server components β add'use client'or switch those to a server-sidet()pattern.
Non-React stacks (Vue, Svelte, β¦): the instrument step transforms React/JSX out of the box. For other stacks, add a plugin that covers your file type (community: i18next-cli-vue, i18next-cli-plugin-svelte β or write your own via the Plugin System instrumentOnLoad/onLoad hooks). With a matching plugin configured, localize runs the full flow; without one, the instrument step is skipped with guidance and the remaining steps (extract β Locize β auto-translate) still run.
Agent Skill (recommended): install the flow as a skill and your agent picks it up on its own, no copy-pasting:
npx skills add i18next/i18next-cli
Then just ask it to "add i18n to this project". See skills/i18next-localization β it ships in this repo, so it stays version-matched to the commands it drives.
Agent prompt: the same flow is also available as a copy-paste prompt for AI coding agents (Claude Code, Cursor, β¦):
npx i18next-cli localize --print-agent-prompt
This prints step-by-step instructions an agent can follow using the individual CLI commands β version-matched to your installed CLI, so it never drifts from what the supercommand does. Prefer the command output over the copy below, which is a snapshot for reference:
You are localizing this app with i18next + Locize. Execute these steps in order,
verifying each before continuing. Use `npx i18next-cli` for all commands.
1. Detect: confirm this is a React/Next.js project (check package.json).
- If Vue/Svelte: install a stack plugin (`i18next-cli-vue` /
`i18next-cli-plugin-svelte`) and add it to the `plugins` array of
i18next.config.ts, or write one via the plugin hooks
(instrumentOnLoad/onLoad) instead of wrapping strings manually.
- If the app uses inlang Paraglide (`@inlang/paraglide-js`), STOP β
instrumenting i18next calls would conflict; ask the user how to proceed.
2. Config: if no i18next.config.{ts,js} exists, run `npx i18next-cli init`
and answer the prompts (pick Locize as backend if the user wants managed
translations and AI auto-translate).
3. Instrument: run `npx i18next-cli instrument --dry-run` and review the
planned changes; then `npx i18next-cli instrument` to apply. Inspect the
git diff carefully: fix any t() wrapping inside Next.js *server components*
(add 'use client' or refactor to a server-side t() pattern). Commit.
4. Extract: run `npx i18next-cli extract`. Verify the locale JSON files were
written (check the extract.output path in the config).
5. Locize: ask the user for LOCIZE_PROJECTID and LOCIZE_API_KEY (they create
the project at https://www.locize.app/register?from=i18next_cli__agent-prompt
β any write-capable API key works; the target languages from
i18next.config.ts are created automatically on the first sync.
Auto-translation and quality estimation are enabled by default for new
projects; translations run once the project is subscribed or an AI/MT
provider is configured). Export both as environment variables.
6. Translate & deliver:
`npx i18next-cli locize-sync --auto-translate true`
then `npx i18next-cli locize-download` to pull the AI translations, and
`npx i18next-cli status` β confirm all languages are (near) 100%.
AI translation is asynchronous; if targets are still empty, wait a minute
and re-run locize-download.
7. Optionally switch runtime loading to i18next-locize-backend (CDN delivery,
so translation fixes go live without redeploying). NEVER put the API key
in client-side code β the CDN only needs the project ID.
migrate-configAutomatically migrates a legacy i18next-parser.config.js file to the new i18next.config.ts format.
npx i18next-cli migrate-config
# Using custom path for old config
npx i18next-cli migrate-config i18next-parser.config.mjs
βΉοΈ Coming from
i18next-parser? Note that i18next-cli requires Node.js >= 22 (i18next-parser still ran on Node 18/20), so CI images may need a runtime bump alongside the config migration.
rename-keySafely refactor translation keys across your entire codebase. This command updates both source files and translation files atomically.
npx i18next-cli rename-key <oldKey> <newKey> [options]
Options:
--dry-run: Preview changes without modifying any filesUsage Examples:
# Basic rename
npx i18next-cli rename-key "old.key" "new.key"
# With namespace prefix
npx i18next-cli rename-key "common:button.submit" "common:button.save"
# Preview changes without modifying files
npx i18next-cli rename-key "old.key" "new.key" --dry-run
# Refactor from mnemonic ID to meaningful key
npx i18next-cli rename-key "Invalid username or password" "login.form.invalid-credentials"
First-time setup: the easiest way to wire up Locize is to run
npx i18next-cli init and pick Locize at the "Translation backend?"
prompt β the wizard will open the signup page, ask for your Project ID
and API key, and write the locize block into your config for you. See
the init command for details.
Prerequisites: The locize commands require locize-cli to be installed:
# Install globally (recommended)
npm install -g locize-cli
Sync translations with the Locize translation management platform:
# Download translations from Locize
npx i18next-cli locize-download
# Upload/sync translations to Locize
npx i18next-cli locize-sync
# Migrate local translations to Locize
npx i18next-cli locize-migrate
Locize Command Options:
The locize-sync command supports additional options:
npx i18next-cli locize-sync [options]
Options:
--update-values: Update values of existing translations on locize--src-lng-only <true|false>: Check for changes in source language only (default: true). Pass --src-lng-only false to sync all languages--compare-mtime: Compare modification times when syncing--dry-run: Run the command without making any changes--auto-translate <true|false>: Trigger AI/MT auto-translation of newly synced keys. Requires auto-translation in your Locize project (enabled by default for new projects; runs once the project is subscribed or an AI/MT provider is configured)--auto-translate-review <true|false>: Route auto-translated segments through the review workflow for languages that have review enabled--auto-translate-languages <lng1,lng2>: Restrict auto-translation to these target languages (defaults to all)The same options can be set persistently in the locize block of your config:
export default defineConfig({
// ...
locize: {
projectId: '...',
apiKey: process.env.LOCIZE_API_KEY,
autoTranslate: true,
autoTranslateReview: false,
autoTranslateLanguages: ['de', 'fr'],
},
});
Note: auto-translation only fires when the reference language is updated, and the translation itself happens asynchronously on the Locize side β run
locize-download(or letlocalizewait for you) to pull the results.
Interactive Setup: If your locize credentials are missing or invalid, the toolkit will guide you through an interactive setup process to configure your Project ID, API Key, and version.
-c, --config <path> β Override automatic config detection and use the specified config file (relative to cwd or absolute). This option is forwarded to commands that load or ensure a config (e.g. extract, status, types, sync, locize-*).Examples:
# Use a config file stored in a package subfolder (monorepo)
npx i18next-cli extract --config ./packages/my-package/config/i18next.config.ts
# Short flag variant, for status
npx i18next-cli status de -c ./packages/my-package/config/i18next.config.ts
The configuration file supports both TypeScript (.ts) and JavaScript (.js) formats. Use the defineConfig helper for type safety and IntelliSense.
π‘ No Installation Required? If you don't want to install
i18next-clias a dependency, you can skip thedefineConfighelper and return a plain JavaScript object or JSON instead. ThedefineConfigfunction is purely for TypeScript support and doesn't affect functionality.
// i18next.config.ts
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de', 'fr'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
});
β Important: Only
.js,.jsx,.ts, and.tsxfiles are extracted by default. If you want to extract from other file types (e.g.,.pug,.vue), you must use or create a plugin. See the Plugin System section for more information.
Alternative without local installation:
// i18next.config.js
export default {
locales: ['en', 'de', 'fr'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
};
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de', 'fr'],
// Key extraction settings
extract: {
input: ['src/**/*.{ts,tsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
/** Glob pattern(s) for files to ignore during extraction */
ignore: ['node_modules/**'],
// Use '.ts' files with `export default` instead of '.json'
// Or use 'json5' to enable JSON5 features (comments, trailing commas, formatting are tried to be preserved)
// Or use 'yaml' for YAML format (.yaml or .yml extensions)
// if the file ending is .json5, .yaml, or .yml it automatically uses the corresponding format
outputFormat: 'ts',
// Combine all namespaces into a single file per language (e.g., locales/en.ts)
// Note: `output` path must not contain `{{namespace}}` when this is true.
mergeNamespaces: false,
// Translation functions to detect. Defaults to ['t', '*.t'].
// Supports a leading wildcard to match any object (suffix match), e.g.
// '*.t' matches `i18n.t` / `this._i18n.t`, and a trailing wildcard to match
// any method on an object (prefix match), e.g. 'tProps.*' matches
// `tProps.label` / `tProps.title`.
functions: ['t', '*.t', 'i18next.t', 'tProps.*'],
// React components to analyze
transComponents: ['Trans', 'Translation'],
// HTML tags to preserve in Trans component default values
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
// Hook-like functions that return a t function.
// Supports strings for default behavior or objects for custom argument positions.
useTranslationNames: [
'useTranslation', // Standard hook (ns: arg 0, keyPrefix: arg 1)
'getT',
'useT',
{
name: 'loadPageTranslations',
nsArg: 1, // Namespace is the 2nd argument (index 1)
keyPrefixArg: 2 // Options with keyPrefix is the 3rd (index 2)
}
],
// Namespace and key configuration
defaultNS: 'translation', // If set to false it will not generate any namespace, useful if i.e. the output is a single language json with 1 namespace (and no nesting).
fallbackNS: 'fallback', // Namespace(s) (string or array, like the i18next option) to use as fallback when a key is missing in the current namespace for a locale. Keys already translated in a fallback namespace are not duplicated into other namespace files by `extract`. (default undefined)
nsSeparator: ':',
keySeparator: '.', // Or `false` to disable nesting and use flat keys
contextSeparator: '_',
pluralSeparator: '_',
// Preserve dynamic keys matching patterns
preservePatterns: [
// Key patterns
'dynamic.feature.*', // Matches dynamic.feature.anything
'generated.*.key', // Matches generated.anything.key
// Namespace patterns
'assets:*', // Preserves ALL keys in the 'assets' namespace
'common:button.*', // Preserves keys like common:button.save, common:button.cancel
'errors:api.*', // Preserves keys like errors:api.timeout, errors:api.server
// Specific key preservation across namespaces
'dynamic:user.*.profile', // Matches dynamic:user.admin.profile, dynamic:user.guest.profile
],
/**
* When true, preserves all context variants of keys that use context parameters,
* across every configured locale. For example, if 'friend' is used with a context
* option in source code, variants like 'friend_male' and 'friend_female' are kept
* in the primary language even when they're not referenced explicitly, and are
* propagated to secondary locales with empty placeholders so every language ends
* up with the same key skeleton.
* (default: false)
*/
preserveContextVariants: false,
// Output formatting
sort: true, // can be also a sort function => i.e. (a, b) => a.key > b.key ? -1 : a.key < b.key ? 1 : 0, // sort in reverse order
indentation: 2, // can be also a string
// Primary language settings
primaryLanguage: 'en', // Defaults to the first locale in the `locales` array
secondaryLanguages: ['de', 'fr'], // Defaults to all locales except primaryLanguage
// Default value for missing keys in secondary languages
// Can be a string, function, or object for flexible fallback strategies
defaultValue: '', // Simple string: all missing keys get this value
// Or use a function for dynamic defaults:
// defaultValue: (key, namespace, language, value) => key, // i18next-parser style: use key as value
// defaultValue: (key, namespace, language, value) => `TODO: translate ${key}`, // Mark untranslated keys
// defaultValue: (key, namespace, language, value) => language === 'de' ? 'German TODO' : 'TODO', // Language-specific
/** If true, keys that are not found in the source code will be removed from translation files. (default: true) */
removeUnusedKeys: true,
// Namespaces to ignore during extraction, status, and sync operations.
// Useful for monorepos where shared namespaces are managed elsewhere.
// Keys using these namespaces will be excluded from processing.
// An ignored namespace can still act as a `fallbackNS` source: its
// translations are read (never written) for fallback accounting. If it
// lives in its own file outside a merged output (`mergeNamespaces: true`),
// use an `output` function that maps that namespace to its path, e.g.
// output: (lng, ns) => ns === 'shared' ? `locales/${lng}/${ns}.json` : `locales/${lng}.json`
ignoreNamespaces: ['shared', 'common'], // Optional
// When true (default), the extractor also scans code comments for t(...) / Trans examples and will extract keys found there.
// Set to false to ignore translation-like patterns in comments (useful to avoid extracting example/documentation strings).
extractFromComments: true,
// Control whether base plural forms are generated when context is present
// When false, t('key', { context: 'male', count: 1 }) will only generate
// key_male_one, key_male_other but NOT key_one, key_other
generateBasePluralForms: true, // Default: true
// Completely disable plural generation, even when count is present
// When true, t('key', { count: 1 }) will only generate 'key' (no _one, _other suffixes)
// The count option can still be used for {{count}} interpolation in the translation value
disablePlurals: false, // Default: false
// Generate the union of all configured locales' plural forms for every language.
// For example, if your locales are ['en', 'pl'], English normally only gets _one/_other,
// but with this option it also gets _few/_many (needed by Polish).
// Useful when you want a consistent set of plural keys across all locales.
allPluralForms: false, // Default: false
// Prefix for nested translations.
// Controls how nested $t(...) calls inside strings are detected.
// Nested references are scanned in BOTH source code (keys and defaultValues
// passed to t()) and in the values of existing translation files, so keys
// reachable only via `$t(...)` inside a translation value are preserved by
// `extract` and expanded into the correct per-locale plural skeleton.
// Example: '$t('
nestingPrefix: '$t(', // Default: '$t('
// Suffix for nested translations.
// Example: ')'
nestingSuffix: ')', // Default: ')'
// Separator for nested translation options.
// Used to split key vs options inside $t(key, {...}).
nestingOptionsSeparator: ',', // Default: ','
// Interpolation prefix used in defaultValue templates and runtime interpolation.
// Example: '{{'
interpolationPrefix: '{{', // Default: '{{'
// Interpolation suffix used in defaultValue templates and runtime interpolation.
// Example: '}}'
interpolationSuffix: '}}', // Default: '}}'
// Warn (or error) when the same ns:key is extracted with different default values.
warnOnConflicts: true // Default: false
},
// options for linter
lint: {
/** Optional accept-list of JSX attribute names to exclusively lint (takes precedence over ignoredAttributes). */
acceptedAttributes: ['title'],
/** Optional accept-list of JSX tag names to exclusively lint (takes precedence over ignoredTags).
* Pass 'all' to lint every tag (including custom JSX components); ignoredTags still apply. */
acceptedTags: ['p'],
// Optional custom JSX attributes to ignore during linting
ignoredAttributes: ['data-testid', 'aria-label'],
// Optional JSX tag names whose content should be ignored when linting
ignoredTags: ['pre'],
/** Glob pattern(s) for files to ignore during lint (in addition to those defined during extract) */
ignore: ['additional/stuff/**'],
/** Enable linting for interpolation parameter errors in translation calls (default: true) */
checkInterpolationParams: true,
/** Lint string concatenation involving translated strings (default: 'warn').
* 'warn'/true reports without failing the run, 'error' fails the run (exit non-zero,
* good for CI), 'off'/false disables it. */
checkConcatenation: 'warn',
/** Lint punctuation glued onto a translation, e.g. <Trans>Email</Trans>: (default: 'off').
* Opt-in; accepts the same values as checkConcatenation. */
checkPunctuationConcatenation: 'off',
},
// `status` command
status: {
// Glob patterns for keys that `status` should not report, e.g. keys that are
// intentionally empty in some locales. Same shape as `preservePatterns`; a `ns:`
// prefix limits the pattern to one namespace. Only affects `status`, not `extract`.
ignoreKeys: ['*-href', 'common:empty-table-subtitle'], // Optional
},
// TypeScript type generation
types: {
input: ['locales/en/*.json'], // or use '**/*.json' with basePath for nested namespaces
basePath: 'locales/en', // Optional: enables nested directory structures as namespaces
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
enableSelector: true, // Enable type-safe key selection
},
// Locize integration
locize: {
projectId: 'your-project-id',
apiKey: process.env.LOCIZE_API_KEY, // Recommended: use environment variables
version: 'latest',
cdnType: 'standard' // or 'pro'
},
// Plugin system
plugins: [
// Add custom plugins here
],
});
You can extend the built-in recommended lists for linting by importing and spreading them in your config:
import { defineConfig, recommendedAcceptedTags, recommendedAcceptedAttributes } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{js,jsx,ts,tsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
lint: {
acceptedTags: ['my-web-component', ...recommendedAcceptedTags],
acceptedAttributes: ['data-label', ...recommendedAcceptedAttributes]
}
});
Create custom plugins to extend the capabilities of i18next-cli. The plugin system provides hooks for extraction, linting, and instrumentation, with a single unified plugins array.
Available Hooks:
setup: Runs once when the CLI is initialized. Use it for any setup tasks.onLoad: Runs for each file before it is parsed. You can use this to transform code (e.g., transpile a custom language to JavaScript).onVisitNode: Runs for every node in the Abstract Syntax Tree (AST) of a parsed JavaScript/TypeScript file. This provides access to the full parsing context, including variable scope and TypeScript-specific syntax like satisfies and as operators.onKeySubmitted: Hook called synchronously for every translation key submitted to the extractor, including duplicates, before the deduplication decision is made.extractKeysFromExpression: Runs for specific expressions during AST traversal to extract additional translation keys. This is ideal for handling custom syntax patterns or complex key generation logic without managing pluralization manually.extractContextFromExpression: Runs for specific expressions to extract context values that can't be statically analyzed. Useful for dynamic context patterns or custom context resolution logic.onEnd: Runs after all JS/TS files have been parsed but before the final keys are compared with existing translation files. This is the ideal hook for parsing non-JavaScript files (like .html, .vue, or .svelte) and adding their keys to the collection.afterSync: Runs after the extractor has compared the found keys with your translation files and generated the final results. This is perfect for post-processing tasks, like generating a report of newly added keys.Lint Plugin Hooks:
lintSetup(context): Runs once before linting starts. Receives LintPluginContext with config and logger.lintExtensions: Optional extension hint (for example ['.vue']). Used as a skip hint/optimization.lintOnLoad(code, filePath): Runs before lint parsing for each file.
string to replace source code for linting.undefined to pass through unchanged.null to skip linting the file entirely.lintOnResult(filePath, issues): Runs after each file is linted. Return a new issues array to filter/augment results, or undefined to keep as-is.Instrument Plugin Hooks:
instrumentSetup(context): Runs once before instrumentation starts. Receives InstrumentPluginContext with config and logger.instrumentExtensions: Optional extension hint (for example ['.vue']). Used as a skip hint/optimization.instrumentOnLoad(code, filePath): Runs before scanning each file for hardcoded strings.
string to replace source code before scanning.undefined to pass through unchanged.null to skip instrumenting the file entirely.instrumentOnResult(filePath, candidates): Runs after candidates are detected. Return a new CandidateString[] to filter/augment results, or undefined to keep as-is.import type {
Plugin,
LinterPlugin,
LintPluginContext,
LintIssue,
} from 'i18next-cli';
// You can type your plugin as Plugin (full surface), LinterPlugin (lint-focused),
// or InstrumenterPlugin (instrument-focused)
export const vueLintPlugin = (): LinterPlugin => ({
name: 'vue-lint-plugin',
lintExtensions: ['.vue'],
lintSetup: async (context: LintPluginContext) => {
context.logger.info('vue lint plugin initialized');
},
lintOnLoad: async (code, filePath) => {
if (!filePath.endsWith('.vue')) return undefined;
// preprocess SFC/template to lintable JS/TS/JSX text
return code;
},
lintOnResult: async (_filePath, issues: LintIssue[]) => {
// Example: keep only interpolation issues
return issues.filter(issue => issue.type === 'interpolation');
}
});
import type {
InstrumenterPlugin,
InstrumentPluginContext,
CandidateString,
} from 'i18next-cli';
export const vueInstrumentPlugin = (): InstrumenterPlugin => ({
name: 'vue-instrument-plugin',
instrumentExtensions: ['.vue'],
instrumentSetup: async (context: InstrumentPluginContext) => {
context.logger.info('vue instrument plugin initialized');
},
instrumentOnLoad: async (code, filePath) => {
if (!filePath.endsWith('.vue')) return undefined;
// Extract template block from SFC and return as JSX-like code
return code;
},
instrumentOnResult: async (_filePath, candidates: CandidateString[]) => {
// Example: only keep high-confidence candidates
return candidates.filter(c => c.confidence >= 0.5);
}
});
Config usage (same plugins list for extract + lint + instrument):
import { defineConfig } from 'i18next-cli';
import { vueLintPlugin } from './plugins/vue-lint-plugin.mjs';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx,vue}'],
output: 'locales/{{language}}/{{namespace}}.json'
},
plugins: [
vueLintPlugin()
]
});
Basic Plugin Example:
import { glob } from 'glob';
import { readFile, writeFile } from 'node:fs/promises';
export const myCustomPlugin = () => ({
name: 'my-custom-plugin',
// Handle custom file formats
async onEnd(keys) {
// Extract keys from .vue files
const vueFiles = await glob('src/**/*.vue');
for (const file of vueFiles) {
const content = await readFile(file, 'utf-8');
const keyMatches = content.matchAll(/\{\{\s*\$t\(['"]([^'"]+)['"]\)/g);
for (const match of keyMatches) {
keys.set(`translation:${match[1]}`, {
key: match[1],
defaultValue: match[1],
ns: 'translation'
});
}
}
}
});
Advanced Plugin with Expression Parsing:
export const advancedExtractionPlugin = () => ({
name: 'advanced-extraction-plugin',
// Extract keys from TypeScript satisfies expressions
extractKeysFromExpression: (expression, config, logger) => {
const keys = [];
// Handle template literals with variable substitutions
if (expression.type === 'TemplateLiteral') {
// Extract pattern: `user.${role}.permission`
const parts = expression.quasis.map(q => q.cooked);
const variables = expression.expressions.map(e =>
e.type === 'Identifier' ? e.value : 'dynamic'
);
if (variables.includes('role')) {
// Generate keys for known roles
keys.push('user.admin.permission', 'user.manager.permission', 'user.employee.permission');
}
}
// Handle TypeScript satisfies expressions
if (expression.type === 'TsAsExpression' &&
expression.typeAnnotation?.type === 'TsUnionType') {
const unionTypes = expression.typeAnnotation.types;
for (const unionType of unionTypes) {
if (unionType.type === 'TsLiteralType' &&
unionType.literal?.type === 'StringLiteral') {
keys.push(`dynamic.${unionType.literal.value}.extracted`);
}
}
}
return keys;
},
// Extract context from conditional expressions
extractContextFromExpression: (expression, config, logger) => {
const contexts = [];
// Handle ternary operators: isAdmin ? 'admin' : 'user'
if (expression.type === 'ConditionalExpression') {
if (expression.consequent.type === 'StringLiteral') {
contexts.push(expression.consequent.value);
}
if (expression.alternate.type === 'StringLiteral') {
contexts.push(expression.alternate.value);
}
}
// Handle template literals: `${role}.${level}`
if (expression.type === 'TemplateLiteral') {
const parts = expression.expressions.map(expr =>
expr.type === 'Identifier' ? expr.value : 'unknown'
);
if (parts.length > 0) {
const joins = expression.quasis.map(quasi => quasi.cooked);
contexts.push(joins.reduce((acc, join, i) =>
acc + (join || '') + (parts[i] || ''), ''
));
}
}
return contexts;
},
// Handle complex AST patterns
onVisitNode: (node, context) => {
// Custom extraction for specific component patterns
if (node.type === 'JSXElement' &&
node.opening.name.type === 'Identifier' &&
node.opening.name.value === 'CustomTransComponent') {
const keyAttr = node.opening.attributes?.find(attr =>
attr.type === 'JSXAttribute' &&
attr.name.value === 'translationKey'
);
if (keyAttr?.value?.type === 'StringLiteral') {
context.addKey({
key: keyAttr.value.value,
defaultValue: 'Custom component translation',
ns: 'components'
});
}
}
}
});
Configuration:
import { defineConfig } from 'i18next-cli';
import { myCustomPlugin, advancedExtractionPlugin } from './my-plugins.mjs';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,vue}'],
output: 'locales/{{language}}/{{namespace}}.json'
},
plugins: [
myCustomPlugin(),
advancedExtractionPlugin()
]
});
Track where each translation key is used in your codebase with a custom metadata plugin.
Example Plugin Implementation:
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import type { Plugin } from 'i18next-cli';
interface LocationMetadataOptions {
/** Output path for the metadata file (default: 'locales/metadata.json') */
output?: string;
/** Include line and column numbers (default: true) */
includePosition?: boolean;
}
export const locationMetadataPlugin = (options: LocationMetadataOptions = {}): Plugin => {
const {
output = 'locales/metadata.json',
includePosition = true,
} = options;
return {
name: 'location-metadata',
async onEnd(keys) {
const metadata: Record<string, any> = {};
for (const [uniqueKey, extractedKey] of keys.entries()) {
const { key, ns, locations } = extractedKey;
// Skip keys without location data
if (!locations || locations.length === 0) {
continue;
}
// Format location data
const locationData = locations.map(loc => {
if (includePosition && loc.line !== undefined) {
return `${loc.file}:${loc.line}:${loc.column ?? 0}`;
}
return loc.file;
});
// Organize metadata
const namespace = ns || 'translation';
if (!metadata[namespace]) {
metadata[namespace] = {};
}
metadata[namespace][key] = locationData;
}
// Write metadata file
await mkdir(dirname(output), { recursive: true });
await writeFile(output, JSON.stringify(metadata, null, 2), 'utf-8');
console.log(`π Location metadata written to ${output}`);
}
};
};
Configuration:
// i18next.config.ts
import { defineConfig } from 'i18next-cli';
import { locationMetadataPlugin } from './plugins/location-metadata.mjs';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
plugins: [
locationMetadataPlugin({
output: 'locales/metadata.json'
})
]
});
Example Output (locales/metadata.json):
{
"translation": {
"app.title": [
"src/App.tsx:12:15",
"src/components/Header.tsx:8:22"
],
"user.greeting": [
"src/pages/Profile.tsx:45:10"
]
},
"common": {
"button.save": [
"src/components/SaveButton.tsx:18:7",
"src/forms/UserForm.tsx:92:5"
]
}
}
Many "dynamic" keys don't need any configuration: since v1.49 the extractor performs TypeScript type-aware resolution of finite dynamic keys and expands every possible variant automatically (#210):
// Template literals with unions / ternaries / nullish coalescing:
t(`state.${isDone ? 'done' : 'notDone'}.title`) // -> state.done.title + state.notDone.title
type Status = 'active' | 'inactive'
declare const status: Status
t(`status.${status}`) // -> status.active + status.inactive
// `as const` maps and arrays (also when imported from another file):
const KEYS = ['a', 'b'] as const
KEYS.map((k) => t(`item.${k}`)) // -> item.a + item.b
const MAP = { ok: 'result.ok', err: 'result.err' } as const
t(MAP[someCondition ? 'ok' : 'err']) // -> result.ok + result.err
// Helper-function return types and `satisfies`-constrained values
Only keys that are truly runtime-dynamic (e.g. built from API data) cannot
be statically resolved by any tool. For those, use preservePatterns to keep
the existing entries in your translation files:
// Code like this:
const key = `user.${role}.permission`; // role comes from the server
t(key);
// With this config:
export default defineConfig({
extract: {
preservePatterns: ['user.*.permission']
}
});
// Will preserve existing keys matching the pattern
Extract keys from comments for documentation or edge cases:
// t('welcome.message', 'Welcome to our app!')
// t('user.greeting', { defaultValue: 'Hello!', ns: 'common' })
For projects that prefer to keep everything in a single module type, you can configure the CLI to output JavaScript or TypeScript files instead of JSON.
Configuration (i18next.config.ts):
export default defineConfig({
extract: {
output: 'src/locales/{{language}}/{{namespace}}.ts', // Note the .ts extension
outputFormat: 'ts', // Use TypeScript with ES Modules
}
});
This will generate files like src/locales/en/translation.ts with the following content:
export default {
"myKey": "My value"
} as const;
For projects that prefer YAML for better readability and compatibility with other tools, you can configure the CLI to output YAML files instead of JSON.
Configuration (i18next.config.ts):
export default defineConfig({
extract: {
output: 'locales/{{language}}/{{namespace}}.yaml', // Use .yaml or .yml
outputFormat: 'yaml', // Optional - inferred from file extension
}
});
This will generate files like locales/en/translation.yaml with the following content:
app:
title: My Application
description: Welcome to our app
button:
save: Save
cancel: Cancel
π‘ Note: Both
.yamland.ymlextensions are supported and preserved. TheoutputFormat: 'yaml'option is optional when using these extensions - the format is automatically inferred from the file extension.
A common concern with runtime i18n is "you ship every language and every namespace to the client". You don't have to: the i18next runtime loads translations per namespace, per language, so namespace granularity is your code-splitting boundary. The runtime core itself is ~13.5 kB gzipped; what grows with your app is translation payload, and that is entirely controlled by how you slice namespaces.
The recipe:
useTranslation('checkout'), t('checkout:title'), or the ns option. The extractor detects the namespace from your code and writes one file per namespace and language:export default defineConfig({
locales: ['en', 'de'],
extract: {
input: 'src/**/*.{ts,tsx}',
output: 'src/locales/{{language}}/{{namespace}}.json',
}
});
import i18next from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
i18next
.use(resourcesToBackend((lng, ns) => import(`./locales/${lng}/${ns}.json`)))
.init({
fallbackLng: 'en',
defaultNS: 'app',
ns: ['app'], // only the app-shell namespace loads upfront
});
useTranslation('checkout') (react-i18next) or i18next.loadNamespaces('checkout') fetches exactly that chunk when the route renders. The initial payload contains only the namespaces the entry route uses, in the active language; other languages transfer nothing until switched to.Prefer not to bundle translations at all? Serve the same per-namespace files from any static host/CDN via i18next-http-backend, or directly from the Locize CDN via i18next-locize-backend β same wire profile, plus translation updates without redeploying.
You can also combine all namespaces into a single file per language. This is useful for reducing the number of network requests in some application setups.
Configuration (i18next.config.ts):
export default defineConfig({
extract: {
// Note: The `output` path no longer contains the {{namespace}} placeholder
output: 'src/locales/{{language}}.ts',
outputFormat: 'ts',
mergeNamespaces: true,
}
});
This will generate a single file per language, like src/locales/en.ts, with namespaces as top-level keys:
export default {
"translation": {
"key1": "Value 1"
},
"common": {
"keyA": "Value A"
}
} as const;
When generating TypeScript types, namespaces are derived from the filename only by default (e.g., locales/en/dashboard/user.json β namespace: user). If you organize translation files in nested directories and want the generated types to preserve that structure as part of the namespace, use the basePath option in your types configuration.
Configuration (i18next.config.ts):
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
types: {
input: 'public/locales/en/**/*.json',
basePath: 'public/locales/en',
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
}
});
With this configuration:
public/locales/en/common.json β type namespace: commonpublic/locales/en/dashboard/user.json β type namespace: dashboard/userpublic/locales/en/dashboard/settings.json β type namespace: dashboard/settingspublic/locales/en/features/auth/login.json β type namespace: features/auth/loginThe basePath can include the {{language}} placeholder for flexibility:
types: {
input: 'public/locales/en/**/*.json',
basePath: 'public/locales/{{language}}',
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
}
This is useful for organizing translations into logical groups while maintaining type safety across your entire namespace hierarchy.
Automatically migrate from legacy i18next-parser.config.js:
npx i18next-cli migrate-config
This will:
i18next.config.ts fileImportant: File Management Differences
Unlike i18next-parser, i18next-cli takes full ownership of translation files in the output directory. If you have manually managed translation files that should not be modified, place them in a separate directory or use different naming patterns to avoid conflicts.
Use the --ci flag to fail builds when translations are outdated:
# GitHub Actions example
- name: Check translations
run: npx i18next-cli extract --ci
Likewise, fail the build when the generated TypeScript definitions are out of date:
# Fail the build if generated TypeScript definitions are out of date
- name: Check i18n types
run: npx i18next-cli types --ci
For development, use watch mode to automatically update translations:
npx i18next-cli extract --watch
npx i18next-cli lint --watch
Generate TypeScript definitions for full type safety:
// Generated types enable autocomplete and validation
t('user.profile.name'); // β
Valid key
t('invalid.key'); // β TypeScript error
The toolkit automatically detects these i18next usage patterns:
// Basic usage
t('key')
t('key', 'Default value')
t('key', { defaultValue: 'Default' })
// With namespaces
t('ns:key')
t('key', { ns: 'namespace' })
// With interpolation
t('key', { name: 'John' })
// With plurals and context
t('key', { count: 1 }); // Cardinal plural
t('keyWithContext', { context: 'male' });
t('keyWithDynContext', { context: isMale ? 'male' : 'female' });
// With ordinal plurals
t('place', { count: 1, ordinal: true });
t('place', {
count: 2,
ordinal: true,
defaultValue_ordinal_one: '{{count}}st place',
defaultValue_ordinal_two: '{{count}}nd place',
defaultValue_ordinal_other: '{{count}}th place'
});
// With key fallbacks
t(['key.primary', 'key.fallback']);
t(['key.primary', 'key.fallback'], { defaultValue: 'The fallback value' });
// With structured content (returnObjects)
t('countries', { returnObjects: true });
The extractor correctly handles cardinal and ordinal plurals (count), as well as context options, generating all necessary suffixed keys (e.g., key_one, key_ordinal_one, keyWithContext_male). It can even statically analyze ternary expressions in the context option to extract all possible variations.
// Trans component
<Trans i18nKey="welcome">Welcome {{name}}</Trans>
<Trans ns="common">user.greeting</Trans>
<Trans count={num}>You have {{num}} message</Trans>
<Trans context={isMale ? 'male' : 'female'}>A friend</Trans>
// useTranslation hook
const { t } = useTranslation('namespace');
const { t } = useTranslation(['ns1', 'ns2']);
// Aliased functions
const translate = t;
translate('key');
// Destructured hooks
const { t: translate } = useTranslation();
// getFixedT
const fixedT = getFixedT('en', 'namespace');
fixedT('key');
The extractor handles the type-safe Selector API and mirrors the runtime namespace-routing rule from i18next v25.8.19. With a single-namespace hook, selector paths are extracted into the bound namespace as-is:
const { t } = useTranslation('common');
t($ => $.button.save); // β common.json: button.save
t($ => $.button.save, { ns: 'auth' }); // β auth.json: button.save
When the hook is called with a multi-namespace array, a leading path segment that matches a secondary namespace is treated as a namespace prefix and the rest of the path is routed to that namespace's file. The primary namespace (the array's first entry) is never rewritten β its keys are exposed flat on the selector proxy:
const { t } = useTranslation(['auth', 'validation']);
t($ => $.login['Welcome Back!']); // β auth.json: login.Welcome Back!
t($ => $.validation.email['Required']); // β validation.json: email.Required
t($ => $.email['Required'], { ns: 'validation'}); // β validation.json: email.Required
This matches the behavior of i18next/src/selector.js exactly: paths whose
first segment is the primary namespace, or doesn't appear in the hook's
namespace list at all, are joined with the configured keySeparator and
routed to the primary. Secondary-prefixed paths are joined as
<ns><nsSeparator><rest> so the standard ns:key routing places them in
the correct file.
Set types.enableSelector: 'strict' (requires i18next β₯ 26.1.0 with
the matching runtime option) to drop the flattened-primary form entirely.
Every selector path must lead with an explicit namespace segment, and the
extractor rewrites leading segments uniformly β primary, secondary,
single- or multi-ns hooks all behave the same:
// useTranslation('common');
t($ => $.common.button.save); // β common.json: button.save
// useTranslation(['auth', 'validation']);
t($ => $.auth.login['Welcome Back!']); // β auth.json: login.Welcome Back!
t($ => $.validation.email['Required']); // β validation.json: email.Required
Strict mode is opt-in and incompatible with the #2405
pattern (a key inside a namespace whose name matches a sibling namespace).
If you have keys like Resources['config'].common.name while common is
also a sibling namespace, leave strict mode off β the rewrite would route
that key into the wrong file.
In addition to the CLI commands, i18next-cli can be used programmatically in your build scripts, Gulp tasks, or any Node.js application:
import { runExtractor, runLinter, runSyncer, runStatus, runTypesGenerator } from 'i18next-cli';
import type { I18nextToolkitConfig } from 'i18next-cli';
const config: I18nextToolkitConfig = {
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
};
// Run the complete extraction process
const { anyFileUpdated, hasErrors } = await runExtractor(config);
console.log('Files updated:', anyFileUpdated);
// Check translation status programmatically
await runStatus(config);
// Run linting and get results
const { success, message, files } = await runLinter(config);
if (!success) {
console.error(message);
for (const [filename, issues] of Object.entries(files)) {
console.error(`${issues.length} issues found in ${filename}.`);
}
}
// Sync translation files
await runSyncer(config);
// types generattion
await runTypesGenerator(config);
Gulp Example:
import gulp from 'gulp';
import { runExtractor } from 'i18next-cli';
gulp.task('i18next-extract', async () => {
const config = {
locales: ['en', 'de', 'fr'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
};
await runExtractor(config);
});
Webpack Plugin Example:
class I18nextExtractionPlugin {
apply(compiler) {
compiler.hooks.afterEmit.tapAsync('I18nextExtractionPlugin', async (compilation, callback) => {
await runExtractor(config);
callback();
});
}
}
runExtractor(config, options?) - Complete extraction with file writingrunLinter(config) - Run linting analysis and return resultsrunSyncer(config) - Sync translation filesrunStatus(config, options?) - Get translation statusrunTypesGenerator(config) - Generate typesrunLocalize(options?, configPath?) - The full localize flow (detect β instrument β extract β Locize sync with auto-translate β download)Linter - Class that lints your codebase and emits events along the wayExample usage
import { Linter } from 'i18next-cli';
import type { I18nextToolkitConfig } from 'i18next-cli';
const config: I18nextToolkitConfig = {
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
};
const linter = new Linter(config);
linter.addEventListener('progress', ({ message }) => console.log(message));
await linter.run();
This programmatic API gives you the same power as the CLI but with full control over when and how it runs in your build process.
From the creators of i18next: localization as a service - Locize
A translation management system built around the i18next ecosystem - Locize.
Now with a Free plan for small projects! Perfect for hobbyists or getting started.

With using Locize you directly support the future of i18next.
TypeScript
99.0%
JavaScript
1.0%
A unified, high-performance i18next CLI.
236
stars
937
commits
TypeScript
primary language
Sep 10, 2026
updated
A unified, high-performance i18next CLI toolchain, powered by SWC.
By default,
i18next-clionly extracts translation keys from JavaScript and TypeScript files (.js,.jsx,.ts,.tsx). To extract from other file types (such as.pug,.vue,.svelte, etc.), you must use or create a plugin. Specifying additional file extensions in theextract.inputconfig is not sufficient on its ownβplugins are required for non-JS/TS formats. See the Plugin System section for details and examples.
i18next-cli is a complete reimagining of the static analysis toolchain for the i18next ecosystem. It consolidates key extraction, type safety generation, locale syncing, linting, and cloud integrations into a single, cohesive, and blazing-fast CLI.
π Try it Now - Zero Config!
You can get an instant analysis of your existing i18next project without any configuration. Just run this command in your repository's root directory:
npx i18next-cli statusOr find hardcoded strings:
npx i18next-cli lint
If you're looking for a managed backend to pair with i18next-cli, take a look at Locize β i18next-cli already ships with locize-download, locize-sync, and locize-migrate commands. Built by the same team behind i18next, with CDN delivery, AI translation, review workflow, and no redeploys for copy changes.
i18next-cli is built from the ground up to meet the demands of modern web development.
useTranslation('ns1', { keyPrefix: '...' }), getFixedT, and aliased t functions, minimizing the need for manual workarounds.--watch modes, CLI output, and a migration from legacy tools.i18next-parser configurations.npm install --save-dev i18next-cli
Zero-to-localized in one command: starting from an app with hardcoded strings (e.g. generated with v0, Lovable, Bolt or Cursor)? Run
npx i18next-cli localizeβ it detects your setup, wraps hardcoded strings int()calls, extracts keys, connects to Locize and AI-translates your app. See thelocalizecommand. Working with an AI coding agent (Claude Code, Cursor, ...)?npx i18next-cli localize --print-agent-promptprints the same flow as a copy-paste agent runbook. The steps below are the manual path.
Create a configuration interactively:
npx i18next-cli init
Or manually create i18next.config.ts in your project root:
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{js,jsx,ts,tsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
});
Get an overview of your project's localization health:
npx i18next-cli status
npx i18next-cli extract
npx i18next-cli types
initInteractive setup wizard to create your configuration file.
npx i18next-cli init
Options:
--ci: Skip the browser launch when a backend (e.g. Locize) is selected;
the signup URL is printed instead. Useful for scripted runs. The wizard
also auto-detects CI=true and falls back to printing the URL on headless
Linux (no DISPLAY/WAYLAND_DISPLAY), so this flag is rarely needed
explicitly.--inlang: Also scaffold an inlang project
(project.inlang/settings.json) so inlang tooling β the
Sherlock VS Code
extension, the Fink web editor for translators,
and the Paraglide
compiler β works directly on your translation files. Skips the
corresponding wizard question.The wizard asks for the config file type, locales, source-file glob, output path, and finally "Translation backend?" with three options:
locize
block into the generated config so locize-sync
works out of the box. The API key prompt can be left empty (read-only
mode); add it later via a LOCIZE_API_KEY environment variable.The wizard then offers to set up inlang tooling (default: no β or pass
--inlang to skip the question). If accepted, it scaffolds a
project.inlang/settings.json that points the
inlang i18next plugin
at your existing translation files: baseLocale/locales come from your
config, and pathPattern is derived from extract.output (the namespaced
object form when your layout uses {{namespace}}, with namespaces discovered
from the primary language's files; a plain pattern otherwise). It also adds
the Sherlock extension to .vscode/extensions.json recommendations (merging
comment-aware, never clobbering existing entries). Your i18next JSON files
remain the single source of truth β inlang tools read and write them in
place, so there is no second catalog to drift. An existing
project.inlang/settings.json is never overwritten; re-running init is
safe. Requires JSON resource files. The plugin is pinned to an exact verified
version (@inlang/plugin-i18next@6.2.1) β bump the modules URL in
settings.json to pick up newer plugin releases. Only settings.json is
scaffolded by design: project.inlang/ is the
unpacked (git-friendly) project
form, and inlang tools generate and manage its remaining files (.gitignore,
README.md, cache/) on first use β so expect a few new files there after
opening the project with Sherlock or Paraglide.
extractParses source files, extracts keys, and updates your JSON translation files.
npx i18next-cli extract [options]
Options:
--watch, -w: Re-run automatically when files change--ci: Exit with non-zero status if any files are updated (for CI/CD)--dry-run: Does not change any files - useful in combination with --ci (for CI/CD)--sync-primary: Sync primary language values with default values from code--sync-all: Sync primary language values with default values from code AND clear synced keys in all other locales (implies --sync-primary)--trust-derived: When used with --sync-primary or --sync-all, also trust defaults inferred from keys such as t('Hello') or keyPrefix-derived values. This keeps the default sync behavior strict unless you opt in.--with-types: After extraction (and on every re-run in --watch mode), regenerate the TypeScript definitions whenever translation files changed. Avoids the need to run extract -w and types -w as two separate processes.--quiet: Suppress spinner and non-essential output (for CI or scripting)All commands that show progress spinners (extract, types, lint, sync) now support:
--quiet flag to silence spinner and non-essential output (for CI, scripting, or log capture)CLI Example:
npx i18next-cli extract --quiet
Programmatic Example:
import { runExtractor } from 'i18next-cli';
const logger = {
info: (msg) => myLogStream.write(msg + '\n'),
warn: (msg) => myWarnStream.write(msg + '\n'),
error: (msg) => myErrStream.write(msg + '\n'),
};
await runExtractor(config, { quiet: false, logger });
If you pass a logger, spinner output and all progress/info messages are routed to your logger instead of the interactive spinner.
Examples:
# One-time extraction
npx i18next-cli extract
# Watch mode for development
npx i18next-cli extract --watch
# CI mode (fails if files changed)
npx i18next-cli extract --ci
# Sync primary language with code defaults
npx i18next-cli extract --sync-primary
# Sync primary and clear synced keys in all other locales
npx i18next-cli extract --sync-all
# Sync using explicit defaults plus inferred key-derived defaults
npx i18next-cli extract --sync-all --trust-derived
# Combine options for optimal development workflow
npx i18next-cli extract --sync-primary --watch
# Keep TypeScript definitions in sync from a single process (no separate `types -w` needed)
npx i18next-cli extract --watch --with-types
status [locale]Displays a health check of your project's translation status. Can run without a config file. Exits with a non-zero status code when translations are missing.
The primary language is checked too: any key used in your code but absent from the primary language's translation files (a typo, or extract was never run) is reported and causes a non-zero exit code. Empty-string placeholders written by extract are considered present and do not fail the check. Running npx i18next-cli status <primaryLanguage> shows the absent keys in detail.
Options:
--namespace <ns>, -n <ns>: Filter the report by a specific namespace.--hide-translated: Hide already translated keys in the detailed view, showing only missing translations.status.ignoreKeys (config): glob patterns for keys that should not be reported (and don't fail the check), e.g. keys that are intentionally left empty in some locales. Optional ns: prefix (common:help.*-href). Only affects status.--unused: Report only unused translation keys β keys present in your translation files that are no longer used in your source code (i.e. what extract with removeUnusedKeys would delete). Never modifies any files and exits with a non-zero status code when unused keys are found, so it can serve as a dedicated CI check alongside the regular missing-translations check. Note that static analysis cannot detect dynamically constructed keys (e.g. t(`error.${code}`)); to find keys that are truly unused at runtime, see find unused translations with locize.Usage Examples:
# Get a high-level summary for all locales and namespaces
npx i18next-cli status
# Get a detailed, key-by-key report for the 'de' locale
npx i18next-cli status de
# Get a summary for only the 'common' namespace across all locales
npx i18next-cli status --namespace common
# Get a detailed report for the 'de' locale, showing only the 'common' namespace
npx i18next-cli status de --namespace common
# Show only the untranslated keys for the 'de' locale
npx i18next-cli status de --hide-translated
# Combine options to see only missing translations in a specific namespace
npx i18next-cli status de --namespace common --hide-translated
# Report only unused keys across all locales (read-only, exits 1 when any are found)
npx i18next-cli status --unused
# Report only unused keys in the 'en' files β e.g. as a separate CI check
npx i18next-cli status en --unused
The detailed view provides a rich, at-a-glance summary for each namespace, followed by a list of every key and its translation status.
Example Output (npx i18next-cli status de):
Key Status for "de":
Overall: [β β β β β β β β β β β β β β β β β β β β ] 100% (12/12)
Namespace: common
Namespace Progress: [β β β β β β β β β β β β β β β β β β β β ] 100% (4/4)
β button.save
β button.cancel
β greeting
β farewell
Namespace: translation
Namespace Progress: [β β β β β β β β β β β β β β β β β‘β‘β‘β‘] 80% (8/10)
β app.title
β app.welcome
β app.description
...
typesGenerates TypeScript definitions from your translation files for full type-safety and autocompletion.
Note: When
extract.defaultNSis set tofalse, the generateddefaultNSis derived from your resource files (i18next's type system cannot expressdefaultNS: false). Adjust the generatedi18next.d.tsif your runtime i18next config uses a different default namespace.
npx i18next-cli types [options]
Options:
--watch, -w: Re-run automatically when translation files change--ci: Exit with a non-zero status if the generated TypeScript definitions are out of date (check-only, writes nothing). Cannot be combined with --watch.--quiet, -q: Suppress spinner and non-essential output (for CI or scripting)syncSynchronizes secondary language files against your primary language file, adding missing keys and removing extraneous ones.
npx i18next-cli sync
lintAnalyzes your source code for internationalization issues. Can run without a config file.
npx i18next-cli lint
What it checks:
Hardcoded strings (error) β user-facing text in JSX elements and attributes that isn't wrapped in t()/<Trans>.
Interpolation parameters (error) β mismatches between {{placeholders}} in a translation and the params passed to t() (missing or unused). Toggle with lint.checkInterpolationParams (default: true).
String concatenation (warning by default) β translated strings glued together with +, or a sentence split across multiple adjacent translations (<Trans> components and/or {t()} expressions rendered as siblings). This breaks in languages that reorder or inflect the pieces; use a single key with placeholders instead. Configure with lint.checkConcatenation: 'warn' / true (default) reports it without failing the run, 'error' makes it fail (exit non-zero, useful for CI), and 'off' / false disables it.
// β οΈ Flagged β word order can't be translated
t('greeting') + ', ' + name
<p><Trans>Hello</Trans> and <Trans>World</Trans></p>
<p><Trans>new</Trans>{t('cat')}</p>
// β
Preferred β one key, placeholders
t('greeting', { name }) // "Hello, {{name}}"
<Trans i18nKey="greeting">Hello {{name}}</Trans>
Punctuation concatenation (off by default) β punctuation glued onto a translation, e.g. <label><Trans>Email</Trans>:</label> or <div>- <Trans>item</Trans></div>. Punctuation spacing and form differ across languages (French needs a narrow no-break space before :, CJK uses fullwidth οΌ, RTL reorders), so it belongs inside the translation or in semantic markup (a real <ul>/<li> for bullets). This is opt-in, since keeping punctuation out of a translation is often deliberate. Enable with lint.checkPunctuationConcatenation: 'warn', 'error', or 'off' / false (default).
// β οΈ Flagged when enabled
<label><Trans>Email</Trans>:</label>
<div>- <Trans>item</Trans></div>
// β
Preferred
<label><Trans i18nKey="emailLabel">Email:</Trans></label>
<ul><li><Trans>item</Trans></li></ul>
The linter exits non-zero only when it finds errors; a run with only warnings succeeds. Individual spots can be excused with the i18next-instrument-ignore directive.
To suppress warnings for code you intentionally aren't translating yet, use the i18next-instrument-ignore directive β the same comment recognized by the instrument command.
instrumentScans your source code for hardcoded user-facing strings and instruments them with i18next translation calls. This is useful for adding i18next instrumentation to an existing codebase that wasn't built with internationalization in mind. You can see this in action in this video or in this blog post.
β οΈ First-Step Tool: The
instrumentcommand uses heuristic-based detection and is designed as a first pass to identify and suggest transformation candidates. It will not catch 100% of cases, and you should expect both false positives and false negatives. Always review the suggested transformations carefully before committing them to your codebase. Think of it as an intelligent code assistant, not an automated compiler.
npx i18next-cli instrument
Options:
--dry-run: Preview changes without writing files to disk--interactive: Prompt for approval of each candidate string--namespace <ns>: Target a specific namespace for extracted keys-q, --quiet: Suppress spinner and outputWhat it transforms:
The instrument command detects four types of transformations:
Simple string β t() call:
// Before
const msg = 'Welcome back';
// After
const msg = t('welcomeBack', 'Welcome back');
Template literal (static only) β t() call:
// Before
const msg = `Welcome back`;
// After
const msg = t('welcomeBack', 'Welcome back');
Template literals with interpolation (e.g.
`Hello ${name}`) are skipped β they require manual wrapping.
JSX text β JSX expression with t():
// Before
<h1>Welcome back</h1>
// After
<h1>{t('welcomeBack', 'Welcome back')}</h1>
JSX mixed content β <Trans> component:
// Before
<p>Click <a href="/docs">here</a> to continue</p>
// After
<p><Trans i18nKey="clickHereLabel">Click <a href="/docs">here</a> to continue</Trans></p>
Namespace targeting:
Use --namespace <ns> to direct extracted keys into a specific namespace. When a non-default namespace is specified:
useTranslation('<ns>') with clean keysi18next.t('key', 'default', { ns: '<ns>' })--interactive mode you are prompted for the target namespacenpx i18next-cli instrument --namespace common
Custom scorer hook:
Override the built-in confidence heuristic via extract.instrumentScorer in your config. The function receives each candidate string and its context, and can:
null to force-skip the candidateundefined to fall back to the built-in heuristicexport default defineConfig({
// ...
extract: {
// ...
instrumentScorer: (content, { file, code, beforeContext, afterContext }) => {
// Skip strings that belong to your analytics domain
if (content.startsWith('track_')) return null;
// Boost strings in your UI layer
if (file.includes('/components/')) return 0.95;
// Fall back to built-in detection for everything else
return undefined;
}
}
});
What it skips (by design):
The instrumenter uses confidence heuristics to avoid transforming:
*.test.*, *.spec.*)ERROR_NOT_FOUND)console.log/warn/error argumentst() calls or <Trans> componentsexport const SETTINGS_SECTIONS = [
{ id: 'appearance', label: 'Appearance' }, // left untouched
]
A t() call there would be evaluated once, when the module is first imported β possibly before i18next
is initialized, and never again when the language changes. Move the text into a component, or expose the
registry as a hook that calls useTranslation() internally:
export const useSettingsSections = () => {
const { t } = useTranslation()
return [{ id: 'appearance', label: t('appearance', 'Appearance') }]
}
Auto-injection:
When transformations are applied, the command automatically:
import { useTranslation } from 'react-i18next' in React files (or import i18next from 'i18next' for non-React files)const { t } = useTranslation() into each React function component that contains transformed stringspackage.json dependencies (React, Next.js, Vue, etc.)useTranslation() hook style t() inside React components, or i18next.t() for utility / non-component codei18n.ts (or i18n.js for JS-only projects) initialization file if none exists, pre-configured with i18next-resources-to-backend to lazy-load your translation files via dynamic importsRecommended workflow:
Preview first: Always run with --dry-run to see what will change:
npx i18next-cli instrument --dry-run
Interactive mode for initial migration: Use --interactive to approve each candidate:
npx i18next-cli instrument --interactive
Review and commit: Check the changes, then commit to git before proceeding
Run extraction: After instrumentation, run extract to sync with translation files:
npx i18next-cli extract
Limitations:
The instrument command uses heuristic-based detection and has the following limitations:
t() calls without plural handling).Expected Workflow:
The intended usage pattern is:
--dry-run to preview all suggestions--interactive and carefully review each suggestion β consider using edit-key or skip liberallyextract to finalize translation filesi18next-instrument-ignoreBoth the lint and instrument commands honor an ignore comment so you can skip placeholder or intentionally-untranslated content. It works as a line or block comment, including the JSX {/* ... */} form, and comes in two variants:
| Directive | Scope |
|---|---|
i18next-instrument-ignore | The entire JSX element that begins on the next line β its opening tag, all nested children, and its closing tag. Falls back to a single line when the next line isn't a JSX element (e.g. a plain t() call). |
i18next-instrument-ignore-next-line | Only the single line immediately after the directive. |
// Suppress a whole element (including multi-line opening tags and nested children)
{/* i18next-instrument-ignore */}
<div
css={css`text-align: center;`}>
Hi, I'm Bob π
<p>This nested text is ignored too</p>
</div>
// Suppress just one line
{/* i18next-instrument-ignore-next-line */}
<p>Only this line is ignored</p>
// Also works for t() interpolation warnings in the linter
// i18next-instrument-ignore
const msg = t('Hello {{name}}!', { wrong: 'world' })
localizeOne command from hardcoded strings to a fully localized app: detect, instrument, extract, connect to Locize, AI-auto-translate, deliver. Built for taking a mono-lingual app (often AI-generated via v0/Lovable/Bolt/Cursor) to fully localized in one sitting.
npx i18next-cli localize
The command walks through six steps:
i18next.config.ts, or starts the init wizard if none exists.t() calls / <Trans> components (interactive by default β instrument is an assistant, review each change). Skipped automatically if your code can't be instrumented; a dirty git tree prompts for confirmation first.locize.projectId/locize.apiKey from your config or the LOCIZE_PROJECTID/LOCIZE_API_KEY environment variables; otherwise it opens the signup page and asks you to paste them (the one manual step). Any write-capable API key works: your target languages are created automatically on the first sync (locize-cli β₯ 12.3), and auto-translate + Quality Estimation are on by default for new Locize projects.--auto-translate, waits for the AI translations to arrive, downloads them, and prints the i18next-locize-backend CDN wiring snippet (so translation fixes go live without redeploying your app).Options:
--dry-run: Preview every step; nothing is written or pushed-y, --yes: Accept defaults; auto-approve instrumentation candidates (no per-string prompts)--ci: Non-interactive; never opens a browser or prompts. Instrumentation is skipped in CI (it rewrites source files and needs human review) unless combined with --yes--skip-instrument: Skip the code-instrumentation step (your code already calls t())--skip-translate: Sync to Locize but don't request AI auto-translation--skip-locize: Stop after extraction (local files only)--namespace <ns>: Target namespace for instrumented keys--update-values: Also update existing translation values on Locize--cdn-type <standard|pro>: Locize CDN endpoint type--print-agent-prompt: Print a copy-paste prompt for AI coding agents, then exit (see below)Behavior matrix:
| Step | interactive (default) | --yes | --ci | --dry-run |
|---|---|---|---|---|
| Instrument | per-string prompts | auto-approve | skipped (force with --yes) | candidate preview |
| Connect Locize | browser + paste credentials | same | env vars required, else exit 1 | report only |
| Sync + translate | runs | runs | runs | --dry forwarded |
| Poll + download | watches translations arrive | same | single download, no wait | skipped |
Safe to re-run: the command is idempotent. Already-wrapped strings are not re-instrumented, extraction is deterministic, and syncing never overwrites translations edited remotely (no --update-values unless you pass it; locize-cli's --reference-language-only default keeps target languages safe).
Next.js App Router: instrument injects
useTranslation(), which is client-only. Review the diff for server components β add'use client'or switch those to a server-sidet()pattern.
Non-React stacks (Vue, Svelte, β¦): the instrument step transforms React/JSX out of the box. For other stacks, add a plugin that covers your file type (community: i18next-cli-vue, i18next-cli-plugin-svelte β or write your own via the Plugin System instrumentOnLoad/onLoad hooks). With a matching plugin configured, localize runs the full flow; without one, the instrument step is skipped with guidance and the remaining steps (extract β Locize β auto-translate) still run.
Agent Skill (recommended): install the flow as a skill and your agent picks it up on its own, no copy-pasting:
npx skills add i18next/i18next-cli
Then just ask it to "add i18n to this project". See skills/i18next-localization β it ships in this repo, so it stays version-matched to the commands it drives.
Agent prompt: the same flow is also available as a copy-paste prompt for AI coding agents (Claude Code, Cursor, β¦):
npx i18next-cli localize --print-agent-prompt
This prints step-by-step instructions an agent can follow using the individual CLI commands β version-matched to your installed CLI, so it never drifts from what the supercommand does. Prefer the command output over the copy below, which is a snapshot for reference:
You are localizing this app with i18next + Locize. Execute these steps in order,
verifying each before continuing. Use `npx i18next-cli` for all commands.
1. Detect: confirm this is a React/Next.js project (check package.json).
- If Vue/Svelte: install a stack plugin (`i18next-cli-vue` /
`i18next-cli-plugin-svelte`) and add it to the `plugins` array of
i18next.config.ts, or write one via the plugin hooks
(instrumentOnLoad/onLoad) instead of wrapping strings manually.
- If the app uses inlang Paraglide (`@inlang/paraglide-js`), STOP β
instrumenting i18next calls would conflict; ask the user how to proceed.
2. Config: if no i18next.config.{ts,js} exists, run `npx i18next-cli init`
and answer the prompts (pick Locize as backend if the user wants managed
translations and AI auto-translate).
3. Instrument: run `npx i18next-cli instrument --dry-run` and review the
planned changes; then `npx i18next-cli instrument` to apply. Inspect the
git diff carefully: fix any t() wrapping inside Next.js *server components*
(add 'use client' or refactor to a server-side t() pattern). Commit.
4. Extract: run `npx i18next-cli extract`. Verify the locale JSON files were
written (check the extract.output path in the config).
5. Locize: ask the user for LOCIZE_PROJECTID and LOCIZE_API_KEY (they create
the project at https://www.locize.app/register?from=i18next_cli__agent-prompt
β any write-capable API key works; the target languages from
i18next.config.ts are created automatically on the first sync.
Auto-translation and quality estimation are enabled by default for new
projects; translations run once the project is subscribed or an AI/MT
provider is configured). Export both as environment variables.
6. Translate & deliver:
`npx i18next-cli locize-sync --auto-translate true`
then `npx i18next-cli locize-download` to pull the AI translations, and
`npx i18next-cli status` β confirm all languages are (near) 100%.
AI translation is asynchronous; if targets are still empty, wait a minute
and re-run locize-download.
7. Optionally switch runtime loading to i18next-locize-backend (CDN delivery,
so translation fixes go live without redeploying). NEVER put the API key
in client-side code β the CDN only needs the project ID.
migrate-configAutomatically migrates a legacy i18next-parser.config.js file to the new i18next.config.ts format.
npx i18next-cli migrate-config
# Using custom path for old config
npx i18next-cli migrate-config i18next-parser.config.mjs
βΉοΈ Coming from
i18next-parser? Note that i18next-cli requires Node.js >= 22 (i18next-parser still ran on Node 18/20), so CI images may need a runtime bump alongside the config migration.
rename-keySafely refactor translation keys across your entire codebase. This command updates both source files and translation files atomically.
npx i18next-cli rename-key <oldKey> <newKey> [options]
Options:
--dry-run: Preview changes without modifying any filesUsage Examples:
# Basic rename
npx i18next-cli rename-key "old.key" "new.key"
# With namespace prefix
npx i18next-cli rename-key "common:button.submit" "common:button.save"
# Preview changes without modifying files
npx i18next-cli rename-key "old.key" "new.key" --dry-run
# Refactor from mnemonic ID to meaningful key
npx i18next-cli rename-key "Invalid username or password" "login.form.invalid-credentials"
First-time setup: the easiest way to wire up Locize is to run
npx i18next-cli init and pick Locize at the "Translation backend?"
prompt β the wizard will open the signup page, ask for your Project ID
and API key, and write the locize block into your config for you. See
the init command for details.
Prerequisites: The locize commands require locize-cli to be installed:
# Install globally (recommended)
npm install -g locize-cli
Sync translations with the Locize translation management platform:
# Download translations from Locize
npx i18next-cli locize-download
# Upload/sync translations to Locize
npx i18next-cli locize-sync
# Migrate local translations to Locize
npx i18next-cli locize-migrate
Locize Command Options:
The locize-sync command supports additional options:
npx i18next-cli locize-sync [options]
Options:
--update-values: Update values of existing translations on locize--src-lng-only <true|false>: Check for changes in source language only (default: true). Pass --src-lng-only false to sync all languages--compare-mtime: Compare modification times when syncing--dry-run: Run the command without making any changes--auto-translate <true|false>: Trigger AI/MT auto-translation of newly synced keys. Requires auto-translation in your Locize project (enabled by default for new projects; runs once the project is subscribed or an AI/MT provider is configured)--auto-translate-review <true|false>: Route auto-translated segments through the review workflow for languages that have review enabled--auto-translate-languages <lng1,lng2>: Restrict auto-translation to these target languages (defaults to all)The same options can be set persistently in the locize block of your config:
export default defineConfig({
// ...
locize: {
projectId: '...',
apiKey: process.env.LOCIZE_API_KEY,
autoTranslate: true,
autoTranslateReview: false,
autoTranslateLanguages: ['de', 'fr'],
},
});
Note: auto-translation only fires when the reference language is updated, and the translation itself happens asynchronously on the Locize side β run
locize-download(or letlocalizewait for you) to pull the results.
Interactive Setup: If your locize credentials are missing or invalid, the toolkit will guide you through an interactive setup process to configure your Project ID, API Key, and version.
-c, --config <path> β Override automatic config detection and use the specified config file (relative to cwd or absolute). This option is forwarded to commands that load or ensure a config (e.g. extract, status, types, sync, locize-*).Examples:
# Use a config file stored in a package subfolder (monorepo)
npx i18next-cli extract --config ./packages/my-package/config/i18next.config.ts
# Short flag variant, for status
npx i18next-cli status de -c ./packages/my-package/config/i18next.config.ts
The configuration file supports both TypeScript (.ts) and JavaScript (.js) formats. Use the defineConfig helper for type safety and IntelliSense.
π‘ No Installation Required? If you don't want to install
i18next-clias a dependency, you can skip thedefineConfighelper and return a plain JavaScript object or JSON instead. ThedefineConfigfunction is purely for TypeScript support and doesn't affect functionality.
// i18next.config.ts
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de', 'fr'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
});
β Important: Only
.js,.jsx,.ts, and.tsxfiles are extracted by default. If you want to extract from other file types (e.g.,.pug,.vue), you must use or create a plugin. See the Plugin System section for more information.
Alternative without local installation:
// i18next.config.js
export default {
locales: ['en', 'de', 'fr'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
};
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de', 'fr'],
// Key extraction settings
extract: {
input: ['src/**/*.{ts,tsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
/** Glob pattern(s) for files to ignore during extraction */
ignore: ['node_modules/**'],
// Use '.ts' files with `export default` instead of '.json'
// Or use 'json5' to enable JSON5 features (comments, trailing commas, formatting are tried to be preserved)
// Or use 'yaml' for YAML format (.yaml or .yml extensions)
// if the file ending is .json5, .yaml, or .yml it automatically uses the corresponding format
outputFormat: 'ts',
// Combine all namespaces into a single file per language (e.g., locales/en.ts)
// Note: `output` path must not contain `{{namespace}}` when this is true.
mergeNamespaces: false,
// Translation functions to detect. Defaults to ['t', '*.t'].
// Supports a leading wildcard to match any object (suffix match), e.g.
// '*.t' matches `i18n.t` / `this._i18n.t`, and a trailing wildcard to match
// any method on an object (prefix match), e.g. 'tProps.*' matches
// `tProps.label` / `tProps.title`.
functions: ['t', '*.t', 'i18next.t', 'tProps.*'],
// React components to analyze
transComponents: ['Trans', 'Translation'],
// HTML tags to preserve in Trans component default values
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
// Hook-like functions that return a t function.
// Supports strings for default behavior or objects for custom argument positions.
useTranslationNames: [
'useTranslation', // Standard hook (ns: arg 0, keyPrefix: arg 1)
'getT',
'useT',
{
name: 'loadPageTranslations',
nsArg: 1, // Namespace is the 2nd argument (index 1)
keyPrefixArg: 2 // Options with keyPrefix is the 3rd (index 2)
}
],
// Namespace and key configuration
defaultNS: 'translation', // If set to false it will not generate any namespace, useful if i.e. the output is a single language json with 1 namespace (and no nesting).
fallbackNS: 'fallback', // Namespace(s) (string or array, like the i18next option) to use as fallback when a key is missing in the current namespace for a locale. Keys already translated in a fallback namespace are not duplicated into other namespace files by `extract`. (default undefined)
nsSeparator: ':',
keySeparator: '.', // Or `false` to disable nesting and use flat keys
contextSeparator: '_',
pluralSeparator: '_',
// Preserve dynamic keys matching patterns
preservePatterns: [
// Key patterns
'dynamic.feature.*', // Matches dynamic.feature.anything
'generated.*.key', // Matches generated.anything.key
// Namespace patterns
'assets:*', // Preserves ALL keys in the 'assets' namespace
'common:button.*', // Preserves keys like common:button.save, common:button.cancel
'errors:api.*', // Preserves keys like errors:api.timeout, errors:api.server
// Specific key preservation across namespaces
'dynamic:user.*.profile', // Matches dynamic:user.admin.profile, dynamic:user.guest.profile
],
/**
* When true, preserves all context variants of keys that use context parameters,
* across every configured locale. For example, if 'friend' is used with a context
* option in source code, variants like 'friend_male' and 'friend_female' are kept
* in the primary language even when they're not referenced explicitly, and are
* propagated to secondary locales with empty placeholders so every language ends
* up with the same key skeleton.
* (default: false)
*/
preserveContextVariants: false,
// Output formatting
sort: true, // can be also a sort function => i.e. (a, b) => a.key > b.key ? -1 : a.key < b.key ? 1 : 0, // sort in reverse order
indentation: 2, // can be also a string
// Primary language settings
primaryLanguage: 'en', // Defaults to the first locale in the `locales` array
secondaryLanguages: ['de', 'fr'], // Defaults to all locales except primaryLanguage
// Default value for missing keys in secondary languages
// Can be a string, function, or object for flexible fallback strategies
defaultValue: '', // Simple string: all missing keys get this value
// Or use a function for dynamic defaults:
// defaultValue: (key, namespace, language, value) => key, // i18next-parser style: use key as value
// defaultValue: (key, namespace, language, value) => `TODO: translate ${key}`, // Mark untranslated keys
// defaultValue: (key, namespace, language, value) => language === 'de' ? 'German TODO' : 'TODO', // Language-specific
/** If true, keys that are not found in the source code will be removed from translation files. (default: true) */
removeUnusedKeys: true,
// Namespaces to ignore during extraction, status, and sync operations.
// Useful for monorepos where shared namespaces are managed elsewhere.
// Keys using these namespaces will be excluded from processing.
// An ignored namespace can still act as a `fallbackNS` source: its
// translations are read (never written) for fallback accounting. If it
// lives in its own file outside a merged output (`mergeNamespaces: true`),
// use an `output` function that maps that namespace to its path, e.g.
// output: (lng, ns) => ns === 'shared' ? `locales/${lng}/${ns}.json` : `locales/${lng}.json`
ignoreNamespaces: ['shared', 'common'], // Optional
// When true (default), the extractor also scans code comments for t(...) / Trans examples and will extract keys found there.
// Set to false to ignore translation-like patterns in comments (useful to avoid extracting example/documentation strings).
extractFromComments: true,
// Control whether base plural forms are generated when context is present
// When false, t('key', { context: 'male', count: 1 }) will only generate
// key_male_one, key_male_other but NOT key_one, key_other
generateBasePluralForms: true, // Default: true
// Completely disable plural generation, even when count is present
// When true, t('key', { count: 1 }) will only generate 'key' (no _one, _other suffixes)
// The count option can still be used for {{count}} interpolation in the translation value
disablePlurals: false, // Default: false
// Generate the union of all configured locales' plural forms for every language.
// For example, if your locales are ['en', 'pl'], English normally only gets _one/_other,
// but with this option it also gets _few/_many (needed by Polish).
// Useful when you want a consistent set of plural keys across all locales.
allPluralForms: false, // Default: false
// Prefix for nested translations.
// Controls how nested $t(...) calls inside strings are detected.
// Nested references are scanned in BOTH source code (keys and defaultValues
// passed to t()) and in the values of existing translation files, so keys
// reachable only via `$t(...)` inside a translation value are preserved by
// `extract` and expanded into the correct per-locale plural skeleton.
// Example: '$t('
nestingPrefix: '$t(', // Default: '$t('
// Suffix for nested translations.
// Example: ')'
nestingSuffix: ')', // Default: ')'
// Separator for nested translation options.
// Used to split key vs options inside $t(key, {...}).
nestingOptionsSeparator: ',', // Default: ','
// Interpolation prefix used in defaultValue templates and runtime interpolation.
// Example: '{{'
interpolationPrefix: '{{', // Default: '{{'
// Interpolation suffix used in defaultValue templates and runtime interpolation.
// Example: '}}'
interpolationSuffix: '}}', // Default: '}}'
// Warn (or error) when the same ns:key is extracted with different default values.
warnOnConflicts: true // Default: false
},
// options for linter
lint: {
/** Optional accept-list of JSX attribute names to exclusively lint (takes precedence over ignoredAttributes). */
acceptedAttributes: ['title'],
/** Optional accept-list of JSX tag names to exclusively lint (takes precedence over ignoredTags).
* Pass 'all' to lint every tag (including custom JSX components); ignoredTags still apply. */
acceptedTags: ['p'],
// Optional custom JSX attributes to ignore during linting
ignoredAttributes: ['data-testid', 'aria-label'],
// Optional JSX tag names whose content should be ignored when linting
ignoredTags: ['pre'],
/** Glob pattern(s) for files to ignore during lint (in addition to those defined during extract) */
ignore: ['additional/stuff/**'],
/** Enable linting for interpolation parameter errors in translation calls (default: true) */
checkInterpolationParams: true,
/** Lint string concatenation involving translated strings (default: 'warn').
* 'warn'/true reports without failing the run, 'error' fails the run (exit non-zero,
* good for CI), 'off'/false disables it. */
checkConcatenation: 'warn',
/** Lint punctuation glued onto a translation, e.g. <Trans>Email</Trans>: (default: 'off').
* Opt-in; accepts the same values as checkConcatenation. */
checkPunctuationConcatenation: 'off',
},
// `status` command
status: {
// Glob patterns for keys that `status` should not report, e.g. keys that are
// intentionally empty in some locales. Same shape as `preservePatterns`; a `ns:`
// prefix limits the pattern to one namespace. Only affects `status`, not `extract`.
ignoreKeys: ['*-href', 'common:empty-table-subtitle'], // Optional
},
// TypeScript type generation
types: {
input: ['locales/en/*.json'], // or use '**/*.json' with basePath for nested namespaces
basePath: 'locales/en', // Optional: enables nested directory structures as namespaces
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
enableSelector: true, // Enable type-safe key selection
},
// Locize integration
locize: {
projectId: 'your-project-id',
apiKey: process.env.LOCIZE_API_KEY, // Recommended: use environment variables
version: 'latest',
cdnType: 'standard' // or 'pro'
},
// Plugin system
plugins: [
// Add custom plugins here
],
});
You can extend the built-in recommended lists for linting by importing and spreading them in your config:
import { defineConfig, recommendedAcceptedTags, recommendedAcceptedAttributes } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{js,jsx,ts,tsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
lint: {
acceptedTags: ['my-web-component', ...recommendedAcceptedTags],
acceptedAttributes: ['data-label', ...recommendedAcceptedAttributes]
}
});
Create custom plugins to extend the capabilities of i18next-cli. The plugin system provides hooks for extraction, linting, and instrumentation, with a single unified plugins array.
Available Hooks:
setup: Runs once when the CLI is initialized. Use it for any setup tasks.onLoad: Runs for each file before it is parsed. You can use this to transform code (e.g., transpile a custom language to JavaScript).onVisitNode: Runs for every node in the Abstract Syntax Tree (AST) of a parsed JavaScript/TypeScript file. This provides access to the full parsing context, including variable scope and TypeScript-specific syntax like satisfies and as operators.onKeySubmitted: Hook called synchronously for every translation key submitted to the extractor, including duplicates, before the deduplication decision is made.extractKeysFromExpression: Runs for specific expressions during AST traversal to extract additional translation keys. This is ideal for handling custom syntax patterns or complex key generation logic without managing pluralization manually.extractContextFromExpression: Runs for specific expressions to extract context values that can't be statically analyzed. Useful for dynamic context patterns or custom context resolution logic.onEnd: Runs after all JS/TS files have been parsed but before the final keys are compared with existing translation files. This is the ideal hook for parsing non-JavaScript files (like .html, .vue, or .svelte) and adding their keys to the collection.afterSync: Runs after the extractor has compared the found keys with your translation files and generated the final results. This is perfect for post-processing tasks, like generating a report of newly added keys.Lint Plugin Hooks:
lintSetup(context): Runs once before linting starts. Receives LintPluginContext with config and logger.lintExtensions: Optional extension hint (for example ['.vue']). Used as a skip hint/optimization.lintOnLoad(code, filePath): Runs before lint parsing for each file.
string to replace source code for linting.undefined to pass through unchanged.null to skip linting the file entirely.lintOnResult(filePath, issues): Runs after each file is linted. Return a new issues array to filter/augment results, or undefined to keep as-is.Instrument Plugin Hooks:
instrumentSetup(context): Runs once before instrumentation starts. Receives InstrumentPluginContext with config and logger.instrumentExtensions: Optional extension hint (for example ['.vue']). Used as a skip hint/optimization.instrumentOnLoad(code, filePath): Runs before scanning each file for hardcoded strings.
string to replace source code before scanning.undefined to pass through unchanged.null to skip instrumenting the file entirely.instrumentOnResult(filePath, candidates): Runs after candidates are detected. Return a new CandidateString[] to filter/augment results, or undefined to keep as-is.import type {
Plugin,
LinterPlugin,
LintPluginContext,
LintIssue,
} from 'i18next-cli';
// You can type your plugin as Plugin (full surface), LinterPlugin (lint-focused),
// or InstrumenterPlugin (instrument-focused)
export const vueLintPlugin = (): LinterPlugin => ({
name: 'vue-lint-plugin',
lintExtensions: ['.vue'],
lintSetup: async (context: LintPluginContext) => {
context.logger.info('vue lint plugin initialized');
},
lintOnLoad: async (code, filePath) => {
if (!filePath.endsWith('.vue')) return undefined;
// preprocess SFC/template to lintable JS/TS/JSX text
return code;
},
lintOnResult: async (_filePath, issues: LintIssue[]) => {
// Example: keep only interpolation issues
return issues.filter(issue => issue.type === 'interpolation');
}
});
import type {
InstrumenterPlugin,
InstrumentPluginContext,
CandidateString,
} from 'i18next-cli';
export const vueInstrumentPlugin = (): InstrumenterPlugin => ({
name: 'vue-instrument-plugin',
instrumentExtensions: ['.vue'],
instrumentSetup: async (context: InstrumentPluginContext) => {
context.logger.info('vue instrument plugin initialized');
},
instrumentOnLoad: async (code, filePath) => {
if (!filePath.endsWith('.vue')) return undefined;
// Extract template block from SFC and return as JSX-like code
return code;
},
instrumentOnResult: async (_filePath, candidates: CandidateString[]) => {
// Example: only keep high-confidence candidates
return candidates.filter(c => c.confidence >= 0.5);
}
});
Config usage (same plugins list for extract + lint + instrument):
import { defineConfig } from 'i18next-cli';
import { vueLintPlugin } from './plugins/vue-lint-plugin.mjs';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx,vue}'],
output: 'locales/{{language}}/{{namespace}}.json'
},
plugins: [
vueLintPlugin()
]
});
Basic Plugin Example:
import { glob } from 'glob';
import { readFile, writeFile } from 'node:fs/promises';
export const myCustomPlugin = () => ({
name: 'my-custom-plugin',
// Handle custom file formats
async onEnd(keys) {
// Extract keys from .vue files
const vueFiles = await glob('src/**/*.vue');
for (const file of vueFiles) {
const content = await readFile(file, 'utf-8');
const keyMatches = content.matchAll(/\{\{\s*\$t\(['"]([^'"]+)['"]\)/g);
for (const match of keyMatches) {
keys.set(`translation:${match[1]}`, {
key: match[1],
defaultValue: match[1],
ns: 'translation'
});
}
}
}
});
Advanced Plugin with Expression Parsing:
export const advancedExtractionPlugin = () => ({
name: 'advanced-extraction-plugin',
// Extract keys from TypeScript satisfies expressions
extractKeysFromExpression: (expression, config, logger) => {
const keys = [];
// Handle template literals with variable substitutions
if (expression.type === 'TemplateLiteral') {
// Extract pattern: `user.${role}.permission`
const parts = expression.quasis.map(q => q.cooked);
const variables = expression.expressions.map(e =>
e.type === 'Identifier' ? e.value : 'dynamic'
);
if (variables.includes('role')) {
// Generate keys for known roles
keys.push('user.admin.permission', 'user.manager.permission', 'user.employee.permission');
}
}
// Handle TypeScript satisfies expressions
if (expression.type === 'TsAsExpression' &&
expression.typeAnnotation?.type === 'TsUnionType') {
const unionTypes = expression.typeAnnotation.types;
for (const unionType of unionTypes) {
if (unionType.type === 'TsLiteralType' &&
unionType.literal?.type === 'StringLiteral') {
keys.push(`dynamic.${unionType.literal.value}.extracted`);
}
}
}
return keys;
},
// Extract context from conditional expressions
extractContextFromExpression: (expression, config, logger) => {
const contexts = [];
// Handle ternary operators: isAdmin ? 'admin' : 'user'
if (expression.type === 'ConditionalExpression') {
if (expression.consequent.type === 'StringLiteral') {
contexts.push(expression.consequent.value);
}
if (expression.alternate.type === 'StringLiteral') {
contexts.push(expression.alternate.value);
}
}
// Handle template literals: `${role}.${level}`
if (expression.type === 'TemplateLiteral') {
const parts = expression.expressions.map(expr =>
expr.type === 'Identifier' ? expr.value : 'unknown'
);
if (parts.length > 0) {
const joins = expression.quasis.map(quasi => quasi.cooked);
contexts.push(joins.reduce((acc, join, i) =>
acc + (join || '') + (parts[i] || ''), ''
));
}
}
return contexts;
},
// Handle complex AST patterns
onVisitNode: (node, context) => {
// Custom extraction for specific component patterns
if (node.type === 'JSXElement' &&
node.opening.name.type === 'Identifier' &&
node.opening.name.value === 'CustomTransComponent') {
const keyAttr = node.opening.attributes?.find(attr =>
attr.type === 'JSXAttribute' &&
attr.name.value === 'translationKey'
);
if (keyAttr?.value?.type === 'StringLiteral') {
context.addKey({
key: keyAttr.value.value,
defaultValue: 'Custom component translation',
ns: 'components'
});
}
}
}
});
Configuration:
import { defineConfig } from 'i18next-cli';
import { myCustomPlugin, advancedExtractionPlugin } from './my-plugins.mjs';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,vue}'],
output: 'locales/{{language}}/{{namespace}}.json'
},
plugins: [
myCustomPlugin(),
advancedExtractionPlugin()
]
});
Track where each translation key is used in your codebase with a custom metadata plugin.
Example Plugin Implementation:
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import type { Plugin } from 'i18next-cli';
interface LocationMetadataOptions {
/** Output path for the metadata file (default: 'locales/metadata.json') */
output?: string;
/** Include line and column numbers (default: true) */
includePosition?: boolean;
}
export const locationMetadataPlugin = (options: LocationMetadataOptions = {}): Plugin => {
const {
output = 'locales/metadata.json',
includePosition = true,
} = options;
return {
name: 'location-metadata',
async onEnd(keys) {
const metadata: Record<string, any> = {};
for (const [uniqueKey, extractedKey] of keys.entries()) {
const { key, ns, locations } = extractedKey;
// Skip keys without location data
if (!locations || locations.length === 0) {
continue;
}
// Format location data
const locationData = locations.map(loc => {
if (includePosition && loc.line !== undefined) {
return `${loc.file}:${loc.line}:${loc.column ?? 0}`;
}
return loc.file;
});
// Organize metadata
const namespace = ns || 'translation';
if (!metadata[namespace]) {
metadata[namespace] = {};
}
metadata[namespace][key] = locationData;
}
// Write metadata file
await mkdir(dirname(output), { recursive: true });
await writeFile(output, JSON.stringify(metadata, null, 2), 'utf-8');
console.log(`π Location metadata written to ${output}`);
}
};
};
Configuration:
// i18next.config.ts
import { defineConfig } from 'i18next-cli';
import { locationMetadataPlugin } from './plugins/location-metadata.mjs';
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
plugins: [
locationMetadataPlugin({
output: 'locales/metadata.json'
})
]
});
Example Output (locales/metadata.json):
{
"translation": {
"app.title": [
"src/App.tsx:12:15",
"src/components/Header.tsx:8:22"
],
"user.greeting": [
"src/pages/Profile.tsx:45:10"
]
},
"common": {
"button.save": [
"src/components/SaveButton.tsx:18:7",
"src/forms/UserForm.tsx:92:5"
]
}
}
Many "dynamic" keys don't need any configuration: since v1.49 the extractor performs TypeScript type-aware resolution of finite dynamic keys and expands every possible variant automatically (#210):
// Template literals with unions / ternaries / nullish coalescing:
t(`state.${isDone ? 'done' : 'notDone'}.title`) // -> state.done.title + state.notDone.title
type Status = 'active' | 'inactive'
declare const status: Status
t(`status.${status}`) // -> status.active + status.inactive
// `as const` maps and arrays (also when imported from another file):
const KEYS = ['a', 'b'] as const
KEYS.map((k) => t(`item.${k}`)) // -> item.a + item.b
const MAP = { ok: 'result.ok', err: 'result.err' } as const
t(MAP[someCondition ? 'ok' : 'err']) // -> result.ok + result.err
// Helper-function return types and `satisfies`-constrained values
Only keys that are truly runtime-dynamic (e.g. built from API data) cannot
be statically resolved by any tool. For those, use preservePatterns to keep
the existing entries in your translation files:
// Code like this:
const key = `user.${role}.permission`; // role comes from the server
t(key);
// With this config:
export default defineConfig({
extract: {
preservePatterns: ['user.*.permission']
}
});
// Will preserve existing keys matching the pattern
Extract keys from comments for documentation or edge cases:
// t('welcome.message', 'Welcome to our app!')
// t('user.greeting', { defaultValue: 'Hello!', ns: 'common' })
For projects that prefer to keep everything in a single module type, you can configure the CLI to output JavaScript or TypeScript files instead of JSON.
Configuration (i18next.config.ts):
export default defineConfig({
extract: {
output: 'src/locales/{{language}}/{{namespace}}.ts', // Note the .ts extension
outputFormat: 'ts', // Use TypeScript with ES Modules
}
});
This will generate files like src/locales/en/translation.ts with the following content:
export default {
"myKey": "My value"
} as const;
For projects that prefer YAML for better readability and compatibility with other tools, you can configure the CLI to output YAML files instead of JSON.
Configuration (i18next.config.ts):
export default defineConfig({
extract: {
output: 'locales/{{language}}/{{namespace}}.yaml', // Use .yaml or .yml
outputFormat: 'yaml', // Optional - inferred from file extension
}
});
This will generate files like locales/en/translation.yaml with the following content:
app:
title: My Application
description: Welcome to our app
button:
save: Save
cancel: Cancel
π‘ Note: Both
.yamland.ymlextensions are supported and preserved. TheoutputFormat: 'yaml'option is optional when using these extensions - the format is automatically inferred from the file extension.
A common concern with runtime i18n is "you ship every language and every namespace to the client". You don't have to: the i18next runtime loads translations per namespace, per language, so namespace granularity is your code-splitting boundary. The runtime core itself is ~13.5 kB gzipped; what grows with your app is translation payload, and that is entirely controlled by how you slice namespaces.
The recipe:
useTranslation('checkout'), t('checkout:title'), or the ns option. The extractor detects the namespace from your code and writes one file per namespace and language:export default defineConfig({
locales: ['en', 'de'],
extract: {
input: 'src/**/*.{ts,tsx}',
output: 'src/locales/{{language}}/{{namespace}}.json',
}
});
import i18next from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
i18next
.use(resourcesToBackend((lng, ns) => import(`./locales/${lng}/${ns}.json`)))
.init({
fallbackLng: 'en',
defaultNS: 'app',
ns: ['app'], // only the app-shell namespace loads upfront
});
useTranslation('checkout') (react-i18next) or i18next.loadNamespaces('checkout') fetches exactly that chunk when the route renders. The initial payload contains only the namespaces the entry route uses, in the active language; other languages transfer nothing until switched to.Prefer not to bundle translations at all? Serve the same per-namespace files from any static host/CDN via i18next-http-backend, or directly from the Locize CDN via i18next-locize-backend β same wire profile, plus translation updates without redeploying.
You can also combine all namespaces into a single file per language. This is useful for reducing the number of network requests in some application setups.
Configuration (i18next.config.ts):
export default defineConfig({
extract: {
// Note: The `output` path no longer contains the {{namespace}} placeholder
output: 'src/locales/{{language}}.ts',
outputFormat: 'ts',
mergeNamespaces: true,
}
});
This will generate a single file per language, like src/locales/en.ts, with namespaces as top-level keys:
export default {
"translation": {
"key1": "Value 1"
},
"common": {
"keyA": "Value A"
}
} as const;
When generating TypeScript types, namespaces are derived from the filename only by default (e.g., locales/en/dashboard/user.json β namespace: user). If you organize translation files in nested directories and want the generated types to preserve that structure as part of the namespace, use the basePath option in your types configuration.
Configuration (i18next.config.ts):
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
types: {
input: 'public/locales/en/**/*.json',
basePath: 'public/locales/en',
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
}
});
With this configuration:
public/locales/en/common.json β type namespace: commonpublic/locales/en/dashboard/user.json β type namespace: dashboard/userpublic/locales/en/dashboard/settings.json β type namespace: dashboard/settingspublic/locales/en/features/auth/login.json β type namespace: features/auth/loginThe basePath can include the {{language}} placeholder for flexibility:
types: {
input: 'public/locales/en/**/*.json',
basePath: 'public/locales/{{language}}',
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
}
This is useful for organizing translations into logical groups while maintaining type safety across your entire namespace hierarchy.
Automatically migrate from legacy i18next-parser.config.js:
npx i18next-cli migrate-config
This will:
i18next.config.ts fileImportant: File Management Differences
Unlike i18next-parser, i18next-cli takes full ownership of translation files in the output directory. If you have manually managed translation files that should not be modified, place them in a separate directory or use different naming patterns to avoid conflicts.
Use the --ci flag to fail builds when translations are outdated:
# GitHub Actions example
- name: Check translations
run: npx i18next-cli extract --ci
Likewise, fail the build when the generated TypeScript definitions are out of date:
# Fail the build if generated TypeScript definitions are out of date
- name: Check i18n types
run: npx i18next-cli types --ci
For development, use watch mode to automatically update translations:
npx i18next-cli extract --watch
npx i18next-cli lint --watch
Generate TypeScript definitions for full type safety:
// Generated types enable autocomplete and validation
t('user.profile.name'); // β
Valid key
t('invalid.key'); // β TypeScript error
The toolkit automatically detects these i18next usage patterns:
// Basic usage
t('key')
t('key', 'Default value')
t('key', { defaultValue: 'Default' })
// With namespaces
t('ns:key')
t('key', { ns: 'namespace' })
// With interpolation
t('key', { name: 'John' })
// With plurals and context
t('key', { count: 1 }); // Cardinal plural
t('keyWithContext', { context: 'male' });
t('keyWithDynContext', { context: isMale ? 'male' : 'female' });
// With ordinal plurals
t('place', { count: 1, ordinal: true });
t('place', {
count: 2,
ordinal: true,
defaultValue_ordinal_one: '{{count}}st place',
defaultValue_ordinal_two: '{{count}}nd place',
defaultValue_ordinal_other: '{{count}}th place'
});
// With key fallbacks
t(['key.primary', 'key.fallback']);
t(['key.primary', 'key.fallback'], { defaultValue: 'The fallback value' });
// With structured content (returnObjects)
t('countries', { returnObjects: true });
The extractor correctly handles cardinal and ordinal plurals (count), as well as context options, generating all necessary suffixed keys (e.g., key_one, key_ordinal_one, keyWithContext_male). It can even statically analyze ternary expressions in the context option to extract all possible variations.
// Trans component
<Trans i18nKey="welcome">Welcome {{name}}</Trans>
<Trans ns="common">user.greeting</Trans>
<Trans count={num}>You have {{num}} message</Trans>
<Trans context={isMale ? 'male' : 'female'}>A friend</Trans>
// useTranslation hook
const { t } = useTranslation('namespace');
const { t } = useTranslation(['ns1', 'ns2']);
// Aliased functions
const translate = t;
translate('key');
// Destructured hooks
const { t: translate } = useTranslation();
// getFixedT
const fixedT = getFixedT('en', 'namespace');
fixedT('key');
The extractor handles the type-safe Selector API and mirrors the runtime namespace-routing rule from i18next v25.8.19. With a single-namespace hook, selector paths are extracted into the bound namespace as-is:
const { t } = useTranslation('common');
t($ => $.button.save); // β common.json: button.save
t($ => $.button.save, { ns: 'auth' }); // β auth.json: button.save
When the hook is called with a multi-namespace array, a leading path segment that matches a secondary namespace is treated as a namespace prefix and the rest of the path is routed to that namespace's file. The primary namespace (the array's first entry) is never rewritten β its keys are exposed flat on the selector proxy:
const { t } = useTranslation(['auth', 'validation']);
t($ => $.login['Welcome Back!']); // β auth.json: login.Welcome Back!
t($ => $.validation.email['Required']); // β validation.json: email.Required
t($ => $.email['Required'], { ns: 'validation'}); // β validation.json: email.Required
This matches the behavior of i18next/src/selector.js exactly: paths whose
first segment is the primary namespace, or doesn't appear in the hook's
namespace list at all, are joined with the configured keySeparator and
routed to the primary. Secondary-prefixed paths are joined as
<ns><nsSeparator><rest> so the standard ns:key routing places them in
the correct file.
Set types.enableSelector: 'strict' (requires i18next β₯ 26.1.0 with
the matching runtime option) to drop the flattened-primary form entirely.
Every selector path must lead with an explicit namespace segment, and the
extractor rewrites leading segments uniformly β primary, secondary,
single- or multi-ns hooks all behave the same:
// useTranslation('common');
t($ => $.common.button.save); // β common.json: button.save
// useTranslation(['auth', 'validation']);
t($ => $.auth.login['Welcome Back!']); // β auth.json: login.Welcome Back!
t($ => $.validation.email['Required']); // β validation.json: email.Required
Strict mode is opt-in and incompatible with the #2405
pattern (a key inside a namespace whose name matches a sibling namespace).
If you have keys like Resources['config'].common.name while common is
also a sibling namespace, leave strict mode off β the rewrite would route
that key into the wrong file.
In addition to the CLI commands, i18next-cli can be used programmatically in your build scripts, Gulp tasks, or any Node.js application:
import { runExtractor, runLinter, runSyncer, runStatus, runTypesGenerator } from 'i18next-cli';
import type { I18nextToolkitConfig } from 'i18next-cli';
const config: I18nextToolkitConfig = {
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
};
// Run the complete extraction process
const { anyFileUpdated, hasErrors } = await runExtractor(config);
console.log('Files updated:', anyFileUpdated);
// Check translation status programmatically
await runStatus(config);
// Run linting and get results
const { success, message, files } = await runLinter(config);
if (!success) {
console.error(message);
for (const [filename, issues] of Object.entries(files)) {
console.error(`${issues.length} issues found in ${filename}.`);
}
}
// Sync translation files
await runSyncer(config);
// types generattion
await runTypesGenerator(config);
Gulp Example:
import gulp from 'gulp';
import { runExtractor } from 'i18next-cli';
gulp.task('i18next-extract', async () => {
const config = {
locales: ['en', 'de', 'fr'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
};
await runExtractor(config);
});
Webpack Plugin Example:
class I18nextExtractionPlugin {
apply(compiler) {
compiler.hooks.afterEmit.tapAsync('I18nextExtractionPlugin', async (compilation, callback) => {
await runExtractor(config);
callback();
});
}
}
runExtractor(config, options?) - Complete extraction with file writingrunLinter(config) - Run linting analysis and return resultsrunSyncer(config) - Sync translation filesrunStatus(config, options?) - Get translation statusrunTypesGenerator(config) - Generate typesrunLocalize(options?, configPath?) - The full localize flow (detect β instrument β extract β Locize sync with auto-translate β download)Linter - Class that lints your codebase and emits events along the wayExample usage
import { Linter } from 'i18next-cli';
import type { I18nextToolkitConfig } from 'i18next-cli';
const config: I18nextToolkitConfig = {
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx,js,jsx}'],
output: 'locales/{{language}}/{{namespace}}.json',
},
};
const linter = new Linter(config);
linter.addEventListener('progress', ({ message }) => console.log(message));
await linter.run();
This programmatic API gives you the same power as the CLI but with full control over when and how it runs in your build process.
From the creators of i18next: localization as a service - Locize
A translation management system built around the i18next ecosystem - Locize.
Now with a Free plan for small projects! Perfect for hobbyists or getting started.

With using Locize you directly support the future of i18next.
TypeScript
99.0%
JavaScript
1.0%