react-autolocale 0.2.0 → 0.3.0-beta.1

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, Anthropic Claude or Google Gemini), 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,153 @@ 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|gemini|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
+
121
+ **Google Gemini** (`{ name: "gemini", model?, apiKeyEnv?, baseURL? }`) works the same way, with nothing to install: it calls Gemini's REST API directly, so there is no SDK.
122
+ - **Key**: create one in [Google AI Studio](https://aistudio.google.com/apikey) and put `GEMINI_API_KEY=...` in `.env` (or let `init` save it for you). Change the variable name with `apiKeyEnv`.
123
+ - **Model**: `gemini-2.5-flash` by default. Set `model` to any model id shown in AI Studio, for example a newer Flash or a Pro model for harder text.
124
+ - **Behavior**: the same batching and context as the other cloud providers, with structured JSON replies. Rate limits (429) and server errors are retried with backoff, honoring `Retry-After`. A rejected key, a wrong model name or an exhausted quota stops the run with a clear message before anything is stored. Free-tier keys have low rate limits, so large first runs may need to be repeated; finished work is saved after every batch.
125
+ - **Choose it**: `npx react-autolocale init --provider gemini`, or in the config: `provider: { name: "gemini" }`.
126
+ - **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.
127
+
128
+ **Ollama** (`{ name: "ollama", model: "llama3.1", url? }`) runs a local LLM that receives the context too.
129
+
130
+ **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:
131
+
132
+ ```ts
133
+ provider: async ({ texts, contexts, from, to }) => texts.map((t, i) => myTranslate(t, from, to, contexts[i])),
134
+ ```
135
+
136
+ 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.
137
+
138
+ ## Generated files and Git
139
+
140
+ ```text
141
+ my-app/
142
+ ├── src/
143
+ │ └── generated/auto-locale/ # commit this: what the app ships
144
+ │ ├── en.json # message id -> source text
145
+ │ ├── fr.json # message id -> translation
146
+ │ ├── banner.json # the language banner's phrases
147
+ │ └── index.ts # sourceLocale, locales, catalog file names
148
+ ├── .auto-locale/
149
+ │ ├── manifest.json # commit this: ids, source text, element, files, status
150
+ │ └── cache.json # ignored by init: every machine translation, for reuse
151
+ └── auto-locale.config.ts
152
+ ```
153
+
154
+ - **Location**:
155
+ - `src/generated/auto-locale/` when the project has a root-level `src/` folder, otherwise `generated/auto-locale/`.
156
+ - An `output` in the config always wins.
157
+ - The CLI, the scanner and the Vite plugin all resolve the location the same way.
158
+ - **Message ids**:
159
+ - `msg_` plus a hash of the whitespace-normalized text, so moving code or editing other files never changes an id.
160
+ - Identical text shares one id and one translation. Use `useLocalized` when one occurrence needs different wording.
161
+ - If two different strings ever produced the same id, `scan` stops and names them; it never silently mixes them up.
162
+ - **Small diffs**:
163
+ - Catalogs are pretty-printed JSON with sorted keys, written only when something changed, and written atomically (temp file + rename).
164
+ - 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.
165
+ - **Removed strings**:
166
+ - Marked `unused` in the manifest. Their translations stay until `clean` removes them, but they are not shipped to the browser.
167
+ - If the text comes back, its translation is restored from `.auto-locale/cache.json` without calling the provider.
168
+ - **Moving the output**:
169
+ - After `output` changes, the next `scan` copies any missing target catalogs to the new folder and warns you.
170
+ - The old folder is never moved or deleted; remove it yourself once you've checked the new one.
171
+
172
+ ## Production builds
173
+
174
+ `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.
175
+
176
+ ## Upgrading from earlier versions
177
+
178
+ Earlier versions translated during `vite build` and kept translations in `node_modules/.cache/react-autolocale/`:
179
+ - **Before you run anything new**: builds still read that folder, with a warning, so nothing breaks.
180
+ - **After `npx react-autolocale generate`**: those translations are copied into the new catalogs without being retranslated. The old folder is left in place.
181
+ - **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
182
 
45
183
  ## Multi-page apps (React Router)
46
184
 
@@ -71,7 +209,9 @@ Your host must serve `/fr/about/index.html` for `/fr/about`, which static hosts
71
209
 
72
210
  ## Fixing a translation
73
211
 
74
- The quickest way is the overrides file. Run `npx react-autolocale export` after a build: it writes every generated translation into `react-autolocale.overrides.json` in your project root. Edit any value, commit the file and rebuild. Entries in it always win over machine translation and survive rebuilds (edits made inside `dist/` or the cache do not).
212
+ Edit the value in the catalog (`src/generated/auto-locale/fr.json`) and commit it. Later runs keep your edit.
213
+
214
+ 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`.
75
215
 
76
216
  You can also fix a few strings in code:
77
217
 
@@ -109,26 +249,15 @@ format.date(new Date()); // "Oct 9, 2026" / "9 oct. 2026"
109
249
 
110
250
  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.
111
251
 
112
- ## Better translations: glossary and choosing the engine
252
+ ## Better translations: glossary and context
113
253
 
114
- The built-in model is free and runs on your machine, but it translates each string without knowing where it appears. You can improve it:
254
+ 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:
115
255
 
116
256
  ```ts
117
- import reactAutolocale, { ollama } from "react-autolocale/vite";
118
-
119
- reactAutolocale({
120
- // Terms that must always translate a given way (applied before the model sees the text)
121
- glossary: { fr: { Roadmap: "Feuille de route" }, es: { Roadmap: "Hoja de ruta" } },
122
-
123
- // Swap the engine. Free and local, through Ollama (https://ollama.com):
124
- engine: ollama({ model: "llama3.1" }),
125
-
126
- // ...or your own function: DeepL, an LLM API, anything. It is told where each string appears.
127
- // engine: async ({ texts, contexts, from, to }) => texts.map((t, i) => myTranslate(t, from, to, contexts[i])),
128
- });
257
+ glossary: { fr: { Roadmap: "Feuille de route" }, es: { Roadmap: "Hoja de ruta" } },
129
258
  ```
130
259
 
131
- `contexts[i]` is the element or attribute the string sits in (`"button"`, `"a"`, `"h1"`, `"placeholder"`, ...), which is what lets a language model translate a lone "Home" in a nav bar correctly. The built-in model ignores it. Translations are cached, so you only pay for (or wait for) new and changed text.
260
+ 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.
132
261
 
133
262
  ## Language switcher
134
263
 
@@ -148,10 +277,10 @@ const { language, languages, setLanguage, basePath } = useLanguage();
148
277
 
149
278
  ## What gets translated
150
279
 
151
- - Static JSX text, plus `placeholder`, `title`, `alt` and `aria-label` on HTML elements.
280
+ - 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.
152
281
  - **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.
153
282
  - **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).
154
- - **Numbers** are kept exactly as written. The build 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.
283
+ - **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.
155
284
 
156
285
  ```tsx
157
286
  <p translate="no">Acme Cloud</p> // never translated
@@ -203,15 +332,11 @@ Use these codes in `languages={[...]}`. The source (`original`) language can be
203
332
  | `tl` | Tagalog | NLLB |
204
333
  | `en` | English (default source) | NLLB when used as a target |
205
334
 
206
- 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.
335
+ 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.
207
336
 
208
337
  ### Quality by language
209
338
 
210
- 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 below). You can re-run the check yourself with `RUN_MODELS=1 pnpm exec vitest run test/models.test.ts`.
211
-
212
- ## Translations and caching
213
-
214
- 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.
339
+ 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`.
215
340
 
216
341
  ## Automatic language navigation
217
342
 
@@ -271,11 +396,23 @@ With `seo` the build also:
271
396
 
272
397
  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`.
273
398
 
399
+ ## Troubleshooting
400
+
401
+ - **"No translations in src/generated/auto-locale/ yet" during a build**: run `npx react-autolocale generate`, then commit the folder.
402
+ - **"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.
403
+ - **"This provider needs the openai package"** (or `@anthropic-ai/sdk`): install it as a dev dependency of your app.
404
+ - **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.
405
+ - **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.
406
+ - **Interrupted run**: everything finished before the interruption was saved; run the same command again.
407
+ - **Corrupted JSON**: commands stop and name the file instead of treating it as empty; fix it or restore it from Git.
408
+
274
409
  ## Limits
275
410
 
276
411
  - 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.
277
412
  - Variables in text must be plain names or property paths; no plural rules.
278
413
  - Dynamic routes (`/users/:id`) only get pages you list in `routes`, or that are linked.
279
414
  - `<AutoScale>` props must be literals so the plugin can read them at build time.
415
+ - The CLI reads `auto-locale.config.ts`, not `vite.config`. Set `include` there if you change the plugin's `include`.
416
+ - Identical text always shares one translation.
280
417
  - Use Vite's default `base: "/"`.
281
418
  - Machine translation is draft quality. Short labels, long sentences with variables and less common languages need a human look: use the overrides file.