Skip to content

Semih702/safe-i18n

Repository files navigation

safe-i18n

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.

For AI coding agents and MCP clients

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 --ci

The 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.

Why not just ask an LLM to do it?

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 rollback restores them.
  • The LLM is only used for translations, semantic descriptions, and key/namespace suggestions. It operates on extracted string values, not on code.

Installation

npm install -g safe-i18n

Or run directly without installing:

npx safe-i18n

Requires Node.js 18 or later.

Quick Start

# 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 validate

Each command guides you to the next step. The plan is always created and reviewed before any source files are changed.

Commands

init

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-intl

Options:

  • --source <locale> -- Source locale code (default: en)
  • --locales <locales> -- Comma-separated target locale codes
  • --adapter <adapter> -- i18n adapter (default: next-intl)

scan

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.json

Options:

  • --json -- Output results as JSON
  • --output <file> -- Save full scan results to a file

plan

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,de

Options:

  • --source <locale> -- Override source locale
  • --locales <locales> -- Override target locales

apply

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-dirty

Options:

  • --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

translate

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-compatible

Options:

  • --to <locales> (required) -- Comma-separated target locales
  • --provider <name> -- Override the LLM provider from config

validate

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 --ci

Options:

  • --ci -- Exit with code 1 if any errors are found (for CI pipelines)

rollback

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 --force

Options:

  • --force -- Skip the confirmation warning

Safety Model

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.

Risk Categories

Every scanned string is assigned one of four risk levels:

AUTO_SAFE

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)

REVIEW_REQUIRED

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)

SKIP_NON_UI

The string is not user-visible. It is excluded from the migration plan.

Examples:

  • Strings in console.log calls
  • data-testid values
  • CSS class names
  • Import paths
  • Route definitions
  • Enum values
  • Analytics event names

SKIP_DANGEROUS

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)

Configuration

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"]
  }
}

Configuration Fields

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 []

Namespace Strategies

  • 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 namespace
  • component-based -- Uses the component name as the namespace
  • flat -- All keys in a single namespace

next-intl Integration

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"
  }
}

LLM Provider Setup

Mock Provider (Testing)

The default provider. Returns the source text unchanged. Useful for testing the pipeline without API calls or costs.

{
  "llm": {
    "provider": "mock"
  }
}

OpenAI-Compatible Provider

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-..."

Variables in Translations

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."

Multi-Language Translation

Generate translations for multiple locales in a single run:

safe-i18n translate --to tr,de,fr,ja,ko

This 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.

Validation and CI

Local Validation

safe-i18n validate

Checks 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)

CI Integration

Use --ci to fail the pipeline when validation errors are found:

safe-i18n validate --ci

Example GitHub Actions step:

- name: Validate i18n
  run: npx safe-i18n validate --ci

The command exits with code 0 on success and code 1 if any errors are detected. Warnings do not cause a non-zero exit.

Rollback

If something goes wrong after apply, you can restore all modified files:

safe-i18n rollback --force

The 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

Limitations

  • 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_REQUIRED entries 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_REQUIRED because they may contain dynamic content that cannot be statically analyzed.
  • No incremental re-scan. Running plan re-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.

Roadmap

  • Additional adapters: react-intl, i18next, LinguiJS
  • Interactive review mode for REVIEW_REQUIRED entries
  • 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

License

MIT

Releases

No releases published

Packages

 
 
 

Contributors