i18next-cli 1.61.1 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -0
- package/dist/cjs/cli.js +37 -5
- package/dist/cjs/config.js +5 -1
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/init.js +25 -75
- package/dist/cjs/instrumenter/core/instrumenter.js +33 -11
- package/dist/cjs/instrumenter/core/transformer.js +5 -1
- package/dist/cjs/localize/agent-prompt.js +49 -0
- package/dist/cjs/localize/detect.js +88 -0
- package/dist/cjs/localize/localize.js +475 -0
- package/dist/cjs/locize.js +84 -11
- package/dist/cjs/status.js +5 -1
- package/dist/cjs/types-generator.js +8 -3
- package/dist/cjs/utils/file-utils.js +6 -2
- package/dist/cjs/utils/inlang-scaffold.js +184 -0
- package/dist/cjs/utils/locize-onboarding.js +91 -0
- package/dist/cjs/utils/wrap-ora.js +9 -5
- package/dist/esm/cli.js +30 -2
- package/dist/esm/index.js +4 -0
- package/dist/esm/init.js +19 -73
- package/dist/esm/instrumenter/core/instrumenter.js +22 -8
- package/dist/esm/localize/agent-prompt.js +47 -0
- package/dist/esm/localize/detect.js +85 -0
- package/dist/esm/localize/localize.js +469 -0
- package/dist/esm/locize.js +75 -9
- package/dist/esm/utils/inlang-scaffold.js +182 -0
- package/dist/esm/utils/locize-onboarding.js +83 -0
- package/package.json +10 -10
- package/types/cli.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/init.d.ts +1 -0
- package/types/init.d.ts.map +1 -1
- package/types/instrumenter/core/instrumenter.d.ts +28 -0
- package/types/instrumenter/core/instrumenter.d.ts.map +1 -1
- package/types/instrumenter/index.d.ts +2 -1
- package/types/instrumenter/index.d.ts.map +1 -1
- package/types/localize/agent-prompt.d.ts +11 -0
- package/types/localize/agent-prompt.d.ts.map +1 -0
- package/types/localize/detect.d.ts +37 -0
- package/types/localize/detect.d.ts.map +1 -0
- package/types/localize/index.d.ts +6 -0
- package/types/localize/index.d.ts.map +1 -0
- package/types/localize/localize.d.ts +20 -0
- package/types/localize/localize.d.ts.map +1 -0
- package/types/locize.d.ts +20 -0
- package/types/locize.d.ts.map +1 -1
- package/types/types.d.ts +12 -0
- package/types/types.d.ts.map +1 -1
- package/types/utils/inlang-scaffold.d.ts +28 -0
- package/types/utils/inlang-scaffold.d.ts.map +1 -0
- package/types/utils/locize-onboarding.d.ts +19 -0
- package/types/utils/locize-onboarding.d.ts.map +1 -0
package/dist/esm/index.js
CHANGED
|
@@ -11,3 +11,7 @@ export { runRenameKey } from './rename-key.js';
|
|
|
11
11
|
export { runInstrumenter, writeExtractedKeys } from './instrumenter/core/instrumenter.js';
|
|
12
12
|
import './utils/jsx-attributes.js';
|
|
13
13
|
import 'magic-string';
|
|
14
|
+
export { runLocalize } from './localize/localize.js';
|
|
15
|
+
import 'node:fs/promises';
|
|
16
|
+
import 'node:path';
|
|
17
|
+
export { AGENT_PROMPT } from './localize/agent-prompt.js';
|
package/dist/esm/init.js
CHANGED
|
@@ -1,58 +1,11 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { writeFile, readFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
-
import { execa } from 'execa';
|
|
5
4
|
import { detectConfig } from './heuristic-config.js';
|
|
5
|
+
import { openBrowser, promptLocizeCredentials } from './utils/locize-onboarding.js';
|
|
6
|
+
import { scaffoldInlangProject } from './utils/inlang-scaffold.js';
|
|
6
7
|
|
|
7
|
-
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=
|
|
8
|
-
/** Rough 8-4-4-4-12 hex UUID shape — not strict (locize project IDs may evolve). */
|
|
9
|
-
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
-
/**
|
|
11
|
-
* Opens the given URL in the user's default browser using the platform-native command.
|
|
12
|
-
* Returns true on success, false if there's nowhere to open one (CI, headless Linux)
|
|
13
|
-
* or if spawning the command failed.
|
|
14
|
-
*/
|
|
15
|
-
async function openBrowser(url, opts = {}) {
|
|
16
|
-
// Short-circuit: no point spawning a browser-opener in CI or headless Linux.
|
|
17
|
-
if (opts.ci || process.env.CI === 'true')
|
|
18
|
-
return false;
|
|
19
|
-
const isWSL = !!process.env.WSL_DISTRO_NAME;
|
|
20
|
-
if (process.platform === 'linux' && !isWSL &&
|
|
21
|
-
!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
if (process.platform === 'darwin') {
|
|
26
|
-
await execa('open', [url], { stdio: 'ignore' });
|
|
27
|
-
}
|
|
28
|
-
else if (process.platform === 'win32') {
|
|
29
|
-
// `start` is a cmd.exe builtin; the empty "" is the window-title slot
|
|
30
|
-
await execa('cmd', ['/c', 'start', '""', url], { stdio: 'ignore' });
|
|
31
|
-
}
|
|
32
|
-
else if (isWSL) {
|
|
33
|
-
// WSL: try the wslu / wsl-open shims that bridge to the Windows side
|
|
34
|
-
// before falling back to xdg-open (which usually isn't installed there).
|
|
35
|
-
try {
|
|
36
|
-
await execa('wslview', [url], { stdio: 'ignore' });
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
try {
|
|
40
|
-
await execa('wsl-open', [url], { stdio: 'ignore' });
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
await execa('xdg-open', [url], { stdio: 'ignore' });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
await execa('xdg-open', [url], { stdio: 'ignore' });
|
|
49
|
-
}
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
8
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__init-wizard';
|
|
56
9
|
/**
|
|
57
10
|
* Determines if the current project is configured as an ESM project.
|
|
58
11
|
* Checks the package.json file for `"type": "module"`.
|
|
@@ -202,6 +155,14 @@ async function runInit(options = {}) {
|
|
|
202
155
|
],
|
|
203
156
|
default: 'local',
|
|
204
157
|
},
|
|
158
|
+
{
|
|
159
|
+
type: 'confirm',
|
|
160
|
+
name: 'inlang',
|
|
161
|
+
message: 'Also set up inlang tooling (Sherlock VS Code extension, Fink editor, Paraglide) on these translation files?',
|
|
162
|
+
default: false,
|
|
163
|
+
// Skip the question when already requested via the --inlang flag.
|
|
164
|
+
when: () => !options.inlang,
|
|
165
|
+
},
|
|
205
166
|
]);
|
|
206
167
|
let locizeConfig;
|
|
207
168
|
if (answers.backend === 'locize') {
|
|
@@ -210,29 +171,7 @@ async function runInit(options = {}) {
|
|
|
210
171
|
if (!opened) {
|
|
211
172
|
console.log(`\n👉 Open this URL manually: ${LOCIZE_SIGNUP_URL}\n`);
|
|
212
173
|
}
|
|
213
|
-
|
|
214
|
-
{
|
|
215
|
-
type: 'input',
|
|
216
|
-
name: 'projectId',
|
|
217
|
-
message: 'Locize Project ID (e.g. 4eeb5ce0-a7a7-453f-8eb3-078f6eeb56fe):',
|
|
218
|
-
validate: (input) => input.trim().length > 0 || 'Project ID cannot be empty.',
|
|
219
|
-
filter: (input) => input.trim(),
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
type: 'password',
|
|
223
|
-
name: 'apiKey',
|
|
224
|
-
message: 'Locize API key (needed for saveMissing / auto-publish / sync during development; leave empty to skip and add later via env var):',
|
|
225
|
-
filter: (input) => input.trim(),
|
|
226
|
-
},
|
|
227
|
-
]);
|
|
228
|
-
if (!UUID_SHAPE.test(credentials.projectId)) {
|
|
229
|
-
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.");
|
|
230
|
-
}
|
|
231
|
-
// API keys come in multiple shapes (UUID, `lz_pat_…`, `lz_api_…`, etc.) —
|
|
232
|
-
// treat them as opaque; no client-side format check.
|
|
233
|
-
locizeConfig = { projectId: credentials.projectId };
|
|
234
|
-
if (credentials.apiKey)
|
|
235
|
-
locizeConfig.apiKey = credentials.apiKey;
|
|
174
|
+
locizeConfig = await promptLocizeCredentials();
|
|
236
175
|
}
|
|
237
176
|
const isTypeScript = answers.fileType.includes('TypeScript');
|
|
238
177
|
const isEsm = await isEsmProject();
|
|
@@ -317,6 +256,13 @@ module.exports = ${toJs(configObject)}`;
|
|
|
317
256
|
const outputPath = resolve(process.cwd(), fileName);
|
|
318
257
|
await writeFile(outputPath, fileContent.trim());
|
|
319
258
|
console.log(`✅ Configuration file created at: ${outputPath}`);
|
|
259
|
+
if (options.inlang || answers.inlang) {
|
|
260
|
+
await scaffoldInlangProject({
|
|
261
|
+
locales: answers.locales,
|
|
262
|
+
primaryLanguage: answers.locales[0],
|
|
263
|
+
output: answers.output,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
320
266
|
if (locizeConfig) {
|
|
321
267
|
console.log('\nNext steps for Locize:');
|
|
322
268
|
console.log(' 1. Push your local translations to Locize:');
|
|
@@ -1460,6 +1460,25 @@ const I18N_INIT_FILE_NAMES = [
|
|
|
1460
1460
|
'i18n/index.ts', 'i18n/index.js', 'i18n/index.mjs',
|
|
1461
1461
|
'i18next/index.ts', 'i18next/index.js'
|
|
1462
1462
|
];
|
|
1463
|
+
/**
|
|
1464
|
+
* Searches the common locations (`src/` and the project root) for an existing
|
|
1465
|
+
* i18n initialization file.
|
|
1466
|
+
*
|
|
1467
|
+
* @returns The path of the first init file found (relative to cwd, native
|
|
1468
|
+
* platform separators), or null.
|
|
1469
|
+
*/
|
|
1470
|
+
async function findExistingI18nInitFile() {
|
|
1471
|
+
const cwd = process.cwd();
|
|
1472
|
+
const searchDirs = ['src', '.'];
|
|
1473
|
+
for (const dir of searchDirs) {
|
|
1474
|
+
for (const name of I18N_INIT_FILE_NAMES) {
|
|
1475
|
+
if (await fileExists(join(cwd, dir, name))) {
|
|
1476
|
+
return join(dir, name);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1463
1482
|
/**
|
|
1464
1483
|
* Computes a POSIX-style relative path from the init-file directory to the
|
|
1465
1484
|
* output template path (which still contains {{language}} / {{namespace}} placeholders).
|
|
@@ -1485,13 +1504,8 @@ function buildDynamicImportPath(outputTemplate, initDir) {
|
|
|
1485
1504
|
async function ensureI18nInitFile(hasReact, hasTypeScript, config, logger, usesI18nextT) {
|
|
1486
1505
|
const cwd = process.cwd();
|
|
1487
1506
|
// Check for existing init files in common locations
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
for (const name of I18N_INIT_FILE_NAMES) {
|
|
1491
|
-
if (await fileExists(join(cwd, dir, name))) {
|
|
1492
|
-
return null; // Init file already exists
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1507
|
+
if (await findExistingI18nInitFile()) {
|
|
1508
|
+
return null; // Init file already exists
|
|
1495
1509
|
}
|
|
1496
1510
|
// Check if i18next.init() is called anywhere in the source
|
|
1497
1511
|
try {
|
|
@@ -1939,4 +1953,4 @@ async function runInstrumentOnResultPipeline(filePath, initialCandidates, plugin
|
|
|
1939
1953
|
return candidates;
|
|
1940
1954
|
}
|
|
1941
1955
|
|
|
1942
|
-
export { runInstrumenter, writeExtractedKeys };
|
|
1956
|
+
export { detectProjectEnvironment, findExistingI18nInitFile, isProjectUsingReact, isProjectUsingTypeScript, runInstrumenter, writeExtractedKeys };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy-paste prompt for AI coding agents (Claude Code, Cursor, …) that walks
|
|
3
|
+
* an agent through the same steps the `localize` command orchestrates.
|
|
4
|
+
*
|
|
5
|
+
* This constant is the single source of truth — it is printed by
|
|
6
|
+
* `i18next-cli localize --print-agent-prompt` and mirrored in the README.
|
|
7
|
+
* A test guards that it mentions every orchestrated command, so flow changes
|
|
8
|
+
* that forget this prompt fail CI.
|
|
9
|
+
*/
|
|
10
|
+
const AGENT_PROMPT = `You are localizing this app with i18next + Locize. Execute these steps in order,
|
|
11
|
+
verifying each before continuing. Use \`npx i18next-cli\` for all commands.
|
|
12
|
+
|
|
13
|
+
1. Detect: confirm this is a React/Next.js project (check package.json).
|
|
14
|
+
- If Vue/Svelte: install a stack plugin (\`i18next-cli-vue\` /
|
|
15
|
+
\`i18next-cli-plugin-svelte\`) and add it to the \`plugins\` array of
|
|
16
|
+
i18next.config.ts, or write one via the plugin hooks
|
|
17
|
+
(instrumentOnLoad/onLoad) instead of wrapping strings manually.
|
|
18
|
+
- If the app uses inlang Paraglide (\`@inlang/paraglide-js\`), STOP —
|
|
19
|
+
instrumenting i18next calls would conflict; ask the user how to proceed.
|
|
20
|
+
2. Config: if no i18next.config.{ts,js} exists, run \`npx i18next-cli init\`
|
|
21
|
+
and answer the prompts (pick Locize as backend if the user wants managed
|
|
22
|
+
translations and AI auto-translate).
|
|
23
|
+
3. Instrument: run \`npx i18next-cli instrument --dry-run\` and review the
|
|
24
|
+
planned changes; then \`npx i18next-cli instrument\` to apply. Inspect the
|
|
25
|
+
git diff carefully: fix any t() wrapping inside Next.js *server components*
|
|
26
|
+
(add 'use client' or refactor to a server-side t() pattern). Commit.
|
|
27
|
+
4. Extract: run \`npx i18next-cli extract\`. Verify the locale JSON files were
|
|
28
|
+
written (check the extract.output path in the config).
|
|
29
|
+
5. Locize: ask the user for LOCIZE_PROJECTID and LOCIZE_API_KEY (they create
|
|
30
|
+
the project at https://www.locize.app/register?from=i18next_cli__agent-prompt
|
|
31
|
+
— any write-capable API key works; the target languages from
|
|
32
|
+
i18next.config.ts are created automatically on the first sync.
|
|
33
|
+
Auto-translation and quality estimation are enabled by default for new
|
|
34
|
+
projects; translations run once the project is subscribed or an AI/MT
|
|
35
|
+
provider is configured). Export both as environment variables.
|
|
36
|
+
6. Translate & deliver:
|
|
37
|
+
\`npx i18next-cli locize-sync --auto-translate true\`
|
|
38
|
+
then \`npx i18next-cli locize-download\` to pull the AI translations, and
|
|
39
|
+
\`npx i18next-cli status\` — confirm all languages are (near) 100%.
|
|
40
|
+
AI translation is asynchronous; if targets are still empty, wait a minute
|
|
41
|
+
and re-run locize-download.
|
|
42
|
+
7. Optionally switch runtime loading to i18next-locize-backend (CDN delivery,
|
|
43
|
+
so translation fixes go live without redeploying). NEVER put the API key
|
|
44
|
+
in client-side code — the CDN only needs the project ID.
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export { AGENT_PROMPT };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile, access } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function pathExists(path) {
|
|
5
|
+
try {
|
|
6
|
+
await access(path);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function readPackageDeps() {
|
|
14
|
+
try {
|
|
15
|
+
const content = await readFile(join(process.cwd(), 'package.json'), 'utf-8');
|
|
16
|
+
const packageJson = JSON.parse(content);
|
|
17
|
+
return { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Detects the project stack relevant to the `localize` orchestrator:
|
|
25
|
+
* frontend framework, i18next presence, an existing i18n init file,
|
|
26
|
+
* Next.js App Router usage and inlang Paraglide usage.
|
|
27
|
+
*
|
|
28
|
+
* All checks are `process.cwd()`-relative (run from the package directory
|
|
29
|
+
* in monorepos).
|
|
30
|
+
*
|
|
31
|
+
* @param findInitFile - locator for an existing i18n init file
|
|
32
|
+
* (injected to reuse the instrumenter's implementation)
|
|
33
|
+
*/
|
|
34
|
+
async function detectStack(findInitFile) {
|
|
35
|
+
const deps = await readPackageDeps();
|
|
36
|
+
const has = (name) => !!deps[name];
|
|
37
|
+
let framework = 'unknown';
|
|
38
|
+
if (has('next'))
|
|
39
|
+
framework = 'next';
|
|
40
|
+
else if (has('react') || has('react-i18next'))
|
|
41
|
+
framework = 'react';
|
|
42
|
+
else if (has('vue') || has('nuxt'))
|
|
43
|
+
framework = 'vue';
|
|
44
|
+
else if (has('svelte') || has('@sveltejs/kit'))
|
|
45
|
+
framework = 'svelte';
|
|
46
|
+
else if (has('@angular/core'))
|
|
47
|
+
framework = 'angular';
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const hasAppRouter = framework === 'next' &&
|
|
50
|
+
(await pathExists(join(cwd, 'app')) || await pathExists(join(cwd, 'src', 'app')));
|
|
51
|
+
const hasParaglide = has('@inlang/paraglide-js') || await pathExists(join(cwd, 'project.inlang'));
|
|
52
|
+
return {
|
|
53
|
+
framework,
|
|
54
|
+
hasI18next: has('i18next') || has('react-i18next'),
|
|
55
|
+
hasTypeScript: await pathExists(join(cwd, 'tsconfig.json')),
|
|
56
|
+
initFile: await findInitFile(),
|
|
57
|
+
hasAppRouter,
|
|
58
|
+
hasParaglide,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** File extensions associated with frameworks the instrumenter cannot transform natively. */
|
|
62
|
+
const STACK_EXTENSIONS = {
|
|
63
|
+
vue: ['.vue', 'vue'],
|
|
64
|
+
svelte: ['.svelte', 'svelte'],
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Checks whether a configured plugin covers the detected stack's file
|
|
68
|
+
* extension via `instrumentExtensions` or `lintExtensions` — in which case
|
|
69
|
+
* the instrument/extract runners can process the stack's files through the
|
|
70
|
+
* plugin hooks and `localize` runs the full flow.
|
|
71
|
+
*/
|
|
72
|
+
function hasStackPlugin(config, framework) {
|
|
73
|
+
const extensions = STACK_EXTENSIONS[framework];
|
|
74
|
+
if (!extensions || !config.plugins?.length)
|
|
75
|
+
return false;
|
|
76
|
+
return config.plugins.some((plugin) => {
|
|
77
|
+
const declared = [
|
|
78
|
+
...(plugin.instrumentExtensions || []),
|
|
79
|
+
...(plugin.lintExtensions || []),
|
|
80
|
+
];
|
|
81
|
+
return declared.some(ext => extensions.includes(ext));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { detectStack, hasStackPlugin };
|