i18next-cli 1.41.1 → 1.41.3
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/dist/cjs/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ const program = new commander.Command();
|
|
|
28
28
|
program
|
|
29
29
|
.name('i18next-cli')
|
|
30
30
|
.description('A unified, high-performance i18next CLI.')
|
|
31
|
-
.version('1.41.
|
|
31
|
+
.version('1.41.3'); // This string is replaced with the actual version at build time by rollup
|
|
32
32
|
// new: global config override option
|
|
33
33
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
34
34
|
program
|
|
@@ -8,6 +8,7 @@ var defaultValue = require('../../utils/default-value.js');
|
|
|
8
8
|
|
|
9
9
|
// used for natural language check
|
|
10
10
|
const chars = [' ', ',', '?', '!', ';'];
|
|
11
|
+
const pluralForms = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
11
12
|
/**
|
|
12
13
|
* Converts a glob pattern to a regular expression for matching keys
|
|
13
14
|
* @param glob - The glob pattern to convert
|
|
@@ -37,7 +38,6 @@ function isContextVariantOfAcceptingKey(existingKey, keysAcceptingContext, plura
|
|
|
37
38
|
// Try to extract the base key from this existing key by removing context and/or plural suffixes
|
|
38
39
|
let potentialBaseKey = existingKey;
|
|
39
40
|
// First, try removing plural suffixes if present
|
|
40
|
-
const pluralForms = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
41
41
|
for (const form of pluralForms) {
|
|
42
42
|
if (potentialBaseKey.endsWith(`${pluralSeparator}${form}`)) {
|
|
43
43
|
potentialBaseKey = potentialBaseKey.slice(0, -(pluralSeparator.length + form.length));
|
|
@@ -97,8 +97,7 @@ function sortObject(obj, config, customSort) {
|
|
|
97
97
|
const sortedObj = {};
|
|
98
98
|
const pluralSeparator = config?.extract?.pluralSeparator ?? '_';
|
|
99
99
|
// Define the canonical order for plural forms
|
|
100
|
-
const
|
|
101
|
-
const ordinalPluralOrder = pluralOrder.map(form => `ordinal${pluralSeparator}${form}`);
|
|
100
|
+
const ordinalPluralOrder = pluralForms.map(form => `ordinal${pluralSeparator}${form}`);
|
|
102
101
|
const keys = Object.keys(obj).sort((a, b) => {
|
|
103
102
|
// Helper function to extract base key and form info
|
|
104
103
|
const getKeyInfo = (key) => {
|
|
@@ -110,7 +109,7 @@ function sortObject(obj, config, customSort) {
|
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
// Handle cardinal plurals: key_form or key_context_form
|
|
113
|
-
for (const form of
|
|
112
|
+
for (const form of pluralForms) {
|
|
114
113
|
if (key.endsWith(`${pluralSeparator}${form}`)) {
|
|
115
114
|
const base = key.slice(0, -(pluralSeparator.length + form.length));
|
|
116
115
|
return { base, form, isOrdinal: false, isPlural: true, fullKey: key };
|
|
@@ -135,7 +134,7 @@ function sortObject(obj, config, customSort) {
|
|
|
135
134
|
return aInfo.isOrdinal ? 1 : -1;
|
|
136
135
|
}
|
|
137
136
|
// Both same type (cardinal or ordinal), sort by canonical order
|
|
138
|
-
const orderArray = aInfo.isOrdinal ? ordinalPluralOrder :
|
|
137
|
+
const orderArray = aInfo.isOrdinal ? ordinalPluralOrder : pluralForms;
|
|
139
138
|
const aIndex = orderArray.indexOf(aInfo.form);
|
|
140
139
|
const bIndex = orderArray.indexOf(bInfo.form);
|
|
141
140
|
if (aIndex !== -1 && bIndex !== -1) {
|
|
@@ -218,16 +217,14 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
218
217
|
}
|
|
219
218
|
}
|
|
220
219
|
}
|
|
221
|
-
// Get the plural categories for the target language
|
|
220
|
+
// Get the plural categories for the target language (only used for filtering extracted keys)
|
|
222
221
|
const targetLanguagePluralCategories = new Set();
|
|
223
222
|
// Track cardinal plural categories separately so we can special-case single-"other" languages
|
|
224
223
|
let cardinalCategories = [];
|
|
225
|
-
let ordinalCategories = [];
|
|
226
224
|
try {
|
|
227
225
|
const cardinalRules = new Intl.PluralRules(locale, { type: 'cardinal' });
|
|
228
226
|
const ordinalRules = new Intl.PluralRules(locale, { type: 'ordinal' });
|
|
229
227
|
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
|
|
230
|
-
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
|
|
231
228
|
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
|
|
232
229
|
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
|
|
233
230
|
}
|
|
@@ -237,7 +234,6 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
237
234
|
const cardinalRules = new Intl.PluralRules(fallbackLang, { type: 'cardinal' });
|
|
238
235
|
const ordinalRules = new Intl.PluralRules(fallbackLang, { type: 'ordinal' });
|
|
239
236
|
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
|
|
240
|
-
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
|
|
241
237
|
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
|
|
242
238
|
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
|
|
243
239
|
}
|
|
@@ -366,6 +362,49 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
366
362
|
nestedObject.setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
|
|
367
363
|
}
|
|
368
364
|
}
|
|
365
|
+
// PRESERVE LOCALE-SPECIFIC PLURAL FORMS: When dealing with plural keys in non-primary locales,
|
|
366
|
+
// preserve any existing plural forms that are NOT being explicitly generated.
|
|
367
|
+
// This ensures that locale-specific forms (like _few, _many) added by translators are preserved.
|
|
368
|
+
if (locale !== primaryLanguage && removeUnusedKeys) {
|
|
369
|
+
const existingKeys = nestedObject.getNestedKeys(existingTranslations, keySeparator ?? '.');
|
|
370
|
+
for (const existingKey of existingKeys) {
|
|
371
|
+
// Check if this is a plural form variant (ends with _form)
|
|
372
|
+
let isPluralForm = false;
|
|
373
|
+
let baseKey = existingKey;
|
|
374
|
+
let foundForm = '';
|
|
375
|
+
for (const form of pluralForms) {
|
|
376
|
+
if (existingKey.endsWith(`${pluralSeparator}${form}`)) {
|
|
377
|
+
baseKey = existingKey.slice(0, -(pluralSeparator.length + form.length));
|
|
378
|
+
foundForm = form;
|
|
379
|
+
isPluralForm = true;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (isPluralForm && foundForm) {
|
|
384
|
+
// Check if the base key is in our filtered keys (meaning it's a plural key we're handling)
|
|
385
|
+
const isBaseInExtracted = filteredKeys.some(({ key }) => {
|
|
386
|
+
let extractedBase = key;
|
|
387
|
+
for (const form of pluralForms) {
|
|
388
|
+
if (extractedBase.endsWith(`${pluralSeparator}${form}`)) {
|
|
389
|
+
extractedBase = extractedBase.slice(0, -(pluralSeparator.length + form.length));
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return extractedBase === baseKey;
|
|
394
|
+
});
|
|
395
|
+
if (isBaseInExtracted) {
|
|
396
|
+
// This is a plural form for a key we're handling.
|
|
397
|
+
// Check if it's already in newTranslations (will be set by the normal flow)
|
|
398
|
+
const isAlreadySet = nestedObject.getNestedValue(newTranslations, existingKey, keySeparator ?? '.') !== undefined;
|
|
399
|
+
if (!isAlreadySet) {
|
|
400
|
+
// This plural form is NOT being generated by our code, so preserve it
|
|
401
|
+
const value = nestedObject.getNestedValue(existingTranslations, existingKey, keySeparator ?? '.');
|
|
402
|
+
nestedObject.setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
369
408
|
// SPECIAL HANDLING: Preserve existing _zero forms even if not in extracted keys
|
|
370
409
|
// This ensures that optional _zero forms are not removed when they exist
|
|
371
410
|
if (removeUnusedKeys) {
|
|
@@ -408,49 +447,104 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
408
447
|
continue;
|
|
409
448
|
}
|
|
410
449
|
}
|
|
411
|
-
// If this is a base plural key (no explicit suffix)
|
|
412
|
-
//
|
|
413
|
-
//
|
|
450
|
+
// If this is a base plural key (no explicit suffix), expand it into locale-specific plural variants.
|
|
451
|
+
// For non-primary locales, we generate forms for that specific locale from CLDR.
|
|
452
|
+
// Additionally, we generate empty placeholders for ALL other CLDR forms not in the target locale
|
|
453
|
+
// (so translators can add them manually if needed).
|
|
414
454
|
if (hasCount && !isExpandedPlural) {
|
|
415
455
|
const parts = String(key).split(pluralSeparator);
|
|
416
456
|
const isBaseKey = parts.length === 1;
|
|
417
|
-
if (isBaseKey
|
|
457
|
+
if (isBaseKey) {
|
|
418
458
|
// If explicit expanded variants exist, do not expand the base.
|
|
419
459
|
const base = key;
|
|
420
460
|
if (expandedBases.has(base)) ;
|
|
421
461
|
else {
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
const
|
|
431
|
-
//
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
462
|
+
// Determine which plural forms to generate
|
|
463
|
+
let formsToGenerate;
|
|
464
|
+
if (locale !== primaryLanguage) {
|
|
465
|
+
// For non-primary locales:
|
|
466
|
+
// 1. Generate the forms that locale actually needs
|
|
467
|
+
formsToGenerate = cardinalCategories;
|
|
468
|
+
// 2. Also prepare empty placeholders for all OTHER CLDR forms not in this locale
|
|
469
|
+
// so translators can add them manually without --sync-primary removing them
|
|
470
|
+
const otherForms = pluralForms.filter(f => !cardinalCategories.includes(f));
|
|
471
|
+
// Process the locale-specific forms normally
|
|
472
|
+
for (const form of formsToGenerate) {
|
|
473
|
+
const finalKey = isOrdinal
|
|
474
|
+
? `${base}${pluralSeparator}${form}`
|
|
475
|
+
: `${base}${pluralSeparator}${form}`;
|
|
476
|
+
const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
|
|
477
|
+
const existingVariantValue = nestedObject.getNestedValue(existingTranslations, finalKey, separator);
|
|
478
|
+
if (existingVariantValue === undefined) {
|
|
479
|
+
// Use the default value for secondary locale forms
|
|
480
|
+
let resolvedValue;
|
|
481
|
+
if (typeof defaultValue$1 === 'string') {
|
|
482
|
+
resolvedValue = defaultValue$1;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
resolvedValue = defaultValue.resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
|
|
486
|
+
}
|
|
487
|
+
nestedObject.setNestedValue(newTranslations, finalKey, resolvedValue, separator);
|
|
439
488
|
}
|
|
440
489
|
else {
|
|
441
|
-
|
|
442
|
-
resolvedValue = defaultValue.resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
|
|
490
|
+
nestedObject.setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
|
|
443
491
|
}
|
|
444
|
-
|
|
492
|
+
}
|
|
493
|
+
// Now process other CLDR forms: set empty placeholders for forms this locale doesn't use
|
|
494
|
+
// but preserve any that were manually added by translators
|
|
495
|
+
for (const form of otherForms) {
|
|
496
|
+
const finalKey = isOrdinal
|
|
497
|
+
? `${base}${pluralSeparator}${form}`
|
|
498
|
+
: `${base}${pluralSeparator}${form}`;
|
|
499
|
+
const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
|
|
500
|
+
const existingVariantValue = nestedObject.getNestedValue(existingTranslations, finalKey, separator);
|
|
501
|
+
if (existingVariantValue !== undefined) {
|
|
502
|
+
// Preserve manually-added forms
|
|
503
|
+
nestedObject.setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
|
|
504
|
+
}
|
|
505
|
+
// Don't generate empty placeholders - only generate what the locale needs and preserve what's manual
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// For primary language, only expand if it has multiple plural forms
|
|
510
|
+
// Single-"other" languages (ja, zh, ko) should NOT expand the base key
|
|
511
|
+
if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other') {
|
|
512
|
+
// Single-"other" language - don't expand, keep just the base key
|
|
513
|
+
formsToGenerate = [];
|
|
445
514
|
}
|
|
446
515
|
else {
|
|
447
|
-
//
|
|
448
|
-
|
|
516
|
+
// Multi-form language - expand to its plural forms
|
|
517
|
+
formsToGenerate = cardinalCategories;
|
|
518
|
+
for (const form of formsToGenerate) {
|
|
519
|
+
const finalKey = isOrdinal
|
|
520
|
+
? `${base}${pluralSeparator}${form}`
|
|
521
|
+
: `${base}${pluralSeparator}${form}`;
|
|
522
|
+
const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
|
|
523
|
+
const existingVariantValue = nestedObject.getNestedValue(existingTranslations, finalKey, separator);
|
|
524
|
+
if (existingVariantValue === undefined) {
|
|
525
|
+
// Prefer explicit defaultValue extracted for this key; fall back to configured defaultValue
|
|
526
|
+
let resolvedValue;
|
|
527
|
+
if (typeof defaultValue$1 === 'string') {
|
|
528
|
+
resolvedValue = defaultValue$1;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
resolvedValue = defaultValue.resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue$1);
|
|
532
|
+
}
|
|
533
|
+
nestedObject.setNestedValue(newTranslations, finalKey, resolvedValue, separator);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
nestedObject.setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
449
539
|
}
|
|
450
540
|
}
|
|
541
|
+
if (formsToGenerate && formsToGenerate.length > 0) {
|
|
542
|
+
// We've handled expansion for this base key; skip the normal single-key handling.
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
// else: formsToGenerate is empty (single-"other" primary language)
|
|
546
|
+
// Fall through to normal key handling below
|
|
451
547
|
}
|
|
452
|
-
// We've expanded variants for this base key; skip the normal single-key handling.
|
|
453
|
-
continue;
|
|
454
548
|
}
|
|
455
549
|
}
|
|
456
550
|
// If the key looks like a serialized Trans component (starts with <), treat it as a flat key
|
package/dist/esm/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ const program = new Command();
|
|
|
26
26
|
program
|
|
27
27
|
.name('i18next-cli')
|
|
28
28
|
.description('A unified, high-performance i18next CLI.')
|
|
29
|
-
.version('1.41.
|
|
29
|
+
.version('1.41.3'); // This string is replaced with the actual version at build time by rollup
|
|
30
30
|
// new: global config override option
|
|
31
31
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
32
32
|
program
|
|
@@ -6,6 +6,7 @@ import { resolveDefaultValue } from '../../utils/default-value.js';
|
|
|
6
6
|
|
|
7
7
|
// used for natural language check
|
|
8
8
|
const chars = [' ', ',', '?', '!', ';'];
|
|
9
|
+
const pluralForms = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
9
10
|
/**
|
|
10
11
|
* Converts a glob pattern to a regular expression for matching keys
|
|
11
12
|
* @param glob - The glob pattern to convert
|
|
@@ -35,7 +36,6 @@ function isContextVariantOfAcceptingKey(existingKey, keysAcceptingContext, plura
|
|
|
35
36
|
// Try to extract the base key from this existing key by removing context and/or plural suffixes
|
|
36
37
|
let potentialBaseKey = existingKey;
|
|
37
38
|
// First, try removing plural suffixes if present
|
|
38
|
-
const pluralForms = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
39
39
|
for (const form of pluralForms) {
|
|
40
40
|
if (potentialBaseKey.endsWith(`${pluralSeparator}${form}`)) {
|
|
41
41
|
potentialBaseKey = potentialBaseKey.slice(0, -(pluralSeparator.length + form.length));
|
|
@@ -95,8 +95,7 @@ function sortObject(obj, config, customSort) {
|
|
|
95
95
|
const sortedObj = {};
|
|
96
96
|
const pluralSeparator = config?.extract?.pluralSeparator ?? '_';
|
|
97
97
|
// Define the canonical order for plural forms
|
|
98
|
-
const
|
|
99
|
-
const ordinalPluralOrder = pluralOrder.map(form => `ordinal${pluralSeparator}${form}`);
|
|
98
|
+
const ordinalPluralOrder = pluralForms.map(form => `ordinal${pluralSeparator}${form}`);
|
|
100
99
|
const keys = Object.keys(obj).sort((a, b) => {
|
|
101
100
|
// Helper function to extract base key and form info
|
|
102
101
|
const getKeyInfo = (key) => {
|
|
@@ -108,7 +107,7 @@ function sortObject(obj, config, customSort) {
|
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
// Handle cardinal plurals: key_form or key_context_form
|
|
111
|
-
for (const form of
|
|
110
|
+
for (const form of pluralForms) {
|
|
112
111
|
if (key.endsWith(`${pluralSeparator}${form}`)) {
|
|
113
112
|
const base = key.slice(0, -(pluralSeparator.length + form.length));
|
|
114
113
|
return { base, form, isOrdinal: false, isPlural: true, fullKey: key };
|
|
@@ -133,7 +132,7 @@ function sortObject(obj, config, customSort) {
|
|
|
133
132
|
return aInfo.isOrdinal ? 1 : -1;
|
|
134
133
|
}
|
|
135
134
|
// Both same type (cardinal or ordinal), sort by canonical order
|
|
136
|
-
const orderArray = aInfo.isOrdinal ? ordinalPluralOrder :
|
|
135
|
+
const orderArray = aInfo.isOrdinal ? ordinalPluralOrder : pluralForms;
|
|
137
136
|
const aIndex = orderArray.indexOf(aInfo.form);
|
|
138
137
|
const bIndex = orderArray.indexOf(bInfo.form);
|
|
139
138
|
if (aIndex !== -1 && bIndex !== -1) {
|
|
@@ -216,16 +215,14 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
217
|
}
|
|
219
|
-
// Get the plural categories for the target language
|
|
218
|
+
// Get the plural categories for the target language (only used for filtering extracted keys)
|
|
220
219
|
const targetLanguagePluralCategories = new Set();
|
|
221
220
|
// Track cardinal plural categories separately so we can special-case single-"other" languages
|
|
222
221
|
let cardinalCategories = [];
|
|
223
|
-
let ordinalCategories = [];
|
|
224
222
|
try {
|
|
225
223
|
const cardinalRules = new Intl.PluralRules(locale, { type: 'cardinal' });
|
|
226
224
|
const ordinalRules = new Intl.PluralRules(locale, { type: 'ordinal' });
|
|
227
225
|
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
|
|
228
|
-
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
|
|
229
226
|
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
|
|
230
227
|
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
|
|
231
228
|
}
|
|
@@ -235,7 +232,6 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
235
232
|
const cardinalRules = new Intl.PluralRules(fallbackLang, { type: 'cardinal' });
|
|
236
233
|
const ordinalRules = new Intl.PluralRules(fallbackLang, { type: 'ordinal' });
|
|
237
234
|
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories;
|
|
238
|
-
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories;
|
|
239
235
|
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat));
|
|
240
236
|
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`));
|
|
241
237
|
}
|
|
@@ -364,6 +360,49 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
364
360
|
setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
|
|
365
361
|
}
|
|
366
362
|
}
|
|
363
|
+
// PRESERVE LOCALE-SPECIFIC PLURAL FORMS: When dealing with plural keys in non-primary locales,
|
|
364
|
+
// preserve any existing plural forms that are NOT being explicitly generated.
|
|
365
|
+
// This ensures that locale-specific forms (like _few, _many) added by translators are preserved.
|
|
366
|
+
if (locale !== primaryLanguage && removeUnusedKeys) {
|
|
367
|
+
const existingKeys = getNestedKeys(existingTranslations, keySeparator ?? '.');
|
|
368
|
+
for (const existingKey of existingKeys) {
|
|
369
|
+
// Check if this is a plural form variant (ends with _form)
|
|
370
|
+
let isPluralForm = false;
|
|
371
|
+
let baseKey = existingKey;
|
|
372
|
+
let foundForm = '';
|
|
373
|
+
for (const form of pluralForms) {
|
|
374
|
+
if (existingKey.endsWith(`${pluralSeparator}${form}`)) {
|
|
375
|
+
baseKey = existingKey.slice(0, -(pluralSeparator.length + form.length));
|
|
376
|
+
foundForm = form;
|
|
377
|
+
isPluralForm = true;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (isPluralForm && foundForm) {
|
|
382
|
+
// Check if the base key is in our filtered keys (meaning it's a plural key we're handling)
|
|
383
|
+
const isBaseInExtracted = filteredKeys.some(({ key }) => {
|
|
384
|
+
let extractedBase = key;
|
|
385
|
+
for (const form of pluralForms) {
|
|
386
|
+
if (extractedBase.endsWith(`${pluralSeparator}${form}`)) {
|
|
387
|
+
extractedBase = extractedBase.slice(0, -(pluralSeparator.length + form.length));
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return extractedBase === baseKey;
|
|
392
|
+
});
|
|
393
|
+
if (isBaseInExtracted) {
|
|
394
|
+
// This is a plural form for a key we're handling.
|
|
395
|
+
// Check if it's already in newTranslations (will be set by the normal flow)
|
|
396
|
+
const isAlreadySet = getNestedValue(newTranslations, existingKey, keySeparator ?? '.') !== undefined;
|
|
397
|
+
if (!isAlreadySet) {
|
|
398
|
+
// This plural form is NOT being generated by our code, so preserve it
|
|
399
|
+
const value = getNestedValue(existingTranslations, existingKey, keySeparator ?? '.');
|
|
400
|
+
setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
367
406
|
// SPECIAL HANDLING: Preserve existing _zero forms even if not in extracted keys
|
|
368
407
|
// This ensures that optional _zero forms are not removed when they exist
|
|
369
408
|
if (removeUnusedKeys) {
|
|
@@ -406,49 +445,104 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
406
445
|
continue;
|
|
407
446
|
}
|
|
408
447
|
}
|
|
409
|
-
// If this is a base plural key (no explicit suffix)
|
|
410
|
-
//
|
|
411
|
-
//
|
|
448
|
+
// If this is a base plural key (no explicit suffix), expand it into locale-specific plural variants.
|
|
449
|
+
// For non-primary locales, we generate forms for that specific locale from CLDR.
|
|
450
|
+
// Additionally, we generate empty placeholders for ALL other CLDR forms not in the target locale
|
|
451
|
+
// (so translators can add them manually if needed).
|
|
412
452
|
if (hasCount && !isExpandedPlural) {
|
|
413
453
|
const parts = String(key).split(pluralSeparator);
|
|
414
454
|
const isBaseKey = parts.length === 1;
|
|
415
|
-
if (isBaseKey
|
|
455
|
+
if (isBaseKey) {
|
|
416
456
|
// If explicit expanded variants exist, do not expand the base.
|
|
417
457
|
const base = key;
|
|
418
458
|
if (expandedBases.has(base)) ;
|
|
419
459
|
else {
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
const
|
|
429
|
-
//
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
460
|
+
// Determine which plural forms to generate
|
|
461
|
+
let formsToGenerate;
|
|
462
|
+
if (locale !== primaryLanguage) {
|
|
463
|
+
// For non-primary locales:
|
|
464
|
+
// 1. Generate the forms that locale actually needs
|
|
465
|
+
formsToGenerate = cardinalCategories;
|
|
466
|
+
// 2. Also prepare empty placeholders for all OTHER CLDR forms not in this locale
|
|
467
|
+
// so translators can add them manually without --sync-primary removing them
|
|
468
|
+
const otherForms = pluralForms.filter(f => !cardinalCategories.includes(f));
|
|
469
|
+
// Process the locale-specific forms normally
|
|
470
|
+
for (const form of formsToGenerate) {
|
|
471
|
+
const finalKey = isOrdinal
|
|
472
|
+
? `${base}${pluralSeparator}${form}`
|
|
473
|
+
: `${base}${pluralSeparator}${form}`;
|
|
474
|
+
const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
|
|
475
|
+
const existingVariantValue = getNestedValue(existingTranslations, finalKey, separator);
|
|
476
|
+
if (existingVariantValue === undefined) {
|
|
477
|
+
// Use the default value for secondary locale forms
|
|
478
|
+
let resolvedValue;
|
|
479
|
+
if (typeof defaultValue === 'string') {
|
|
480
|
+
resolvedValue = defaultValue;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
resolvedValue = resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
|
|
484
|
+
}
|
|
485
|
+
setNestedValue(newTranslations, finalKey, resolvedValue, separator);
|
|
437
486
|
}
|
|
438
487
|
else {
|
|
439
|
-
|
|
440
|
-
resolvedValue = resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
|
|
488
|
+
setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
|
|
441
489
|
}
|
|
442
|
-
|
|
490
|
+
}
|
|
491
|
+
// Now process other CLDR forms: set empty placeholders for forms this locale doesn't use
|
|
492
|
+
// but preserve any that were manually added by translators
|
|
493
|
+
for (const form of otherForms) {
|
|
494
|
+
const finalKey = isOrdinal
|
|
495
|
+
? `${base}${pluralSeparator}${form}`
|
|
496
|
+
: `${base}${pluralSeparator}${form}`;
|
|
497
|
+
const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
|
|
498
|
+
const existingVariantValue = getNestedValue(existingTranslations, finalKey, separator);
|
|
499
|
+
if (existingVariantValue !== undefined) {
|
|
500
|
+
// Preserve manually-added forms
|
|
501
|
+
setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
|
|
502
|
+
}
|
|
503
|
+
// Don't generate empty placeholders - only generate what the locale needs and preserve what's manual
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// For primary language, only expand if it has multiple plural forms
|
|
508
|
+
// Single-"other" languages (ja, zh, ko) should NOT expand the base key
|
|
509
|
+
if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other') {
|
|
510
|
+
// Single-"other" language - don't expand, keep just the base key
|
|
511
|
+
formsToGenerate = [];
|
|
443
512
|
}
|
|
444
513
|
else {
|
|
445
|
-
//
|
|
446
|
-
|
|
514
|
+
// Multi-form language - expand to its plural forms
|
|
515
|
+
formsToGenerate = cardinalCategories;
|
|
516
|
+
for (const form of formsToGenerate) {
|
|
517
|
+
const finalKey = isOrdinal
|
|
518
|
+
? `${base}${pluralSeparator}${form}`
|
|
519
|
+
: `${base}${pluralSeparator}${form}`;
|
|
520
|
+
const separator = finalKey.startsWith('<') ? false : (keySeparator ?? '.');
|
|
521
|
+
const existingVariantValue = getNestedValue(existingTranslations, finalKey, separator);
|
|
522
|
+
if (existingVariantValue === undefined) {
|
|
523
|
+
// Prefer explicit defaultValue extracted for this key; fall back to configured defaultValue
|
|
524
|
+
let resolvedValue;
|
|
525
|
+
if (typeof defaultValue === 'string') {
|
|
526
|
+
resolvedValue = defaultValue;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
resolvedValue = resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue);
|
|
530
|
+
}
|
|
531
|
+
setNestedValue(newTranslations, finalKey, resolvedValue, separator);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
setNestedValue(newTranslations, finalKey, existingVariantValue, separator);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
447
537
|
}
|
|
448
538
|
}
|
|
539
|
+
if (formsToGenerate && formsToGenerate.length > 0) {
|
|
540
|
+
// We've handled expansion for this base key; skip the normal single-key handling.
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
// else: formsToGenerate is empty (single-"other" primary language)
|
|
544
|
+
// Fall through to normal key handling below
|
|
449
545
|
}
|
|
450
|
-
// We've expanded variants for this base key; skip the normal single-key handling.
|
|
451
|
-
continue;
|
|
452
546
|
}
|
|
453
547
|
}
|
|
454
548
|
// If the key looks like a serialized Trans component (starts with <), treat it as a flat key
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18next-cli",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.3",
|
|
4
4
|
"description": "A unified, high-performance i18next CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"@rollup/plugin-replace": "6.0.3",
|
|
56
56
|
"@rollup/plugin-terser": "0.4.4",
|
|
57
57
|
"@types/inquirer": "9.0.9",
|
|
58
|
-
"@types/node": "25.0
|
|
59
|
-
"@types/react": "19.2.
|
|
58
|
+
"@types/node": "25.2.0",
|
|
59
|
+
"@types/react": "19.2.10",
|
|
60
60
|
"@vitest/coverage-v8": "4.0.18",
|
|
61
61
|
"eslint": "9.39.2",
|
|
62
62
|
"eslint-plugin-import": "2.32.0",
|
|
@@ -70,20 +70,20 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@croct/json5-parser": "0.2.2",
|
|
73
|
-
"@swc/core": "1.15.
|
|
73
|
+
"@swc/core": "1.15.11",
|
|
74
74
|
"chalk": "5.6.2",
|
|
75
75
|
"chokidar": "5.0.0",
|
|
76
|
-
"commander": "14.0.
|
|
76
|
+
"commander": "14.0.3",
|
|
77
77
|
"execa": "9.6.1",
|
|
78
78
|
"glob": "13.0.0",
|
|
79
79
|
"i18next-resources-for-ts": "2.0.0",
|
|
80
|
-
"inquirer": "13.2.
|
|
80
|
+
"inquirer": "13.2.2",
|
|
81
81
|
"jiti": "2.6.1",
|
|
82
82
|
"jsonc-parser": "3.3.1",
|
|
83
83
|
"minimatch": "10.1.1",
|
|
84
84
|
"ora": "9.1.0",
|
|
85
|
-
"react": "^19.2.
|
|
86
|
-
"react-i18next": "^16.5.
|
|
85
|
+
"react": "^19.2.4",
|
|
86
|
+
"react-i18next": "^16.5.4",
|
|
87
87
|
"swc-walk": "1.0.1"
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgyBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA0I9B"}
|