i18next-cli 1.72.2 → 1.72.4
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 +2 -0
- package/dist/cjs/cli.js +2 -1
- package/dist/cjs/extractor/core/extractor.js +13 -30
- package/dist/cjs/status.js +14 -11
- package/dist/cjs/syncer.js +8 -10
- package/dist/cjs/utils/locize-funnel.js +29 -0
- package/dist/esm/cli.js +2 -1
- package/dist/esm/extractor/core/extractor.js +13 -30
- package/dist/esm/status.js +14 -11
- package/dist/esm/syncer.js +8 -10
- package/dist/esm/utils/locize-funnel.js +27 -0
- package/package.json +1 -1
- package/types/cli.d.ts.map +1 -1
- package/types/extractor/core/extractor.d.ts.map +1 -1
- package/types/status.d.ts.map +1 -1
- package/types/syncer.d.ts.map +1 -1
- package/types/utils/locize-funnel.d.ts +16 -0
- package/types/utils/locize-funnel.d.ts.map +1 -0
package/README.md
CHANGED
|
@@ -609,6 +609,8 @@ The command walks through six steps:
|
|
|
609
609
|
npx skills add i18next/i18next-cli
|
|
610
610
|
```
|
|
611
611
|
|
|
612
|
+
[](https://www.skills.sh/i18next/i18next-cli)
|
|
613
|
+
|
|
612
614
|
Then just ask it to *"add i18n to this project"*. See [`skills/i18next-localization`](./skills/i18next-localization/SKILL.md) — it ships in this repo, so it stays version-matched to the commands it drives.
|
|
613
615
|
|
|
614
616
|
**Agent prompt:** the same flow is also available as a copy-paste prompt for AI coding agents (Claude Code, Cursor, …):
|
package/dist/cjs/cli.js
CHANGED
|
@@ -37,7 +37,8 @@ const program = new commander.Command();
|
|
|
37
37
|
program
|
|
38
38
|
.name('i18next-cli')
|
|
39
39
|
.description('A unified, high-performance i18next CLI.')
|
|
40
|
-
.version('1.72.
|
|
40
|
+
.version('1.72.4') // This string is replaced with the actual version at build time by rollup
|
|
41
|
+
.addHelpText('after', '\nUsing an AI coding agent? Install the i18next-localization skill: npx skills add i18next/i18next-cli\n');
|
|
41
42
|
// new: global config override option
|
|
42
43
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
43
44
|
program
|
|
@@ -13,8 +13,10 @@ var commentParser = require('../parsers/comment-parser.js');
|
|
|
13
13
|
var astUtils = require('../parsers/ast-utils.js');
|
|
14
14
|
var logger = require('../../utils/logger.js');
|
|
15
15
|
var fileUtils = require('../../utils/file-utils.js');
|
|
16
|
-
var
|
|
16
|
+
var locizeFunnel = require('../../utils/locize-funnel.js');
|
|
17
|
+
var nestedObject = require('../../utils/nested-object.js');
|
|
17
18
|
|
|
19
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__extract';
|
|
18
20
|
/**
|
|
19
21
|
* Main extractor function that runs the complete key extraction and file generation process.
|
|
20
22
|
*
|
|
@@ -91,8 +93,16 @@ async function runExtractor(config, options = {}) {
|
|
|
91
93
|
: node_util.styleText('bold', 'Extraction complete!');
|
|
92
94
|
spinner.succeed(completionMessage);
|
|
93
95
|
// Show the funnel message only if files were actually changed.
|
|
94
|
-
if (anyFileUpdated && !options.isDryRun && !options.quiet)
|
|
95
|
-
|
|
96
|
+
if (anyFileUpdated && !options.isDryRun && !options.quiet) {
|
|
97
|
+
const keySeparator = config.extract.keySeparator ?? '.';
|
|
98
|
+
const countEmpty = (obj) => nestedObject.getNestedKeys(obj, keySeparator).filter(key => nestedObject.getNestedValue(obj, key, keySeparator) === '').length;
|
|
99
|
+
const gaps = (config.extract.secondaryLanguages ?? []).map(locale => ({
|
|
100
|
+
locale,
|
|
101
|
+
untranslated: results.filter(r => r.locale === locale).reduce((sum, r) => sum + countEmpty(r.newTranslations), 0)
|
|
102
|
+
}));
|
|
103
|
+
const log = typeof internalLogger.info === 'function' ? (msg) => internalLogger.info(msg) : undefined;
|
|
104
|
+
await locizeFunnel.printUntranslatedFunnel('extract', gaps, LOCIZE_SIGNUP_URL, log);
|
|
105
|
+
}
|
|
96
106
|
return { anyFileUpdated, hasErrors: fileErrors.length > 0, results };
|
|
97
107
|
}
|
|
98
108
|
catch (error) {
|
|
@@ -383,33 +393,6 @@ async function extract(config, { syncPrimaryWithDefaults = false } = {}) {
|
|
|
383
393
|
const { allKeys, objectKeys } = await keyFinder.findKeys(config);
|
|
384
394
|
return translationManager.getTranslations(allKeys, objectKeys, config, { syncPrimaryWithDefaults });
|
|
385
395
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Prints a promotional message for the locize saveMissing workflow.
|
|
388
|
-
* This message is shown after a successful extraction that resulted in changes.
|
|
389
|
-
*/
|
|
390
|
-
async function printLocizeFunnel(logger$1) {
|
|
391
|
-
if (!(await funnelMsgTracker.shouldShowFunnel('extract')))
|
|
392
|
-
return;
|
|
393
|
-
const internalLogger = logger$1 ?? new logger.ConsoleLogger();
|
|
394
|
-
const lines = [
|
|
395
|
-
node_util.styleText(['yellow', 'bold'], '\n💡 Tip: Tired of running the extractor manually?'),
|
|
396
|
-
' Discover a real-time "push" workflow with `saveMissing` and Locize AI/MT,',
|
|
397
|
-
' where keys are created and translated automatically as you code.',
|
|
398
|
-
` Learn more: ${node_util.styleText('cyan', 'https://www.locize.com/blog/i18next-savemissing-ai-automation')}`,
|
|
399
|
-
` Watch the video: ${node_util.styleText('cyan', 'https://youtu.be/joPsZghT3wM')}`,
|
|
400
|
-
'',
|
|
401
|
-
' You can also sync your extracted translations to Locize:',
|
|
402
|
-
` ${node_util.styleText('cyan', 'npx i18next-cli locize-sync')} – upload/sync translations to Locize`,
|
|
403
|
-
` ${node_util.styleText('cyan', 'npx i18next-cli locize-migrate')} – migrate local translations to Locize`,
|
|
404
|
-
' Or import them manually via the Locize UI, API, or locize-cli.',
|
|
405
|
-
];
|
|
406
|
-
const log = typeof internalLogger.info === 'function'
|
|
407
|
-
? (msg) => internalLogger.info(msg)
|
|
408
|
-
: (msg) => console.log(msg);
|
|
409
|
-
for (const line of lines)
|
|
410
|
-
log(line);
|
|
411
|
-
return funnelMsgTracker.recordFunnelShown('extract');
|
|
412
|
-
}
|
|
413
396
|
|
|
414
397
|
exports.extract = extract;
|
|
415
398
|
exports.preScanFile = preScanFile;
|
package/dist/cjs/status.js
CHANGED
|
@@ -13,11 +13,13 @@ var nesting = require('./utils/nesting.js');
|
|
|
13
13
|
var contextVariants = require('./utils/context-variants.js');
|
|
14
14
|
require('node:module');
|
|
15
15
|
var funnelMsgTracker = require('./utils/funnel-msg-tracker.js');
|
|
16
|
+
var locizeFunnel = require('./utils/locize-funnel.js');
|
|
16
17
|
|
|
17
18
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
19
|
|
|
19
20
|
var ora__default = /*#__PURE__*/_interopDefault(ora);
|
|
20
21
|
|
|
22
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__status';
|
|
21
23
|
function globToRegex(glob) {
|
|
22
24
|
const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
23
25
|
return new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
|
|
@@ -693,7 +695,11 @@ async function displayDetailedLocaleReport(report, config, locale, namespaceFilt
|
|
|
693
695
|
else {
|
|
694
696
|
console.log(node_util.styleText(['green', 'bold'], `\nSummary: 🎉 All keys are translated for "${locale}".`));
|
|
695
697
|
}
|
|
696
|
-
|
|
698
|
+
// Untranslated keys in a secondary locale are the gap Locize fills; absent
|
|
699
|
+
// keys in the primary language are an `extract` problem (handled above).
|
|
700
|
+
const nsData = namespaceFilter ? localeData.namespaces.get(namespaceFilter) : undefined;
|
|
701
|
+
const untranslated = nsData ? nsData.totalKeys - nsData.translatedKeys : missingCount;
|
|
702
|
+
await locizeFunnel.printUntranslatedFunnel('status', isPrimary ? [] : [{ locale, untranslated }], LOCIZE_SIGNUP_URL);
|
|
697
703
|
}
|
|
698
704
|
/**
|
|
699
705
|
* Displays a summary report filtered by a single namespace.
|
|
@@ -728,7 +734,11 @@ async function displayNamespaceSummaryReport(report, config, namespace) {
|
|
|
728
734
|
const { primaryLanguage } = config.extract;
|
|
729
735
|
console.log(node_util.styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${primaryNsData.absentKeys} key(s) that are used in code.`));
|
|
730
736
|
}
|
|
731
|
-
|
|
737
|
+
const gaps = Array.from(report.locales, ([locale, localeData]) => {
|
|
738
|
+
const nsLocaleData = localeData.namespaces.get(namespace);
|
|
739
|
+
return { locale, untranslated: nsLocaleData ? nsLocaleData.totalKeys - nsLocaleData.translatedKeys : 0 };
|
|
740
|
+
});
|
|
741
|
+
await locizeFunnel.printUntranslatedFunnel('status', gaps, LOCIZE_SIGNUP_URL);
|
|
732
742
|
}
|
|
733
743
|
/**
|
|
734
744
|
* Displays the default, high-level summary report for all locales.
|
|
@@ -762,7 +772,8 @@ async function displayOverallSummaryReport(report, config) {
|
|
|
762
772
|
console.log(node_util.styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${report.primary.totalAbsent} key(s) that are used in code.`));
|
|
763
773
|
console.log(node_util.styleText('red', ` Run "i18next-cli status ${primaryLanguage}" for details, or "i18next-cli extract" to add them.`));
|
|
764
774
|
}
|
|
765
|
-
|
|
775
|
+
const gaps = Array.from(report.locales, ([locale, localeData]) => ({ locale, untranslated: localeData.totalKeys - localeData.totalTranslated }));
|
|
776
|
+
await locizeFunnel.printUntranslatedFunnel('status', gaps, LOCIZE_SIGNUP_URL);
|
|
766
777
|
}
|
|
767
778
|
/**
|
|
768
779
|
* Prints a formatted progress bar with label, percentage, and counts.
|
|
@@ -872,14 +883,6 @@ async function runUnusedReport(config, options = {}) {
|
|
|
872
883
|
process.exit(1);
|
|
873
884
|
}
|
|
874
885
|
}
|
|
875
|
-
async function printLocizeFunnel() {
|
|
876
|
-
if (!(await funnelMsgTracker.shouldShowFunnel('status')))
|
|
877
|
-
return;
|
|
878
|
-
console.log(node_util.styleText(['yellow', 'bold'], '\n✨ Take your localization to the next level!'));
|
|
879
|
-
console.log('Manage translations with your team in the cloud with Locize => https://www.locize.com/docs/getting-started');
|
|
880
|
-
console.log(`Run ${node_util.styleText('cyan', 'npx i18next-cli locize-migrate')} to get started.`);
|
|
881
|
-
return funnelMsgTracker.recordFunnelShown('status');
|
|
882
|
-
}
|
|
883
886
|
|
|
884
887
|
exports.runStatus = runStatus;
|
|
885
888
|
exports.runUnusedReport = runUnusedReport;
|
package/dist/cjs/syncer.js
CHANGED
|
@@ -8,11 +8,12 @@ var wrapOra = require('./utils/wrap-ora.js');
|
|
|
8
8
|
var logger = require('./utils/logger.js');
|
|
9
9
|
var defaultValue = require('./utils/default-value.js');
|
|
10
10
|
var fileUtils = require('./utils/file-utils.js');
|
|
11
|
-
var
|
|
11
|
+
var locizeFunnel = require('./utils/locize-funnel.js');
|
|
12
12
|
var nestedObject = require('./utils/nested-object.js');
|
|
13
13
|
var pluralRules = require('./utils/plural-rules.js');
|
|
14
14
|
var gitChangedKeys = require('./utils/git-changed-keys.js');
|
|
15
15
|
|
|
16
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__sync';
|
|
16
17
|
/**
|
|
17
18
|
* Synchronizes translation files across different locales by ensuring all secondary
|
|
18
19
|
* language files contain the same keys as the primary language file.
|
|
@@ -56,6 +57,8 @@ async function runSyncer(config, options = {}) {
|
|
|
56
57
|
const { output, keySeparator = '.', outputFormat = 'json', indentation = 2, defaultValue: defaultValue$1 = '', } = config.extract;
|
|
57
58
|
const logMessages = [];
|
|
58
59
|
let wasAnythingSynced = false;
|
|
60
|
+
// Keys still empty after the sync, per secondary locale (the funnel line's gap)
|
|
61
|
+
const untranslatedByLocale = {};
|
|
59
62
|
// 1. Find all namespace files for the primary language
|
|
60
63
|
const primaryNsPatternRaw = fileUtils.getOutputPath(output, primaryLanguage, '*');
|
|
61
64
|
// Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
|
|
@@ -216,6 +219,8 @@ async function runSyncer(config, options = {}) {
|
|
|
216
219
|
nestedObject.setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
|
|
217
220
|
}
|
|
218
221
|
}
|
|
222
|
+
untranslatedByLocale[lang] = (untranslatedByLocale[lang] ?? 0) + nestedObject.getNestedKeys(newSecondaryTranslations, keySeparator ?? '.')
|
|
223
|
+
.filter(key => nestedObject.getNestedValue(newSecondaryTranslations, key, keySeparator ?? '.') === '').length;
|
|
219
224
|
// Use JSON.stringify for a reliable object comparison, regardless of format
|
|
220
225
|
const oldContent = JSON.stringify(existingSecondaryTranslations);
|
|
221
226
|
const newContent = JSON.stringify(newSecondaryTranslations);
|
|
@@ -236,7 +241,8 @@ async function runSyncer(config, options = {}) {
|
|
|
236
241
|
spinner.succeed(node_util.styleText('bold', 'Synchronization complete!'));
|
|
237
242
|
logMessages.forEach(msg => internalLogger.info ? internalLogger.info(msg) : console.log(msg));
|
|
238
243
|
if (wasAnythingSynced && !options.quiet) {
|
|
239
|
-
|
|
244
|
+
const gaps = Object.entries(untranslatedByLocale).map(([locale, untranslated]) => ({ locale, untranslated }));
|
|
245
|
+
await locizeFunnel.printUntranslatedFunnel('syncer', gaps, LOCIZE_SIGNUP_URL);
|
|
240
246
|
}
|
|
241
247
|
else if (!wasAnythingSynced) {
|
|
242
248
|
if (typeof internalLogger.info === 'function')
|
|
@@ -253,13 +259,5 @@ async function runSyncer(config, options = {}) {
|
|
|
253
259
|
console.error(error);
|
|
254
260
|
}
|
|
255
261
|
}
|
|
256
|
-
async function printLocizeFunnel() {
|
|
257
|
-
if (!(await funnelMsgTracker.shouldShowFunnel('syncer')))
|
|
258
|
-
return;
|
|
259
|
-
console.log(node_util.styleText(['green', 'bold'], '\n✅ Sync complete.'));
|
|
260
|
-
console.log(node_util.styleText('yellow', '🚀 Ready to collaborate with translators? Move your files to the cloud.'));
|
|
261
|
-
console.log(` Get started with the official TMS for i18next: ${node_util.styleText('cyan', 'npx i18next-cli locize-migrate')}`);
|
|
262
|
-
return funnelMsgTracker.recordFunnelShown('syncer');
|
|
263
|
-
}
|
|
264
262
|
|
|
265
263
|
exports.runSyncer = runSyncer;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_util = require('node:util');
|
|
4
|
+
var funnelMsgTracker = require('./funnel-msg-tracker.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prints the one-line Locize funnel for commands that have just reported
|
|
8
|
+
* untranslated keys: the gap, the one command that fills it, and the
|
|
9
|
+
* register link tagged `?from=i18next_cli__<command>` so registrations stay
|
|
10
|
+
* attributable per command.
|
|
11
|
+
*
|
|
12
|
+
* Prints nothing when there is no gap, and is gated like every other funnel
|
|
13
|
+
* message (never in CI/non-TTY, once per 24h per `funnelKey`).
|
|
14
|
+
*/
|
|
15
|
+
async function printUntranslatedFunnel(funnelKey, gaps, signupUrl, log = console.log) {
|
|
16
|
+
const open = gaps.filter(g => g.untranslated > 0);
|
|
17
|
+
const total = open.reduce((sum, g) => sum + g.untranslated, 0);
|
|
18
|
+
if (total === 0)
|
|
19
|
+
return;
|
|
20
|
+
if (!(await funnelMsgTracker.shouldShowFunnel(funnelKey)))
|
|
21
|
+
return;
|
|
22
|
+
const where = open.length === 1
|
|
23
|
+
? `in ${open[0].locale}`
|
|
24
|
+
: `(${open.map(g => `${g.locale} ${g.untranslated}`).join(', ')})`;
|
|
25
|
+
log(`\n💡 ${total} untranslated key${total === 1 ? '' : 's'} ${where}. Translate them with AI in one command: ${node_util.styleText('cyan', 'npx i18next-cli localize')} (sign up: ${signupUrl})`);
|
|
26
|
+
return funnelMsgTracker.recordFunnelShown(funnelKey);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
exports.printUntranslatedFunnel = printUntranslatedFunnel;
|
package/dist/esm/cli.js
CHANGED
|
@@ -31,7 +31,8 @@ const program = new Command();
|
|
|
31
31
|
program
|
|
32
32
|
.name('i18next-cli')
|
|
33
33
|
.description('A unified, high-performance i18next CLI.')
|
|
34
|
-
.version('1.72.
|
|
34
|
+
.version('1.72.4') // This string is replaced with the actual version at build time by rollup
|
|
35
|
+
.addHelpText('after', '\nUsing an AI coding agent? Install the i18next-localization skill: npx skills add i18next/i18next-cli\n');
|
|
35
36
|
// new: global config override option
|
|
36
37
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
37
38
|
program
|
|
@@ -11,8 +11,10 @@ import { extractKeysFromComments } from '../parsers/comment-parser.js';
|
|
|
11
11
|
import { normalizeSpansToCharIndices } from '../parsers/ast-utils.js';
|
|
12
12
|
import { ConsoleLogger } from '../../utils/logger.js';
|
|
13
13
|
import { inferFormatFromPath, loadRawJson5Content, serializeTranslationFile } from '../../utils/file-utils.js';
|
|
14
|
-
import {
|
|
14
|
+
import { printUntranslatedFunnel } from '../../utils/locize-funnel.js';
|
|
15
|
+
import { getNestedKeys, getNestedValue } from '../../utils/nested-object.js';
|
|
15
16
|
|
|
17
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__extract';
|
|
16
18
|
/**
|
|
17
19
|
* Main extractor function that runs the complete key extraction and file generation process.
|
|
18
20
|
*
|
|
@@ -89,8 +91,16 @@ async function runExtractor(config, options = {}) {
|
|
|
89
91
|
: styleText('bold', 'Extraction complete!');
|
|
90
92
|
spinner.succeed(completionMessage);
|
|
91
93
|
// Show the funnel message only if files were actually changed.
|
|
92
|
-
if (anyFileUpdated && !options.isDryRun && !options.quiet)
|
|
93
|
-
|
|
94
|
+
if (anyFileUpdated && !options.isDryRun && !options.quiet) {
|
|
95
|
+
const keySeparator = config.extract.keySeparator ?? '.';
|
|
96
|
+
const countEmpty = (obj) => getNestedKeys(obj, keySeparator).filter(key => getNestedValue(obj, key, keySeparator) === '').length;
|
|
97
|
+
const gaps = (config.extract.secondaryLanguages ?? []).map(locale => ({
|
|
98
|
+
locale,
|
|
99
|
+
untranslated: results.filter(r => r.locale === locale).reduce((sum, r) => sum + countEmpty(r.newTranslations), 0)
|
|
100
|
+
}));
|
|
101
|
+
const log = typeof internalLogger.info === 'function' ? (msg) => internalLogger.info(msg) : undefined;
|
|
102
|
+
await printUntranslatedFunnel('extract', gaps, LOCIZE_SIGNUP_URL, log);
|
|
103
|
+
}
|
|
94
104
|
return { anyFileUpdated, hasErrors: fileErrors.length > 0, results };
|
|
95
105
|
}
|
|
96
106
|
catch (error) {
|
|
@@ -381,32 +391,5 @@ async function extract(config, { syncPrimaryWithDefaults = false } = {}) {
|
|
|
381
391
|
const { allKeys, objectKeys } = await findKeys(config);
|
|
382
392
|
return getTranslations(allKeys, objectKeys, config, { syncPrimaryWithDefaults });
|
|
383
393
|
}
|
|
384
|
-
/**
|
|
385
|
-
* Prints a promotional message for the locize saveMissing workflow.
|
|
386
|
-
* This message is shown after a successful extraction that resulted in changes.
|
|
387
|
-
*/
|
|
388
|
-
async function printLocizeFunnel(logger) {
|
|
389
|
-
if (!(await shouldShowFunnel('extract')))
|
|
390
|
-
return;
|
|
391
|
-
const internalLogger = logger ?? new ConsoleLogger();
|
|
392
|
-
const lines = [
|
|
393
|
-
styleText(['yellow', 'bold'], '\n💡 Tip: Tired of running the extractor manually?'),
|
|
394
|
-
' Discover a real-time "push" workflow with `saveMissing` and Locize AI/MT,',
|
|
395
|
-
' where keys are created and translated automatically as you code.',
|
|
396
|
-
` Learn more: ${styleText('cyan', 'https://www.locize.com/blog/i18next-savemissing-ai-automation')}`,
|
|
397
|
-
` Watch the video: ${styleText('cyan', 'https://youtu.be/joPsZghT3wM')}`,
|
|
398
|
-
'',
|
|
399
|
-
' You can also sync your extracted translations to Locize:',
|
|
400
|
-
` ${styleText('cyan', 'npx i18next-cli locize-sync')} – upload/sync translations to Locize`,
|
|
401
|
-
` ${styleText('cyan', 'npx i18next-cli locize-migrate')} – migrate local translations to Locize`,
|
|
402
|
-
' Or import them manually via the Locize UI, API, or locize-cli.',
|
|
403
|
-
];
|
|
404
|
-
const log = typeof internalLogger.info === 'function'
|
|
405
|
-
? (msg) => internalLogger.info(msg)
|
|
406
|
-
: (msg) => console.log(msg);
|
|
407
|
-
for (const line of lines)
|
|
408
|
-
log(line);
|
|
409
|
-
return recordFunnelShown('extract');
|
|
410
|
-
}
|
|
411
394
|
|
|
412
395
|
export { extract, preScanFile, processFile, runExtractor };
|
package/dist/esm/status.js
CHANGED
|
@@ -11,7 +11,9 @@ import { parseNestedReferences } from './utils/nesting.js';
|
|
|
11
11
|
import { isContextVariantOfAcceptingKey } from './utils/context-variants.js';
|
|
12
12
|
import 'node:module';
|
|
13
13
|
import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker.js';
|
|
14
|
+
import { printUntranslatedFunnel } from './utils/locize-funnel.js';
|
|
14
15
|
|
|
16
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__status';
|
|
15
17
|
function globToRegex(glob) {
|
|
16
18
|
const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
17
19
|
return new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
|
|
@@ -687,7 +689,11 @@ async function displayDetailedLocaleReport(report, config, locale, namespaceFilt
|
|
|
687
689
|
else {
|
|
688
690
|
console.log(styleText(['green', 'bold'], `\nSummary: 🎉 All keys are translated for "${locale}".`));
|
|
689
691
|
}
|
|
690
|
-
|
|
692
|
+
// Untranslated keys in a secondary locale are the gap Locize fills; absent
|
|
693
|
+
// keys in the primary language are an `extract` problem (handled above).
|
|
694
|
+
const nsData = namespaceFilter ? localeData.namespaces.get(namespaceFilter) : undefined;
|
|
695
|
+
const untranslated = nsData ? nsData.totalKeys - nsData.translatedKeys : missingCount;
|
|
696
|
+
await printUntranslatedFunnel('status', isPrimary ? [] : [{ locale, untranslated }], LOCIZE_SIGNUP_URL);
|
|
691
697
|
}
|
|
692
698
|
/**
|
|
693
699
|
* Displays a summary report filtered by a single namespace.
|
|
@@ -722,7 +728,11 @@ async function displayNamespaceSummaryReport(report, config, namespace) {
|
|
|
722
728
|
const { primaryLanguage } = config.extract;
|
|
723
729
|
console.log(styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${primaryNsData.absentKeys} key(s) that are used in code.`));
|
|
724
730
|
}
|
|
725
|
-
|
|
731
|
+
const gaps = Array.from(report.locales, ([locale, localeData]) => {
|
|
732
|
+
const nsLocaleData = localeData.namespaces.get(namespace);
|
|
733
|
+
return { locale, untranslated: nsLocaleData ? nsLocaleData.totalKeys - nsLocaleData.translatedKeys : 0 };
|
|
734
|
+
});
|
|
735
|
+
await printUntranslatedFunnel('status', gaps, LOCIZE_SIGNUP_URL);
|
|
726
736
|
}
|
|
727
737
|
/**
|
|
728
738
|
* Displays the default, high-level summary report for all locales.
|
|
@@ -756,7 +766,8 @@ async function displayOverallSummaryReport(report, config) {
|
|
|
756
766
|
console.log(styleText(['red', 'bold'], `\n⚠ Primary language "${primaryLanguage}" is missing ${report.primary.totalAbsent} key(s) that are used in code.`));
|
|
757
767
|
console.log(styleText('red', ` Run "i18next-cli status ${primaryLanguage}" for details, or "i18next-cli extract" to add them.`));
|
|
758
768
|
}
|
|
759
|
-
|
|
769
|
+
const gaps = Array.from(report.locales, ([locale, localeData]) => ({ locale, untranslated: localeData.totalKeys - localeData.totalTranslated }));
|
|
770
|
+
await printUntranslatedFunnel('status', gaps, LOCIZE_SIGNUP_URL);
|
|
760
771
|
}
|
|
761
772
|
/**
|
|
762
773
|
* Prints a formatted progress bar with label, percentage, and counts.
|
|
@@ -866,13 +877,5 @@ async function runUnusedReport(config, options = {}) {
|
|
|
866
877
|
process.exit(1);
|
|
867
878
|
}
|
|
868
879
|
}
|
|
869
|
-
async function printLocizeFunnel() {
|
|
870
|
-
if (!(await shouldShowFunnel('status')))
|
|
871
|
-
return;
|
|
872
|
-
console.log(styleText(['yellow', 'bold'], '\n✨ Take your localization to the next level!'));
|
|
873
|
-
console.log('Manage translations with your team in the cloud with Locize => https://www.locize.com/docs/getting-started');
|
|
874
|
-
console.log(`Run ${styleText('cyan', 'npx i18next-cli locize-migrate')} to get started.`);
|
|
875
|
-
return recordFunnelShown('status');
|
|
876
|
-
}
|
|
877
880
|
|
|
878
881
|
export { runStatus, runUnusedReport };
|
package/dist/esm/syncer.js
CHANGED
|
@@ -6,11 +6,12 @@ import { createSpinnerLike } from './utils/wrap-ora.js';
|
|
|
6
6
|
import { ConsoleLogger } from './utils/logger.js';
|
|
7
7
|
import { resolveDefaultValue } from './utils/default-value.js';
|
|
8
8
|
import { getOutputPath, loadTranslationFile, inferFormatFromPath, loadRawJson5Content, serializeTranslationFile } from './utils/file-utils.js';
|
|
9
|
-
import {
|
|
9
|
+
import { printUntranslatedFunnel } from './utils/locize-funnel.js';
|
|
10
10
|
import { getNestedKeys, getNestedValue, setNestedValue } from './utils/nested-object.js';
|
|
11
11
|
import { safePluralRules } from './utils/plural-rules.js';
|
|
12
12
|
import { resolveGitCompareRef, readFileAtRef, parseTranslationContent, makeChangedKeyMatcher } from './utils/git-changed-keys.js';
|
|
13
13
|
|
|
14
|
+
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__sync';
|
|
14
15
|
/**
|
|
15
16
|
* Synchronizes translation files across different locales by ensuring all secondary
|
|
16
17
|
* language files contain the same keys as the primary language file.
|
|
@@ -54,6 +55,8 @@ async function runSyncer(config, options = {}) {
|
|
|
54
55
|
const { output, keySeparator = '.', outputFormat = 'json', indentation = 2, defaultValue = '', } = config.extract;
|
|
55
56
|
const logMessages = [];
|
|
56
57
|
let wasAnythingSynced = false;
|
|
58
|
+
// Keys still empty after the sync, per secondary locale (the funnel line's gap)
|
|
59
|
+
const untranslatedByLocale = {};
|
|
57
60
|
// 1. Find all namespace files for the primary language
|
|
58
61
|
const primaryNsPatternRaw = getOutputPath(output, primaryLanguage, '*');
|
|
59
62
|
// Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
|
|
@@ -214,6 +217,8 @@ async function runSyncer(config, options = {}) {
|
|
|
214
217
|
setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
|
|
215
218
|
}
|
|
216
219
|
}
|
|
220
|
+
untranslatedByLocale[lang] = (untranslatedByLocale[lang] ?? 0) + getNestedKeys(newSecondaryTranslations, keySeparator ?? '.')
|
|
221
|
+
.filter(key => getNestedValue(newSecondaryTranslations, key, keySeparator ?? '.') === '').length;
|
|
217
222
|
// Use JSON.stringify for a reliable object comparison, regardless of format
|
|
218
223
|
const oldContent = JSON.stringify(existingSecondaryTranslations);
|
|
219
224
|
const newContent = JSON.stringify(newSecondaryTranslations);
|
|
@@ -234,7 +239,8 @@ async function runSyncer(config, options = {}) {
|
|
|
234
239
|
spinner.succeed(styleText('bold', 'Synchronization complete!'));
|
|
235
240
|
logMessages.forEach(msg => internalLogger.info ? internalLogger.info(msg) : console.log(msg));
|
|
236
241
|
if (wasAnythingSynced && !options.quiet) {
|
|
237
|
-
|
|
242
|
+
const gaps = Object.entries(untranslatedByLocale).map(([locale, untranslated]) => ({ locale, untranslated }));
|
|
243
|
+
await printUntranslatedFunnel('syncer', gaps, LOCIZE_SIGNUP_URL);
|
|
238
244
|
}
|
|
239
245
|
else if (!wasAnythingSynced) {
|
|
240
246
|
if (typeof internalLogger.info === 'function')
|
|
@@ -251,13 +257,5 @@ async function runSyncer(config, options = {}) {
|
|
|
251
257
|
console.error(error);
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
|
-
async function printLocizeFunnel() {
|
|
255
|
-
if (!(await shouldShowFunnel('syncer')))
|
|
256
|
-
return;
|
|
257
|
-
console.log(styleText(['green', 'bold'], '\n✅ Sync complete.'));
|
|
258
|
-
console.log(styleText('yellow', '🚀 Ready to collaborate with translators? Move your files to the cloud.'));
|
|
259
|
-
console.log(` Get started with the official TMS for i18next: ${styleText('cyan', 'npx i18next-cli locize-migrate')}`);
|
|
260
|
-
return recordFunnelShown('syncer');
|
|
261
|
-
}
|
|
262
260
|
|
|
263
261
|
export { runSyncer };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { shouldShowFunnel, recordFunnelShown } from './funnel-msg-tracker.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prints the one-line Locize funnel for commands that have just reported
|
|
6
|
+
* untranslated keys: the gap, the one command that fills it, and the
|
|
7
|
+
* register link tagged `?from=i18next_cli__<command>` so registrations stay
|
|
8
|
+
* attributable per command.
|
|
9
|
+
*
|
|
10
|
+
* Prints nothing when there is no gap, and is gated like every other funnel
|
|
11
|
+
* message (never in CI/non-TTY, once per 24h per `funnelKey`).
|
|
12
|
+
*/
|
|
13
|
+
async function printUntranslatedFunnel(funnelKey, gaps, signupUrl, log = console.log) {
|
|
14
|
+
const open = gaps.filter(g => g.untranslated > 0);
|
|
15
|
+
const total = open.reduce((sum, g) => sum + g.untranslated, 0);
|
|
16
|
+
if (total === 0)
|
|
17
|
+
return;
|
|
18
|
+
if (!(await shouldShowFunnel(funnelKey)))
|
|
19
|
+
return;
|
|
20
|
+
const where = open.length === 1
|
|
21
|
+
? `in ${open[0].locale}`
|
|
22
|
+
: `(${open.map(g => `${g.locale} ${g.untranslated}`).join(', ')})`;
|
|
23
|
+
log(`\n💡 ${total} untranslated key${total === 1 ? '' : 's'} ${where}. Translate them with AI in one command: ${styleText('cyan', 'npx i18next-cli localize')} (sign up: ${signupUrl})`);
|
|
24
|
+
return recordFunnelShown(funnelKey);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { printUntranslatedFunnel };
|
package/package.json
CHANGED
package/types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;AA2d7B,OAAO,EAAE,OAAO,EAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAO5G,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAO5G,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAQ/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,iBAAiB,EAAE,CAAA;CAAE,CAAC,CAiFxF;AAwBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAiJf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,MAAM,GAAE,MAA4B,EACpC,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CA4Df;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAO1K"}
|
package/types/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAmC,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAmC,MAAM,YAAY,CAAA;AAevF;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAyHD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAoDzF;AAqqBD;;GAEG;AACH,UAAU,aAAa;IACrB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuE/F"}
|
package/types/syncer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAU9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,iBAiOzF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Untranslated-key count for one secondary locale. */
|
|
2
|
+
export interface LocaleGap {
|
|
3
|
+
locale: string;
|
|
4
|
+
untranslated: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Prints the one-line Locize funnel for commands that have just reported
|
|
8
|
+
* untranslated keys: the gap, the one command that fills it, and the
|
|
9
|
+
* register link tagged `?from=i18next_cli__<command>` so registrations stay
|
|
10
|
+
* attributable per command.
|
|
11
|
+
*
|
|
12
|
+
* Prints nothing when there is no gap, and is gated like every other funnel
|
|
13
|
+
* message (never in CI/non-TTY, once per 24h per `funnelKey`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function printUntranslatedFunnel(funnelKey: string, gaps: LocaleGap[], signupUrl: string, log?: (msg: string) => void): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=locize-funnel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locize-funnel.d.ts","sourceRoot":"","sources":["../../src/utils/locize-funnel.ts"],"names":[],"mappings":"AAGA,uDAAuD;AACvD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,EAAE,EACjB,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAkB,GACvC,OAAO,CAAC,IAAI,CAAC,CAYf"}
|