i18next-cli 1.49.5 → 1.49.7
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 +1 -1
- package/dist/cjs/syncer.js +98 -5
- package/dist/esm/cli.js +1 -1
- package/dist/esm/syncer.js +98 -5
- package/package.json +1 -1
- package/types/syncer.d.ts.map +1 -1
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.
|
|
34
|
+
.version('1.49.7'); // 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
|
package/dist/cjs/syncer.js
CHANGED
|
@@ -79,18 +79,111 @@ 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
|
+
};
|
|
142
|
+
// Build newSecondaryTranslations in a single, order-preserving pass so
|
|
143
|
+
// that the syncer and the extractor produce byte-identical files after
|
|
144
|
+
// the first extract run (issue #216).
|
|
145
|
+
//
|
|
146
|
+
// Strategy:
|
|
147
|
+
// 1. Walk every key in the *existing* secondary file in its current
|
|
148
|
+
// order. Keep it if it belongs to the primary key set, or if it is
|
|
149
|
+
// a valid locale-specific plural extension with a non-empty value.
|
|
150
|
+
// Obsolete keys (neither primary nor a locale extension) are dropped.
|
|
151
|
+
// 2. Append any primary keys that are genuinely new (not present in the
|
|
152
|
+
// existing secondary file at all) so they get picked up on first sync.
|
|
153
|
+
//
|
|
154
|
+
// This means that once `extract` has written the secondary file in its
|
|
155
|
+
// canonical order, a subsequent `sync` will read that order and reproduce
|
|
156
|
+
// it exactly — the pipeline becomes idempotent.
|
|
157
|
+
const existingSecondaryKeys = nestedObject.getNestedKeys(existingSecondaryTranslations, keySeparator ?? '.');
|
|
158
|
+
const handledKeys = new Set();
|
|
159
|
+
// Pass 1: existing keys in their current order (preserves extract's ordering)
|
|
160
|
+
for (const key of existingSecondaryKeys) {
|
|
161
|
+
if (primaryKeySet.has(key)) {
|
|
162
|
+
const primaryValue = nestedObject.getNestedValue(primaryTranslations, key, keySeparator ?? '.');
|
|
163
|
+
const existingValue = nestedObject.getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
|
|
164
|
+
const valueToSet = existingValue ?? defaultValue.resolveDefaultValue(defaultValue$1, key, ns, lang, primaryValue);
|
|
165
|
+
nestedObject.setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
|
|
166
|
+
handledKeys.add(key);
|
|
167
|
+
}
|
|
168
|
+
else if (isLocaleSpecificPluralExtension(key)) {
|
|
169
|
+
const existingValue = nestedObject.getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
|
|
170
|
+
// Only preserve non-empty values; an empty string was likely a
|
|
171
|
+
// placeholder left by a previous (buggy) sync run and should not
|
|
172
|
+
// be perpetuated.
|
|
173
|
+
if (existingValue !== '' && existingValue != null) {
|
|
174
|
+
nestedObject.setNestedValue(newSecondaryTranslations, key, existingValue, keySeparator ?? '.');
|
|
175
|
+
handledKeys.add(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// else: obsolete key — omit it from the output
|
|
179
|
+
}
|
|
180
|
+
// Pass 2: new primary keys not yet in the secondary file
|
|
88
181
|
for (const key of primaryKeys) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
182
|
+
if (!handledKeys.has(key)) {
|
|
183
|
+
const primaryValue = nestedObject.getNestedValue(primaryTranslations, key, keySeparator ?? '.');
|
|
184
|
+
const valueToSet = defaultValue.resolveDefaultValue(defaultValue$1, key, ns, lang, primaryValue);
|
|
185
|
+
nestedObject.setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
|
|
186
|
+
}
|
|
94
187
|
}
|
|
95
188
|
// Use JSON.stringify for a reliable object comparison, regardless of format
|
|
96
189
|
const oldContent = JSON.stringify(existingSecondaryTranslations);
|
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.
|
|
32
|
+
.version('1.49.7'); // 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
|
package/dist/esm/syncer.js
CHANGED
|
@@ -77,18 +77,111 @@ 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
|
+
};
|
|
140
|
+
// Build newSecondaryTranslations in a single, order-preserving pass so
|
|
141
|
+
// that the syncer and the extractor produce byte-identical files after
|
|
142
|
+
// the first extract run (issue #216).
|
|
143
|
+
//
|
|
144
|
+
// Strategy:
|
|
145
|
+
// 1. Walk every key in the *existing* secondary file in its current
|
|
146
|
+
// order. Keep it if it belongs to the primary key set, or if it is
|
|
147
|
+
// a valid locale-specific plural extension with a non-empty value.
|
|
148
|
+
// Obsolete keys (neither primary nor a locale extension) are dropped.
|
|
149
|
+
// 2. Append any primary keys that are genuinely new (not present in the
|
|
150
|
+
// existing secondary file at all) so they get picked up on first sync.
|
|
151
|
+
//
|
|
152
|
+
// This means that once `extract` has written the secondary file in its
|
|
153
|
+
// canonical order, a subsequent `sync` will read that order and reproduce
|
|
154
|
+
// it exactly — the pipeline becomes idempotent.
|
|
155
|
+
const existingSecondaryKeys = getNestedKeys(existingSecondaryTranslations, keySeparator ?? '.');
|
|
156
|
+
const handledKeys = new Set();
|
|
157
|
+
// Pass 1: existing keys in their current order (preserves extract's ordering)
|
|
158
|
+
for (const key of existingSecondaryKeys) {
|
|
159
|
+
if (primaryKeySet.has(key)) {
|
|
160
|
+
const primaryValue = getNestedValue(primaryTranslations, key, keySeparator ?? '.');
|
|
161
|
+
const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
|
|
162
|
+
const valueToSet = existingValue ?? resolveDefaultValue(defaultValue, key, ns, lang, primaryValue);
|
|
163
|
+
setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
|
|
164
|
+
handledKeys.add(key);
|
|
165
|
+
}
|
|
166
|
+
else if (isLocaleSpecificPluralExtension(key)) {
|
|
167
|
+
const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
|
|
168
|
+
// Only preserve non-empty values; an empty string was likely a
|
|
169
|
+
// placeholder left by a previous (buggy) sync run and should not
|
|
170
|
+
// be perpetuated.
|
|
171
|
+
if (existingValue !== '' && existingValue != null) {
|
|
172
|
+
setNestedValue(newSecondaryTranslations, key, existingValue, keySeparator ?? '.');
|
|
173
|
+
handledKeys.add(key);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// else: obsolete key — omit it from the output
|
|
177
|
+
}
|
|
178
|
+
// Pass 2: new primary keys not yet in the secondary file
|
|
86
179
|
for (const key of primaryKeys) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
180
|
+
if (!handledKeys.has(key)) {
|
|
181
|
+
const primaryValue = getNestedValue(primaryTranslations, key, keySeparator ?? '.');
|
|
182
|
+
const valueToSet = resolveDefaultValue(defaultValue, key, ns, lang, primaryValue);
|
|
183
|
+
setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
|
|
184
|
+
}
|
|
92
185
|
}
|
|
93
186
|
// Use JSON.stringify for a reliable object comparison, regardless of format
|
|
94
187
|
const oldContent = JSON.stringify(existingSecondaryTranslations);
|
package/package.json
CHANGED
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;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,
|
|
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,iBA+LnD"}
|