i18next-cli 1.61.1 → 1.63.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 +147 -0
- package/dist/cjs/cli.js +37 -5
- package/dist/cjs/config.js +5 -1
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/init.js +25 -75
- package/dist/cjs/instrumenter/core/instrumenter.js +33 -11
- package/dist/cjs/instrumenter/core/transformer.js +5 -1
- package/dist/cjs/localize/agent-prompt.js +49 -0
- package/dist/cjs/localize/detect.js +88 -0
- package/dist/cjs/localize/localize.js +475 -0
- package/dist/cjs/locize.js +84 -11
- package/dist/cjs/status.js +5 -1
- package/dist/cjs/types-generator.js +8 -3
- package/dist/cjs/utils/file-utils.js +6 -2
- package/dist/cjs/utils/inlang-scaffold.js +184 -0
- package/dist/cjs/utils/locize-onboarding.js +91 -0
- package/dist/cjs/utils/wrap-ora.js +9 -5
- package/dist/esm/cli.js +30 -2
- package/dist/esm/index.js +4 -0
- package/dist/esm/init.js +19 -73
- package/dist/esm/instrumenter/core/instrumenter.js +22 -8
- package/dist/esm/localize/agent-prompt.js +47 -0
- package/dist/esm/localize/detect.js +85 -0
- package/dist/esm/localize/localize.js +469 -0
- package/dist/esm/locize.js +75 -9
- package/dist/esm/utils/inlang-scaffold.js +182 -0
- package/dist/esm/utils/locize-onboarding.js +83 -0
- package/package.json +10 -10
- package/types/cli.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/init.d.ts +1 -0
- package/types/init.d.ts.map +1 -1
- package/types/instrumenter/core/instrumenter.d.ts +28 -0
- package/types/instrumenter/core/instrumenter.d.ts.map +1 -1
- package/types/instrumenter/index.d.ts +2 -1
- package/types/instrumenter/index.d.ts.map +1 -1
- package/types/localize/agent-prompt.d.ts +11 -0
- package/types/localize/agent-prompt.d.ts.map +1 -0
- package/types/localize/detect.d.ts +37 -0
- package/types/localize/detect.d.ts.map +1 -0
- package/types/localize/index.d.ts +6 -0
- package/types/localize/index.d.ts.map +1 -0
- package/types/localize/localize.d.ts +20 -0
- package/types/localize/localize.d.ts.map +1 -0
- package/types/locize.d.ts +20 -0
- package/types/locize.d.ts.map +1 -1
- package/types/types.d.ts +12 -0
- package/types/types.d.ts.map +1 -1
- package/types/utils/inlang-scaffold.d.ts +28 -0
- package/types/utils/inlang-scaffold.d.ts.map +1 -0
- package/types/utils/locize-onboarding.d.ts +19 -0
- package/types/utils/locize-onboarding.d.ts.map +1 -0
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ npm install --save-dev i18next-cli
|
|
|
58
58
|
|
|
59
59
|
## Quick Start
|
|
60
60
|
|
|
61
|
+
> **Zero-to-localized in one command:** starting from an app with hardcoded strings (e.g. generated with v0, Lovable, Bolt or Cursor)? Run `npx i18next-cli localize` — it detects your setup, wraps hardcoded strings in `t()` calls, extracts keys, connects to [Locize](https://www.locize.com) and AI-translates your app. See [the `localize` command](#localize). The steps below are the manual path.
|
|
62
|
+
|
|
61
63
|
### 1. Initialize Configuration
|
|
62
64
|
|
|
63
65
|
Create a configuration interactively:
|
|
@@ -116,6 +118,13 @@ npx i18next-cli init
|
|
|
116
118
|
also auto-detects `CI=true` and falls back to printing the URL on headless
|
|
117
119
|
Linux (no `DISPLAY`/`WAYLAND_DISPLAY`), so this flag is rarely needed
|
|
118
120
|
explicitly.
|
|
121
|
+
- `--inlang`: Also scaffold an [inlang](https://inlang.com) project
|
|
122
|
+
(`project.inlang/settings.json`) so inlang tooling — the
|
|
123
|
+
[Sherlock](https://inlang.com/m/r7kp499g/app-inlang-ideExtension) VS Code
|
|
124
|
+
extension, the [Fink](https://fink.inlang.com) web editor for translators,
|
|
125
|
+
and the [Paraglide](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)
|
|
126
|
+
compiler — works directly on your translation files. Skips the
|
|
127
|
+
corresponding wizard question.
|
|
119
128
|
|
|
120
129
|
The wizard asks for the config file type, locales, source-file glob, output
|
|
121
130
|
path, and finally **"Translation backend?"** with three options:
|
|
@@ -129,6 +138,28 @@ path, and finally **"Translation backend?"** with three options:
|
|
|
129
138
|
mode); add it later via a `LOCIZE_API_KEY` environment variable.
|
|
130
139
|
- **Other / skip** — same as "Local files only" for the wizard's purposes.
|
|
131
140
|
|
|
141
|
+
The wizard then offers to **set up inlang tooling** (default: no — or pass
|
|
142
|
+
`--inlang` to skip the question). If accepted, it scaffolds a
|
|
143
|
+
`project.inlang/settings.json` that points the
|
|
144
|
+
[inlang i18next plugin](https://inlang.com/m/3i8bor92/plugin-inlang-i18next)
|
|
145
|
+
at your existing translation files: `baseLocale`/`locales` come from your
|
|
146
|
+
config, and `pathPattern` is derived from `extract.output` (the namespaced
|
|
147
|
+
object form when your layout uses `{{namespace}}`, with namespaces discovered
|
|
148
|
+
from the primary language's files; a plain pattern otherwise). It also adds
|
|
149
|
+
the Sherlock extension to `.vscode/extensions.json` recommendations (merging
|
|
150
|
+
comment-aware, never clobbering existing entries). Your i18next JSON files
|
|
151
|
+
remain the single source of truth — inlang tools read and write them in
|
|
152
|
+
place, so there is no second catalog to drift. An existing
|
|
153
|
+
`project.inlang/settings.json` is never overwritten; re-running `init` is
|
|
154
|
+
safe. Requires JSON resource files. The plugin is pinned to an exact verified
|
|
155
|
+
version (`@inlang/plugin-i18next@6.2.0`) — bump the `modules` URL in
|
|
156
|
+
`settings.json` to pick up newer plugin releases. Only `settings.json` is
|
|
157
|
+
scaffolded by design: `project.inlang/` is the
|
|
158
|
+
[unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) project
|
|
159
|
+
form, and inlang tools generate and manage its remaining files (`.gitignore`,
|
|
160
|
+
`README.md`, `cache/`) on first use — so expect a few new files there after
|
|
161
|
+
opening the project with Sherlock or Paraglide.
|
|
162
|
+
|
|
132
163
|
### `extract`
|
|
133
164
|
Parses source files, extracts keys, and updates your JSON translation files.
|
|
134
165
|
|
|
@@ -468,6 +499,101 @@ Both the `lint` and `instrument` commands honor an ignore comment so you can ski
|
|
|
468
499
|
const msg = t('Hello {{name}}!', { wrong: 'world' })
|
|
469
500
|
```
|
|
470
501
|
|
|
502
|
+
### `localize`
|
|
503
|
+
|
|
504
|
+
One command from hardcoded strings to a fully localized app: detect, instrument, extract, connect to [Locize](https://www.locize.com), AI-auto-translate, deliver. Built for taking a mono-lingual app (often AI-generated via v0/Lovable/Bolt/Cursor) to fully localized in one sitting.
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
npx i18next-cli localize
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
The command walks through six steps:
|
|
511
|
+
|
|
512
|
+
1. **Detect** — framework (React/Next.js natively; see below for other stacks), TypeScript, existing i18next setup.
|
|
513
|
+
2. **Configuration** — uses your `i18next.config.ts`, or starts the [`init`](#init) wizard if none exists.
|
|
514
|
+
3. **Instrument** — wraps hardcoded strings in `t()` calls / `<Trans>` components (interactive by default — [instrument](#instrument) is an assistant, review each change). Skipped automatically if your code can't be instrumented; a dirty git tree prompts for confirmation first.
|
|
515
|
+
4. **Extract** — extracts all translation keys into your locale files.
|
|
516
|
+
5. **Connect Locize** — uses `locize.projectId`/`locize.apiKey` from your config or the `LOCIZE_PROJECTID`/`LOCIZE_API_KEY` environment variables; otherwise it opens the signup page and asks you to paste them (the one manual step). Any write-capable API key works: your target languages are created automatically on the first sync (locize-cli ≥ 12.3), and auto-translate + Quality Estimation are on by default for new Locize projects.
|
|
517
|
+
6. **Translate & deliver** — syncs your keys with `--auto-translate`, waits for the AI translations to arrive, downloads them, and prints the [i18next-locize-backend](https://github.com/locize/i18next-locize-backend) CDN wiring snippet (so translation fixes go live without redeploying your app).
|
|
518
|
+
|
|
519
|
+
**Options:**
|
|
520
|
+
- `--dry-run`: Preview every step; nothing is written or pushed
|
|
521
|
+
- `-y, --yes`: Accept defaults; auto-approve instrumentation candidates (no per-string prompts)
|
|
522
|
+
- `--ci`: Non-interactive; never opens a browser or prompts. Instrumentation is **skipped** in CI (it rewrites source files and needs human review) unless combined with `--yes`
|
|
523
|
+
- `--skip-instrument`: Skip the code-instrumentation step (your code already calls `t()`)
|
|
524
|
+
- `--skip-translate`: Sync to Locize but don't request AI auto-translation
|
|
525
|
+
- `--skip-locize`: Stop after extraction (local files only)
|
|
526
|
+
- `--namespace <ns>`: Target namespace for instrumented keys
|
|
527
|
+
- `--update-values`: Also update existing translation values on Locize
|
|
528
|
+
- `--cdn-type <standard|pro>`: Locize CDN endpoint type
|
|
529
|
+
- `--print-agent-prompt`: Print a copy-paste prompt for AI coding agents, then exit (see below)
|
|
530
|
+
|
|
531
|
+
**Behavior matrix:**
|
|
532
|
+
|
|
533
|
+
| Step | interactive (default) | `--yes` | `--ci` | `--dry-run` |
|
|
534
|
+
|---|---|---|---|---|
|
|
535
|
+
| Instrument | per-string prompts | auto-approve | skipped (force with `--yes`) | candidate preview |
|
|
536
|
+
| Connect Locize | browser + paste credentials | same | env vars required, else exit 1 | report only |
|
|
537
|
+
| Sync + translate | runs | runs | runs | `--dry` forwarded |
|
|
538
|
+
| Poll + download | watches translations arrive | same | single download, no wait | skipped |
|
|
539
|
+
|
|
540
|
+
**Safe to re-run:** the command is idempotent. Already-wrapped strings are not re-instrumented, extraction is deterministic, and syncing never overwrites translations edited remotely (no `--update-values` unless you pass it; locize-cli's `--reference-language-only` default keeps target languages safe).
|
|
541
|
+
|
|
542
|
+
> **Next.js App Router:** instrument injects `useTranslation()`, which is client-only. Review the diff for server components — add `'use client'` or switch those to a server-side `t()` pattern.
|
|
543
|
+
|
|
544
|
+
**Non-React stacks (Vue, Svelte, …):** the instrument step transforms React/JSX out of the box. For other stacks, add a plugin that covers your file type (community: [i18next-cli-vue](https://github.com/PBK-B/i18next-cli-vue), [i18next-cli-plugin-svelte](https://github.com/dreamscached/i18next-cli-plugin-svelte) — or write your own via the [Plugin System](#plugin-system) `instrumentOnLoad`/`onLoad` hooks). With a matching plugin configured, `localize` runs the full flow; without one, the instrument step is skipped with guidance and the remaining steps (extract → Locize → auto-translate) still run.
|
|
545
|
+
|
|
546
|
+
**Agent prompt:** the same flow is available as a copy-paste prompt for AI coding agents (Claude Code, Cursor, …):
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
npx i18next-cli localize --print-agent-prompt
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
This prints step-by-step instructions an agent can follow using the individual CLI commands — version-matched to your installed CLI, so it never drifts from what the supercommand does. Prefer the command output over the copy below, which is a snapshot for reference:
|
|
553
|
+
|
|
554
|
+
<details>
|
|
555
|
+
<summary>Agent prompt (snapshot)</summary>
|
|
556
|
+
|
|
557
|
+
```text
|
|
558
|
+
You are localizing this app with i18next + Locize. Execute these steps in order,
|
|
559
|
+
verifying each before continuing. Use `npx i18next-cli` for all commands.
|
|
560
|
+
|
|
561
|
+
1. Detect: confirm this is a React/Next.js project (check package.json).
|
|
562
|
+
- If Vue/Svelte: install a stack plugin (`i18next-cli-vue` /
|
|
563
|
+
`i18next-cli-plugin-svelte`) and add it to the `plugins` array of
|
|
564
|
+
i18next.config.ts, or write one via the plugin hooks
|
|
565
|
+
(instrumentOnLoad/onLoad) instead of wrapping strings manually.
|
|
566
|
+
- If the app uses inlang Paraglide (`@inlang/paraglide-js`), STOP —
|
|
567
|
+
instrumenting i18next calls would conflict; ask the user how to proceed.
|
|
568
|
+
2. Config: if no i18next.config.{ts,js} exists, run `npx i18next-cli init`
|
|
569
|
+
and answer the prompts (pick Locize as backend if the user wants managed
|
|
570
|
+
translations and AI auto-translate).
|
|
571
|
+
3. Instrument: run `npx i18next-cli instrument --dry-run` and review the
|
|
572
|
+
planned changes; then `npx i18next-cli instrument` to apply. Inspect the
|
|
573
|
+
git diff carefully: fix any t() wrapping inside Next.js *server components*
|
|
574
|
+
(add 'use client' or refactor to a server-side t() pattern). Commit.
|
|
575
|
+
4. Extract: run `npx i18next-cli extract`. Verify the locale JSON files were
|
|
576
|
+
written (check the extract.output path in the config).
|
|
577
|
+
5. Locize: ask the user for LOCIZE_PROJECTID and LOCIZE_API_KEY (they create
|
|
578
|
+
the project at https://www.locize.app/register?from=i18next_cli__agent-prompt
|
|
579
|
+
— any write-capable API key works; the target languages from
|
|
580
|
+
i18next.config.ts are created automatically on the first sync.
|
|
581
|
+
Auto-translation and quality estimation are enabled by default for new
|
|
582
|
+
projects; translations run once the project is subscribed or an AI/MT
|
|
583
|
+
provider is configured). Export both as environment variables.
|
|
584
|
+
6. Translate & deliver:
|
|
585
|
+
`npx i18next-cli locize-sync --auto-translate true`
|
|
586
|
+
then `npx i18next-cli locize-download` to pull the AI translations, and
|
|
587
|
+
`npx i18next-cli status` — confirm all languages are (near) 100%.
|
|
588
|
+
AI translation is asynchronous; if targets are still empty, wait a minute
|
|
589
|
+
and re-run locize-download.
|
|
590
|
+
7. Optionally switch runtime loading to i18next-locize-backend (CDN delivery,
|
|
591
|
+
so translation fixes go live without redeploying). NEVER put the API key
|
|
592
|
+
in client-side code — the CDN only needs the project ID.
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
</details>
|
|
596
|
+
|
|
471
597
|
### `migrate-config`
|
|
472
598
|
Automatically migrates a legacy `i18next-parser.config.js` file to the new `i18next.config.ts` format.
|
|
473
599
|
|
|
@@ -546,6 +672,26 @@ npx i18next-cli locize-sync [options]
|
|
|
546
672
|
- `--src-lng-only <true|false>`: Check for changes in source language only (default: `true`). Pass `--src-lng-only false` to sync all languages
|
|
547
673
|
- `--compare-mtime`: Compare modification times when syncing
|
|
548
674
|
- `--dry-run`: Run the command without making any changes
|
|
675
|
+
- `--auto-translate <true|false>`: Trigger AI/MT auto-translation of newly synced keys. Requires auto-translation in your Locize project (enabled by default for new projects; runs once the project is subscribed or an AI/MT provider is configured)
|
|
676
|
+
- `--auto-translate-review <true|false>`: Route auto-translated segments through the review workflow for languages that have review enabled
|
|
677
|
+
- `--auto-translate-languages <lng1,lng2>`: Restrict auto-translation to these target languages (defaults to all)
|
|
678
|
+
|
|
679
|
+
The same options can be set persistently in the `locize` block of your config:
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
export default defineConfig({
|
|
683
|
+
// ...
|
|
684
|
+
locize: {
|
|
685
|
+
projectId: '...',
|
|
686
|
+
apiKey: process.env.LOCIZE_API_KEY,
|
|
687
|
+
autoTranslate: true,
|
|
688
|
+
autoTranslateReview: false,
|
|
689
|
+
autoTranslateLanguages: ['de', 'fr'],
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
> **Note:** auto-translation only fires when the **reference language** is updated, and the translation itself happens asynchronously on the Locize side — run `locize-download` (or let [`localize`](#localize) wait for you) to pull the results.
|
|
549
695
|
|
|
550
696
|
**Interactive Setup:** If your locize credentials are missing or invalid, the toolkit will guide you through an interactive setup process to configure your Project ID, API Key, and version.
|
|
551
697
|
|
|
@@ -1599,6 +1745,7 @@ class I18nextExtractionPlugin {
|
|
|
1599
1745
|
- `runSyncer(config)` - Sync translation files
|
|
1600
1746
|
- `runStatus(config, options?)` - Get translation status
|
|
1601
1747
|
- `runTypesGenerator(config)` - Generate types
|
|
1748
|
+
- `runLocalize(options?, configPath?)` - The full [`localize`](#localize) flow (detect → instrument → extract → Locize sync with auto-translate → download)
|
|
1602
1749
|
|
|
1603
1750
|
### Advanced Usage
|
|
1604
1751
|
|
package/dist/cjs/cli.js
CHANGED
|
@@ -27,12 +27,17 @@ var renameKey = require('./rename-key.js');
|
|
|
27
27
|
var instrumenter = require('./instrumenter/core/instrumenter.js');
|
|
28
28
|
require('./utils/jsx-attributes.js');
|
|
29
29
|
require('magic-string');
|
|
30
|
+
var localize = require('./localize/localize.js');
|
|
31
|
+
|
|
32
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
33
|
+
|
|
34
|
+
var chokidar__default = /*#__PURE__*/_interopDefault(chokidar);
|
|
30
35
|
|
|
31
36
|
const program = new commander.Command();
|
|
32
37
|
program
|
|
33
38
|
.name('i18next-cli')
|
|
34
39
|
.description('A unified, high-performance i18next CLI.')
|
|
35
|
-
.version('1.
|
|
40
|
+
.version('1.63.0'); // This string is replaced with the actual version at build time by rollup
|
|
36
41
|
// new: global config override option
|
|
37
42
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
38
43
|
program
|
|
@@ -93,7 +98,7 @@ program
|
|
|
93
98
|
const ignoreGlobs = [...configuredIgnore, ...derivedIgnore].filter(Boolean);
|
|
94
99
|
// filter expanded files by ignore globs
|
|
95
100
|
const watchFiles = expanded.filter(f => !ignoreGlobs.some(g => minimatch.minimatch(f, g, { dot: true })));
|
|
96
|
-
const watcher =
|
|
101
|
+
const watcher = chokidar__default.default.watch(watchFiles, {
|
|
97
102
|
ignored: /node_modules/,
|
|
98
103
|
persistent: true,
|
|
99
104
|
});
|
|
@@ -163,7 +168,7 @@ program
|
|
|
163
168
|
const watchTypes = expandedTypes.filter(f => !ignoredTypes.some(g => minimatch.minimatch(f, g, { dot: true })));
|
|
164
169
|
// awaitWriteFinish avoids triggering mid-write when another process (e.g. `extract -w`)
|
|
165
170
|
// is rewriting the same translation files. See i18next/i18next-cli#257.
|
|
166
|
-
const watcher =
|
|
171
|
+
const watcher = chokidar__default.default.watch(watchTypes, {
|
|
167
172
|
persistent: true,
|
|
168
173
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
|
169
174
|
});
|
|
@@ -192,7 +197,8 @@ program
|
|
|
192
197
|
.command('init')
|
|
193
198
|
.description('Create a new i18next.config.ts/js file with an interactive setup wizard.')
|
|
194
199
|
.option('--ci', 'Skip the browser launch when a backend (e.g. Locize) is selected. The signup URL is printed instead.')
|
|
195
|
-
.
|
|
200
|
+
.option('--inlang', 'Also scaffold an inlang project (project.inlang/settings.json) so inlang tooling (Sherlock, Fink, Paraglide) works on the translation files. Skips the corresponding wizard question.')
|
|
201
|
+
.action((options) => init.runInit({ ci: !!options.ci, inlang: !!options.inlang }));
|
|
196
202
|
program
|
|
197
203
|
.command('lint')
|
|
198
204
|
.description('Find potential issues like hardcoded strings in your codebase.')
|
|
@@ -229,7 +235,7 @@ program
|
|
|
229
235
|
const derivedIgnore2 = deriveOutputIgnore(config$1.extract.output);
|
|
230
236
|
const ignoredLint = [...configuredIgnore2, ...derivedIgnore2].filter(Boolean);
|
|
231
237
|
const watchLint = expandedLint.filter(f => !ignoredLint.some(g => minimatch.minimatch(f, g, { dot: true })));
|
|
232
|
-
const watcher =
|
|
238
|
+
const watcher = chokidar__default.default.watch(watchLint, {
|
|
233
239
|
ignored: /node_modules/,
|
|
234
240
|
persistent: true,
|
|
235
241
|
});
|
|
@@ -286,6 +292,29 @@ program
|
|
|
286
292
|
process.exit(1);
|
|
287
293
|
}
|
|
288
294
|
});
|
|
295
|
+
program
|
|
296
|
+
.command('localize')
|
|
297
|
+
.description('One command from hardcoded strings to a fully localized app: detect, instrument, extract, connect to Locize, auto-translate, deliver.')
|
|
298
|
+
.option('--dry-run', 'Preview every step; nothing is written or pushed.')
|
|
299
|
+
.option('-y, --yes', 'Accept defaults; auto-approve instrumentation candidates (no per-string prompts).')
|
|
300
|
+
.option('--ci', 'Non-interactive: never open a browser or prompt; instrument is skipped (combine with --yes to force non-interactive instrumentation).')
|
|
301
|
+
.option('--skip-instrument', 'Skip the code-instrumentation step (use when your code already calls t()).')
|
|
302
|
+
.option('--skip-translate', 'Sync to Locize but do not request AI auto-translation.')
|
|
303
|
+
.option('--skip-locize', 'Stop after extraction (local files only; steps 5-6 skipped).')
|
|
304
|
+
.option('--namespace <ns>', 'Target namespace for instrumented keys (forwarded to instrument).')
|
|
305
|
+
.option('--update-values', 'Also update existing translation values on Locize (forwarded to sync).')
|
|
306
|
+
.option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your Locize project)')
|
|
307
|
+
.option('--print-agent-prompt', 'Print a copy-paste prompt for AI coding agents (Claude Code, Cursor) that performs the same steps, then exit.')
|
|
308
|
+
.action(async (options) => {
|
|
309
|
+
try {
|
|
310
|
+
const cfgPath = program.opts().config;
|
|
311
|
+
await localize.runLocalize(options, cfgPath);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
console.error(node_util.styleText('red', 'Error running localize command:'), error);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
289
318
|
program
|
|
290
319
|
.command('locize-sync')
|
|
291
320
|
.description('Synchronize local translations with your Locize project.')
|
|
@@ -294,6 +323,9 @@ program
|
|
|
294
323
|
.option('--compare-mtime', 'Compare modification times when syncing.')
|
|
295
324
|
.option('--dry-run', 'Run the command without making any changes.')
|
|
296
325
|
.option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your locize project)')
|
|
326
|
+
.option('--auto-translate <true|false>', 'Trigger AI/MT auto-translation of newly synced keys (requires auto-translation enabled in your Locize project; on by default for new projects).')
|
|
327
|
+
.option('--auto-translate-review <true|false>', 'Route auto-translated segments through the review workflow for languages that have review enabled.')
|
|
328
|
+
.option('--auto-translate-languages <lng1,lng2>', 'Restrict auto-translation to these target languages (comma separated; defaults to all languages).')
|
|
297
329
|
.action(async (options) => {
|
|
298
330
|
const cfgPath = program.opts().config;
|
|
299
331
|
const config$1 = await config.ensureConfig(cfgPath);
|
package/dist/cjs/config.js
CHANGED
|
@@ -10,6 +10,10 @@ var node_util = require('node:util');
|
|
|
10
10
|
var init = require('./init.js');
|
|
11
11
|
var logger = require('./utils/logger.js');
|
|
12
12
|
|
|
13
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
|
+
|
|
15
|
+
var inquirer__default = /*#__PURE__*/_interopDefault(inquirer);
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* List of supported configuration file names in order of precedence
|
|
15
19
|
*/
|
|
@@ -128,7 +132,7 @@ async function ensureConfig(configPath, logger$1 = new logger.ConsoleLogger()) {
|
|
|
128
132
|
return config;
|
|
129
133
|
}
|
|
130
134
|
// No config found, so we prompt the user.
|
|
131
|
-
const { shouldInit } = await
|
|
135
|
+
const { shouldInit } = await inquirer__default.default.prompt([{
|
|
132
136
|
type: 'confirm',
|
|
133
137
|
name: 'shouldInit',
|
|
134
138
|
message: node_util.styleText('yellow', 'Configuration file not found. Would you like to create one now?'),
|
package/dist/cjs/index.js
CHANGED
|
@@ -13,6 +13,10 @@ var renameKey = require('./rename-key.js');
|
|
|
13
13
|
var instrumenter = require('./instrumenter/core/instrumenter.js');
|
|
14
14
|
require('./utils/jsx-attributes.js');
|
|
15
15
|
require('magic-string');
|
|
16
|
+
var localize = require('./localize/localize.js');
|
|
17
|
+
require('node:fs/promises');
|
|
18
|
+
require('node:path');
|
|
19
|
+
var agentPrompt = require('./localize/agent-prompt.js');
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
|
|
@@ -30,3 +34,5 @@ exports.runTypesGenerator = typesGenerator.runTypesGenerator;
|
|
|
30
34
|
exports.runRenameKey = renameKey.runRenameKey;
|
|
31
35
|
exports.runInstrumenter = instrumenter.runInstrumenter;
|
|
32
36
|
exports.writeExtractedKeys = instrumenter.writeExtractedKeys;
|
|
37
|
+
exports.runLocalize = localize.runLocalize;
|
|
38
|
+
exports.AGENT_PROMPT = agentPrompt.AGENT_PROMPT;
|
package/dist/cjs/init.js
CHANGED
|
@@ -3,58 +3,15 @@
|
|
|
3
3
|
var inquirer = require('inquirer');
|
|
4
4
|
var promises = require('node:fs/promises');
|
|
5
5
|
var node_path = require('node:path');
|
|
6
|
-
var execa = require('execa');
|
|
7
6
|
var heuristicConfig = require('./heuristic-config.js');
|
|
7
|
+
var locizeOnboarding = require('./utils/locize-onboarding.js');
|
|
8
|
+
var inlangScaffold = require('./utils/inlang-scaffold.js');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* Returns true on success, false if there's nowhere to open one (CI, headless Linux)
|
|
15
|
-
* or if spawning the command failed.
|
|
16
|
-
*/
|
|
17
|
-
async function openBrowser(url, opts = {}) {
|
|
18
|
-
// Short-circuit: no point spawning a browser-opener in CI or headless Linux.
|
|
19
|
-
if (opts.ci || process.env.CI === 'true')
|
|
20
|
-
return false;
|
|
21
|
-
const isWSL = !!process.env.WSL_DISTRO_NAME;
|
|
22
|
-
if (process.platform === 'linux' && !isWSL &&
|
|
23
|
-
!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
if (process.platform === 'darwin') {
|
|
28
|
-
await execa.execa('open', [url], { stdio: 'ignore' });
|
|
29
|
-
}
|
|
30
|
-
else if (process.platform === 'win32') {
|
|
31
|
-
// `start` is a cmd.exe builtin; the empty "" is the window-title slot
|
|
32
|
-
await execa.execa('cmd', ['/c', 'start', '""', url], { stdio: 'ignore' });
|
|
33
|
-
}
|
|
34
|
-
else if (isWSL) {
|
|
35
|
-
// WSL: try the wslu / wsl-open shims that bridge to the Windows side
|
|
36
|
-
// before falling back to xdg-open (which usually isn't installed there).
|
|
37
|
-
try {
|
|
38
|
-
await execa.execa('wslview', [url], { stdio: 'ignore' });
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
try {
|
|
42
|
-
await execa.execa('wsl-open', [url], { stdio: 'ignore' });
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
await execa.execa('xdg-open', [url], { stdio: 'ignore' });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
await execa.execa('xdg-open', [url], { stdio: 'ignore' });
|
|
51
|
-
}
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var inquirer__default = /*#__PURE__*/_interopDefault(inquirer);
|
|
13
|
+
|
|
14
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__init-wizard';
|
|
58
15
|
/**
|
|
59
16
|
* Determines if the current project is configured as an ESM project.
|
|
60
17
|
* Checks the package.json file for `"type": "module"`.
|
|
@@ -164,7 +121,7 @@ async function runInit(options = {}) {
|
|
|
164
121
|
const tsChoice = 'TypeScript (i18next.config.ts)';
|
|
165
122
|
const jsChoice = 'JavaScript (i18next.config.js)';
|
|
166
123
|
const fileTypeChoices = projectUsesTs ? [tsChoice, jsChoice] : [jsChoice, tsChoice];
|
|
167
|
-
const answers = await
|
|
124
|
+
const answers = await inquirer__default.default.prompt([
|
|
168
125
|
{
|
|
169
126
|
type: 'select',
|
|
170
127
|
name: 'fileType',
|
|
@@ -204,37 +161,23 @@ async function runInit(options = {}) {
|
|
|
204
161
|
],
|
|
205
162
|
default: 'local',
|
|
206
163
|
},
|
|
164
|
+
{
|
|
165
|
+
type: 'confirm',
|
|
166
|
+
name: 'inlang',
|
|
167
|
+
message: 'Also set up inlang tooling (Sherlock VS Code extension, Fink editor, Paraglide) on these translation files?',
|
|
168
|
+
default: false,
|
|
169
|
+
// Skip the question when already requested via the --inlang flag.
|
|
170
|
+
when: () => !options.inlang,
|
|
171
|
+
},
|
|
207
172
|
]);
|
|
208
173
|
let locizeConfig;
|
|
209
174
|
if (answers.backend === 'locize') {
|
|
210
175
|
console.log('\nOpening the Locize signup page in your browser. After you create your account and project, come back here and paste your Project ID and API key.');
|
|
211
|
-
const opened = await openBrowser(LOCIZE_SIGNUP_URL, { ci: options.ci });
|
|
176
|
+
const opened = await locizeOnboarding.openBrowser(LOCIZE_SIGNUP_URL, { ci: options.ci });
|
|
212
177
|
if (!opened) {
|
|
213
178
|
console.log(`\n👉 Open this URL manually: ${LOCIZE_SIGNUP_URL}\n`);
|
|
214
179
|
}
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
type: 'input',
|
|
218
|
-
name: 'projectId',
|
|
219
|
-
message: 'Locize Project ID (e.g. 4eeb5ce0-a7a7-453f-8eb3-078f6eeb56fe):',
|
|
220
|
-
validate: (input) => input.trim().length > 0 || 'Project ID cannot be empty.',
|
|
221
|
-
filter: (input) => input.trim(),
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
type: 'password',
|
|
225
|
-
name: 'apiKey',
|
|
226
|
-
message: 'Locize API key (needed for saveMissing / auto-publish / sync during development; leave empty to skip and add later via env var):',
|
|
227
|
-
filter: (input) => input.trim(),
|
|
228
|
-
},
|
|
229
|
-
]);
|
|
230
|
-
if (!UUID_SHAPE.test(credentials.projectId)) {
|
|
231
|
-
console.log("⚠️ The Project ID doesn't look like a UUID (8-4-4-4-12 hex). It will still be written — double-check it in your Locize project settings.");
|
|
232
|
-
}
|
|
233
|
-
// API keys come in multiple shapes (UUID, `lz_pat_…`, `lz_api_…`, etc.) —
|
|
234
|
-
// treat them as opaque; no client-side format check.
|
|
235
|
-
locizeConfig = { projectId: credentials.projectId };
|
|
236
|
-
if (credentials.apiKey)
|
|
237
|
-
locizeConfig.apiKey = credentials.apiKey;
|
|
180
|
+
locizeConfig = await locizeOnboarding.promptLocizeCredentials();
|
|
238
181
|
}
|
|
239
182
|
const isTypeScript = answers.fileType.includes('TypeScript');
|
|
240
183
|
const isEsm = await isEsmProject();
|
|
@@ -319,6 +262,13 @@ module.exports = ${toJs(configObject)}`;
|
|
|
319
262
|
const outputPath = node_path.resolve(process.cwd(), fileName);
|
|
320
263
|
await promises.writeFile(outputPath, fileContent.trim());
|
|
321
264
|
console.log(`✅ Configuration file created at: ${outputPath}`);
|
|
265
|
+
if (options.inlang || answers.inlang) {
|
|
266
|
+
await inlangScaffold.scaffoldInlangProject({
|
|
267
|
+
locales: answers.locales,
|
|
268
|
+
primaryLanguage: answers.locales[0],
|
|
269
|
+
output: answers.output,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
322
272
|
if (locizeConfig) {
|
|
323
273
|
console.log('\nNext steps for Locize:');
|
|
324
274
|
console.log(' 1. Push your local translations to Locize:');
|
|
@@ -15,6 +15,10 @@ var jsxAttributes = require('../../utils/jsx-attributes.js');
|
|
|
15
15
|
var astUtils = require('../../extractor/parsers/ast-utils.js');
|
|
16
16
|
var fileUtils = require('../../utils/file-utils.js');
|
|
17
17
|
|
|
18
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
19
|
+
|
|
20
|
+
var inquirer__default = /*#__PURE__*/_interopDefault(inquirer);
|
|
21
|
+
|
|
18
22
|
/**
|
|
19
23
|
* Main orchestrator for the instrument command.
|
|
20
24
|
* Scans source files for hardcoded strings and instruments them with i18next calls.
|
|
@@ -50,7 +54,7 @@ async function runInstrumenter(config, options, logger$1 = new logger.ConsoleLog
|
|
|
50
54
|
let targetNamespace = options.namespace;
|
|
51
55
|
if (!targetNamespace && options.isInteractive) {
|
|
52
56
|
const defaultNS = config.extract.defaultNS ?? 'translation';
|
|
53
|
-
const { ns } = await
|
|
57
|
+
const { ns } = await inquirer__default.default.prompt([
|
|
54
58
|
{
|
|
55
59
|
type: 'input',
|
|
56
60
|
name: 'ns',
|
|
@@ -83,7 +87,7 @@ async function runInstrumenter(config, options, logger$1 = new logger.ConsoleLog
|
|
|
83
87
|
if (options.isInteractive) {
|
|
84
88
|
// Ask user about each candidate
|
|
85
89
|
for (const candidate of candidates) {
|
|
86
|
-
const { action } = await
|
|
90
|
+
const { action } = await inquirer__default.default.prompt([
|
|
87
91
|
{
|
|
88
92
|
type: 'list',
|
|
89
93
|
name: 'action',
|
|
@@ -104,7 +108,7 @@ async function runInstrumenter(config, options, logger$1 = new logger.ConsoleLog
|
|
|
104
108
|
candidate.skipReason = 'User skipped';
|
|
105
109
|
break;
|
|
106
110
|
case 'edit-key': {
|
|
107
|
-
const { key } = await
|
|
111
|
+
const { key } = await inquirer__default.default.prompt([
|
|
108
112
|
{
|
|
109
113
|
type: 'input',
|
|
110
114
|
name: 'key',
|
|
@@ -116,7 +120,7 @@ async function runInstrumenter(config, options, logger$1 = new logger.ConsoleLog
|
|
|
116
120
|
break;
|
|
117
121
|
}
|
|
118
122
|
case 'edit-value': {
|
|
119
|
-
const { value } = await
|
|
123
|
+
const { value } = await inquirer__default.default.prompt([
|
|
120
124
|
{
|
|
121
125
|
type: 'input',
|
|
122
126
|
name: 'value',
|
|
@@ -1462,6 +1466,25 @@ const I18N_INIT_FILE_NAMES = [
|
|
|
1462
1466
|
'i18n/index.ts', 'i18n/index.js', 'i18n/index.mjs',
|
|
1463
1467
|
'i18next/index.ts', 'i18next/index.js'
|
|
1464
1468
|
];
|
|
1469
|
+
/**
|
|
1470
|
+
* Searches the common locations (`src/` and the project root) for an existing
|
|
1471
|
+
* i18n initialization file.
|
|
1472
|
+
*
|
|
1473
|
+
* @returns The path of the first init file found (relative to cwd, native
|
|
1474
|
+
* platform separators), or null.
|
|
1475
|
+
*/
|
|
1476
|
+
async function findExistingI18nInitFile() {
|
|
1477
|
+
const cwd = process.cwd();
|
|
1478
|
+
const searchDirs = ['src', '.'];
|
|
1479
|
+
for (const dir of searchDirs) {
|
|
1480
|
+
for (const name of I18N_INIT_FILE_NAMES) {
|
|
1481
|
+
if (await fileExists(node_path.join(cwd, dir, name))) {
|
|
1482
|
+
return node_path.join(dir, name);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1465
1488
|
/**
|
|
1466
1489
|
* Computes a POSIX-style relative path from the init-file directory to the
|
|
1467
1490
|
* output template path (which still contains {{language}} / {{namespace}} placeholders).
|
|
@@ -1487,13 +1510,8 @@ function buildDynamicImportPath(outputTemplate, initDir) {
|
|
|
1487
1510
|
async function ensureI18nInitFile(hasReact, hasTypeScript, config, logger, usesI18nextT) {
|
|
1488
1511
|
const cwd = process.cwd();
|
|
1489
1512
|
// Check for existing init files in common locations
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
for (const name of I18N_INIT_FILE_NAMES) {
|
|
1493
|
-
if (await fileExists(node_path.join(cwd, dir, name))) {
|
|
1494
|
-
return null; // Init file already exists
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1513
|
+
if (await findExistingI18nInitFile()) {
|
|
1514
|
+
return null; // Init file already exists
|
|
1497
1515
|
}
|
|
1498
1516
|
// Check if i18next.init() is called anywhere in the source
|
|
1499
1517
|
try {
|
|
@@ -1941,5 +1959,9 @@ async function runInstrumentOnResultPipeline(filePath, initialCandidates, plugin
|
|
|
1941
1959
|
return candidates;
|
|
1942
1960
|
}
|
|
1943
1961
|
|
|
1962
|
+
exports.detectProjectEnvironment = detectProjectEnvironment;
|
|
1963
|
+
exports.findExistingI18nInitFile = findExistingI18nInitFile;
|
|
1964
|
+
exports.isProjectUsingReact = isProjectUsingReact;
|
|
1965
|
+
exports.isProjectUsingTypeScript = isProjectUsingTypeScript;
|
|
1944
1966
|
exports.runInstrumenter = runInstrumenter;
|
|
1945
1967
|
exports.writeExtractedKeys = writeExtractedKeys;
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
var MagicString = require('magic-string');
|
|
4
4
|
var keyGenerator = require('./key-generator.js');
|
|
5
5
|
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var MagicString__default = /*#__PURE__*/_interopDefault(MagicString);
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Transforms a source file, replacing candidate strings with instrumented code.
|
|
8
12
|
* Also injects useTranslation() hooks into React function components that
|
|
@@ -15,7 +19,7 @@ var keyGenerator = require('./key-generator.js');
|
|
|
15
19
|
* @returns TransformResult with modified content and diff
|
|
16
20
|
*/
|
|
17
21
|
function transformFile(content, file, candidates, options) {
|
|
18
|
-
const s = new
|
|
22
|
+
const s = new MagicString__default.default(content);
|
|
19
23
|
const errors = [];
|
|
20
24
|
const warnings = [];
|
|
21
25
|
let transformCount = 0;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copy-paste prompt for AI coding agents (Claude Code, Cursor, …) that walks
|
|
5
|
+
* an agent through the same steps the `localize` command orchestrates.
|
|
6
|
+
*
|
|
7
|
+
* This constant is the single source of truth — it is printed by
|
|
8
|
+
* `i18next-cli localize --print-agent-prompt` and mirrored in the README.
|
|
9
|
+
* A test guards that it mentions every orchestrated command, so flow changes
|
|
10
|
+
* that forget this prompt fail CI.
|
|
11
|
+
*/
|
|
12
|
+
const AGENT_PROMPT = `You are localizing this app with i18next + Locize. Execute these steps in order,
|
|
13
|
+
verifying each before continuing. Use \`npx i18next-cli\` for all commands.
|
|
14
|
+
|
|
15
|
+
1. Detect: confirm this is a React/Next.js project (check package.json).
|
|
16
|
+
- If Vue/Svelte: install a stack plugin (\`i18next-cli-vue\` /
|
|
17
|
+
\`i18next-cli-plugin-svelte\`) and add it to the \`plugins\` array of
|
|
18
|
+
i18next.config.ts, or write one via the plugin hooks
|
|
19
|
+
(instrumentOnLoad/onLoad) instead of wrapping strings manually.
|
|
20
|
+
- If the app uses inlang Paraglide (\`@inlang/paraglide-js\`), STOP —
|
|
21
|
+
instrumenting i18next calls would conflict; ask the user how to proceed.
|
|
22
|
+
2. Config: if no i18next.config.{ts,js} exists, run \`npx i18next-cli init\`
|
|
23
|
+
and answer the prompts (pick Locize as backend if the user wants managed
|
|
24
|
+
translations and AI auto-translate).
|
|
25
|
+
3. Instrument: run \`npx i18next-cli instrument --dry-run\` and review the
|
|
26
|
+
planned changes; then \`npx i18next-cli instrument\` to apply. Inspect the
|
|
27
|
+
git diff carefully: fix any t() wrapping inside Next.js *server components*
|
|
28
|
+
(add 'use client' or refactor to a server-side t() pattern). Commit.
|
|
29
|
+
4. Extract: run \`npx i18next-cli extract\`. Verify the locale JSON files were
|
|
30
|
+
written (check the extract.output path in the config).
|
|
31
|
+
5. Locize: ask the user for LOCIZE_PROJECTID and LOCIZE_API_KEY (they create
|
|
32
|
+
the project at https://www.locize.app/register?from=i18next_cli__agent-prompt
|
|
33
|
+
— any write-capable API key works; the target languages from
|
|
34
|
+
i18next.config.ts are created automatically on the first sync.
|
|
35
|
+
Auto-translation and quality estimation are enabled by default for new
|
|
36
|
+
projects; translations run once the project is subscribed or an AI/MT
|
|
37
|
+
provider is configured). Export both as environment variables.
|
|
38
|
+
6. Translate & deliver:
|
|
39
|
+
\`npx i18next-cli locize-sync --auto-translate true\`
|
|
40
|
+
then \`npx i18next-cli locize-download\` to pull the AI translations, and
|
|
41
|
+
\`npx i18next-cli status\` — confirm all languages are (near) 100%.
|
|
42
|
+
AI translation is asynchronous; if targets are still empty, wait a minute
|
|
43
|
+
and re-run locize-download.
|
|
44
|
+
7. Optionally switch runtime loading to i18next-locize-backend (CDN delivery,
|
|
45
|
+
so translation fixes go live without redeploying). NEVER put the API key
|
|
46
|
+
in client-side code — the CDN only needs the project ID.
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
exports.AGENT_PROMPT = AGENT_PROMPT;
|