Internationalizing a React application used to mean a tedious weekend of hunting down every hardcoded string, manually wrapping each one in a t() call, injecting useTranslation() hooks into every component, and praying you didn't miss a corner case. The reality is that most apps ship without i18n simply because the upfront cost feels too high.
The new instrument command in i18next-cli changes that equation entirely. In this post, we'll take a React + Vite + TypeScript project — Taskly — from zero i18n to a cloud-synced, multi-language application in just a few steps.
The Starting Point: A Standard React App
Taskly is a typical modern project: Vite, React, and TypeScript. It has no i18n dependencies and no translation files—just English strings scattered throughout the JSX:
// DashboardPage.tsx (before)
<h1>Here's your overview</h1>
<p>You have {activeCount} tasks left to complete.</p>
This is the "Before" state. Let's start the transformation.
Step 1 — Instrument: Let the CLI Do the Heavy Lifting
Instead of manual search-and-replace, we run:
npx i18next-cli instrument
If no i18next.config.ts exists, the CLI opens an interactive setup wizard first:
Welcome to the i18next-cli setup wizard!
✔ What kind of configuration file do you want? TypeScript (i18next.config.ts)
✔ What locales does your project support? en,de,fr,it,es,ja
✔ What is the glob pattern for your source files? src/**/*.{js,jsx,ts,tsx}
✔ What is the path for your output resource files? public/locales/{{language}}/{{namespace}}.json
✔ Configuration file created at: i18next.config.ts
Then the instrumentation begins. The tool performs a deep static analysis of your codebase and:
- Wraps hardcoded user-facing strings in
t()calls, generating a key from the content - Injects
useTranslation()hooks into React components that need them - Adds
import { useTranslation } from 'react-i18next'where missing - Falls back to
import i18next from 'i18next'withi18next.t()in non-component files - Generates a ready-to-use
src/i18n.tsinitialization file - Automatically injects
import './i18n'into your entry file (src/main.tsx) - Detects language-switcher patterns and injects
i18n.changeLanguage()calls
After the run you'll see a summary like:
✔ Scanned complete: 101 candidates, 101 approved, 1 language-change site(s)
Instrumentation Summary:
Total candidates: 101
Approved: 101
Skipped: 0
Language-change sites: 1
✔ 5 file(s) ready for instrumentation
▶ Next step: run 'i18next-cli extract' to extract the translation keys into your locale files.
The generated src/i18n.ts is already wired up to load your translation files using lazy dynamic imports, and i.e. src/main.tsx now imports it automatically:
// src/main.tsx — modified by the instrument command
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App'
import './i18n' // ← injected automatically
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
// src/i18n.ts — generated by the instrument command
import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
i18next
.use(initReactI18next)
.use(resourcesToBackend((language: string, namespace: string) =>
import(`../public/locales/${language}/${namespace}.json`)))
.init({
returnEmptyString: false,
fallbackLng: 'en',
defaultNS: 'translation'
})
export default i18next
Step 2 — The Human Touch: Manual Adjustments
⚠️ 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.
Static analysis is powerful, but some strings need a guiding hand. The CLI provides the // i18next-instrument-ignore comment to opt individual strings out of instrumentation.
Brand names and UI chrome
In Sidebar.tsx for example, "Taskly" is a brand name — it must never be translated:
{/* i18next-instrument-ignore */}
<span className={styles.brandName}>Taskly</span>
Dynamic labels mapped from data
The navigation labels are stored in a NAV_ITEMS array. The CLI instruments each label literal independently, but the right approach is a single t() call keyed by item.page. You can place comment-hints to guide the CLI:
const NAV_ITEMS = [
{
// t('navLabel.dashboard', 'Dashboard')
page: 'dashboard',
// i18next-instrument-ignore
label: 'Dashboard',
// ...
},
// ...
]
// Then in JSX, adjust to use the data-driven key:
<span className={styles.navLabel}>
{t(`navLabel.${item.page}`, item.label)}
</span>
The ignore comment keeps the literal in the array (needed at runtime as the fallback), while the comment-hint registers the key for extraction. A quick manual edit connects the two.
Language switcher — detected automatically
The CLI detects patterns that change the application language and injects i18n.changeLanguage() alongside them. In SettingsPage.tsx, the original onClick only called updateSettings:
// Before
onClick={() => updateSettings({ language: lang.code })}
// After — instrument detects the language-change pattern and injects:
onClick={() => { i18n.changeLanguage(lang.code); updateSettings({ language: lang.code }); }}
No manual change needed here — the CLI handled it.
Step 3 — Extract: Generate Your Translation Files
With the code instrumented, now run:
npx i18next-cli extract
This parses every source file, collects all t() calls or Trans component keys and their default values, and writes them into your locale files:
Updated: public/locales/en/translation.json
Updated: public/locales/de/translation.json
Updated: public/locales/fr/translation.json
Updated: public/locales/it/translation.json
Updated: public/locales/es/translation.json
Updated: public/locales/ja/translation.json
✔ Extraction complete!
Your public/locales/en/translation.json now contains every key with its English default value:
{
"heresYourOverview": "Here's your overview",
"highPriority": "High priority",
"howAreYou": "How are you today?",
"navLabel": {
"dashboard": "Dashboard",
"tasks": "My Tasks",
"settings": "Settings",
},
...
}
You now have a complete English baseline ready for the world.
The other locale files have the same keys but empty values — ready to be translated.
Bonus: TypeScript Definitions
If you're using TypeScript, one extra command gives you fully typed translation keys — so typos in t() calls and Trans components become compile errors:
npx i18next-cli types
✔ TypeScript definitions generated successfully.
✓ Resources interface written to src/@types/resources.d.ts
✓ TypeScript definitions written to src/@types/i18next.d.ts
Run this after every extract and your editor will autocomplete translation keys and flag any that don't exist.
Step 4 — Migrate: Push to Locize
At this point, you have a fully extracted English baseline.
You could manually share these JSON files with translators, wait for them to finish, and try to merge their changes back into your codebase. But dealing with new keys, deleted keys, and merge conflicts quickly becomes a maintainability nightmare. Instead, it's time to streamline the process and move your localization workflow to the cloud.
Add your Locize credentials to i18next.config.ts (you can also use environment variables or just follow the interactive prompt):
// i18next.config.ts
export default {
locales: ['en', 'de', 'fr', 'it', 'es', 'ja'],
extract: {
input: 'src/**/*.{js,jsx,ts,tsx}',
output: 'public/locales/{{language}}/{{namespace}}.json'
},
locize: {
projectId: 'your-project-id',
apiKey: process.env.LOCIZE_API_KEY,
version: 'latest'
}
}
Then push everything to Locize in one command:
npx i18next-cli locize-migrate
If necessary, the CLI prompts for your credentials on the first run, offers to save them securely, then migrates all locale files:
✔ Retry successful!
added language de...
added language es...
added language fr...
added language it...
added language ja...
transfering latest/en/translation...
transfered 94 keys latest/en/translation...
...
downloading translations after migration...
✔ 'locize migrate' completed successfully.
💡 Automatic Translation: If you have Locize's Machine / AI Translation feature enabled, your target languages will be translated automatically after migration — asynchronously.
Pro Tip: In case the translated files are not ready yet, wait a moment after migrating and runnpx i18next-cli locize-downloadto pull those new translations back into your local project.npx i18next-cli locize-download
Step 5 — Iterate: Adding New Keys
Development doesn't stop after the initial migration. When you add a new feature:
// DashboardPage.tsx
<p>{t('howAreYou', 'How are you today?')}</p>
Just run two commands:
npx i18next-cli extract # picks up the new key locally
npx i18next-cli locize-sync # syncs new keys to Locize, downloads translations
The new key appears in public/locales/en/translation.json immediately after extract, and after locize-sync all other languages will have it translated (instantly if Auto-Translation is on, or pending your translators' review).
Command reference
| Command | When to use |
|---|---|
extract |
After every code change — keeps local JSON in sync with your source |
types |
After extract — generates TypeScript definitions for type-safe translation keys |
locize-migrate |
Once, for the initial bulk push of local resources to Locize |
locize-sync |
Ongoing — two-way sync between local files and Locize |
locize-download |
Pull the latest translations from Locize without pushing changes |
Step 6 — Going Further: Dynamic Translation Loading
Bundling JSON files works well, but i18next-locize-backend lets you decouple your translations entirely from your deployment cycle.
npm install i18next-locize-backend
// src/i18n.ts
import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import LocizeBackend from 'i18next-locize-backend'
i18next
.use(initReactI18next)
.use(LocizeBackend)
.init({
fallbackLng: 'en',
backend: {
projectId: 'your-project-id',
version: 'latest'
}
})
export default i18next
Benefits:
- No re-deploys for copy changes. Fix a typo in the Locize editor and it appears in your live app at the next page load — no build, no deploy.
- Smaller bundles. Only the user's language is fetched, on demand.
- Phased rollouts. Use Locize versioning to promote translations independently of code versions.
Step 7 — Power Features: saveMissing and the InContext Editor
saveMissing — Zero-friction key registration
During development, enable saveMissing to automatically push newly written keys to Locize the moment they appear in the running app — no extract step needed:
i18next.use(LocizeBackend).init({
saveMissing: true, // push unknown keys to Locize immediately
backend: { projectId: 'your-project-id', apiKey: 'your-api-key' }
})
This pairs beautifully with Locize AI: as soon as a new key arrives, Locize translates it into every configured language automatically.
InContext Editor — Edit translations in place
Add the Locize InContext Editor to your development build for a visual, in-page translation workflow — click any string, edit it directly in the browser, and save it back to Locize without ever leaving the app.
npm install locize-lastused locize
import LastUsed from 'locize-lastused'
import { locizePlugin } from 'locize'
i18next
.use(LastUsed) // tracks which keys are actually used in production
.use(locizePlugin) // enables the InContext Editor
// ...
locize-lastused also flags keys in your Locize dashboard that haven't been used recently — making it easy to clean up stale translations alongside your code.
The Full Journey at a Glance
1. npx i18next-cli instrument → Wrap strings, inject hooks, generate i18n.ts
2. [manual tweaks] → Ignore brand names, fix dynamic key patterns
3. npx i18next-cli extract → Generate local translation JSON
npx i18next-cli types → Generate TypeScript definitions (optional)
4. npx i18next-cli locize-migrate → Push everything to Locize (first time)
[Locize AI translates async]
npx i18next-cli locize-download → Pull auto-translated files back locally
5. [code new feature]
npx i18next-cli extract → Pick up new keys
npx i18next-cli locize-sync → Two-way sync with Locize
Optional upgrades:
→ i18next-locize-backend → Fetch translations dynamically (no re-deploy)
→ saveMissing: true → Auto-register new keys from running app
→ locize InContext Editor → Edit translations visually in the browser
→ locize-lastused → Track and clean up stale keys
We went from a standard React app with 100+ hardcoded strings to a fully instrumented, cloud-synced, multi-language application in minutes — not a weekend.
Summary: The Workflow
| Phase | Commands |
|---|---|
| Setup |
instrument → extract → locize-migrate
|
| Daily Dev |
extract → locize-sync
|
| Live Updates | Use i18next-locize-backend for instant publishing |
By leveraging the i18next-cli, we've turned what used to be a grueling manual task into a streamlined, automated pipeline.
Want to see if your project is ready? Run:
npx i18next-cli status
It gives you an instant overview of your current i18n health.
- Example app: github.com/locize/taskly
- i18next-cli: github.com/i18next/i18next-cli
- Locize: locize.com
- Video: Youtube
Top comments (0)