i18next-cli 1.41.1 → 1.41.2

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.1'); // This string is replaced with the actual version at build time by rollup
31
+ .version('1.41.2'); // 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 pluralOrder = ['zero', 'one', 'two', 'few', 'many', 'other'];
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 pluralOrder) {
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 : pluralOrder;
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) {
@@ -366,6 +365,49 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
366
365
  nestedObject.setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
367
366
  }
368
367
  }
368
+ // PRESERVE LOCALE-SPECIFIC PLURAL FORMS: When dealing with plural keys in non-primary locales,
369
+ // preserve any existing plural forms that are NOT being explicitly generated.
370
+ // This ensures that locale-specific forms (like _few, _many) added by translators are preserved.
371
+ if (locale !== primaryLanguage && removeUnusedKeys) {
372
+ const existingKeys = nestedObject.getNestedKeys(existingTranslations, keySeparator ?? '.');
373
+ for (const existingKey of existingKeys) {
374
+ // Check if this is a plural form variant (ends with _form)
375
+ let isPluralForm = false;
376
+ let baseKey = existingKey;
377
+ let foundForm = '';
378
+ for (const form of pluralForms) {
379
+ if (existingKey.endsWith(`${pluralSeparator}${form}`)) {
380
+ baseKey = existingKey.slice(0, -(pluralSeparator.length + form.length));
381
+ foundForm = form;
382
+ isPluralForm = true;
383
+ break;
384
+ }
385
+ }
386
+ if (isPluralForm && foundForm) {
387
+ // Check if the base key is in our filtered keys (meaning it's a plural key we're handling)
388
+ const isBaseInExtracted = filteredKeys.some(({ key }) => {
389
+ let extractedBase = key;
390
+ for (const form of pluralForms) {
391
+ if (extractedBase.endsWith(`${pluralSeparator}${form}`)) {
392
+ extractedBase = extractedBase.slice(0, -(pluralSeparator.length + form.length));
393
+ break;
394
+ }
395
+ }
396
+ return extractedBase === baseKey;
397
+ });
398
+ if (isBaseInExtracted) {
399
+ // This is a plural form for a key we're handling.
400
+ // Check if it's already in newTranslations (will be set by the normal flow)
401
+ const isAlreadySet = nestedObject.getNestedValue(newTranslations, existingKey, keySeparator ?? '.') !== undefined;
402
+ if (!isAlreadySet) {
403
+ // This plural form is NOT being generated by our code, so preserve it
404
+ const value = nestedObject.getNestedValue(existingTranslations, existingKey, keySeparator ?? '.');
405
+ nestedObject.setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
406
+ }
407
+ }
408
+ }
409
+ }
410
+ }
369
411
  // SPECIAL HANDLING: Preserve existing _zero forms even if not in extracted keys
370
412
  // This ensures that optional _zero forms are not removed when they exist
371
413
  if (removeUnusedKeys) {
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.1'); // This string is replaced with the actual version at build time by rollup
29
+ .version('1.41.2'); // 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 pluralOrder = ['zero', 'one', 'two', 'few', 'many', 'other'];
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 pluralOrder) {
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 : pluralOrder;
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) {
@@ -364,6 +363,49 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
364
363
  setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
365
364
  }
366
365
  }
366
+ // PRESERVE LOCALE-SPECIFIC PLURAL FORMS: When dealing with plural keys in non-primary locales,
367
+ // preserve any existing plural forms that are NOT being explicitly generated.
368
+ // This ensures that locale-specific forms (like _few, _many) added by translators are preserved.
369
+ if (locale !== primaryLanguage && removeUnusedKeys) {
370
+ const existingKeys = getNestedKeys(existingTranslations, keySeparator ?? '.');
371
+ for (const existingKey of existingKeys) {
372
+ // Check if this is a plural form variant (ends with _form)
373
+ let isPluralForm = false;
374
+ let baseKey = existingKey;
375
+ let foundForm = '';
376
+ for (const form of pluralForms) {
377
+ if (existingKey.endsWith(`${pluralSeparator}${form}`)) {
378
+ baseKey = existingKey.slice(0, -(pluralSeparator.length + form.length));
379
+ foundForm = form;
380
+ isPluralForm = true;
381
+ break;
382
+ }
383
+ }
384
+ if (isPluralForm && foundForm) {
385
+ // Check if the base key is in our filtered keys (meaning it's a plural key we're handling)
386
+ const isBaseInExtracted = filteredKeys.some(({ key }) => {
387
+ let extractedBase = key;
388
+ for (const form of pluralForms) {
389
+ if (extractedBase.endsWith(`${pluralSeparator}${form}`)) {
390
+ extractedBase = extractedBase.slice(0, -(pluralSeparator.length + form.length));
391
+ break;
392
+ }
393
+ }
394
+ return extractedBase === baseKey;
395
+ });
396
+ if (isBaseInExtracted) {
397
+ // This is a plural form for a key we're handling.
398
+ // Check if it's already in newTranslations (will be set by the normal flow)
399
+ const isAlreadySet = getNestedValue(newTranslations, existingKey, keySeparator ?? '.') !== undefined;
400
+ if (!isAlreadySet) {
401
+ // This plural form is NOT being generated by our code, so preserve it
402
+ const value = getNestedValue(existingTranslations, existingKey, keySeparator ?? '.');
403
+ setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.');
404
+ }
405
+ }
406
+ }
407
+ }
408
+ }
367
409
  // SPECIAL HANDLING: Preserve existing _zero forms even if not in extracted keys
368
410
  // This ensures that optional _zero forms are not removed when they exist
369
411
  if (removeUnusedKeys) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.41.1",
3
+ "version": "1.41.2",
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.10",
59
- "@types/react": "19.2.9",
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.10",
73
+ "@swc/core": "1.15.11",
74
74
  "chalk": "5.6.2",
75
75
  "chokidar": "5.0.0",
76
- "commander": "14.0.2",
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.1",
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.3",
86
- "react-i18next": "^16.5.3",
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;AA0rBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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;AA2uBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}