AI-assisted i18n migration CLI for React and Next.js projects. Deterministic, review-first, rollback-safe.
safe-i18n scans your codebase for hardcoded strings, classifies them by risk, builds a migration plan, and applies AST-based codemods to replace them with i18n calls. An LLM handles translations and key suggestions -- it never rewrites your source code.
Use safe-i18n when a user asks an AI coding agent to add localization or new language support to a React/Next.js project, for example:
- "Add Turkish/German/French support to this Next.js app"
- "Localize this React app"
- "Migrate this project to next-intl"
- "Find hardcoded UI strings and generate translation files"
Recommended agent workflow:
npm install -D safe-i18n
npx safe-i18n migrate --to tr,de,fr --api-key-env OPENAI_API_KEY
npx safe-i18n validate --ciThe tool is designed for agents because source edits are deterministic AST transforms, while the LLM is limited to classification, translation, and key suggestions. See docs/agent-usage.md and llms.txt for a compact agent-facing guide.
When you ask an LLM to rewrite source files for i18n, it can silently break logic by translating comparison operands, API endpoints, CSS class names, localStorage keys, or route paths. It may hallucinate import statements, invent nonexistent APIs, or restructure code in ways that change behavior.
safe-i18n takes a different approach:
- All code changes are deterministic AST transforms. The LLM never sees or modifies your source code.
- Every string is risk-classified before anything happens. Dangerous or ambiguous strings are skipped or flagged for review.
- A full migration plan is saved as JSON before any file is touched. You review it first.
- Rollback is built in. Original file contents are stored in the manifest, and
safe-i18n rollbackrestores them. - The LLM is only used for translations, semantic descriptions, and key/namespace suggestions. It operates on extracted string values, not on code.
npm install -g safe-i18nOr run directly without installing:
npx safe-i18nRequires Node.js 18 or later.
# 1. Initialize configuration (detects your project structure)
safe-i18n init --source en --locales tr,de,fr
# 2. Scan for translatable strings
safe-i18n scan
# 3. Create a migration plan (no files are modified)
safe-i18n plan
# 4. Apply safe transformations only
safe-i18n apply
# 5. Generate translations for target locales
safe-i18n translate --to tr,de,fr
# 6. Validate the result
safe-i18n validateEach command guides you to the next step. The plan is always created and reviewed before any source files are changed.
Detects your project framework, TypeScript setup, router type, and package manager. Writes a safe-i18n.config.json file.
safe-i18n init --source en --locales tr,de,fr --adapter next-intlOptions:
--source <locale>-- Source locale code (default:en)--locales <locales>-- Comma-separated target locale codes--adapter <adapter>-- i18n adapter (default:next-intl)
Scans source files for hardcoded strings and classifies each by risk level. No files are modified.
safe-i18n scan
safe-i18n scan --json
safe-i18n scan --output scan-results.jsonOptions:
--json-- Output results as JSON--output <file>-- Save full scan results to a file
Creates a migration plan from scanned strings. The plan is saved as a JSON manifest at .safe-i18n/migration-plan.json. No source files are modified.
safe-i18n plan
safe-i18n plan --source en --locales tr,deOptions:
--source <locale>-- Override source locale--locales <locales>-- Override target locales
Applies the migration plan to your source files. By default, only AUTO_SAFE entries are applied. Checks for a clean git working tree before proceeding.
safe-i18n apply
safe-i18n apply --dry-run
safe-i18n apply --no-safe-only
safe-i18n apply --allow-dirtyOptions:
--dry-run-- Preview changes without writing files--no-safe-only-- Apply all entries, not just auto-safe ones--allow-dirty-- Skip the git working tree check
Generates translation files for target locales using the configured LLM provider. Reads the migration plan and produces locale-specific message files.
safe-i18n translate --to tr,de,fr
safe-i18n translate --to ja --provider openai-compatibleOptions:
--to <locales>(required) -- Comma-separated target locales--provider <name>-- Override the LLM provider from config
Validates locale files for missing keys, unused keys, placeholder mismatches, ICU syntax errors, and duplicate keys. Optionally runs user-defined validation commands (type checking, builds).
safe-i18n validate
safe-i18n validate --ciOptions:
--ci-- Exit with code 1 if any errors are found (for CI pipelines)
Restores all files modified by apply to their original state using the saved manifest. Removes generated files and cleans up the manifest.
safe-i18n rollback
safe-i18n rollback --forceOptions:
--force-- Skip the confirmation warning
safe-i18n never lets an LLM touch your source code. The architecture enforces a strict separation:
| Responsibility | Who does it |
|---|---|
| Scanning strings in source files | Deterministic AST parser (Babel) |
| Classifying risk | Rule-based classifier |
| Generating code transforms | Deterministic AST codemod |
| Generating translation keys | LLM (suggestions only, saved in plan) |
| Translating strings | LLM |
Writing .ts / .tsx files |
AST printer (recast) |
The LLM receives extracted string values with context (component name, file path, description). It returns translations. It never receives or produces source code.
Every scanned string is assigned one of four risk levels:
The string can be automatically migrated. It appears in a context where translation is clearly correct.
Examples:
- Static text inside
<p>,<h1>,<button>,<label>,<span> - Text children of common UI components (
Button,Link,Badge,Alert,Tooltip) - Strings in i18n-safe props (
placeholder,aria-label,title,alt,label,description)
The string might be translatable, but the context is ambiguous. It is included in the plan but not applied by default.
Examples:
- Strings inside conditional expressions (
isAdmin ? "Admin" : "User") - Strings in template literals (may contain dynamic content)
- Strings passed to function calls (could be a UI label or an internal identifier)
- Strings in arrays or objects (could be menu items or config values)
The string is not user-visible. It is excluded from the migration plan.
Examples:
- Strings in
console.logcalls data-testidvalues- CSS class names
- Import paths
- Route definitions
- Enum values
- Analytics event names
Translating this string would break application logic. It is excluded and flagged.
Examples:
- Strings used as comparison operands (
if (status === "active")) - localStorage/sessionStorage keys
- API endpoints and parameters
- URLs and file paths
- Strings that look like identifiers (camelCase, no spaces)
safe-i18n init generates a safe-i18n.config.json file in your project root. You can also create or edit it manually.
{
"sourceLocale": "en",
"targetLocales": ["tr", "de", "fr"],
"include": ["src/**/*.{ts,tsx,js,jsx}", "app/**/*.{ts,tsx,js,jsx}"],
"i18n": {
"adapter": "next-intl",
"messagesPath": "messages",
"namespaceStrategy": "route-based"
},
"llm": {
"provider": "openai-compatible",
"model": "gpt-4.1-mini",
"baseUrl": "https://api.openai.com/v1",
"apiKeyEnv": "SAFE_I18N_OPENAI_API_KEY"
},
"validation": {
"commands": ["npm run typecheck", "npm run build"]
}
}| Field | Description | Default |
|---|---|---|
sourceLocale |
The locale of your existing hardcoded strings | "en" |
targetLocales |
Locales to translate into | [] |
include |
Glob patterns for files to scan | ["src/**/*.{ts,tsx,js,jsx}", "app/**/*.{ts,tsx,js,jsx}"] |
exclude |
Glob patterns to skip | ["node_modules/**", ".next/**", "dist/**", ...] |
i18n.adapter |
i18n library adapter | "next-intl" |
i18n.messagesPath |
Directory for locale message files | "messages" |
i18n.namespaceStrategy |
How namespaces are derived | "route-based" |
llm.provider |
LLM provider (mock or openai-compatible) |
"mock" |
llm.model |
Model name for the LLM provider | -- |
llm.baseUrl |
Base URL for the LLM API | -- |
llm.apiKeyEnv |
Environment variable name containing the API key | -- |
validation.commands |
Shell commands to run during validate |
[] |
route-based-- Derives namespaces from the file's route path (e.g.,app/settings/page.tsx->settings)file-based-- Uses the file name as the namespacecomponent-based-- Uses the component name as the namespaceflat-- All keys in a single namespace
safe-i18n currently supports next-intl as its i18n adapter.
For client components, the generated code uses the useTranslations hook:
import { useTranslations } from "next-intl";
export default function LoginForm() {
const t = useTranslations("auth");
return <button>{t("submitButton")}</button>;
}For server components, it uses getTranslations:
import { getTranslations } from "next-intl/server";
export default async function HomePage() {
const t = await getTranslations("home");
return <h1>{t("welcomeTitle")}</h1>;
}Message files are generated in the configured messagesPath directory:
messages/
en.json
tr.json
de.json
fr.json
Each file contains namespaced messages:
{
"home": {
"welcomeTitle": "Welcome to our platform",
"description": "Get started by exploring our features"
},
"auth": {
"submitButton": "Sign in"
}
}The default provider. Returns the source text unchanged. Useful for testing the pipeline without API calls or costs.
{
"llm": {
"provider": "mock"
}
}Works with any OpenAI-compatible API (OpenAI, Azure OpenAI, local models via Ollama or LM Studio, etc.).
{
"llm": {
"provider": "openai-compatible",
"model": "gpt-4.1-mini",
"baseUrl": "https://api.openai.com/v1",
"apiKeyEnv": "SAFE_I18N_OPENAI_API_KEY"
}
}Set the API key as an environment variable. The key name is configured via apiKeyEnv -- it is never stored in the config file.
export SAFE_I18N_OPENAI_API_KEY="sk-..."When a string contains dynamic values, safe-i18n extracts them as ICU-style placeholders. The LLM is instructed to preserve these tokens:
Source: "Hello, {name}! You have {count} messages."
Turkish: "Merhaba, {name}! {count} mesajiniz var."
Generate translations for multiple locales in a single run:
safe-i18n translate --to tr,de,fr,ja,koThis produces one message file per locale in the messages/ directory. Each translation request includes context (component name, file path, semantic description) to improve translation quality.
safe-i18n validateChecks performed:
- Missing keys -- Keys present in the source locale but absent in a target locale
- Unused keys -- Keys in a target locale that do not exist in the source
- Placeholder parity -- Ensures
{variables}match between source and translated strings - ICU syntax -- Validates that ICU message format syntax is well-formed
- Duplicate keys -- Detects duplicate keys within a single locale file
- Custom commands -- Runs any commands listed in
validation.commands(e.g.,npm run typecheck,npm run build)
Use --ci to fail the pipeline when validation errors are found:
safe-i18n validate --ciExample GitHub Actions step:
- name: Validate i18n
run: npx safe-i18n validate --ciThe command exits with code 0 on success and code 1 if any errors are detected. Warnings do not cause a non-zero exit.
If something goes wrong after apply, you can restore all modified files:
safe-i18n rollback --forceThe rollback restores original file contents from the manifest saved during apply and removes any generated locale files. The manifest is stored at .safe-i18n/apply-manifest.json.
Rollback is only available for the most recent apply run. For more granular recovery, use git:
git diff # Review what changed
safe-i18n rollback --force # Or restore from manifest
git checkout -- . # Or restore from git- next-intl only. The current release supports next-intl as the sole i18n adapter. react-intl, i18next, and other libraries are not yet supported.
- No interactive review UI.
REVIEW_REQUIREDentries must be reviewed manually in the migration plan JSON or addressed after apply. - Template literals are not auto-applied. Strings in template literals are flagged as
REVIEW_REQUIREDbecause they may contain dynamic content that cannot be statically analyzed. - No incremental re-scan. Running
planre-scans the entire project. There is no diff-based incremental mode yet. - Single adapter per project. You cannot mix adapters (e.g., next-intl in some files and i18next in others) within one configuration.
- LLM translation quality depends on the model. Using a more capable model (e.g., gpt-4.1 over gpt-4.1-mini) generally produces better translations for complex or context-dependent strings.
- Additional adapters: react-intl, i18next, LinguiJS
- Interactive review mode for
REVIEW_REQUIREDentries - Incremental scanning (only re-scan changed files)
- Translation memory and glossary support
- Plural and gender-aware ICU message generation
- VS Code extension for inline review
- Pages Router support for Next.js
- Batch translation API calls for performance
MIT