i18next-cli 1.46.4 → 1.47.0

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.
Files changed (51) hide show
  1. package/README.md +198 -3
  2. package/dist/cjs/cli.js +50 -1
  3. package/dist/cjs/extractor/core/extractor.js +27 -18
  4. package/dist/cjs/extractor/core/translation-manager.js +10 -3
  5. package/dist/cjs/extractor/parsers/call-expression-handler.js +9 -1
  6. package/dist/cjs/extractor/parsers/jsx-handler.js +10 -1
  7. package/dist/cjs/index.js +5 -0
  8. package/dist/cjs/init.js +68 -12
  9. package/dist/cjs/instrumenter/core/instrumenter.js +1633 -0
  10. package/dist/cjs/instrumenter/core/key-generator.js +71 -0
  11. package/dist/cjs/instrumenter/core/string-detector.js +290 -0
  12. package/dist/cjs/instrumenter/core/transformer.js +339 -0
  13. package/dist/cjs/linter.js +6 -7
  14. package/dist/cjs/utils/jsx-attributes.js +131 -0
  15. package/dist/esm/cli.js +50 -1
  16. package/dist/esm/extractor/core/extractor.js +27 -18
  17. package/dist/esm/extractor/core/translation-manager.js +10 -3
  18. package/dist/esm/extractor/parsers/call-expression-handler.js +9 -1
  19. package/dist/esm/extractor/parsers/jsx-handler.js +10 -1
  20. package/dist/esm/index.js +3 -0
  21. package/dist/esm/init.js +68 -12
  22. package/dist/esm/instrumenter/core/instrumenter.js +1630 -0
  23. package/dist/esm/instrumenter/core/key-generator.js +68 -0
  24. package/dist/esm/instrumenter/core/string-detector.js +288 -0
  25. package/dist/esm/instrumenter/core/transformer.js +336 -0
  26. package/dist/esm/linter.js +6 -7
  27. package/dist/esm/utils/jsx-attributes.js +121 -0
  28. package/package.json +2 -1
  29. package/types/cli.d.ts.map +1 -1
  30. package/types/extractor/core/extractor.d.ts.map +1 -1
  31. package/types/extractor/core/translation-manager.d.ts.map +1 -1
  32. package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
  33. package/types/extractor/parsers/jsx-handler.d.ts.map +1 -1
  34. package/types/index.d.ts +2 -1
  35. package/types/index.d.ts.map +1 -1
  36. package/types/init.d.ts.map +1 -1
  37. package/types/instrumenter/core/instrumenter.d.ts +16 -0
  38. package/types/instrumenter/core/instrumenter.d.ts.map +1 -0
  39. package/types/instrumenter/core/key-generator.d.ts +30 -0
  40. package/types/instrumenter/core/key-generator.d.ts.map +1 -0
  41. package/types/instrumenter/core/string-detector.d.ts +27 -0
  42. package/types/instrumenter/core/string-detector.d.ts.map +1 -0
  43. package/types/instrumenter/core/transformer.d.ts +31 -0
  44. package/types/instrumenter/core/transformer.d.ts.map +1 -0
  45. package/types/instrumenter/index.d.ts +6 -0
  46. package/types/instrumenter/index.d.ts.map +1 -0
  47. package/types/linter.d.ts.map +1 -1
  48. package/types/types.d.ts +285 -1
  49. package/types/types.d.ts.map +1 -1
  50. package/types/utils/jsx-attributes.d.ts +68 -0
  51. package/types/utils/jsx-attributes.d.ts.map +1 -0
@@ -336,7 +336,7 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
336
336
  return false;
337
337
  };
338
338
  // Filter nsKeys to only include keys relevant to this language
339
- const filteredKeys = nsKeys.filter(({ key, hasCount, isOrdinal }) => {
339
+ const filteredKeys = nsKeys.filter(({ key, hasCount, isOrdinal, explicitDefault }) => {
340
340
  // FIRST: Check if key matches preservePatterns and should be excluded
341
341
  if (shouldFilterKey(key)) {
342
342
  return false;
@@ -362,14 +362,21 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
362
362
  return true;
363
363
  // Otherwise fall through and check the explicit suffix as before.
364
364
  }
365
+ // i18next supports a special _zero form that is NOT part of CLDR plural
366
+ // rules. When the key was explicitly extracted (e.g. from a t() call with
367
+ // `defaultValue_zero`), always include it regardless of the target
368
+ // language's Intl.PluralRules categories.
369
+ // See: https://www.i18next.com/translation-function/plurals#special-zero
370
+ const lastPart = keyParts[keyParts.length - 1];
371
+ if (lastPart === 'zero' && explicitDefault) {
372
+ return true;
373
+ }
365
374
  if (isOrdinal && keyParts.includes('ordinal')) {
366
375
  // For ordinal plurals: key_context_ordinal_category or key_ordinal_category
367
- const lastPart = keyParts[keyParts.length - 1];
368
376
  return targetLanguagePluralCategories.has(`ordinal_${lastPart}`);
369
377
  }
370
378
  else if (hasCount) {
371
379
  // For cardinal plurals: key_context_category or key_category
372
- const lastPart = keyParts[keyParts.length - 1];
373
380
  return targetLanguagePluralCategories.has(lastPart);
374
381
  }
375
382
  return true;
@@ -703,8 +703,16 @@ class CallExpressionHandler {
703
703
  categories.forEach(cat => allPluralCategories.add(cat));
704
704
  }
705
705
  }
706
- const pluralCategories = Array.from(allPluralCategories).sort();
707
706
  const pluralSeparator = this.config.extract.pluralSeparator ?? '_';
707
+ // i18next supports a special _zero form (not part of CLDR plural rules).
708
+ // When defaultValue_zero is present in the options, include 'zero' in the
709
+ // categories so that key_zero is generated with the correct default value.
710
+ // See: https://www.i18next.com/translation-function/plurals#special-zero
711
+ const zeroDefault = getObjectPropValue(options, `defaultValue${pluralSeparator}zero`);
712
+ if (typeof zeroDefault === 'string' && !allPluralCategories.has('zero')) {
713
+ allPluralCategories.add('zero');
714
+ }
715
+ const pluralCategories = Array.from(allPluralCategories).sort();
708
716
  // Get all possible default values once at the start
709
717
  const defaultValue = getObjectPropValue(options, 'defaultValue');
710
718
  const otherDefault = getObjectPropValue(options, `defaultValue${pluralSeparator}other`);
@@ -364,8 +364,17 @@ class JSXHandler {
364
364
  categories.forEach(cat => allPluralCategories.add(cat));
365
365
  }
366
366
  }
367
- const pluralCategories = Array.from(allPluralCategories).sort();
368
367
  const pluralSeparator = this.config.extract.pluralSeparator ?? '_';
368
+ // i18next supports a special _zero form (not part of CLDR plural rules).
369
+ // When defaultValue_zero is present in tOptions, include 'zero' in the
370
+ // categories so that key_zero is generated with the correct default value.
371
+ if (optionsNode) {
372
+ const zeroDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}zero`);
373
+ if (typeof zeroDefault === 'string' && !allPluralCategories.has('zero')) {
374
+ allPluralCategories.add('zero');
375
+ }
376
+ }
377
+ const pluralCategories = Array.from(allPluralCategories).sort();
369
378
  // Get plural-specific default values from tOptions if available
370
379
  let otherDefault;
371
380
  let ordinalOtherDefault;
package/dist/esm/index.js CHANGED
@@ -8,3 +8,6 @@ export { runSyncer } from './syncer.js';
8
8
  export { runStatus } from './status.js';
9
9
  export { runTypesGenerator } from './types-generator.js';
10
10
  export { runRenameKey } from './rename-key.js';
11
+ export { runInstrumenter, writeExtractedKeys } from './instrumenter/core/instrumenter.js';
12
+ import './utils/jsx-attributes.js';
13
+ import 'magic-string';
package/dist/esm/init.js CHANGED
@@ -30,6 +30,39 @@ async function isEsmProject() {
30
30
  return true; // Default to ESM if package.json is not found or readable
31
31
  }
32
32
  }
33
+ /**
34
+ * Checks whether i18next-cli is listed as a local dependency of the current project.
35
+ * When running via `npx` without a local install, `defineConfig` would not be available
36
+ * at runtime, so the generated config should fall back to a plain object export.
37
+ *
38
+ * @returns Promise resolving to true if i18next-cli is in dependencies or devDependencies
39
+ */
40
+ async function isCliLocallyInstalled() {
41
+ try {
42
+ const packageJsonPath = resolve(process.cwd(), 'package.json');
43
+ const content = await readFile(packageJsonPath, 'utf-8');
44
+ const packageJson = JSON.parse(content);
45
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
46
+ return !!deps['i18next-cli'];
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ /**
53
+ * Checks whether the project uses TypeScript by looking for a tsconfig.json.
54
+ *
55
+ * @returns Promise resolving to true if tsconfig.json exists in the project root
56
+ */
57
+ async function isTypeScriptProject() {
58
+ try {
59
+ await readFile(resolve(process.cwd(), 'tsconfig.json'));
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
33
66
  /**
34
67
  * Interactive setup wizard for creating a new i18next-cli configuration file.
35
68
  *
@@ -74,12 +107,17 @@ async function runInit() {
74
107
  if (detectedConfig && typeof detectedConfig.extract?.output === 'function') {
75
108
  delete detectedConfig.extract.output;
76
109
  }
110
+ // Detect whether the project uses TypeScript to set the preferred default
111
+ const projectUsesTs = await isTypeScriptProject();
112
+ const tsChoice = 'TypeScript (i18next.config.ts)';
113
+ const jsChoice = 'JavaScript (i18next.config.js)';
114
+ const fileTypeChoices = projectUsesTs ? [tsChoice, jsChoice] : [jsChoice, tsChoice];
77
115
  const answers = await inquirer.prompt([
78
116
  {
79
117
  type: 'select',
80
118
  name: 'fileType',
81
119
  message: 'What kind of configuration file do you want?',
82
- choices: ['TypeScript (i18next.config.ts)', 'JavaScript (i18next.config.js)'],
120
+ choices: fileTypeChoices,
83
121
  },
84
122
  {
85
123
  type: 'input',
@@ -146,23 +184,41 @@ async function runInit() {
146
184
  // Fallback
147
185
  return JSON.stringify(value);
148
186
  }
187
+ const isLocallyInstalled = await isCliLocallyInstalled();
149
188
  let fileContent = '';
150
- if (isTypeScript) {
151
- fileContent = `import { defineConfig } from 'i18next-cli';
189
+ if (isLocallyInstalled) {
190
+ // i18next-cli is a local dependency — use defineConfig for type-safety
191
+ if (isTypeScript) {
192
+ fileContent = `import { defineConfig } from 'i18next-cli'
152
193
 
153
- export default defineConfig(${toJs(configObject)});`;
154
- }
155
- else if (isEsm) {
156
- fileContent = `import { defineConfig } from 'i18next-cli';
194
+ export default defineConfig(${toJs(configObject)})`;
195
+ }
196
+ else if (isEsm) {
197
+ fileContent = `import { defineConfig } from 'i18next-cli'
157
198
 
158
199
  /** @type {import('i18next-cli').I18nextToolkitConfig} */
159
- export default defineConfig(${toJs(configObject)});`;
160
- }
161
- else { // CJS
162
- fileContent = `const { defineConfig } = require('i18next-cli');
200
+ export default defineConfig(${toJs(configObject)})`;
201
+ }
202
+ else { // CJS
203
+ fileContent = `const { defineConfig } = require('i18next-cli')
163
204
 
164
205
  /** @type {import('i18next-cli').I18nextToolkitConfig} */
165
- module.exports = defineConfig(${toJs(configObject)});`;
206
+ module.exports = defineConfig(${toJs(configObject)})`;
207
+ }
208
+ }
209
+ else {
210
+ // i18next-cli is not locally installed (e.g. npx) — plain config object
211
+ if (isTypeScript) {
212
+ fileContent = `export default ${toJs(configObject)}`;
213
+ }
214
+ else if (isEsm) {
215
+ fileContent = `/** @type {import('i18next-cli').I18nextToolkitConfig} */
216
+ export default ${toJs(configObject)}`;
217
+ }
218
+ else { // CJS
219
+ fileContent = `/** @type {import('i18next-cli').I18nextToolkitConfig} */
220
+ module.exports = ${toJs(configObject)}`;
221
+ }
166
222
  }
167
223
  const outputPath = resolve(process.cwd(), fileName);
168
224
  await writeFile(outputPath, fileContent.trim());