react-autolocale 0.1.0 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # react-autolocale
2
2
 
3
- Wrap your React + Vite app in one component. `vite build` produces a fully translated static page for every language (`/en/`, `/fr/`, `/es/`, ...) using a free local translation model. No `t()`, no keys, no translation files in your repo, and your source code stays untouched.
3
+ Write your React + Vite app in one language. react-autolocale extracts the text, translates it with the engine you choose (a free local model, OpenAI or Anthropic Claude), and `vite build` produces a static page for every language (`/en/`, `/fr/`, `/es/`, ...). You don't call `t()` or manage keys, and your source files are never modified.
4
+
5
+ Translation is a separate, explicit command. Builds only read the generated catalogs. They never download a model, call an API or wait for a translation job.
4
6
 
5
7
  ## Quick start
6
8
 
@@ -10,12 +12,19 @@ Wrap your React + Vite app in one component. `vite build` produces a fully trans
10
12
  npm install react-autolocale # or: yarn add react-autolocale | pnpm add react-autolocale
11
13
  ```
12
14
 
13
- **2. Add the plugin to your Vite config**
15
+ **2. Set it up**
14
16
 
15
17
  ```bash
16
18
  npx react-autolocale init # or: yarn react-autolocale init | pnpm exec react-autolocale init
17
19
  ```
18
20
 
21
+ The wizard detects your project and asks for:
22
+ - your source language and target languages
23
+ - a translation engine: a local model or a cloud provider (see [Choosing a translation engine](#choosing-a-translation-engine))
24
+ - where the catalogs go
25
+
26
+ It then writes `auto-locale.config.ts` and adds the plugin to `vite.config`. It downloads and translates nothing.
27
+
19
28
  **3. Wrap your app once**
20
29
 
21
30
  ```tsx
@@ -23,24 +32,147 @@ npx react-autolocale init # or: yarn react-autolocale init | pnpm exe
23
32
  import { AutoScale } from "react-autolocale";
24
33
 
25
34
  createRoot(document.getElementById("root")!).render(
26
- <AutoScale original="en" languages={["fr", "es", "de", "pt"]} exclude={["Acme Cloud"]}>
35
+ <AutoScale exclude={["Acme Cloud"]}>
27
36
  <App />
28
37
  </AutoScale>,
29
38
  );
30
39
  ```
31
40
 
32
- **4. Build**
41
+ **4. Generate translations, then develop and build as usual**
33
42
 
34
43
  ```bash
35
- npm run build # or: yarn build | pnpm build
36
- npm run preview # optional: check the built site locally
44
+ npx react-autolocale generate # extract strings, translate new ones, write the catalogs
45
+ npm run dev
46
+ npm run build # one static page per language and route: dist/fr/, dist/fr/about/, ...
37
47
  ```
38
48
 
39
- The output folder has one page per language and route (`dist/fr/`, `dist/fr/about/`, ...), deployable anywhere that serves static files. The first build takes a few minutes: it installs the free local translation engine (about 440 MB, once, into `~/.cache/react-autolocale/engine`, shared by all your projects) and downloads the models. Later builds reuse the cache. Prefer to manage the engine yourself? `npm install -D @huggingface/transformers` and it will be used instead.
49
+ After changing text in your components, run `npx react-autolocale sync`. Only new strings are translated, and a running dev server reloads when the command finishes.
40
50
 
41
51
  Optional: `npx react-autolocale add switcher` copies an editable language switcher into your project (see below).
42
52
 
43
- Write normal JSX; there is no `t()` to call and no translation file to maintain.
53
+ ## Workflow and commands
54
+
55
+ | Command | What it does | Network / models |
56
+ | --- | --- | --- |
57
+ | `init` | Creates `auto-locale.config.ts`, sets up the provider (API key into `.env` if you paste one), adds the Vite plugin. Keeps an existing config unless you confirm (or pass `--force`). | None |
58
+ | `scan` | Extracts strings and updates `.auto-locale/manifest.json` and the source catalog (`en.json`). | None |
59
+ | `translate` | Translates strings missing from the catalogs with the configured provider. Retries anything that failed before. `--lang fr,es` limits it. | Provider only |
60
+ | `generate` | `scan` then `translate`. Incremental: existing translations are kept. | Provider only |
61
+ | `sync` | Same as `generate`. Use it after editing text. | Provider only |
62
+ | `clean` | Lists strings no longer in the source; removes their translations only after you confirm (`--yes` in scripts). | None |
63
+ | `export` | Copies the translations into `react-autolocale.overrides.json` for hand editing. | None |
64
+ | `add switcher` / `add banner` | Copies an editable component into your project. | None |
65
+
66
+ `init` also accepts flags, so it can run without prompts in scripts: `--source en --locales fr,es --provider local|openai|anthropic|ollama --model <id> --api-key-env <NAME> --output <dir> --yes`. Every command takes `--root <dir>`; the default is the nearest folder with a `package.json`.
67
+
68
+ A `generate` run looks like this:
69
+
70
+ ```text
71
+ React Auto Locale
72
+
73
+ ✓ Scanned 24 source files
74
+ ✓ Found 3 new strings (84 in use)
75
+ Using openai:gpt-6-astra.
76
+ ✓ Kept 243 existing translations
77
+ ✓ Translated 9 strings (fr 3, es 3, de 3)
78
+ ✓ Updated 3 locale catalogs in src/generated/auto-locale/
79
+
80
+ No source files modified.
81
+ ```
82
+
83
+ Failures are listed by language and message id, and the command exits with code 1. They show the source text until you fix them. `npx react-autolocale translate` retries just those strings.
84
+
85
+ ## Configuration
86
+
87
+ `auto-locale.config.ts` (written by `init`, also `.mts`/`.js`/`.mjs`):
88
+
89
+ ```ts
90
+ import { defineConfig } from "react-autolocale/config";
91
+
92
+ export default defineConfig({
93
+ sourceLocale: "en",
94
+ locales: ["en", "fr", "es", "de"],
95
+ provider: { name: "openai", model: "gpt-6-astra", apiKeyEnv: "OPENAI_API_KEY" },
96
+ // output: "./src/generated/auto-locale", // optional, see "Generated files"
97
+ // include: ["src"], // folders scanned for JSX
98
+ // exclude: ["Acme Cloud"], // phrases never translated (adds to <AutoScale exclude>)
99
+ // glossary: { fr: { Roadmap: "Feuille de route" } },
100
+ });
101
+ ```
102
+
103
+ When the config sets `sourceLocale` and `locales`, `<AutoScale>` needs no language props. Projects without a config file keep working: the languages come from `<AutoScale original="en" languages={["fr", "es"]}>` and the local model is used. `seo` and `siteUrl` stay on `<AutoScale>`. Invalid settings, such as an unknown language code, a half-set language pair or an unknown provider, stop every command with a message naming the setting.
104
+
105
+ ## Choosing a translation engine
106
+
107
+ The engine only matters to `translate`/`generate`/`sync`. Extraction, the catalogs, the build and the runtime work the same with every engine.
108
+
109
+ **Local model** (`{ name: "local" }`, the default). It is free and runs on your machine with no API key: OPUS-MT for English sources and NLLB-200 for everything else, through Transformers.js. The cost is disk, RAM and CPU time:
110
+ - **Downloads**: on the first `translate`, about 110 MB per English→X language and 870 MB for NLLB-200 (non-English sources, and Portuguese, Japanese, Korean and the other NLLB languages below). If your project doesn't have `@huggingface/transformers`, a 470 MB engine is also installed once into `~/.cache/react-autolocale/engine`.
111
+ - **Reuse**: models are cached in `~/.cache/react-autolocale` and shared by every project. A loaded model is kept for the whole run.
112
+ - **Speed**: OPUS-MT translates 8 strings per call; on an Apple-silicon Mac that measured about 5× faster than one at a time (32 UI strings in about 0.65 s per language once the model was loaded). NLLB runs one string at a time, because batching produced garbage, so it is much slower.
113
+ - **Progress and interruption**: progress is printed per language, and work is saved after every batch, so Ctrl+C or a crash loses at most one batch.
114
+
115
+ **OpenAI** (`{ name: "openai", model?, apiKeyEnv?, baseURL? }`) and **Anthropic Claude** (`{ name: "anthropic", model?, apiKeyEnv?, effort? }`) avoid the downloads and are usually quicker for large jobs. You need an API key, and usage is billed by the provider.
116
+ - **Setup**: install the official SDK in your project (`npm install -D openai` or `npm install -D @anthropic-ai/sdk`; `init` offers to do it). Only the CLI loads it; it never reaches the browser bundle.
117
+ - **Keys**: they are read from `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` (change the name with `apiKeyEnv`), from the environment or from `.env` / `.env.local` in the project root. Variables already set in your shell win. If you paste a key into `init`, it is appended to `.env` (never over an existing value) and `.env` is added to `.gitignore`. Keys are never printed or written into the config, the catalogs or the bundle. A missing key stops the command before any request is made.
118
+ - **Defaults**: `gpt-6-astra` for OpenAI, and `claude-opus-5-5` at `effort: "low"` for Anthropic. Short UI strings rarely need more; Claude refusals fall back server-side on models that support it.
119
+ - **Requests**: strings are sent in batches of 25, 3 at a time, with each string's element (`button`, `h1`, ...) as context. Replies are structured JSON with one translation per string; a batch that comes back the wrong size is retried one string at a time. The SDKs time out after 2 minutes and retry rate limits and server errors 3 times.
120
+ - **Errors**: a rejected key, an unknown model or a used-up quota stops the run instead of failing every batch. A failing provider is never swapped for another one.
121
+
122
+ **Ollama** (`{ name: "ollama", model: "llama3.1", url? }`) runs a local LLM that receives the context too.
123
+
124
+ **Your own engine**: set `provider` to a function (or an object with `translate`). You can wrap DeepL, Google Cloud Translation, a self-hosted API or another LLM:
125
+
126
+ ```ts
127
+ provider: async ({ texts, contexts, from, to }) => texts.map((t, i) => myTranslate(t, from, to, contexts[i])),
128
+ ```
129
+
130
+ Engines receive text with protected pieces replaced by tokens (`X0X`, `{name}`) and must return them unchanged. Whatever the engine, the results are checked: a translation that drops a token or changes a number is retried once with numbers locked, and is otherwise reported as failed. It is never stored.
131
+
132
+ ## Generated files and Git
133
+
134
+ ```text
135
+ my-app/
136
+ ├── src/
137
+ │ └── generated/auto-locale/ # commit this: what the app ships
138
+ │ ├── en.json # message id -> source text
139
+ │ ├── fr.json # message id -> translation
140
+ │ ├── banner.json # the language banner's phrases
141
+ │ └── index.ts # sourceLocale, locales, catalog file names
142
+ ├── .auto-locale/
143
+ │ ├── manifest.json # commit this: ids, source text, element, files, status
144
+ │ └── cache.json # ignored by init: every machine translation, for reuse
145
+ └── auto-locale.config.ts
146
+ ```
147
+
148
+ - **Location**:
149
+ - `src/generated/auto-locale/` when the project has a root-level `src/` folder, otherwise `generated/auto-locale/`.
150
+ - An `output` in the config always wins.
151
+ - The CLI, the scanner and the Vite plugin all resolve the location the same way.
152
+ - **Message ids**:
153
+ - `msg_` plus a hash of the whitespace-normalized text, so moving code or editing other files never changes an id.
154
+ - Identical text shares one id and one translation. Use `useLocalized` when one occurrence needs different wording.
155
+ - If two different strings ever produced the same id, `scan` stops and names them; it never silently mixes them up.
156
+ - **Small diffs**:
157
+ - Catalogs are pretty-printed JSON with sorted keys, written only when something changed, and written atomically (temp file + rename).
158
+ - A new string adds one line per catalog, and existing entries are never rewritten. You can edit `fr.json` by hand, and later runs keep your edit. An empty value counts as missing.
159
+ - **Removed strings**:
160
+ - Marked `unused` in the manifest. Their translations stay until `clean` removes them, but they are not shipped to the browser.
161
+ - If the text comes back, its translation is restored from `.auto-locale/cache.json` without calling the provider.
162
+ - **Moving the output**:
163
+ - After `output` changes, the next `scan` copies any missing target catalogs to the new folder and warns you.
164
+ - The old folder is never moved or deleted; remove it yourself once you've checked the new one.
165
+
166
+ ## Production builds
167
+
168
+ `vite build` and `vite dev` only read the catalogs, the manifest and the overrides file. They never download or load a translation model, call a translation API, regenerate catalogs, or need an API key. A build works offline in CI with only the committed files. A string without a translation renders its source text, and the build prints a one-line count (`Untranslated strings (shown in en): fr 3. Run npx react-autolocale sync.`). In development the same notice appears when you add new text.
169
+
170
+ ## Upgrading from earlier versions
171
+
172
+ Earlier versions translated during `vite build` and kept translations in `node_modules/.cache/react-autolocale/`:
173
+ - **Before you run anything new**: builds still read that folder, with a warning, so nothing breaks.
174
+ - **After `npx react-autolocale generate`**: those translations are copied into the new catalogs without being retranslated. The old folder is left in place.
175
+ - **Moved options**: `engine` and `glossary` in `vite.config` are now ignored, with a warning. Move them to `auto-locale.config.ts` as `provider` and `glossary`. The `overrides` and `routes` plugin options and `react-autolocale.overrides.json` work as before.
44
176
 
45
177
  ## Multi-page apps (React Router)
46
178
 
@@ -71,12 +203,56 @@ Your host must serve `/fr/about/index.html` for `/fr/about`, which static hosts
71
203
 
72
204
  ## Fixing a translation
73
205
 
206
+ Edit the value in the catalog (`src/generated/auto-locale/fr.json`) and commit it. Later runs keep your edit.
207
+
208
+ Prefer to edit by source text? Run `npx react-autolocale export`: it writes every translation into `react-autolocale.overrides.json` in your project root, keyed by the original text. Entries in that file always win over the catalogs, apply on the next build or dev reload, and are skipped by `translate`.
209
+
210
+ You can also fix a few strings in code:
211
+
74
212
  Machine translation sometimes gets short UI words wrong (a nav link "Home" may come back as "Sommaire"). Correct any string in the plugin options; overrides always win:
75
213
 
76
214
  ```ts
77
215
  reactAutolocale({ overrides: { fr: { Home: "Accueil" }, es: { Home: "Inicio" } } })
78
216
  ```
79
217
 
218
+ ## Different values per language (prices, links, and more)
219
+
220
+ Some things should differ by language, not just be translated: a price, a link to a localized page, a phone number.
221
+
222
+ ```tsx
223
+ import { useLocalized, Localized, ForLanguages, useFormat } from "react-autolocale";
224
+
225
+ // A value: falls back to `default`, then to the original language
226
+ const href = useLocalized({ en: "/pricing", fr: "/tarifs", es: "/precios", default: "/pricing" });
227
+ <a href={href}>Pricing</a> // "Pricing" itself is still translated for you
228
+
229
+ // A block of UI
230
+ <Localized en={<UsPhone />} fr={<FrPhone />} default={<UsPhone />} />
231
+
232
+ // Content for some languages only (local offers, legal notices)
233
+ <ForLanguages only={["fr", "es"]}>Free shipping in Europe</ForLanguages>
234
+ <ForLanguages except={["ja"]}><CookieNotice /></ForLanguages>
235
+
236
+ // Numbers, prices and dates in the visitor's format
237
+ const format = useFormat();
238
+ format.currency(1234.5, "EUR"); // "€1,234.50" in English, "1 234,50 €" in French
239
+ format.number(1250000); // "1,250,000" / "1 250 000"
240
+ format.percent(0.15); // "15%" / "15 %"
241
+ format.date(new Date()); // "Oct 9, 2026" / "9 oct. 2026"
242
+ ```
243
+
244
+ All of these are part of the prerendered pages, so `/fr/` and `/en/` ship with their own values in the HTML. Values you pick in code are your own data and are never machine-translated. `basePath` from `useLanguage()` is the current prefix (`/fr`) for building internal links. With React Router, the page you link to must be a route you define (here `/tarifs`); the build does not rename routes by language.
245
+
246
+ ## Better translations: glossary and context
247
+
248
+ Add a `glossary` in `auto-locale.config.ts` for terms that must always translate a given way. It is applied before the engine sees the text, with every engine:
249
+
250
+ ```ts
251
+ glossary: { fr: { Roadmap: "Feuille de route" }, es: { Roadmap: "Hoja de ruta" } },
252
+ ```
253
+
254
+ The local models translate each string without knowing where it appears, so a lone "Home" in a nav bar can come back wrong. OpenAI, Claude, Ollama and custom engines receive the element each string sits in (`"button"`, `"a"`, `"h1"`, `"placeholder"`, ...), which fixes most of these.
255
+
80
256
  ## Language switcher
81
257
 
82
258
  Switching is just navigation to `/fr/` (instant, no reload; a page load when `seo` is on). You choose the UI:
@@ -95,7 +271,15 @@ const { language, languages, setLanguage, basePath } = useLanguage();
95
271
 
96
272
  ## What gets translated
97
273
 
98
- Static JSX text, plus `placeholder`, `title`, `alt`, `aria-label` on HTML elements. `exclude` phrases (brand names) are kept as-is, even inside longer sentences.
274
+ - Static JSX text, plus `placeholder`, `title`, `alt`, `aria-label`, `aria-placeholder` and `aria-description` on HTML elements. Other attributes (class names, styles, URLs, ids, event handlers) and props on your own components are never touched. JSX whitespace is collapsed the way React renders it.
275
+ - **Text with variables**: `<p>Welcome, {user.name}. You have {count} new messages</p>` is translated as one sentence (`Welcome, {name}. You have {count} new messages`) and your values are filled in at runtime. Variables must be plain names or property paths (`name`, `user.name`, `props.count`). Text mixed with function calls, conditions or nested elements is left as written. There is no plural handling, so write `{count} message(s)` or branch in your code.
276
+ - **Not translated**: anything with `translate="no"` (the standard HTML attribute, inherited by children), `exclude` phrases, `<code>` / `<pre>` / `<script>` / `<style>`, text made only of numbers and symbols, and data that arrives at runtime (API responses, user content).
277
+ - **Numbers** are kept exactly as written. `translate` checks that every digit (prices, phone numbers, hours, versions) survives translation and re-translates with the number locked if not. If that still fails, the original text is kept instead of shipping a wrong number. Use `useFormat()` (below) for numbers you compute.
278
+
279
+ ```tsx
280
+ <p translate="no">Acme Cloud</p> // never translated
281
+ <section translate="no"><Terminal /></section> // nothing inside is translated
282
+ ```
99
283
 
100
284
  ## Supported languages
101
285
 
@@ -142,15 +326,50 @@ Use these codes in `languages={[...]}`. The source (`original`) language can be
142
326
  | `tl` | Tagalog | NLLB |
143
327
  | `en` | English (default source) | NLLB when used as a target |
144
328
 
145
- OPUS-MT models are small (about 75 MB each) and are used when the source language is English. NLLB-200 is one larger download (about 600 MB) shared by all the languages marked NLLB, and is also used for any non-English source. Right-to-left languages automatically get `dir="rtl"` on the page.
329
+ With the local engine, OPUS-MT models (about 110 MB each on disk) are used when the source language is English. NLLB-200 is one larger download (about 870 MB) shared by all the languages marked NLLB, and is also used for any non-English source. Cloud engines handle every code in the table. Right-to-left languages automatically get `dir="rtl"` on the page.
146
330
 
147
- ## Translations and caching
331
+ ### Quality by language
148
332
 
149
- Translations are generated on first build into `node_modules/.cache/react-autolocale/` and reused; only new or changed strings are translated afterwards. Cache that folder in CI. Models download once into `~/.cache/react-autolocale` (about 75 MB per OPUS-MT language, about 600 MB for NLLB: Japanese, Korean, Portuguese and others). The dev server translates text you add while it runs and reloads.
333
+ Every supported language was run through the real models on sample UI sentences: all 37 produce output in the right writing system, with no runaway or repeated text. How good that output is varies. French, Spanish, German, Italian, Dutch, Swedish, Danish, Japanese and Korean read naturally on ordinary sentences. Short imperative labels are the weak spot in many languages: "Save 20%" comes back as "save a file" in Russian, Ukrainian, Hungarian, Hindi and Romanian, "Sign in" comes back wrong in Russian, Chinese, Arabic and Vietnamese, and Arabic and Vietnamese could not keep the number in "Save 20% on your first order" (the original English is kept for such strings rather than risking a wrong number). Treat these as drafts: review the languages you ship in `react-autolocale.overrides.json`, add a `glossary` for key terms, or use an engine that gets context (see above). You can re-run the check yourself with `RUN_MODELS=1 pnpm exec vitest run test/models.test.ts`.
150
334
 
151
335
  ## Automatic language navigation
152
336
 
153
- Whenever a visitor lands on a URL without a language prefix (like `/` or `/about`), they are sent to the page for their saved language, or otherwise their browser language, if you support it: a French browser lands on `/fr/`. A language chosen in the switcher is remembered and wins on later visits; the browser language itself is not saved, it is simply checked each time. Explicit URLs such as `/fr/about/` or `/en/` are never redirected, and unsupported browser languages stay on the original.
337
+ Whether a visitor is moved depends on the URL they land on:
338
+
339
+ | Where they land | What happens |
340
+ | --- | --- |
341
+ | `/` or `/about` (no language in the URL) | Sent to their saved language, otherwise their browser language, if you support it (a French browser lands on `/fr/about`). |
342
+ | `/en/about` or `/fr/about` (explicit language URL) | Never redirected. The page they asked for is the page they get. |
343
+
344
+ - A language chosen in the switcher is remembered and wins on later visits. The browser language itself is not saved; it is checked each time.
345
+ - The browser's language setting is used, not the visitor's physical location. Unsupported browser languages stay on the original.
346
+ - The root page redirects before it paints, so visitors don't see a flash of the wrong language.
347
+
348
+ This follows common practice. Redirecting the bare root by browser language is widespread, while forcing visitors off an explicit language URL is avoided: shared links and search results should open the page they point to, redirect loops are avoided, and it respects someone who chose a language on purpose. Search engines are told which URL belongs to which language through `hreflang` tags (see SEO below), so a French searcher is normally shown the `/fr/` URL in the first place. Visitors who land on another language's URL can use the language switcher.
349
+
350
+ ## Suggesting the visitor's language (optional banner)
351
+
352
+ Instead of forcing a redirect on an explicit URL, you can offer the visitor their browser language: a French visitor on `/en/about` sees "Cette page est également disponible en Français." with a button to switch and one to dismiss. It appears only when the browser language is supported and differs from the page, is shown after the page loads (so it never changes the HTML search engines see), remembers a dismissal, and stays hidden once the visitor has picked a language. The text is shown in the visitor's own language.
353
+
354
+ It is off unless you add it, and you decide how it looks:
355
+
356
+ ```tsx
357
+ import { LanguageBanner } from "react-autolocale";
358
+
359
+ <LanguageBanner /> // ready-made bar, style it with className / style
360
+ <LanguageBanner className="my-bar" /> // your own CSS replaces the default look
361
+
362
+ <LanguageBanner> // or build the markup yourself
363
+ {(s) => (
364
+ <aside>
365
+ {s.text} <button onClick={s.accept}>{s.switchLabel}</button>
366
+ <button onClick={s.dismiss}>{s.dismissLabel}</button>
367
+ </aside>
368
+ )}
369
+ </LanguageBanner>
370
+ ```
371
+
372
+ Prefer to own the component? `npx react-autolocale add banner` copies an editable `LanguageBanner` into your project. For full control use the hook, `useLanguageSuggestion()`, which returns `null` or `{ language, native, text, switchLabel, dismissLabel, accept, dismiss }`.
154
373
 
155
374
  ## SEO (opt-in)
156
375
 
@@ -171,10 +390,23 @@ With `seo` the build also:
171
390
 
172
391
  With `seo`, switching language navigates to the real page (so its head tags are correct) instead of swapping in place. Brand names in the title are translated like any text, so add them to `exclude`.
173
392
 
174
- ## v1 limits
393
+ ## Troubleshooting
394
+
395
+ - **"No translations in src/generated/auto-locale/ yet" during a build**: run `npx react-autolocale generate`, then commit the folder.
396
+ - **"OPENAI_API_KEY is not set"**: add it to `.env` in the project root or export it in your shell, then rerun `translate`. No request was made.
397
+ - **"This provider needs the openai package"** (or `@anthropic-ai/sdk`): install it as a dev dependency of your app.
398
+ - **Some strings failed**: the report lists them by id, language and reason. Rerun `npx react-autolocale translate`, or write the value into the catalog or the overrides file yourself.
399
+ - **The local model is slow**: NLLB languages run one string at a time on the CPU. Translate a subset with `--lang`, or switch `provider` to OpenAI or Claude for big jobs. Existing translations are kept either way.
400
+ - **Interrupted run**: everything finished before the interruption was saved; run the same command again.
401
+ - **Corrupted JSON**: commands stop and name the file instead of treating it as empty; fix it or restore it from Git.
402
+
403
+ ## Limits
175
404
 
176
- - Dynamic content (`Hello {name}`, API data) is not translated; planned for v1.5. Text next to `{expressions}` stays in the original language.
405
+ - Data that arrives at runtime (API responses, user-written content) is not translated: it does not exist at build time. Text you write in JSX around those values is.
406
+ - Variables in text must be plain names or property paths; no plural rules.
177
407
  - Dynamic routes (`/users/:id`) only get pages you list in `routes`, or that are linked.
178
408
  - `<AutoScale>` props must be literals so the plugin can read them at build time.
409
+ - The CLI reads `auto-locale.config.ts`, not `vite.config`. Set `include` there if you change the plugin's `include`.
410
+ - Identical text always shares one translation.
179
411
  - Use Vite's default `base: "/"`.
180
- - Machine translation is draft quality.
412
+ - Machine translation is draft quality. Short labels, long sentences with variables and less common languages need a human look: use the overrides file.