i18next-cli 1.49.6 → 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 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.6'); // This string is replaced with the actual version at build time by rollup
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
@@ -139,29 +139,51 @@ async function runSyncer(config, options = {}) {
139
139
  }
140
140
  return false;
141
141
  };
142
- for (const key of primaryKeys) {
143
- const primaryValue = nestedObject.getNestedValue(primaryTranslations, key, keySeparator ?? '.');
144
- const existingValue = nestedObject.getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
145
- // Use the resolved default value if no existing value
146
- const valueToSet = existingValue ?? defaultValue.resolveDefaultValue(defaultValue$1, key, ns, lang, primaryValue);
147
- nestedObject.setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
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.
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.
154
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)
155
160
  for (const key of existingSecondaryKeys) {
156
- if (!primaryKeySet.has(key) && isLocaleSpecificPluralExtension(key)) {
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)) {
157
169
  const existingValue = nestedObject.getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
158
170
  // Only preserve non-empty values; an empty string was likely a
159
171
  // placeholder left by a previous (buggy) sync run and should not
160
172
  // be perpetuated.
161
173
  if (existingValue !== '' && existingValue != null) {
162
174
  nestedObject.setNestedValue(newSecondaryTranslations, key, existingValue, keySeparator ?? '.');
175
+ handledKeys.add(key);
163
176
  }
164
177
  }
178
+ // else: obsolete key — omit it from the output
179
+ }
180
+ // Pass 2: new primary keys not yet in the secondary file
181
+ for (const key of primaryKeys) {
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
+ }
165
187
  }
166
188
  // Use JSON.stringify for a reliable object comparison, regardless of format
167
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.6'); // This string is replaced with the actual version at build time by rollup
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
@@ -137,29 +137,51 @@ async function runSyncer(config, options = {}) {
137
137
  }
138
138
  return false;
139
139
  };
140
- for (const key of primaryKeys) {
141
- const primaryValue = getNestedValue(primaryTranslations, key, keySeparator ?? '.');
142
- const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
143
- // Use the resolved default value if no existing value
144
- const valueToSet = existingValue ?? resolveDefaultValue(defaultValue, key, ns, lang, primaryValue);
145
- setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.');
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.
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.
152
155
  const existingSecondaryKeys = getNestedKeys(existingSecondaryTranslations, keySeparator ?? '.');
156
+ const handledKeys = new Set();
157
+ // Pass 1: existing keys in their current order (preserves extract's ordering)
153
158
  for (const key of existingSecondaryKeys) {
154
- if (!primaryKeySet.has(key) && isLocaleSpecificPluralExtension(key)) {
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)) {
155
167
  const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.');
156
168
  // Only preserve non-empty values; an empty string was likely a
157
169
  // placeholder left by a previous (buggy) sync run and should not
158
170
  // be perpetuated.
159
171
  if (existingValue !== '' && existingValue != null) {
160
172
  setNestedValue(newSecondaryTranslations, key, existingValue, keySeparator ?? '.');
173
+ handledKeys.add(key);
161
174
  }
162
175
  }
176
+ // else: obsolete key — omit it from the output
177
+ }
178
+ // Pass 2: new primary keys not yet in the secondary file
179
+ for (const key of primaryKeys) {
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
+ }
163
185
  }
164
186
  // Use JSON.stringify for a reliable object comparison, regardless of format
165
187
  const oldContent = JSON.stringify(existingSecondaryTranslations);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.49.6",
3
+ "version": "1.49.7",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,iBA0KnD"}
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"}