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.
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
i18next-cli is built from the ground up to meet the demands of modern web development.
- 🚀 Performance: By leveraging a native Rust-based parser (SWC), it delivers orders-of-magnitude faster performance than JavaScript-based parsers.
- 🧠 Intelligence: A stateful, scope-aware analyzer correctly understands complex patterns like
useTranslation('ns1', { keyPrefix: '...' }),getFixedT, and aliasedtfunctions, minimizing the need for manual workarounds. - ✅ Unified Workflow: One tool, one configuration file, one integrated workflow. It replaces various syncing scripts.
- 🔌 Extensibility: A modern plugin architecture allows the tool to adapt to any framework or custom workflow.
- 🧑💻 Developer Experience: A fully-typed configuration file, live
--watchmodes, CLI output, and a migration from legacy tools.
- Key Extraction: Extraction means automatically finding and collecting all translation keys used in your source code (JavaScript/TypeScript, etc.) by analyzing the code's structure (AST). This ensures every string that needs translation is identified and included in your translation files, reducing manual work and preventing missing keys.
- Type Safety: Generate TypeScript definitions for full autocomplete and type safety.
- Locale Synchronization: Keep all language files in sync with your primary language.
- Accurate Code Linting: Detect hardcoded strings with high precision and configurable rules.
- Translation Status: Get a high-level overview or a detailed, key-by-key report of your project's translation completeness.
- Plugin System: Extensible architecture for custom extraction patterns and file types (e.g., HTML, Handlebars).
- Legacy Migration: Automatic migration from
i18next-parserconfigurations. - Cloud Integration: Seamless integration with the Locize translation management platform.
npm install --save-dev i18next-cliCreate a configuration interactively:
npx i18next-cli initOr 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 statusnpx i18next-cli extractnpx i18next-cli typesInteractive setup wizard to create your configuration file.
npx i18next-cli initParses 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)--quiet: Suppress spinner and non-essential output (for CI or scripting)
All commands that show progress spinners (extract, types, lint, sync) now support:
--quietflag to silence spinner and non-essential output (for CI, scripting, or log capture)- Programmatic logger support: pass a custom logger object to capture output in your own format or stream
CLI Example:
npx i18next-cli extract --quietProgrammatic 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
# Combine options for optimal development workflow
npx i18next-cli extract --sync-primary --watchDisplays 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.
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.
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-translatedThe 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
...Generates TypeScript definitions from your translation files for full type-safety and autocompletion.
npx i18next-cli types [options]Options:
--watch, -w: Re-run automatically when translation files change
Synchronizes secondary language files against your primary language file, adding missing keys and removing extraneous ones.
npx i18next-cli syncAnalyzes your source code for internationalization issues like hardcoded strings. Can run without a config file.
npx i18next-cli lintAutomatically 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.mjsSafely 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 files
Usage 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"Prerequisites: The locize commands require locize-cli to be installed:
# Install globally (recommended)
npm install -g locize-cliSync 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-migrateLocize 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: Check for changes in source language only--compare-mtime: Compare modification times when syncing--dry-run: Run the command without making any changes
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.tsThe 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 wildcards for suffixes.
functions: ['t', '*.t', 'i18next.t'],
// 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 to use as fallback when a key is missing in the current namespace for a locale. (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.
* For example, if 'friend' is used with context, all variants like 'friend_male',
* 'friend_female', etc. are preserved even if not explicitly found in source code.
* (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.
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
// Prefix for nested translations.
// Controls how nested $t(...) calls inside strings are detected.
// 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: '}}'
},
// 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). */
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,
},
// TypeScript type generation
types: {
input: ['locales/en/*.json'],
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 both extraction and linting, 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 likesatisfiesandasoperators.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. ReceivesLintPluginContextwithconfigandlogger.lintExtensions: Optional extension hint (for example['.vue']). Used as a skip hint/optimization.lintOnLoad(code, filePath): Runs before lint parsing for each file.- Return
stringto replace source code for linting. - Return
undefinedto pass through unchanged. - Return
nullto skip linting the file entirely.
- Return
lintOnResult(filePath, issues): Runs after each file is linted. Return a new issues array to filter/augment results, orundefinedto keep as-is.
import type {
Plugin,
LinterPlugin,
LintPluginContext,
LintIssue,
} from 'i18next-cli';
// You can type your plugin as Plugin (full surface) or LinterPlugin (lint-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');
}
});Config usage (same plugins list for extract + lint):
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"
]
}
}Use preservePatterns to maintain dynamically generated keys:
// Code like this:
const key = `user.${role}.permission`;
t(key);
// With this config:
export default defineConfig({
extract: {
preservePatterns: ['user.*.permission']
}
});
// Will preserve existing keys matching the patternExtract 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.
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;Automatically migrate from legacy i18next-parser.config.js:
npx i18next-cli migrate-configThis will:
- Convert your existing configuration to the new format
- Map old options to new equivalents
- Preserve custom settings where possible
- Create a new
i18next.config.tsfile
Important: 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 --ciFor development, use watch mode to automatically update translations:
npx i18next-cli extract --watch
npx i18next-cli lint --watchGenerate TypeScript definitions for full type safety:
// Generated types enable autocomplete and validation
t('user.profile.name'); // ✅ Valid key
t('invalid.key'); // ❌ TypeScript errorThe 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');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 types
Example 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.
- i18next-cli-plugin-svelte — a simple plugin to extract translation keys from Svelte components
- rsbuild-plugin-i18next-extractor — A Rsbuild plugin that leverages the Rspack module graph to extract only the i18n translations that are actually imported and used in your code, preventing unused translations from being bundled.
- i18next-cli-vue — i18next-cli plugin for extracting i18n keys from Vue SFC files, applicable to vue2 and vue3
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.
