i18next-cli 1.61.0 → 1.62.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 +119 -1
- package/dist/cjs/cli.js +36 -5
- package/dist/cjs/config.js +5 -1
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/init.js +9 -75
- package/dist/cjs/instrumenter/core/instrumenter.js +32 -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 +91 -13
- 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/locize-onboarding.js +91 -0
- package/dist/cjs/utils/wrap-ora.js +9 -5
- package/dist/esm/cli.js +29 -2
- package/dist/esm/index.js +4 -0
- package/dist/esm/init.js +3 -73
- package/dist/esm/instrumenter/core/instrumenter.js +21 -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 +82 -11
- 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.map +1 -1
- package/types/instrumenter/core/instrumenter.d.ts +27 -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/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:
|
|
@@ -468,6 +470,101 @@ Both the `lint` and `instrument` commands honor an ignore comment so you can ski
|
|
|
468
470
|
const msg = t('Hello {{name}}!', { wrong: 'world' })
|
|
469
471
|
```
|
|
470
472
|
|
|
473
|
+
### `localize`
|
|
474
|
+
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
npx i18next-cli localize
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The command walks through six steps:
|
|
482
|
+
|
|
483
|
+
1. **Detect** — framework (React/Next.js natively; see below for other stacks), TypeScript, existing i18next setup.
|
|
484
|
+
2. **Configuration** — uses your `i18next.config.ts`, or starts the [`init`](#init) wizard if none exists.
|
|
485
|
+
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.
|
|
486
|
+
4. **Extract** — extracts all translation keys into your locale files.
|
|
487
|
+
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.
|
|
488
|
+
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).
|
|
489
|
+
|
|
490
|
+
**Options:**
|
|
491
|
+
- `--dry-run`: Preview every step; nothing is written or pushed
|
|
492
|
+
- `-y, --yes`: Accept defaults; auto-approve instrumentation candidates (no per-string prompts)
|
|
493
|
+
- `--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`
|
|
494
|
+
- `--skip-instrument`: Skip the code-instrumentation step (your code already calls `t()`)
|
|
495
|
+
- `--skip-translate`: Sync to Locize but don't request AI auto-translation
|
|
496
|
+
- `--skip-locize`: Stop after extraction (local files only)
|
|
497
|
+
- `--namespace <ns>`: Target namespace for instrumented keys
|
|
498
|
+
- `--update-values`: Also update existing translation values on Locize
|
|
499
|
+
- `--cdn-type <standard|pro>`: Locize CDN endpoint type
|
|
500
|
+
- `--print-agent-prompt`: Print a copy-paste prompt for AI coding agents, then exit (see below)
|
|
501
|
+
|
|
502
|
+
**Behavior matrix:**
|
|
503
|
+
|
|
504
|
+
| Step | interactive (default) | `--yes` | `--ci` | `--dry-run` |
|
|
505
|
+
|---|---|---|---|---|
|
|
506
|
+
| Instrument | per-string prompts | auto-approve | skipped (force with `--yes`) | candidate preview |
|
|
507
|
+
| Connect Locize | browser + paste credentials | same | env vars required, else exit 1 | report only |
|
|
508
|
+
| Sync + translate | runs | runs | runs | `--dry` forwarded |
|
|
509
|
+
| Poll + download | watches translations arrive | same | single download, no wait | skipped |
|
|
510
|
+
|
|
511
|
+
**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).
|
|
512
|
+
|
|
513
|
+
> **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.
|
|
514
|
+
|
|
515
|
+
**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.
|
|
516
|
+
|
|
517
|
+
**Agent prompt:** the same flow is available as a copy-paste prompt for AI coding agents (Claude Code, Cursor, …):
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
npx i18next-cli localize --print-agent-prompt
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
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:
|
|
524
|
+
|
|
525
|
+
<details>
|
|
526
|
+
<summary>Agent prompt (snapshot)</summary>
|
|
527
|
+
|
|
528
|
+
```text
|
|
529
|
+
You are localizing this app with i18next + Locize. Execute these steps in order,
|
|
530
|
+
verifying each before continuing. Use `npx i18next-cli` for all commands.
|
|
531
|
+
|
|
532
|
+
1. Detect: confirm this is a React/Next.js project (check package.json).
|
|
533
|
+
- If Vue/Svelte: install a stack plugin (`i18next-cli-vue` /
|
|
534
|
+
`i18next-cli-plugin-svelte`) and add it to the `plugins` array of
|
|
535
|
+
i18next.config.ts, or write one via the plugin hooks
|
|
536
|
+
(instrumentOnLoad/onLoad) instead of wrapping strings manually.
|
|
537
|
+
- If the app uses inlang Paraglide (`@inlang/paraglide-js`), STOP —
|
|
538
|
+
instrumenting i18next calls would conflict; ask the user how to proceed.
|
|
539
|
+
2. Config: if no i18next.config.{ts,js} exists, run `npx i18next-cli init`
|
|
540
|
+
and answer the prompts (pick Locize as backend if the user wants managed
|
|
541
|
+
translations and AI auto-translate).
|
|
542
|
+
3. Instrument: run `npx i18next-cli instrument --dry-run` and review the
|
|
543
|
+
planned changes; then `npx i18next-cli instrument` to apply. Inspect the
|
|
544
|
+
git diff carefully: fix any t() wrapping inside Next.js *server components*
|
|
545
|
+
(add 'use client' or refactor to a server-side t() pattern). Commit.
|
|
546
|
+
4. Extract: run `npx i18next-cli extract`. Verify the locale JSON files were
|
|
547
|
+
written (check the extract.output path in the config).
|
|
548
|
+
5. Locize: ask the user for LOCIZE_PROJECTID and LOCIZE_API_KEY (they create
|
|
549
|
+
the project at https://www.locize.app/register?from=i18next_cli__agent-prompt
|
|
550
|
+
— any write-capable API key works; the target languages from
|
|
551
|
+
i18next.config.ts are created automatically on the first sync.
|
|
552
|
+
Auto-translation and quality estimation are enabled by default for new
|
|
553
|
+
projects; translations run once the project is subscribed or an AI/MT
|
|
554
|
+
provider is configured). Export both as environment variables.
|
|
555
|
+
6. Translate & deliver:
|
|
556
|
+
`npx i18next-cli locize-sync --auto-translate true`
|
|
557
|
+
then `npx i18next-cli locize-download` to pull the AI translations, and
|
|
558
|
+
`npx i18next-cli status` — confirm all languages are (near) 100%.
|
|
559
|
+
AI translation is asynchronous; if targets are still empty, wait a minute
|
|
560
|
+
and re-run locize-download.
|
|
561
|
+
7. Optionally switch runtime loading to i18next-locize-backend (CDN delivery,
|
|
562
|
+
so translation fixes go live without redeploying). NEVER put the API key
|
|
563
|
+
in client-side code — the CDN only needs the project ID.
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
</details>
|
|
567
|
+
|
|
471
568
|
### `migrate-config`
|
|
472
569
|
Automatically migrates a legacy `i18next-parser.config.js` file to the new `i18next.config.ts` format.
|
|
473
570
|
|
|
@@ -543,9 +640,29 @@ npx i18next-cli locize-sync [options]
|
|
|
543
640
|
|
|
544
641
|
**Options:**
|
|
545
642
|
- `--update-values`: Update values of existing translations on locize
|
|
546
|
-
- `--src-lng-only
|
|
643
|
+
- `--src-lng-only <true|false>`: Check for changes in source language only (default: `true`). Pass `--src-lng-only false` to sync all languages
|
|
547
644
|
- `--compare-mtime`: Compare modification times when syncing
|
|
548
645
|
- `--dry-run`: Run the command without making any changes
|
|
646
|
+
- `--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)
|
|
647
|
+
- `--auto-translate-review <true|false>`: Route auto-translated segments through the review workflow for languages that have review enabled
|
|
648
|
+
- `--auto-translate-languages <lng1,lng2>`: Restrict auto-translation to these target languages (defaults to all)
|
|
649
|
+
|
|
650
|
+
The same options can be set persistently in the `locize` block of your config:
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
export default defineConfig({
|
|
654
|
+
// ...
|
|
655
|
+
locize: {
|
|
656
|
+
projectId: '...',
|
|
657
|
+
apiKey: process.env.LOCIZE_API_KEY,
|
|
658
|
+
autoTranslate: true,
|
|
659
|
+
autoTranslateReview: false,
|
|
660
|
+
autoTranslateLanguages: ['de', 'fr'],
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
> **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
666
|
|
|
550
667
|
**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
668
|
|
|
@@ -1599,6 +1716,7 @@ class I18nextExtractionPlugin {
|
|
|
1599
1716
|
- `runSyncer(config)` - Sync translation files
|
|
1600
1717
|
- `runStatus(config, options?)` - Get translation status
|
|
1601
1718
|
- `runTypesGenerator(config)` - Generate types
|
|
1719
|
+
- `runLocalize(options?, configPath?)` - The full [`localize`](#localize) flow (detect → instrument → extract → Locize sync with auto-translate → download)
|
|
1602
1720
|
|
|
1603
1721
|
### Advanced Usage
|
|
1604
1722
|
|
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.62.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
|
});
|
|
@@ -229,7 +234,7 @@ program
|
|
|
229
234
|
const derivedIgnore2 = deriveOutputIgnore(config$1.extract.output);
|
|
230
235
|
const ignoredLint = [...configuredIgnore2, ...derivedIgnore2].filter(Boolean);
|
|
231
236
|
const watchLint = expandedLint.filter(f => !ignoredLint.some(g => minimatch.minimatch(f, g, { dot: true })));
|
|
232
|
-
const watcher =
|
|
237
|
+
const watcher = chokidar__default.default.watch(watchLint, {
|
|
233
238
|
ignored: /node_modules/,
|
|
234
239
|
persistent: true,
|
|
235
240
|
});
|
|
@@ -286,14 +291,40 @@ program
|
|
|
286
291
|
process.exit(1);
|
|
287
292
|
}
|
|
288
293
|
});
|
|
294
|
+
program
|
|
295
|
+
.command('localize')
|
|
296
|
+
.description('One command from hardcoded strings to a fully localized app: detect, instrument, extract, connect to Locize, auto-translate, deliver.')
|
|
297
|
+
.option('--dry-run', 'Preview every step; nothing is written or pushed.')
|
|
298
|
+
.option('-y, --yes', 'Accept defaults; auto-approve instrumentation candidates (no per-string prompts).')
|
|
299
|
+
.option('--ci', 'Non-interactive: never open a browser or prompt; instrument is skipped (combine with --yes to force non-interactive instrumentation).')
|
|
300
|
+
.option('--skip-instrument', 'Skip the code-instrumentation step (use when your code already calls t()).')
|
|
301
|
+
.option('--skip-translate', 'Sync to Locize but do not request AI auto-translation.')
|
|
302
|
+
.option('--skip-locize', 'Stop after extraction (local files only; steps 5-6 skipped).')
|
|
303
|
+
.option('--namespace <ns>', 'Target namespace for instrumented keys (forwarded to instrument).')
|
|
304
|
+
.option('--update-values', 'Also update existing translation values on Locize (forwarded to sync).')
|
|
305
|
+
.option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your Locize project)')
|
|
306
|
+
.option('--print-agent-prompt', 'Print a copy-paste prompt for AI coding agents (Claude Code, Cursor) that performs the same steps, then exit.')
|
|
307
|
+
.action(async (options) => {
|
|
308
|
+
try {
|
|
309
|
+
const cfgPath = program.opts().config;
|
|
310
|
+
await localize.runLocalize(options, cfgPath);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
console.error(node_util.styleText('red', 'Error running localize command:'), error);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
289
317
|
program
|
|
290
318
|
.command('locize-sync')
|
|
291
319
|
.description('Synchronize local translations with your Locize project.')
|
|
292
320
|
.option('--update-values', 'Update values of existing translations on Locize.')
|
|
293
|
-
.option('--src-lng-only', 'Check for changes in source language only.')
|
|
321
|
+
.option('--src-lng-only <true|false>', 'Check for changes in source language only. (default: true)')
|
|
294
322
|
.option('--compare-mtime', 'Compare modification times when syncing.')
|
|
295
323
|
.option('--dry-run', 'Run the command without making any changes.')
|
|
296
324
|
.option('--cdn-type <standard|pro>', 'Specify the cdn endpoint that should be used (depends on which cdn type you\'ve in your locize project)')
|
|
325
|
+
.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).')
|
|
326
|
+
.option('--auto-translate-review <true|false>', 'Route auto-translated segments through the review workflow for languages that have review enabled.')
|
|
327
|
+
.option('--auto-translate-languages <lng1,lng2>', 'Restrict auto-translation to these target languages (comma separated; defaults to all languages).')
|
|
297
328
|
.action(async (options) => {
|
|
298
329
|
const cfgPath = program.opts().config;
|
|
299
330
|
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,14 @@
|
|
|
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
8
|
|
|
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
|
-
}
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var inquirer__default = /*#__PURE__*/_interopDefault(inquirer);
|
|
12
|
+
|
|
13
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__init-wizard';
|
|
58
14
|
/**
|
|
59
15
|
* Determines if the current project is configured as an ESM project.
|
|
60
16
|
* Checks the package.json file for `"type": "module"`.
|
|
@@ -164,7 +120,7 @@ async function runInit(options = {}) {
|
|
|
164
120
|
const tsChoice = 'TypeScript (i18next.config.ts)';
|
|
165
121
|
const jsChoice = 'JavaScript (i18next.config.js)';
|
|
166
122
|
const fileTypeChoices = projectUsesTs ? [tsChoice, jsChoice] : [jsChoice, tsChoice];
|
|
167
|
-
const answers = await
|
|
123
|
+
const answers = await inquirer__default.default.prompt([
|
|
168
124
|
{
|
|
169
125
|
type: 'select',
|
|
170
126
|
name: 'fileType',
|
|
@@ -208,33 +164,11 @@ async function runInit(options = {}) {
|
|
|
208
164
|
let locizeConfig;
|
|
209
165
|
if (answers.backend === 'locize') {
|
|
210
166
|
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 });
|
|
167
|
+
const opened = await locizeOnboarding.openBrowser(LOCIZE_SIGNUP_URL, { ci: options.ci });
|
|
212
168
|
if (!opened) {
|
|
213
169
|
console.log(`\n👉 Open this URL manually: ${LOCIZE_SIGNUP_URL}\n`);
|
|
214
170
|
}
|
|
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;
|
|
171
|
+
locizeConfig = await locizeOnboarding.promptLocizeCredentials();
|
|
238
172
|
}
|
|
239
173
|
const isTypeScript = answers.fileType.includes('TypeScript');
|
|
240
174
|
const isEsm = await isEsmProject();
|
|
@@ -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,24 @@ 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), or null.
|
|
1474
|
+
*/
|
|
1475
|
+
async function findExistingI18nInitFile() {
|
|
1476
|
+
const cwd = process.cwd();
|
|
1477
|
+
const searchDirs = ['src', '.'];
|
|
1478
|
+
for (const dir of searchDirs) {
|
|
1479
|
+
for (const name of I18N_INIT_FILE_NAMES) {
|
|
1480
|
+
if (await fileExists(node_path.join(cwd, dir, name))) {
|
|
1481
|
+
return node_path.join(dir, name);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1465
1487
|
/**
|
|
1466
1488
|
* Computes a POSIX-style relative path from the init-file directory to the
|
|
1467
1489
|
* output template path (which still contains {{language}} / {{namespace}} placeholders).
|
|
@@ -1487,13 +1509,8 @@ function buildDynamicImportPath(outputTemplate, initDir) {
|
|
|
1487
1509
|
async function ensureI18nInitFile(hasReact, hasTypeScript, config, logger, usesI18nextT) {
|
|
1488
1510
|
const cwd = process.cwd();
|
|
1489
1511
|
// 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
|
-
}
|
|
1512
|
+
if (await findExistingI18nInitFile()) {
|
|
1513
|
+
return null; // Init file already exists
|
|
1497
1514
|
}
|
|
1498
1515
|
// Check if i18next.init() is called anywhere in the source
|
|
1499
1516
|
try {
|
|
@@ -1941,5 +1958,9 @@ async function runInstrumentOnResultPipeline(filePath, initialCandidates, plugin
|
|
|
1941
1958
|
return candidates;
|
|
1942
1959
|
}
|
|
1943
1960
|
|
|
1961
|
+
exports.detectProjectEnvironment = detectProjectEnvironment;
|
|
1962
|
+
exports.findExistingI18nInitFile = findExistingI18nInitFile;
|
|
1963
|
+
exports.isProjectUsingReact = isProjectUsingReact;
|
|
1964
|
+
exports.isProjectUsingTypeScript = isProjectUsingTypeScript;
|
|
1944
1965
|
exports.runInstrumenter = runInstrumenter;
|
|
1945
1966
|
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;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('node:fs/promises');
|
|
4
|
+
var node_path = require('node:path');
|
|
5
|
+
|
|
6
|
+
async function pathExists(path) {
|
|
7
|
+
try {
|
|
8
|
+
await promises.access(path);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function readPackageDeps() {
|
|
16
|
+
try {
|
|
17
|
+
const content = await promises.readFile(node_path.join(process.cwd(), 'package.json'), 'utf-8');
|
|
18
|
+
const packageJson = JSON.parse(content);
|
|
19
|
+
return { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Detects the project stack relevant to the `localize` orchestrator:
|
|
27
|
+
* frontend framework, i18next presence, an existing i18n init file,
|
|
28
|
+
* Next.js App Router usage and inlang Paraglide usage.
|
|
29
|
+
*
|
|
30
|
+
* All checks are `process.cwd()`-relative (run from the package directory
|
|
31
|
+
* in monorepos).
|
|
32
|
+
*
|
|
33
|
+
* @param findInitFile - locator for an existing i18n init file
|
|
34
|
+
* (injected to reuse the instrumenter's implementation)
|
|
35
|
+
*/
|
|
36
|
+
async function detectStack(findInitFile) {
|
|
37
|
+
const deps = await readPackageDeps();
|
|
38
|
+
const has = (name) => !!deps[name];
|
|
39
|
+
let framework = 'unknown';
|
|
40
|
+
if (has('next'))
|
|
41
|
+
framework = 'next';
|
|
42
|
+
else if (has('react') || has('react-i18next'))
|
|
43
|
+
framework = 'react';
|
|
44
|
+
else if (has('vue') || has('nuxt'))
|
|
45
|
+
framework = 'vue';
|
|
46
|
+
else if (has('svelte') || has('@sveltejs/kit'))
|
|
47
|
+
framework = 'svelte';
|
|
48
|
+
else if (has('@angular/core'))
|
|
49
|
+
framework = 'angular';
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
const hasAppRouter = framework === 'next' &&
|
|
52
|
+
(await pathExists(node_path.join(cwd, 'app')) || await pathExists(node_path.join(cwd, 'src', 'app')));
|
|
53
|
+
const hasParaglide = has('@inlang/paraglide-js') || await pathExists(node_path.join(cwd, 'project.inlang'));
|
|
54
|
+
return {
|
|
55
|
+
framework,
|
|
56
|
+
hasI18next: has('i18next') || has('react-i18next'),
|
|
57
|
+
hasTypeScript: await pathExists(node_path.join(cwd, 'tsconfig.json')),
|
|
58
|
+
initFile: await findInitFile(),
|
|
59
|
+
hasAppRouter,
|
|
60
|
+
hasParaglide,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** File extensions associated with frameworks the instrumenter cannot transform natively. */
|
|
64
|
+
const STACK_EXTENSIONS = {
|
|
65
|
+
vue: ['.vue', 'vue'],
|
|
66
|
+
svelte: ['.svelte', 'svelte'],
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Checks whether a configured plugin covers the detected stack's file
|
|
70
|
+
* extension via `instrumentExtensions` or `lintExtensions` — in which case
|
|
71
|
+
* the instrument/extract runners can process the stack's files through the
|
|
72
|
+
* plugin hooks and `localize` runs the full flow.
|
|
73
|
+
*/
|
|
74
|
+
function hasStackPlugin(config, framework) {
|
|
75
|
+
const extensions = STACK_EXTENSIONS[framework];
|
|
76
|
+
if (!extensions || !config.plugins?.length)
|
|
77
|
+
return false;
|
|
78
|
+
return config.plugins.some((plugin) => {
|
|
79
|
+
const declared = [
|
|
80
|
+
...(plugin.instrumentExtensions || []),
|
|
81
|
+
...(plugin.lintExtensions || []),
|
|
82
|
+
];
|
|
83
|
+
return declared.some(ext => extensions.includes(ext));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
exports.detectStack = detectStack;
|
|
88
|
+
exports.hasStackPlugin = hasStackPlugin;
|