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.
Files changed (53) hide show
  1. package/README.md +147 -0
  2. package/dist/cjs/cli.js +37 -5
  3. package/dist/cjs/config.js +5 -1
  4. package/dist/cjs/index.js +6 -0
  5. package/dist/cjs/init.js +25 -75
  6. package/dist/cjs/instrumenter/core/instrumenter.js +33 -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/inlang-scaffold.js +184 -0
  16. package/dist/cjs/utils/locize-onboarding.js +91 -0
  17. package/dist/cjs/utils/wrap-ora.js +9 -5
  18. package/dist/esm/cli.js +30 -2
  19. package/dist/esm/index.js +4 -0
  20. package/dist/esm/init.js +19 -73
  21. package/dist/esm/instrumenter/core/instrumenter.js +22 -8
  22. package/dist/esm/localize/agent-prompt.js +47 -0
  23. package/dist/esm/localize/detect.js +85 -0
  24. package/dist/esm/localize/localize.js +469 -0
  25. package/dist/esm/locize.js +75 -9
  26. package/dist/esm/utils/inlang-scaffold.js +182 -0
  27. package/dist/esm/utils/locize-onboarding.js +83 -0
  28. package/package.json +10 -10
  29. package/types/cli.d.ts.map +1 -1
  30. package/types/index.d.ts +2 -0
  31. package/types/index.d.ts.map +1 -1
  32. package/types/init.d.ts +1 -0
  33. package/types/init.d.ts.map +1 -1
  34. package/types/instrumenter/core/instrumenter.d.ts +28 -0
  35. package/types/instrumenter/core/instrumenter.d.ts.map +1 -1
  36. package/types/instrumenter/index.d.ts +2 -1
  37. package/types/instrumenter/index.d.ts.map +1 -1
  38. package/types/localize/agent-prompt.d.ts +11 -0
  39. package/types/localize/agent-prompt.d.ts.map +1 -0
  40. package/types/localize/detect.d.ts +37 -0
  41. package/types/localize/detect.d.ts.map +1 -0
  42. package/types/localize/index.d.ts +6 -0
  43. package/types/localize/index.d.ts.map +1 -0
  44. package/types/localize/localize.d.ts +20 -0
  45. package/types/localize/localize.d.ts.map +1 -0
  46. package/types/locize.d.ts +20 -0
  47. package/types/locize.d.ts.map +1 -1
  48. package/types/types.d.ts +12 -0
  49. package/types/types.d.ts.map +1 -1
  50. package/types/utils/inlang-scaffold.d.ts +28 -0
  51. package/types/utils/inlang-scaffold.d.ts.map +1 -0
  52. package/types/utils/locize-onboarding.d.ts +19 -0
  53. 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.61.1'); // This string is replaced with the actual version at build time by rollup
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 = 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
  });
@@ -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
- .action((options) => init.runInit({ ci: !!options.ci }));
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 = chokidar.watch(watchLint, {
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);
@@ -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,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
- 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
- }
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 inquirer.prompt([
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
- 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;
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 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,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
- 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
- }
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 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;