i18next-cli 1.49.4 → 1.49.6

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
@@ -31,7 +31,7 @@ const program = new commander.Command();
31
31
  program
32
32
  .name('i18next-cli')
33
33
  .description('A unified, high-performance i18next CLI.')
34
- .version('1.49.4'); // This string is replaced with the actual version at build time by rollup
34
+ .version('1.49.6'); // This string is replaced with the actual version at build time by rollup
35
35
  // new: global config override option
36
36
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
37
37
  program
@@ -79,12 +79,66 @@ async function runSyncer(config, options = {}) {
79
79
  continue;
80
80
  }
81
81
  const primaryKeys = nestedObject.getNestedKeys(primaryTranslations, keySeparator ?? '.');
82
+ const primaryKeySet = new Set(primaryKeys);
82
83
  // 3. For each secondary language, sync the current namespace
83
84
  for (const lang of secondaryLanguages) {
84
85
  const secondaryPath = fileUtils.getOutputPath(output, lang, ns);
85
86
  const fullSecondaryPath = node_path.resolve(process.cwd(), secondaryPath);
86
87
  const existingSecondaryTranslations = await fileUtils.loadTranslationFile(fullSecondaryPath) || {};
87
88
  const newSecondaryTranslations = {};
89
+ // Determine CLDR plural categories for this specific secondary locale so
90
+ // we can recognise locale-specific plural suffixes (e.g. `_many` for fr/es)
91
+ // that are not present in the primary language and must not be discarded.
92
+ const sep = config.extract.pluralSeparator ?? '_';
93
+ const localeCardinalCategories = (() => {
94
+ try {
95
+ return new Set(new Intl.PluralRules(lang, { type: 'cardinal' }).resolvedOptions().pluralCategories);
96
+ }
97
+ catch {
98
+ return new Set(['one', 'other']);
99
+ }
100
+ })();
101
+ const localeOrdinalCategories = (() => {
102
+ try {
103
+ return new Set(new Intl.PluralRules(lang, { type: 'ordinal' }).resolvedOptions().pluralCategories);
104
+ }
105
+ catch {
106
+ return new Set(['one', 'other', 'two', 'few']);
107
+ }
108
+ })();
109
+ /**
110
+ * Returns true when `key` is a plural variant that is:
111
+ * 1. Valid for this locale's CLDR rules (cardinal or ordinal), AND
112
+ * 2. Derived from a base key that exists in the primary locale.
113
+ *
114
+ * This handles both cardinal (`title_many`) and ordinal
115
+ * (`place_ordinal_few`) suffixes so neither gets erased during sync.
116
+ */
117
+ const isLocaleSpecificPluralExtension = (key) => {
118
+ // Cardinal: key ends with `{sep}{category}` and base key is in primary
119
+ for (const cat of localeCardinalCategories) {
120
+ const suffix = `${sep}${cat}`;
121
+ if (key.endsWith(suffix)) {
122
+ const base = key.slice(0, -suffix.length);
123
+ // The base itself, or any primary key that starts with `{base}{sep}`,
124
+ // confirms this is a plural family rooted in the primary locale.
125
+ if (primaryKeySet.has(base) || primaryKeys.some(pk => pk.startsWith(`${base}${sep}`))) {
126
+ return true;
127
+ }
128
+ }
129
+ }
130
+ // Ordinal: key ends with `{sep}ordinal{sep}{category}`
131
+ for (const cat of localeOrdinalCategories) {
132
+ const suffix = `${sep}ordinal${sep}${cat}`;
133
+ if (key.endsWith(suffix)) {
134
+ const base = key.slice(0, -suffix.length);
135
+ if (primaryKeySet.has(base) || primaryKeys.some(pk => pk.startsWith(`${base}${sep}`))) {
136
+ return true;
137
+ }
138
+ }
139
+ }
140
+ return false;
141
+ };
88
142
  for (const key of primaryKeys) {
89
143
  const primaryValue = nestedObject.getNestedValue(primaryTranslations, key, keySeparator ?? '.');
90
144
  const existingValue = nestedObject.getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
@@ -92,6 +146,23 @@ async function runSyncer(config, options = {}) {
92
146
  const valueToSet = existingValue ?? defaultValue.resolveDefaultValue(defaultValue$1, key, ns, lang, primaryValue);
93
147
  nestedObject.setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
94
148
  }
149
+ // Preserve locale-specific plural forms that exist in the secondary file
150
+ // but are absent from the primary locale (e.g. `_many` for French/Spanish
151
+ // when the primary language is English, which has no `_many` category).
152
+ // Without this pass the syncer would silently drop those keys, causing
153
+ // `status` to immediately report them as missing.
154
+ const existingSecondaryKeys = nestedObject.getNestedKeys(existingSecondaryTranslations, keySeparator ?? '.');
155
+ for (const key of existingSecondaryKeys) {
156
+ if (!primaryKeySet.has(key) && isLocaleSpecificPluralExtension(key)) {
157
+ const existingValue = nestedObject.getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
158
+ // Only preserve non-empty values; an empty string was likely a
159
+ // placeholder left by a previous (buggy) sync run and should not
160
+ // be perpetuated.
161
+ if (existingValue !== '' && existingValue != null) {
162
+ nestedObject.setNestedValue(newSecondaryTranslations, key, existingValue, keySeparator ?? '.');
163
+ }
164
+ }
165
+ }
95
166
  // Use JSON.stringify for a reliable object comparison, regardless of format
96
167
  const oldContent = JSON.stringify(existingSecondaryTranslations);
97
168
  const newContent = JSON.stringify(newSecondaryTranslations);
@@ -27,10 +27,10 @@ async function loadFile(file) {
27
27
  type: 'commonjs'
28
28
  }
29
29
  });
30
- const exports = {};
31
- const module = { exports };
30
+ const exports$1 = {};
31
+ const module = { exports: exports$1 };
32
32
  const context = vm.createContext({
33
- exports,
33
+ exports: exports$1,
34
34
  module,
35
35
  require: (id) => require(id),
36
36
  console,
package/dist/esm/cli.js CHANGED
@@ -29,7 +29,7 @@ const program = new Command();
29
29
  program
30
30
  .name('i18next-cli')
31
31
  .description('A unified, high-performance i18next CLI.')
32
- .version('1.49.4'); // This string is replaced with the actual version at build time by rollup
32
+ .version('1.49.6'); // This string is replaced with the actual version at build time by rollup
33
33
  // new: global config override option
34
34
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
35
35
  program
@@ -77,12 +77,66 @@ async function runSyncer(config, options = {}) {
77
77
  continue;
78
78
  }
79
79
  const primaryKeys = getNestedKeys(primaryTranslations, keySeparator ?? '.');
80
+ const primaryKeySet = new Set(primaryKeys);
80
81
  // 3. For each secondary language, sync the current namespace
81
82
  for (const lang of secondaryLanguages) {
82
83
  const secondaryPath = getOutputPath(output, lang, ns);
83
84
  const fullSecondaryPath = resolve(process.cwd(), secondaryPath);
84
85
  const existingSecondaryTranslations = await loadTranslationFile(fullSecondaryPath) || {};
85
86
  const newSecondaryTranslations = {};
87
+ // Determine CLDR plural categories for this specific secondary locale so
88
+ // we can recognise locale-specific plural suffixes (e.g. `_many` for fr/es)
89
+ // that are not present in the primary language and must not be discarded.
90
+ const sep = config.extract.pluralSeparator ?? '_';
91
+ const localeCardinalCategories = (() => {
92
+ try {
93
+ return new Set(new Intl.PluralRules(lang, { type: 'cardinal' }).resolvedOptions().pluralCategories);
94
+ }
95
+ catch {
96
+ return new Set(['one', 'other']);
97
+ }
98
+ })();
99
+ const localeOrdinalCategories = (() => {
100
+ try {
101
+ return new Set(new Intl.PluralRules(lang, { type: 'ordinal' }).resolvedOptions().pluralCategories);
102
+ }
103
+ catch {
104
+ return new Set(['one', 'other', 'two', 'few']);
105
+ }
106
+ })();
107
+ /**
108
+ * Returns true when `key` is a plural variant that is:
109
+ * 1. Valid for this locale's CLDR rules (cardinal or ordinal), AND
110
+ * 2. Derived from a base key that exists in the primary locale.
111
+ *
112
+ * This handles both cardinal (`title_many`) and ordinal
113
+ * (`place_ordinal_few`) suffixes so neither gets erased during sync.
114
+ */
115
+ const isLocaleSpecificPluralExtension = (key) => {
116
+ // Cardinal: key ends with `{sep}{category}` and base key is in primary
117
+ for (const cat of localeCardinalCategories) {
118
+ const suffix = `${sep}${cat}`;
119
+ if (key.endsWith(suffix)) {
120
+ const base = key.slice(0, -suffix.length);
121
+ // The base itself, or any primary key that starts with `{base}{sep}`,
122
+ // confirms this is a plural family rooted in the primary locale.
123
+ if (primaryKeySet.has(base) || primaryKeys.some(pk => pk.startsWith(`${base}${sep}`))) {
124
+ return true;
125
+ }
126
+ }
127
+ }
128
+ // Ordinal: key ends with `{sep}ordinal{sep}{category}`
129
+ for (const cat of localeOrdinalCategories) {
130
+ const suffix = `${sep}ordinal${sep}${cat}`;
131
+ if (key.endsWith(suffix)) {
132
+ const base = key.slice(0, -suffix.length);
133
+ if (primaryKeySet.has(base) || primaryKeys.some(pk => pk.startsWith(`${base}${sep}`))) {
134
+ return true;
135
+ }
136
+ }
137
+ }
138
+ return false;
139
+ };
86
140
  for (const key of primaryKeys) {
87
141
  const primaryValue = getNestedValue(primaryTranslations, key, keySeparator ?? '.');
88
142
  const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
@@ -90,6 +144,23 @@ async function runSyncer(config, options = {}) {
90
144
  const valueToSet = existingValue ?? resolveDefaultValue(defaultValue, key, ns, lang, primaryValue);
91
145
  setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
92
146
  }
147
+ // Preserve locale-specific plural forms that exist in the secondary file
148
+ // but are absent from the primary locale (e.g. `_many` for French/Spanish
149
+ // when the primary language is English, which has no `_many` category).
150
+ // Without this pass the syncer would silently drop those keys, causing
151
+ // `status` to immediately report them as missing.
152
+ const existingSecondaryKeys = getNestedKeys(existingSecondaryTranslations, keySeparator ?? '.');
153
+ for (const key of existingSecondaryKeys) {
154
+ if (!primaryKeySet.has(key) && isLocaleSpecificPluralExtension(key)) {
155
+ const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
156
+ // Only preserve non-empty values; an empty string was likely a
157
+ // placeholder left by a previous (buggy) sync run and should not
158
+ // be perpetuated.
159
+ if (existingValue !== '' && existingValue != null) {
160
+ setNestedValue(newSecondaryTranslations, key, existingValue, keySeparator ?? '.');
161
+ }
162
+ }
163
+ }
93
164
  // Use JSON.stringify for a reliable object comparison, regardless of format
94
165
  const oldContent = JSON.stringify(existingSecondaryTranslations);
95
166
  const newContent = JSON.stringify(newSecondaryTranslations);
@@ -25,10 +25,10 @@ async function loadFile(file) {
25
25
  type: 'commonjs'
26
26
  }
27
27
  });
28
- const exports = {};
29
- const module = { exports };
28
+ const exports$1 = {};
29
+ const module = { exports: exports$1 };
30
30
  const context = vm.createContext({
31
- exports,
31
+ exports: exports$1,
32
32
  module,
33
33
  require: (id) => require(id),
34
34
  console,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.49.4",
3
+ "version": "1.49.6",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,39 +52,39 @@
52
52
  "url": "https://github.com/i18next/i18next-cli/issues"
53
53
  },
54
54
  "devDependencies": {
55
- "@rollup/plugin-replace": "6.0.3",
56
- "@rollup/plugin-terser": "0.4.4",
57
- "@types/inquirer": "9.0.9",
58
- "@types/node": "25.3.3",
59
- "@types/react": "19.2.14",
60
- "@typescript-eslint/parser": "^8.56.1",
61
- "@vitest/coverage-v8": "4.0.18",
62
- "eslint": "9.39.2",
55
+ "@rollup/plugin-replace": "^6.0.3",
56
+ "@rollup/plugin-terser": "^1.0.0",
57
+ "@types/inquirer": "^9.0.9",
58
+ "@types/node": "^25.4.0",
59
+ "@types/react": "^19.2.14",
60
+ "@typescript-eslint/parser": "^8.57.0",
61
+ "@vitest/coverage-v8": "^4.0.18",
62
+ "eslint": "^9.39.2",
63
63
  "eslint-import-resolver-typescript": "^4.4.4",
64
- "eslint-plugin-import": "2.32.0",
65
- "memfs": "4.56.11",
66
- "neostandard": "0.13.0",
67
- "rollup-plugin-typescript2": "0.36.0",
68
- "typescript": "5.9.3",
69
- "unplugin-swc": "1.5.9",
70
- "vitest": "4.0.18"
64
+ "eslint-plugin-import": "^2.32.0",
65
+ "memfs": "^4.56.11",
66
+ "neostandard": "^0.13.0",
67
+ "rollup-plugin-typescript2": "^0.36.0",
68
+ "typescript": "^5.9.3",
69
+ "unplugin-swc": "^1.5.9",
70
+ "vitest": "^4.0.18"
71
71
  },
72
72
  "dependencies": {
73
- "@croct/json5-parser": "0.2.2",
74
- "@swc/core": "1.15.18",
75
- "chokidar": "5.0.0",
76
- "commander": "14.0.3",
77
- "execa": "9.6.1",
78
- "glob": "13.0.6",
79
- "i18next-resources-for-ts": "2.0.0",
80
- "inquirer": "13.3.0",
81
- "jiti": "2.6.1",
82
- "jsonc-parser": "3.3.1",
83
- "magic-string": "0.30.21",
84
- "minimatch": "10.2.4",
85
- "ora": "9.3.0",
73
+ "@croct/json5-parser": "^0.2.2",
74
+ "@swc/core": "^1.15.18",
75
+ "chokidar": "^5.0.0",
76
+ "commander": "^14.0.3",
77
+ "execa": "^9.6.1",
78
+ "glob": "^13.0.6",
79
+ "i18next-resources-for-ts": "^2.0.1",
80
+ "inquirer": "^13.3.0",
81
+ "jiti": "^2.6.1",
82
+ "jsonc-parser": "^3.3.1",
83
+ "magic-string": "^0.30.21",
84
+ "minimatch": "^10.2.4",
85
+ "ora": "^9.3.0",
86
86
  "react": "^19.2.4",
87
- "react-i18next": "^16.5.5",
88
- "yaml": "2.8.2"
87
+ "react-i18next": "^16.5.6",
88
+ "yaml": "^2.8.2"
89
89
  }
90
90
  }
@@ -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;AAM9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,iBAkGnD"}
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;AAM9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,iBA0KnD"}