i18next-cli 1.61.1 → 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.
Files changed (48) hide show
  1. package/README.md +118 -0
  2. package/dist/cjs/cli.js +35 -4
  3. package/dist/cjs/config.js +5 -1
  4. package/dist/cjs/index.js +6 -0
  5. package/dist/cjs/init.js +9 -75
  6. package/dist/cjs/instrumenter/core/instrumenter.js +32 -11
  7. package/dist/cjs/instrumenter/core/transformer.js +5 -1
  8. package/dist/cjs/localize/agent-prompt.js +49 -0
  9. package/dist/cjs/localize/detect.js +88 -0
  10. package/dist/cjs/localize/localize.js +475 -0
  11. package/dist/cjs/locize.js +84 -11
  12. package/dist/cjs/status.js +5 -1
  13. package/dist/cjs/types-generator.js +8 -3
  14. package/dist/cjs/utils/file-utils.js +6 -2
  15. package/dist/cjs/utils/locize-onboarding.js +91 -0
  16. package/dist/cjs/utils/wrap-ora.js +9 -5
  17. package/dist/esm/cli.js +28 -1
  18. package/dist/esm/index.js +4 -0
  19. package/dist/esm/init.js +3 -73
  20. package/dist/esm/instrumenter/core/instrumenter.js +21 -8
  21. package/dist/esm/localize/agent-prompt.js +47 -0
  22. package/dist/esm/localize/detect.js +85 -0
  23. package/dist/esm/localize/localize.js +469 -0
  24. package/dist/esm/locize.js +75 -9
  25. package/dist/esm/utils/locize-onboarding.js +83 -0
  26. package/package.json +10 -10
  27. package/types/cli.d.ts.map +1 -1
  28. package/types/index.d.ts +2 -0
  29. package/types/index.d.ts.map +1 -1
  30. package/types/init.d.ts.map +1 -1
  31. package/types/instrumenter/core/instrumenter.d.ts +27 -0
  32. package/types/instrumenter/core/instrumenter.d.ts.map +1 -1
  33. package/types/instrumenter/index.d.ts +2 -1
  34. package/types/instrumenter/index.d.ts.map +1 -1
  35. package/types/localize/agent-prompt.d.ts +11 -0
  36. package/types/localize/agent-prompt.d.ts.map +1 -0
  37. package/types/localize/detect.d.ts +37 -0
  38. package/types/localize/detect.d.ts.map +1 -0
  39. package/types/localize/index.d.ts +6 -0
  40. package/types/localize/index.d.ts.map +1 -0
  41. package/types/localize/localize.d.ts +20 -0
  42. package/types/localize/localize.d.ts.map +1 -0
  43. package/types/locize.d.ts +20 -0
  44. package/types/locize.d.ts.map +1 -1
  45. package/types/types.d.ts +12 -0
  46. package/types/types.d.ts.map +1 -1
  47. package/types/utils/locize-onboarding.d.ts +19 -0
  48. 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
 
@@ -546,6 +643,26 @@ npx i18next-cli locize-sync [options]
546
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.61.1'); // This string is replaced with the actual version at build time by rollup
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 = chokidar.watch(watchFiles, {
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 = chokidar.watch(watchTypes, {
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 = chokidar.watch(watchLint, {
237
+ const watcher = chokidar__default.default.watch(watchLint, {
233
238
  ignored: /node_modules/,
234
239
  persistent: true,
235
240
  });
@@ -286,6 +291,29 @@ 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.')
@@ -294,6 +322,9 @@ program
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);
@@ -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 inquirer.prompt([{
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
- const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next-cli+init+wizard';
10
- /** Rough 8-4-4-4-12 hex UUID shape — not strict (locize project IDs may evolve). */
11
- const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
- /**
13
- * Opens the given URL in the user's default browser using the platform-native command.
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 inquirer.prompt([
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
- const credentials = await inquirer.prompt([
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 inquirer.prompt([
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 inquirer.prompt([
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 inquirer.prompt([
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 inquirer.prompt([
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
- const searchDirs = ['src', '.'];
1491
- for (const dir of searchDirs) {
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 MagicString(content);
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;