react-autolocale 0.3.0-beta.0 → 0.4.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,52 +1,86 @@
1
1
  # react-autolocale
2
2
 
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.
3
+ Write your React + Vite app in one language. react-autolocale extracts the text, translates it with the engine you choose (Google Gemini, OpenAI, Anthropic Claude, or as a last option a free local model), 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
4
 
5
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.
6
6
 
7
7
  ## Quick start
8
8
 
9
+ You need a React + Vite project and an API key from the engine you choose: [Google Gemini](https://aistudio.google.com/apikey), [OpenAI](https://platform.openai.com/api-keys) or [Anthropic](https://console.anthropic.com/settings/keys). A free local model is also available, but it is the last option, not the recommended one (see [Choosing a translation engine](#choosing-a-translation-engine)).
10
+
9
11
  **1. Install** (npm, yarn or pnpm)
10
12
 
11
13
  ```bash
12
- npm install react-autolocale # or: yarn add react-autolocale | pnpm add react-autolocale
14
+ npm install react-autolocale # or: yarn add react-autolocale | pnpm add react-autolocale
13
15
  ```
14
16
 
15
- **2. Set it up**
17
+ **2. Run the setup wizard**
16
18
 
17
19
  ```bash
18
- npx react-autolocale init # or: yarn react-autolocale init | pnpm exec react-autolocale init
20
+ npx react-autolocale init # or: yarn react-autolocale init | pnpm exec react-autolocale init
19
21
  ```
20
22
 
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
23
+ It asks these questions, in this order:
24
+
25
+ | Question | What to answer |
26
+ | --- | --- |
27
+ | **Source language** | The language your code's text is written in. Press **Enter** for English (`en`), or type **one** code such as `fr`. Not a list. |
28
+ | **Target languages** | The languages to add, comma-separated, for example `fr,es,de`. Pressing Enter on an empty line just asks again. |
29
+ | **How would you like to translate?** | Type the **number** of a cloud provider (OpenAI, Anthropic Claude or Google Gemini). Nothing is chosen for you. The local model is the last option. |
30
+ | **Catalog folder** | Press **Enter** to keep the default. |
31
+ | **API key** | Paste your key; what you type is hidden. It is saved to `.env`, and `.env` is added to `.gitignore`. Press Enter to skip and add `GEMINI_API_KEY=...` (or `OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) to `.env` yourself later. |
25
32
 
26
- It then writes `auto-locale.config.ts` and adds the plugin to `vite.config`. It downloads and translates nothing.
33
+ It writes `auto-locale.config.ts` and adds the plugin to your `vite.config`. It translates and downloads nothing. To skip the questions: `npx react-autolocale init --provider gemini --source en --locales fr,es`.
27
34
 
28
- **3. Wrap your app once**
35
+ **3. Wrap your app once.** Open the file that renders `<App />` (usually `src/main.tsx` or `src/main.jsx`) and add the import and the wrapper. Keep `<StrictMode>` on the outside if you have it:
29
36
 
30
37
  ```tsx
31
- // main.tsx
38
+ import { StrictMode } from "react";
39
+ import { createRoot } from "react-dom/client";
32
40
  import { AutoScale } from "react-autolocale";
41
+ import App from "./App";
33
42
 
34
43
  createRoot(document.getElementById("root")!).render(
35
- <AutoScale exclude={["Acme Cloud"]}>
36
- <App />
37
- </AutoScale>,
44
+ <StrictMode>
45
+ <AutoScale>
46
+ <App />
47
+ </AutoScale>
48
+ </StrictMode>,
38
49
  );
39
50
  ```
40
51
 
41
- **4. Generate translations, then develop and build as usual**
52
+ No props are needed: the languages come from `auto-locale.config.ts`. Without this wrapper the translations exist, but the site stays in the original language.
53
+
54
+ **4. Generate the translations**
55
+
56
+ ```bash
57
+ npx react-autolocale generate
58
+ ```
59
+
60
+ You should see which engine is used and a count, for example `Using gemini:gemini-3.8-flash.` and `Translated 314 strings (fr 157, es 157)`. The translations are saved in `src/generated/auto-locale/`.
61
+
62
+ **This is the only step that uses your API key.** You run it on your own computer, once, and again only when your text changes. Nothing after this step needs the key or the internet.
63
+
64
+ **5. Run it and check it**
65
+
66
+ ```bash
67
+ npm run dev # open http://localhost:5173/fr/ and /es/ (use the port it prints)
68
+ npm run build && npm run preview # the finished pages: dist/fr/, dist/es/, ...
69
+ ```
70
+
71
+ You do **not** need the API key for this step, or for deploying: `build` and `preview` only read the translations you already generated. Put the key in `.env` on your own computer and nowhere else, so you never add it to Vercel, Netlify or your CI.
72
+
73
+ Read the translated pages. Machine translation can get short labels or brand names wrong: fix them as described in [Fixing a translation](#fixing-a-translation), and list brand names in `<AutoScale exclude={["Acme Cloud"]}>`.
74
+
75
+ **6. Commit the translations**
42
76
 
43
77
  ```bash
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/, ...
78
+ git add src/generated/auto-locale
47
79
  ```
48
80
 
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.
81
+ Commit that folder so builds and deploys never need an API key or a model. Never commit `.env`.
82
+
83
+ **Whenever you change text in your components**, run `npx react-autolocale sync`. Only new strings are translated, and a running dev server reloads when it finishes.
50
84
 
51
85
  Optional: `npx react-autolocale add switcher` copies an editable language switcher into your project (see below).
52
86
 
@@ -63,7 +97,7 @@ Optional: `npx react-autolocale add switcher` copies an editable language switch
63
97
  | `export` | Copies the translations into `react-autolocale.overrides.json` for hand editing. | None |
64
98
  | `add switcher` / `add banner` | Copies an editable component into your project. | None |
65
99
 
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`.
100
+ `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
101
 
68
102
  A `generate` run looks like this:
69
103
 
@@ -104,19 +138,25 @@ When the config sets `sourceLocale` and `locales`, `<AutoScale>` needs no langua
104
138
 
105
139
  ## Choosing a translation engine
106
140
 
107
- The engine only matters to `translate`/`generate`/`sync`. Extraction, the catalogs, the build and the runtime work the same with every engine.
141
+ The engine only matters to `translate`/`generate`/`sync`. Extraction, the catalogs, the build and the runtime work the same with every engine. `init` never picks one for you.
108
142
 
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.
143
+ | Engine | Quality | What you need | Cost |
144
+ | --- | --- | --- | --- |
145
+ | **Google Gemini**, **OpenAI**, **Anthropic Claude** | Best. Each string is sent with the element it appears in. | An API key | Billed by the provider (Gemini has a free tier with low limits) |
146
+ | **Ollama** | Good, depends on the model. | Ollama installed, with a model | Free |
147
+ | **Local model** (last option) | **May not match the cloud models**, especially short labels and less common languages. | A **large download** on first use and a fast-enough CPU | Free |
114
148
 
115
149
  **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
150
  - **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
151
  - **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
152
  - **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
153
  - **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.
154
+
155
+ **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.
156
+ - **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`.
157
+ - **Model**: `gemini-3.8-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.
158
+ - **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.
159
+ - **Choose it**: `npx react-autolocale init --provider gemini`, or in the config: `provider: { name: "gemini" }`.
120
160
  - **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
161
 
122
162
  **Ollama** (`{ name: "ollama", model: "llama3.1", url? }`) runs a local LLM that receives the context too.
@@ -127,6 +167,16 @@ The engine only matters to `translate`/`generate`/`sync`. Extraction, the catalo
127
167
  provider: async ({ texts, contexts, from, to }) => texts.map((t, i) => myTranslate(t, from, to, contexts[i])),
128
168
  ```
129
169
 
170
+ **Local model** (`{ name: "local" }`) is the last option, not the recommended one. It is free and works offline with no API key, but you should know what you are choosing:
171
+ - **You must download models first.** `init` and builds download nothing; the first `translate`/`generate` does.
172
+ - **The quality may be lower than the cloud models above.** Short labels such as "Home" or "Save" and less common languages are the usual weak spots. Review the result, and fix strings in the overrides file.
173
+
174
+ It runs on your machine through Transformers.js, using OPUS-MT for English sources and NLLB-200 for everything else. The cost is disk, RAM and CPU time:
175
+ - **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`.
176
+ - **Reuse**: models are cached in `~/.cache/react-autolocale` and shared by every project. A loaded model is kept for the whole run.
177
+ - **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.
178
+ - **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.
179
+
130
180
  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
181
 
132
182
  ## Generated files and Git
@@ -392,8 +442,12 @@ With `seo`, switching language navigates to the real page (so its head tags are
392
442
 
393
443
  ## Troubleshooting
394
444
 
445
+ - **`init` says `"fr, es" is not a supported code` at "Source language"**: that question takes one code (your code's language, usually `en`). The list of languages to add comes at the next question.
446
+ - **The site is still in English after `generate`**: wrap your app in `<AutoScale>` (step 3 of the quick start), restart `npm run dev`, and open `/fr/` or `/es/`.
447
+ - **`init` printed instructions with `original="en" languages={...}`**: you have the older 0.2.0, which works differently. Upgrade: `npm install react-autolocale@latest` (see [Upgrading from earlier versions](#upgrading-from-earlier-versions)).
395
448
  - **"No translations in src/generated/auto-locale/ yet" during a build**: run `npx react-autolocale generate`, then commit the folder.
396
449
  - **"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.
450
+ - **Gemini says "model not found" or "no longer available"**: Google retires models over time. Set `model` in the provider to a current id from [AI Studio](https://aistudio.google.com) (`provider: { name: "gemini", model: "<id>" }`) and run `generate` again; strings that already succeeded are kept.
397
451
  - **"This provider needs the openai package"** (or `@anthropic-ai/sdk`): install it as a dev dependency of your app.
398
452
  - **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
453
  - **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.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { F as toTranslator, I as getLanguage, M as TranslationError, N as createLocalTranslator, P as ollama, S as scanProject, a as findSite, b as manifestFile, c as hasRootSrc, d as OVERRIDES_FILE, f as exportTranslations, g as importLegacyCache, h as catalogFile, i as defaultInclude, j as writeJson, k as ensureBanner, l as resolveOutput, m as readOverrides, n as loadConfigFile, o as META_DIR, r as loadProject, s as defaultOutput, t as findConfigFile, u as toProjectPath, v as removeFromCatalogs, x as readManifest, y as translateMissing } from "./project-CGAWETvT.js";
3
- import { n as DEFAULT_MODELS, t as DEFAULT_KEY_ENV } from "./config-CYHyGpdX.js";
2
+ import { F as toTranslator, I as getLanguage, M as TranslationError, N as createLocalTranslator, P as ollama, S as scanProject, a as findSite, b as manifestFile, c as hasRootSrc, d as OVERRIDES_FILE, f as exportTranslations, g as importLegacyCache, h as catalogFile, i as defaultInclude, j as writeJson, k as ensureBanner, l as resolveOutput, m as readOverrides, n as loadConfigFile, o as META_DIR, r as loadProject, s as defaultOutput, t as findConfigFile, u as toProjectPath, v as removeFromCatalogs, x as readManifest, y as translateMissing } from "./project-4C7cUxzd.js";
3
+ import { n as DEFAULT_MODELS, t as DEFAULT_KEY_ENV } from "./config-ChdN65Rg.js";
4
4
  import path from "node:path";
5
5
  import fs from "node:fs";
6
6
  import { spawnSync } from "node:child_process";
@@ -66,6 +66,11 @@ async function createProvider(project, log) {
66
66
  name: `anthropic:${p.model ?? DEFAULT_MODELS.anthropic}`,
67
67
  chunkSize: 100
68
68
  };
69
+ case "gemini": return {
70
+ translator: geminiTranslator(p),
71
+ name: `gemini:${p.model ?? DEFAULT_MODELS.gemini}`,
72
+ chunkSize: 100
73
+ };
69
74
  }
70
75
  }
71
76
  function apiKey(provider, envName) {
@@ -238,6 +243,92 @@ async function anthropicTranslator(root, p) {
238
243
  }
239
244
  });
240
245
  }
246
+ /** Gemini's schema dialect (types in capitals) for {"translations": [string, ...]}. */
247
+ const GEMINI_SCHEMA = {
248
+ type: "OBJECT",
249
+ properties: { translations: {
250
+ type: "ARRAY",
251
+ items: { type: "STRING" }
252
+ } },
253
+ required: ["translations"]
254
+ };
255
+ const GEMINI_RETRYABLE = /* @__PURE__ */ new Set([
256
+ 429,
257
+ 500,
258
+ 502,
259
+ 503,
260
+ 504
261
+ ]);
262
+ const GEMINI_RETRIES = 3;
263
+ /** The model stopped because of its safety rules, not because it finished. */
264
+ const GEMINI_DECLINED = /* @__PURE__ */ new Set([
265
+ "SAFETY",
266
+ "BLOCKLIST",
267
+ "PROHIBITED_CONTENT",
268
+ "SPII",
269
+ "IMAGE_SAFETY",
270
+ "RECITATION"
271
+ ]);
272
+ /** Maps an HTTP failure to a TranslationError. Credential, permission and request errors stop the run. */
273
+ function geminiError(status, data) {
274
+ const msg = String(data?.error?.message ?? `HTTP ${status}`).replace(/[.\s]+$/, "");
275
+ if (status === 401 || status === 403 || /API key not valid|API_KEY_INVALID/i.test(msg)) return new TranslationError(`Gemini: the API key was rejected or lacks permission (${msg})`, true);
276
+ if (status === 404) return new TranslationError(`Gemini: model not found; check the model name (${msg})`, true);
277
+ if (status === 400) return new TranslationError(`Gemini: request rejected (${msg})`, true);
278
+ if (status === 429) return new TranslationError(`Gemini: rate limit or quota reached after retries (${msg}). Run the command again later`, true);
279
+ return new TranslationError(`Gemini: ${msg}`);
280
+ }
281
+ function geminiTranslator(p, deps = {}) {
282
+ const key = apiKey("gemini", p.apiKeyEnv);
283
+ const model = p.model ?? DEFAULT_MODELS.gemini;
284
+ const url = `${(p.baseURL ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "")}/models/${encodeURIComponent(model)}:generateContent`;
285
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
286
+ const backoff = (attempt) => Math.min(3e4, 1e3 * 2 ** attempt + Math.random() * 500);
287
+ return batchedTranslator(async (texts, contexts, from, to) => {
288
+ const body = JSON.stringify({
289
+ systemInstruction: { parts: [{ text: systemPrompt(from, to) }] },
290
+ contents: [{
291
+ role: "user",
292
+ parts: [{ text: userPrompt(texts, contexts) }]
293
+ }],
294
+ generationConfig: {
295
+ temperature: 0,
296
+ responseMimeType: "application/json",
297
+ responseSchema: GEMINI_SCHEMA
298
+ }
299
+ });
300
+ for (let attempt = 0;; attempt++) {
301
+ let res;
302
+ try {
303
+ res = await fetch(url, {
304
+ method: "POST",
305
+ headers: {
306
+ "content-type": "application/json",
307
+ "x-goog-api-key": key
308
+ },
309
+ body,
310
+ signal: AbortSignal.timeout(12e4)
311
+ });
312
+ } catch (err) {
313
+ if (attempt >= GEMINI_RETRIES) throw new TranslationError(`Gemini: could not reach the API (${err.message})`);
314
+ await sleep(backoff(attempt));
315
+ continue;
316
+ }
317
+ if (GEMINI_RETRYABLE.has(res.status) && attempt < GEMINI_RETRIES) {
318
+ const wait = Number(res.headers.get("retry-after"));
319
+ await sleep(Number.isFinite(wait) && wait > 0 ? Math.min(wait, 60) * 1e3 : backoff(attempt));
320
+ continue;
321
+ }
322
+ const data = await res.json().catch(() => ({}));
323
+ if (!res.ok) throw geminiError(res.status, data);
324
+ if (data.promptFeedback?.blockReason) throw new TranslationError(`Gemini declined to translate this batch (${data.promptFeedback.blockReason})`);
325
+ const candidate = data.candidates?.[0];
326
+ if (!candidate || candidate.finishReason === "MAX_TOKENS") return "";
327
+ if (GEMINI_DECLINED.has(candidate.finishReason)) throw new TranslationError(`Gemini declined to translate this batch (${candidate.finishReason})`);
328
+ return (candidate.content?.parts ?? []).map((part) => part.text ?? "").join("");
329
+ }
330
+ });
331
+ }
241
332
  //#endregion
242
333
  //#region src/cli/commands.ts
243
334
  const ok = (out, msg) => out.log(`✓ ${msg}`);
@@ -531,11 +622,6 @@ async function runInit(root, flags, prompt, out) {
531
622
  if (bad.length) throw new Error(`Unsupported language code${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}.`);
532
623
  }
533
624
  const name = flags.provider ?? await prompt.choose("How would you like to translate your application?", [
534
- {
535
- value: "local",
536
- label: "Local model (OPUS-MT / NLLB-200)",
537
- help: "Free and runs on your machine; no API key. Models are large (about 110 MB per English->X language, 870 MB for\nother pairs) and translating on the CPU uses RAM and time, so big apps or many languages can be slow.\nModels are cached in ~/.cache/react-autolocale and downloaded only when you run `translate` or `generate`,\nnever during init or builds."
538
- },
539
625
  {
540
626
  value: "openai",
541
627
  label: "Cloud AI: OpenAI",
@@ -545,10 +631,21 @@ async function runInit(root, flags, prompt, out) {
545
631
  value: "anthropic",
546
632
  label: "Cloud AI: Anthropic Claude",
547
633
  help: "Uses a hosted AI model. No large downloads, and usually faster for big translation jobs. Needs an API key;\nprovider usage may be billed to your account."
634
+ },
635
+ {
636
+ value: "gemini",
637
+ label: "Cloud AI: Google Gemini",
638
+ help: "Uses a hosted AI model. No large downloads, and usually faster for big translation jobs. Needs an API key;\nprovider usage may be billed to your account."
639
+ },
640
+ {
641
+ value: "local",
642
+ label: "Local model (OPUS-MT / NLLB-200): free, but a large download and lower quality",
643
+ help: "Free and works offline, with no API key, but it needs a large download the first time (about 110 MB per\nEnglish->X language, 870 MB for other pairs) and translating on your CPU is slow for big apps. The quality may\nnot match the cloud models, especially for short labels and less common languages, so review the result.\nModels are cached in ~/.cache/react-autolocale and downloaded only when you run `translate` or `generate`,\nnever during init or builds."
548
644
  }
549
- ], "local");
645
+ ]);
646
+ if (!name) throw new Error("Choose a translation engine: --provider openai|anthropic|gemini|local|ollama (add --model for ollama).");
550
647
  let provider;
551
- if (name === "openai" || name === "anthropic") provider = {
648
+ if (name === "openai" || name === "anthropic" || name === "gemini") provider = {
552
649
  name,
553
650
  model: flags.model ?? DEFAULT_MODELS[name],
554
651
  apiKeyEnv: flags.apiKeyEnv ?? DEFAULT_KEY_ENV[name]
@@ -560,7 +657,7 @@ async function runInit(root, flags, prompt, out) {
560
657
  model: flags.model
561
658
  };
562
659
  } else if (name === "local") provider = { name };
563
- else throw new Error(`Unknown provider "${name}". Choose local, openai, anthropic or ollama.`);
660
+ else throw new Error(`Unknown provider "${name}". Choose local, openai, anthropic, gemini or ollama.`);
564
661
  const auto = defaultOutput(root);
565
662
  out.log(`\nGenerated output:\n ${auto}/\nInternal metadata:\n ${META_DIR}/`);
566
663
  let output = flags.output ?? await prompt.ask("Catalog folder (press Enter to keep)", auto);
@@ -578,19 +675,23 @@ async function runInit(root, flags, prompt, out) {
578
675
  const provider = config.provider;
579
676
  const ignore = [`${META_DIR}/cache.json`];
580
677
  let modelNote = "";
581
- if (provider && typeof provider === "object" && "name" in provider && (provider.name === "openai" || provider.name === "anthropic")) {
678
+ if (provider && typeof provider === "object" && "name" in provider && (provider.name === "openai" || provider.name === "anthropic" || provider.name === "gemini")) {
582
679
  const keyEnv = provider.apiKeyEnv ?? DEFAULT_KEY_ENV[provider.name];
583
680
  ignore.push(".env", ".env.local");
584
681
  if (keyIsAvailable(root, keyEnv)) out.log(`${keyEnv} is already set (value hidden).`);
585
682
  else {
586
- const key = await prompt.secret(`Paste your ${provider.name === "openai" ? "OpenAI" : "Anthropic"} API key to save it in .env (leave empty to do it later)`);
683
+ const key = await prompt.secret(`Paste your ${{
684
+ openai: "OpenAI",
685
+ anthropic: "Anthropic",
686
+ gemini: "Google Gemini"
687
+ }[provider.name]} API key to save it in .env (leave empty to do it later)`);
587
688
  if (key) {
588
689
  saveKeyToEnv(root, keyEnv, key);
589
690
  out.log(`Saved ${keyEnv} to .env (value hidden).`);
590
691
  } else out.log(`Add ${keyEnv}=... to .env (or your environment) before running translate. No request is made without it.`);
591
692
  }
592
- const pkg = SDK[provider.name];
593
- if (!sdkInstalled(root, pkg)) {
693
+ const pkg = provider.name === "gemini" ? null : SDK[provider.name];
694
+ if (pkg && !sdkInstalled(root, pkg)) {
594
695
  const cmd = {
595
696
  npm: [
596
697
  "install",
@@ -635,6 +736,7 @@ async function runInit(root, flags, prompt, out) {
635
736
  local: "Local model",
636
737
  openai: "OpenAI",
637
738
  anthropic: "Anthropic Claude",
739
+ gemini: "Google Gemini",
638
740
  ollama: "Ollama"
639
741
  }[p.name];
640
742
  const out_ = toProjectPath(root, resolveOutput(root, config.output));
@@ -695,7 +797,8 @@ function createPrompter(interactive) {
695
797
  console.log(` ${i + 1}) ${o.label}`);
696
798
  if (o.help) console.log(o.help.replace(/^/gm, " ") + "\n");
697
799
  });
698
- const def = String(options.findIndex((o) => o.value === fallback) + 1);
800
+ const index = options.findIndex((o) => o.value === fallback);
801
+ const def = index >= 0 ? String(index + 1) : "";
699
802
  for (;;) {
700
803
  const answer = await ask("Choose a number", def);
701
804
  const picked = options[Number(answer) - 1];
@@ -710,7 +813,7 @@ function createPrompter(interactive) {
710
813
  const HELP = `react-autolocale
711
814
 
712
815
  Usage:
713
- react-autolocale init [--source en] [--locales fr,es] [--provider local|openai|anthropic|ollama]
816
+ react-autolocale init [--source en] [--locales fr,es] [--provider local|openai|anthropic|gemini|ollama]
714
817
  [--model <id>] [--api-key-env <NAME>] [--output <dir>] [--yes] [--force]
715
818
  Sets up auto-locale.config.ts, the translation provider and the Vite plugin. Downloads nothing.
716
819
  react-autolocale generate [--lang fr,es]
@@ -7,15 +7,18 @@ const PROVIDER_NAMES = [
7
7
  "local",
8
8
  "ollama",
9
9
  "openai",
10
- "anthropic"
10
+ "anthropic",
11
+ "gemini"
11
12
  ];
12
13
  const DEFAULT_MODELS = {
13
14
  openai: "gpt-6-astra",
14
- anthropic: "claude-opus-5-5"
15
+ anthropic: "claude-opus-5-5",
16
+ gemini: "gemini-3.8-flash"
15
17
  };
16
18
  const DEFAULT_KEY_ENV = {
17
19
  openai: "OPENAI_API_KEY",
18
- anthropic: "ANTHROPIC_API_KEY"
20
+ anthropic: "ANTHROPIC_API_KEY",
21
+ gemini: "GEMINI_API_KEY"
19
22
  };
20
23
  //#endregion
21
24
  export { defineConfig as i, DEFAULT_MODELS as n, PROVIDER_NAMES as r, DEFAULT_KEY_ENV as t };
package/dist/config.d.ts CHANGED
@@ -30,7 +30,17 @@ interface AnthropicProvider {
30
30
  /** How much the model thinks per request. Short UI strings rarely need more than "low" (the default). */
31
31
  effort?: "low" | "medium" | "high" | "xhigh" | "max";
32
32
  }
33
- type ProviderConfig = LocalProvider | OllamaProvider | OpenAIProvider | AnthropicProvider;
33
+ /** Google Gemini through its REST API. No SDK needed. Get a key at https://aistudio.google.com/apikey. */
34
+ interface GeminiProvider {
35
+ name: "gemini";
36
+ /** Defaults to "gemini-3.8-flash". Use any model id listed in Google AI Studio. */
37
+ model?: string;
38
+ /** Environment variable holding the key. Defaults to GEMINI_API_KEY. */
39
+ apiKeyEnv?: string;
40
+ /** Custom endpoint, e.g. a proxy. Defaults to https://generativelanguage.googleapis.com/v1beta. */
41
+ baseURL?: string;
42
+ }
43
+ type ProviderConfig = LocalProvider | OllamaProvider | OpenAIProvider | AnthropicProvider | GeminiProvider;
34
44
  interface AutoLocaleConfig {
35
45
  /** Language the source code is written in, e.g. "en". */
36
46
  sourceLocale?: string;
@@ -53,4 +63,4 @@ interface AutoLocaleConfig {
53
63
  /** Typed helper for auto-locale.config.ts. */
54
64
  export declare function defineConfig(config: AutoLocaleConfig): AutoLocaleConfig;
55
65
  //#endregion
56
- export type { AnthropicProvider, AutoLocaleConfig, LocalProvider, OllamaProvider, OpenAIProvider, ProviderConfig, TranslateFn, Translator };
66
+ export type { AnthropicProvider, AutoLocaleConfig, GeminiProvider, LocalProvider, OllamaProvider, OpenAIProvider, ProviderConfig, TranslateFn, Translator };
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- export { i as defineConfig } from "./config-CYHyGpdX.js";
1
+ export { i as defineConfig } from "./config-ChdN65Rg.js";
@@ -1,5 +1,5 @@
1
1
  import { t as BANNER_EN } from "./suggestion-CkZQqMvZ.js";
2
- import { r as PROVIDER_NAMES } from "./config-CYHyGpdX.js";
2
+ import { r as PROVIDER_NAMES } from "./config-ChdN65Rg.js";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
5
  import { spawnSync } from "node:child_process";
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as redirectScript } from "./redirect-pDFblLdm.js";
2
- import { A as readBanner, C as isSourceFile, D as translateHead, E as transformJsx, I as getLanguage, O as messageId, P as ollama, T as parserPlugins, _ as loadDicts, d as OVERRIDES_FILE, m as readOverrides, p as mergeOverrides, r as loadProject, w as walk } from "./project-CGAWETvT.js";
2
+ import { A as readBanner, C as isSourceFile, D as translateHead, E as transformJsx, I as getLanguage, O as messageId, P as ollama, T as parserPlugins, _ as loadDicts, d as OVERRIDES_FILE, m as readOverrides, p as mergeOverrides, r as loadProject, w as walk } from "./project-4C7cUxzd.js";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
5
  import { JSDOM, VirtualConsole } from "jsdom";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-autolocale",
3
- "version": "0.3.0-beta.0",
3
+ "version": "0.4.0",
4
4
  "description": "Wrap your React + Vite app in <AutoScale> and get fully translated static pages (/fr, /es, ...). Translations are generated by an explicit CLI step with a local model, OpenAI or Anthropic; builds never translate.",
5
5
  "license": "MIT",
6
6
  "keywords": [