Add i18n to a JS/TS app
This page is for a JS/TS app (React, Next.js, Node, or a CLI) whose buttons, labels, and messages are still plain source-language text. extract records a UI string only when it is a string literal inside t() or i18n.t(), so today the catalog would be empty and translate-ui would have nothing to translate.
The steps: install the tool, install i18next, run extract once, add a small setup file so t exists, wrap the user-facing text, then translate. Not sure this is your case? See Not your case? at the end.
Install the tool and set the key
Install the CLI (Installation) and set the API key for your provider (Provider and API key). Ollama needs no key.
Initialise the config
ai-i18n-tools init -P openrouterThis writes ai-i18n-tools.config.json. Four keys matter for now:
sourceLocale: the language your text is written in. Any language is valid. English is a common example in this guide.targetLocales: the languages to add.ui.sourceRoots: the foldersextractscans, for examplesrc/.ui.stringsJsonandui.flatOutputDir: where the catalog and the per-locale files are written.
Every other key is in Configuration.
Install the runtime libraries
ai-i18n-tools does not depend on i18next or react-i18next. The app installs them.
- Every JS/TS app that looks up strings at runtime:
i18next. - React components that call
useTranslation():react-i18nextas well.
pnpm add i18next
# React components:
pnpm add react-i18nextRun extract once
ai-i18n-tools extractThe setup file in the next step imports strings.json and ui-languages.json, and only extract writes them. It writes both even when it finds no t() calls, so the app compiles. Expect 0 strings, or 1 if your package.json has a description, because the extractor includes it by default (ui.uiExtractor.includePackageDescription).
Add the bootstrap
Create the i18n setup file. This minimal version is enough to wrap strings and translate them. SOURCE_LOCALE must equal sourceLocale in ai-i18n-tools.config.json.
// src/i18n.js — adjust the import paths to match `ui` in your config
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import aiI18n from 'ai-i18n-tools/runtime';
import uiLanguages from './locales/ui-languages.json';
import stringsJson from './locales/strings.json';
export const SOURCE_LOCALE = 'en-GB';
void i18n.use(initReactI18next).init(aiI18n.defaultI18nInitOptions(SOURCE_LOCALE));
aiI18n.setupKeyAsDefaultT(i18n, { stringsJson });
i18n.on('languageChanged', aiI18n.applyDirection);
aiI18n.applyDirection(i18n.language);
const localeLoaders = aiI18n.makeLocaleLoadersFromManifest(
uiLanguages,
SOURCE_LOCALE,
(code) => () => import(`./locales/${code}.json`),
);
export const loadLocale = aiI18n.makeLoadLocale(i18n, localeLoaders, SOURCE_LOCALE);
export default i18n;Not using React? Drop the react-i18next import and the .use(initReactI18next) call.
When the app starts using plurals, add the source-locale flat file so plural forms resolve. That adds a third import and a sourcePluralFlatBundle option, shown in Wire i18next.
Import this file from the app entry before the first render. In Next.js, import it from a client module, the same pattern as examples/nextjs-app/src/lib/i18n.ts.
Replace hardcoded copy
Do this by hand, or paste the JS/TS prompt into a coding agent. The argument to t() is the source-language sentence the user should see. Flat locale files are keyed by that sentence.
JS/TS agent prompt
Copy this into Cursor or another coding agent. It wraps user-facing literals. It does not translate them.
# Wrap user-facing JS/TS strings in t()
## Context
ai-i18n-tools extracts only string literals passed to `t()` or `i18n.t()`. The
literal is the source-language sentence and the catalog key (`t("Save")`, not
`t("saveButton")`). After this edit the app must behave the same when the active
locale is the source locale. Do not translate any string.
Bootstrap and call-shape reference:
https://wsj-br.github.io/ai-i18n-tools/guide/ui-strings/i18next-runtime
https://wsj-br.github.io/ai-i18n-tools/guide/ui-strings/t-calls-and-plurals
## Scope
Read `ui.sourceRoots` in `ai-i18n-tools.config.json` and edit only those trees
(JS/JSX/TS/TSX). Skip `className`, URLs and routes, import paths, ids, test ids,
enum and error codes, object keys used as identifiers, log field names, CSS,
regexes, comments, and text that is already inside `t()` or `i18n.t()`.
## Call shape
Install `i18next`. Install `react-i18next` when a component uses
`useTranslation()`. Run `ai-i18n-tools extract` once so `strings.json` and
`ui-languages.json` exist, then add the bootstrap from the Wire i18next guide.
`SOURCE_LOCALE` must equal config `sourceLocale`.
- React components: `const { t } = useTranslation()` and `{t("Save")}`.
- Module-level constants and non-React files: `i18n.t("…")`. Move a constant
that would call `t()` at import time into a function or `useMemo(..., [t])`.
- Placeholders: `t("Hello {{name}}", { name })`. No template literal as a key.
- Plurals: `t("{{count}} items", { plurals: true, count })`.
- Next.js: import the bootstrap from a client module before render. Do not call
`changeLanguage` on a shared i18next instance inside a server component. Use a
per-request `createInstance()`, or a stateless flat-bundle lookup.
Before:
```jsx
<button>Save</button>
```
After:
```jsx
<button>{t("Save")}</button>
```
Before:
```js
console.log(`Hello ${name}`);
```
After:
```js
console.log(t("Hello {{name}}", { name }));
```
## Order
1. Install the libraries.
2. Run `ai-i18n-tools extract` once.
3. Add the bootstrap.
4. Wrap call sites.
5. Run `ai-i18n-tools extract` again. Do not run a translation yourself and do
not hand-edit catalogs.
## Do not
- Do not translate, rename, or reword the source text.
- Do not introduce semantic keys (`saveButton`, `nav.home`).
- Do not hand-edit `strings.json`, flat locale JSON, or `ui-languages.json`.
- Do not change behaviour, layout, or test selectors.
## Verify
- `ai-i18n-tools extract` writes a non-empty catalog of the sentences you
wrapped.
- The project's build, type check, and tests pass.
- With the source locale active, every wrapped string still shows the original
sentence. Switch one target locale after `translate-ui` and confirm those
sentences change.
## Report back
List files changed, each string you deliberately skipped with a one-line reason,
and any ambiguous case you left for a human (the same word used with different
meanings, concatenated sentences, or a plural you were not sure about).JSX:
<button>Save</button>
<button>{t("Save")}</button>Node, a CLI, or any non-React module:
console.log(`Hello ${name}`);
console.log(t("Hello {{name}}", { name }));Rules (also in t() calls and plurals):
- The key is a literal.
t(variable)and{'t(are not extracted, and non-literal calls are skipped with no warning.Hello ${name})'} - Placeholders inside the literal use i18next form
{{name}}, passed in the second argument. - Plurals: one source sentence plus
{ plurals: true, count }. See Cardinal plurals. Do not invent_one/_otherkeys yourself. - React components take
tfromuseTranslation(). Other files calli18n.t. A constant evaluated at import time must move into a function oruseMemo(..., [t]), sot()runs after the locale loads. - Next.js server components must not call
changeLanguageon a shared i18next instance. Concurrent requests share that singleton. Use a per-requestcreateInstance(), or a stateless lookup of the flat bundle (see the Nextra hybrid).
Leave these alone: className, href and other URLs, import paths, ids, test ids, error codes, object keys used as identifiers, and text inside comments.
Extract and translate
When the literals are in place:
ai-i18n-tools extract
ai-i18n-tools translate-ui
ai-i18n-tools statussync runs extract and translate-ui together when features.translateUIStrings is on. extract should now list the sentences you wrapped. Open one generated locale file under ui.flatOutputDir and confirm the translations are there. Next: UI strings, Step 2 for what extract scans and writes.
examples/console-app is a small Node app whose src/main.js is already wrapped. The Next.js example is the React bootstrap.
Language switching
Restore a saved locale before the first render, as described in Wire i18next. Then add a control that calls await loadLocale(code) followed by await i18n.changeLanguage(code): Language switcher and RTL.
Keep it translated
Re-run sync after changing source text. The cache means only new or changed strings go to the model. Add the scripts from Recommended package.json scripts, and run sync in CI so translations do not drift.
The default init template also translates README.md. See Add i18n to your README and Markdown docs.
Coming from another tool
ai-i18n-tools includes its own string extractor. If you used i18next-scanner, babel-plugin-i18next-extract, or similar, you can remove those dev dependencies after migrating. Existing i18next namespace JSON files can stay on the JSON pipeline. Intlayer .content.ts dictionaries can be imported with migrate-intlayer: see Migrating from Intlayer. To keep Intlayer at runtime and only fill its translations, use translate-intlayer: see Translating Intlayer in place.
Not your case?
| Your code today | Read |
|---|---|
Already t("…") or i18n.t("…") with the source-language sentence | UI strings steps 2–3, then Wire i18next |
i18next namespace JSON (t("saveButton") plus en/translation.json) | JSON. Do not switch those keys to source-language sentences unless you are ready to change every call |
| Intlayer dictionaries, moving off Intlayer | Migrating from Intlayer |
| Intlayer dictionaries, keeping Intlayer | Translating Intlayer in place |
| One HTML file | Plain HTML and mark-html |
| Doc sites, or Astro page HTML | Add i18n to a website or docs site and Astro website. t() is only for shared data and frontmatter |
| A Python app | Add i18n to a Python app |
| A repository README or other Markdown files | Add i18n to your README and Markdown docs |