i18next-cli 0.9.9 → 0.9.10

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 (37) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +60 -0
  3. package/dist/cjs/cli.js +1 -1
  4. package/dist/cjs/extractor/core/extractor.js +1 -1
  5. package/dist/cjs/extractor/core/translation-manager.js +1 -1
  6. package/dist/cjs/extractor/parsers/ast-visitors.js +1 -1
  7. package/dist/cjs/heuristic-config.js +1 -1
  8. package/dist/cjs/status.js +1 -1
  9. package/dist/cjs/syncer.js +1 -1
  10. package/dist/cjs/utils/file-utils.js +1 -1
  11. package/dist/esm/cli.js +1 -1
  12. package/dist/esm/extractor/core/extractor.js +1 -1
  13. package/dist/esm/extractor/core/translation-manager.js +1 -1
  14. package/dist/esm/extractor/parsers/ast-visitors.js +1 -1
  15. package/dist/esm/heuristic-config.js +1 -1
  16. package/dist/esm/status.js +1 -1
  17. package/dist/esm/syncer.js +1 -1
  18. package/dist/esm/utils/file-utils.js +1 -1
  19. package/package.json +1 -1
  20. package/src/cli.ts +1 -1
  21. package/src/extractor/core/extractor.ts +13 -7
  22. package/src/extractor/core/translation-manager.ts +38 -21
  23. package/src/extractor/parsers/ast-visitors.ts +1 -1
  24. package/src/heuristic-config.ts +14 -3
  25. package/src/status.ts +21 -13
  26. package/src/syncer.ts +77 -67
  27. package/src/types.ts +18 -0
  28. package/src/utils/file-utils.ts +54 -1
  29. package/types/extractor/core/extractor.d.ts.map +1 -1
  30. package/types/extractor/core/translation-manager.d.ts.map +1 -1
  31. package/types/heuristic-config.d.ts.map +1 -1
  32. package/types/status.d.ts.map +1 -1
  33. package/types/syncer.d.ts.map +1 -1
  34. package/types/types.d.ts +16 -0
  35. package/types/types.d.ts.map +1 -1
  36. package/types/utils/file-utils.d.ts +16 -1
  37. package/types/utils/file-utils.d.ts.map +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - not yet released
11
11
 
12
+ ## [0.9.10] - 2025-09-25
13
+
14
+ ### Added
15
+ - **JavaScript/TypeScript Translation Files:** Added the `outputFormat` option to support generating translation files as `.json` (default), `.js` (ESM or CJS), or `.ts` modules.
16
+ - **Merged Namespace Files:** Added the `mergeNamespaces` option to combine all namespaces into a single file per language, streamlining imports and file structures.
17
+
12
18
  ## [0.9.9] - 2025-09-25
13
19
 
14
20
  - **Extractor:** Now supports static and dynamic (ternary) `context` options in both `t()` and `<Trans>`.
package/README.md CHANGED
@@ -271,6 +271,13 @@ export default defineConfig({
271
271
  extract: {
272
272
  input: ['src/**/*.{ts,tsx}'],
273
273
  output: 'locales/{{language}}/{{namespace}}.json',
274
+
275
+ // Use '.ts' files with `export default` instead of '.json'
276
+ outputFormat: 'ts',
277
+
278
+ // Combine all namespaces into a single file per language (e.g., locales/en.ts)
279
+ // Note: `output` path must not contain `{{namespace}}` when this is true.
280
+ mergeNamespaces: false,
274
281
 
275
282
  // Translation functions to detect
276
283
  functions: ['t', 'i18n.t', 'i18next.t'],
@@ -416,6 +423,59 @@ Extract keys from comments for documentation or edge cases:
416
423
  // t('user.greeting', { defaultValue: 'Hello!', ns: 'common' })
417
424
  ```
418
425
 
426
+ ### JavaScript & TypeScript Translation Files
427
+
428
+ For projects that prefer to keep everything in a single module type, you can configure the CLI to output JavaScript or TypeScript files instead of JSON.
429
+
430
+ Configuration (`i18next.config.ts`):
431
+
432
+ ```typescript
433
+ export default defineConfig({
434
+ extract: {
435
+ output: 'src/locales/{{language}}/{{namespace}}.ts', // Note the .ts extension
436
+ outputFormat: 'ts', // Use TypeScript with ES Modules
437
+ }
438
+ });
439
+ ```
440
+
441
+ This will generate files like `src/locales/en/translation.ts` with the following content:
442
+
443
+ ```typescript
444
+ export default {
445
+ "myKey": "My value"
446
+ } as const;
447
+ ```
448
+
449
+ ### Merging Namespaces
450
+
451
+ You can also combine all namespaces into a single file per language. This is useful for reducing the number of network requests in some application setups.
452
+
453
+ Configuration (`i18next.config.ts`):
454
+
455
+ ```typescript
456
+ export default defineConfig({
457
+ extract: {
458
+ // Note: The `output` path no longer contains the {{namespace}} placeholder
459
+ output: 'src/locales/{{language}}.ts',
460
+ outputFormat: 'ts',
461
+ mergeNamespaces: true,
462
+ }
463
+ });
464
+ ```
465
+
466
+ This will generate a single file per language, like `src/locales/en.ts`, with namespaces as top-level keys:
467
+
468
+ ```typescript
469
+ export default {
470
+ "translation": {
471
+ "key1": "Value 1"
472
+ },
473
+ "common": {
474
+ "keyA": "Value A"
475
+ }
476
+ } as const;
477
+ ```
478
+
419
479
  ## Migration from i18next-parser
420
480
 
421
481
  Automatically migrate from legacy `i18next-parser.config.js`:
package/dist/cjs/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:fs/promises"),require("node:path");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.9"),f.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a);e.ci&&t&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),f.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(e,t)=>{let o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{detail:e,namespace:t.namespace})}),f.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(n.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),f.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),f.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),f.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),f.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await d.runLinter(e)}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
2
+ "use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.10"),f.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a);e.ci&&t&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),f.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(e,t)=>{let o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{detail:e,namespace:t.namespace})}),f.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(n.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),f.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),f.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),f.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),f.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await d.runLinter(e)}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
@@ -1 +1 @@
1
- "use strict";var e=require("ora"),t=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),o=require("./key-finder.js"),s=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),l=require("../parsers/comment-parser.js"),u=require("../../utils/logger.js");function g(e,t,r,a=new u.ConsoleLogger){if(e&&"object"==typeof e){for(const n of t)try{n.onVisitNode?.(e,r)}catch(e){a.warn(`Plugin ${n.name} onVisitNode failed:`,e)}for(const n of Object.keys(e)){const o=e[n];if(Array.isArray(o))for(const e of o)e&&"object"==typeof e&&g(e,t,r,a);else o&&"object"==typeof o&&g(o,t,r,a)}}}exports.extract=async function(e){e.extract.primaryLanguage||(e.extract.primaryLanguage=e.locales[0]),e.extract.secondaryLanguages||(e.extract.secondaryLanguages=e.locales.filter(t=>t!==e?.extract?.primaryLanguage)),e.extract.functions||(e.extract.functions=["t"]),e.extract.transComponents||(e.extract.transComponents=["Trans"]);const{allKeys:t,objectKeys:r}=await o.findKeys(e);return s.getTranslations(t,r,e)},exports.processFile=async function(e,t,n,o,s=new u.ConsoleLogger){try{let i=await a.readFile(e,"utf-8");for(const r of t.plugins||[])i=await(r.onLoad?.(i,e))??i;const u=await r.parse(i,{syntax:"typescript",tsx:!0,comments:!0}),f=c.createPluginContext(n);l.extractKeysFromComments(i,t.extract.functions||["t"],f,t),o.visit(u),(t.plugins||[]).length>0&&g(u,t.plugins||[],f,s)}catch(t){throw new i.ExtractorError("Failed to process file",e,t)}},exports.runExtractor=async function(r,c=new u.ConsoleLogger){r.extract.primaryLanguage||(r.extract.primaryLanguage=r.locales[0]||"en"),r.extract.secondaryLanguages||(r.extract.secondaryLanguages=r.locales.filter(e=>e!==r?.extract?.primaryLanguage)),i.validateExtractorConfig(r);const l=e("Running i18next key extractor...\n").start();try{const{allKeys:e,objectKeys:i}=await o.findKeys(r,c);l.text=`Found ${e.size} unique keys. Updating translation files...`;const u=await s.getTranslations(e,i,r);let g=!1;for(const e of u)e.updated&&(g=!0,await a.mkdir(n.dirname(e.path),{recursive:!0}),await a.writeFile(e.path,JSON.stringify(e.newTranslations,null,2)),c.info(t.green(`Updated: ${e.path}`)));return l.succeed(t.bold("Extraction complete!")),g}catch(e){throw l.fail(t.red("Extraction failed.")),e}};
1
+ "use strict";var e=require("ora"),t=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),o=require("./key-finder.js"),s=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),l=require("../parsers/comment-parser.js"),u=require("../../utils/logger.js"),f=require("../../utils/file-utils.js");function g(e,t,r,a=new u.ConsoleLogger){if(e&&"object"==typeof e){for(const n of t)try{n.onVisitNode?.(e,r)}catch(e){a.warn(`Plugin ${n.name} onVisitNode failed:`,e)}for(const n of Object.keys(e)){const o=e[n];if(Array.isArray(o))for(const e of o)e&&"object"==typeof e&&g(e,t,r,a);else o&&"object"==typeof o&&g(o,t,r,a)}}}exports.extract=async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(t=>t!==e?.extract?.primaryLanguage),e.extract.functions||=["t"],e.extract.transComponents||=["Trans"];const{allKeys:t,objectKeys:r}=await o.findKeys(e);return s.getTranslations(t,r,e)},exports.processFile=async function(e,t,n,o,s=new u.ConsoleLogger){try{let i=await a.readFile(e,"utf-8");for(const r of t.plugins||[])i=await(r.onLoad?.(i,e))??i;const u=await r.parse(i,{syntax:"typescript",tsx:!0,comments:!0}),f=c.createPluginContext(n);l.extractKeysFromComments(i,t.extract.functions||["t"],f,t),o.visit(u),(t.plugins||[]).length>0&&g(u,t.plugins||[],f,s)}catch(t){throw new i.ExtractorError("Failed to process file",e,t)}},exports.runExtractor=async function(r,c=new u.ConsoleLogger){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(e=>e!==r?.extract?.primaryLanguage),i.validateExtractorConfig(r);const l=e("Running i18next key extractor...\n").start();try{const{allKeys:e,objectKeys:i}=await o.findKeys(r,c);l.text=`Found ${e.size} unique keys. Updating translation files...`;const u=await s.getTranslations(e,i,r);let g=!1;for(const e of u)if(e.updated){g=!0;const o=f.serializeTranslationFile(e.newTranslations,r.extract.outputFormat,r.extract.indentation);await a.mkdir(n.dirname(e.path),{recursive:!0}),await a.writeFile(e.path,o),c.info(t.green(`Updated: ${e.path}`))}return l.succeed(t.bold("Extraction complete!")),g}catch(e){throw l.fail(t.red("Extraction failed.")),e}};
@@ -1 +1 @@
1
- "use strict";var e=require("node:fs/promises"),t=require("node:path"),s=require("../../utils/nested-object.js"),a=require("../../utils/file-utils.js");function r(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}exports.getTranslations=async function(n,o,c){const u=c.extract.defaultNS??"translation",l=c.extract.keySeparator??".",i=[...c.extract.preservePatterns||[]];for(const e of o)i.push(`${e}.*`);const p=i.map(r);c.extract.primaryLanguage||(c.extract.primaryLanguage=c.locales[0]||"en"),c.extract.secondaryLanguages||(c.extract.secondaryLanguages=c.locales.filter(e=>e!==c.extract.primaryLanguage));const g=new Map;for(const e of n.values()){const t=e.ns||u;g.has(t)||g.set(t,[]),g.get(t).push(e)}const f=[];for(const r of c.locales)for(const[n,o]of g.entries()){const u=a.getOutputPath(c.extract.output,r,n),i=t.resolve(process.cwd(),u);let g="",d={};try{g=await e.readFile(i,"utf-8"),d=JSON.parse(g)}catch(e){}const x={},y=s.getNestedKeys(d,l);for(const e of y)if(p.some(t=>t.test(e))){const t=s.getNestedValue(d,e,l);s.setNestedValue(x,e,t,l)}const h=!1===c.extract.sort?o:o.sort((e,t)=>e.key.localeCompare(t.key));for(const{key:e,defaultValue:t}of h){const a=s.getNestedValue(d,e,l)??(r===c.extract?.primaryLanguage?t:"");s.setNestedValue(x,e,a,l)}const m=c.extract.indentation??2,N=JSON.stringify(x,null,m);f.push({path:i,updated:N!==g,newTranslations:x,existingTranslations:d})}return f};
1
+ "use strict";var t=require("node:path"),e=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function a(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}exports.getTranslations=async function(n,r,o){const c=o.extract.defaultNS??"translation",i=o.extract.keySeparator??".",l=[...o.extract.preservePatterns||[]],u=o.extract.mergeNamespaces??!1;for(const t of r)l.push(`${t}.*`);const p=l.map(a);o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(t=>t!==o?.extract?.primaryLanguage);const g=o.extract.primaryLanguage,f=new Map;for(const t of n.values()){const e=t.ns||c;f.has(e)||f.set(e,[]),f.get(e).push(t)}const d=[];for(const a of o.locales){const n={},r={};for(const[c,l]of f.entries()){const f=s.getOutputPath(o.extract.output,a,u?void 0:c),x=t.resolve(process.cwd(),f),y=await s.loadTranslationFile(x)||{},h={},N=e.getNestedKeys(y,i);for(const t of N)if(p.some(e=>e.test(t))){const s=e.getNestedValue(y,t,i);e.setNestedValue(h,t,s,i)}const m=!1===o.extract.sort?l:[...l].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:s}of m){const n=e.getNestedValue(y,t,i)??(a===g?s:o.extract.defaultValue??"");e.setNestedValue(h,t,n,i)}if(u)n[c]=h,Object.keys(y).length>0&&(r[c]=y);else{const t=y?JSON.stringify(y,null,o.extract.indentation??2):"",e=JSON.stringify(h,null,o.extract.indentation??2);d.push({path:x,updated:e!==t,newTranslations:h,existingTranslations:y})}}if(u){const e=s.getOutputPath(o.extract.output,a),c=t.resolve(process.cwd(),e),i=Object.keys(r).length>0?JSON.stringify(r,null,o.extract.indentation??2):"",l=JSON.stringify(n,null,o.extract.indentation??2);d.push({path:c,updated:l!==i,newTranslations:n,existingTranslations:r})}}return d};
@@ -1 +1 @@
1
- "use strict";var e=require("./jsx-parser.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||[]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}};
1
+ "use strict";var e=require("./jsx-parser.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||["t"]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}};
@@ -1 +1 @@
1
- "use strict";var e=require("glob"),s=require("node:fs/promises"),n=require("node:path");const o=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","app/i18n/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json","app/i18n/locales/en/*.json"];exports.detectConfig=async function(){for(const t of o){const o=await e.glob(t,{ignore:"node_modules/**"});if(o.length>0){const e=o[0],t=n.dirname(n.dirname(e));try{let e=(await s.readdir(t)).filter(e=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(e));if(e.length>0)return e.sort(),e.includes("dev")&&(e=["dev",...e.filter(e=>"dev"!==e)]),e.includes("en")&&(e=["en",...e.filter(e=>"en"!==e)]),{locales:e,extract:{input:["src/**/*.{js,jsx,ts,tsx}","app/**/*.{js,jsx,ts,tsx}","pages/**/*.{js,jsx,ts,tsx}","components/**/*.{js,jsx,ts,tsx}"],output:n.join(t,"{{language}}","{{namespace}}.json"),primaryLanguage:e.includes("en")?"en":e[0]}}}catch{continue}}}return null};
1
+ "use strict";var e=require("glob"),s=require("node:fs/promises"),n=require("node:path");const t=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","app/i18n/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json","app/i18n/locales/en/*.json"];exports.detectConfig=async function(){for(const o of t){const t=await e.glob(o,{ignore:"node_modules/**"});if(t.length>0){const e=t[0],o=n.dirname(n.dirname(e)),l=n.extname(e);let a="json";".ts"===l?a="ts":".js"===l&&(a="js");try{let e=(await s.readdir(o)).filter(e=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(e));if(e.length>0)return e.sort(),e.includes("dev")&&(e=["dev",...e.filter(e=>"dev"!==e)]),e.includes("en")&&(e=["en",...e.filter(e=>"en"!==e)]),{locales:e,extract:{input:["src/**/*.{js,jsx,ts,tsx}","app/**/*.{js,jsx,ts,tsx}","pages/**/*.{js,jsx,ts,tsx}","components/**/*.{js,jsx,ts,tsx}"],output:n.join(o,"{{language}}",`{{namespace}}${l}`),outputFormat:a,primaryLanguage:e.includes("en")?"en":e[0]}}}catch{continue}}}return null};
@@ -1 +1 @@
1
- "use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),a=require("node:fs/promises"),s=require("./extractor/core/key-finder.js"),n=require("./utils/nested-object.js"),r=require("./utils/file-utils.js");function l(o,t,a){const s=a>0?Math.round(t/a*100):100,n=c(s);console.log(`${e.bold(o)}: ${n} ${s}% (${t}/${a})`)}function c(o){const t=Math.round(o/100*20),a=20-t;return`[${e.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function i(){console.log(e.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${e.cyan("npx i18next-cli locize-migrate")} to get started.`)}exports.runStatus=async function(u,y={}){u.extract.primaryLanguage||(u.extract.primaryLanguage=u.locales[0]||"en"),u.extract.secondaryLanguages||(u.extract.secondaryLanguages=u.locales.filter(e=>e!==u?.extract?.primaryLanguage));const d=o("Analyzing project localization status...\n").start();try{const o=await async function(e){const{allKeys:o}=await s.findKeys(e),{primaryLanguage:l,keySeparator:c=".",defaultNS:i="translation"}=e.extract,u=e.locales.filter(e=>e!==l),y=new Map;for(const e of o.values()){const o=e.ns||i;y.has(o)||y.set(o,[]),y.get(o).push(e)}const d={totalKeys:o.size,keysByNs:y,locales:new Map};for(const o of u){let s=0;const l=new Map;for(const[i,u]of y.entries()){const y=r.getOutputPath(e.extract.output,o,i);let d={};try{const e=await a.readFile(t.resolve(process.cwd(),y),"utf-8");d=JSON.parse(e)}catch{}let g=0;const f=u.map(({key:e})=>{const o=!!n.getNestedValue(d,e,c??".");return o&&g++,{key:e,isTranslated:o}});l.set(i,{totalKeys:u.length,translatedKeys:g,keyDetails:f}),s+=g}d.locales.set(o,{totalTranslated:s,namespaces:l})}return d}(u);d.succeed("Analysis complete."),function(o,t,a){a.detail?function(o,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(e.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(e.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=o.locales.get(a);if(!n)return void console.error(e.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(e.bold(`\nKey Status for "${e.cyan(a)}":`));const r=Array.from(o.keysByNs.values()).flat().length;l("Overall",n.totalTranslated,r);const c=s?[s]:Array.from(n.namespaces.keys()).sort();for(const o of c){const t=n.namespaces.get(o);t&&(console.log(e.cyan.bold(`\nNamespace: ${o}`)),l("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:o,isTranslated:t})=>{const a=t?e.green("✓"):e.red("✗");console.log(` ${a} ${o}`)}))}const u=r-n.totalTranslated;u>0?console.log(e.yellow.bold(`\nSummary: Found ${u} missing translations for "${a}".`)):console.log(e.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));i()}(o,t,a.detail,a.namespace):a.namespace?function(o,t,a){const s=o.keysByNs.get(a);if(!s)return void console.error(e.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(e.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[e,t]of o.locales.entries()){const o=t.namespaces.get(a);if(o){const t=o.totalKeys>0?Math.round(o.translatedKeys/o.totalKeys*100):100,a=c(t);console.log(`- ${e}: ${a} ${t}% (${o.translatedKeys}/${o.totalKeys} keys)`)}}i()}(o,0,a.namespace):function(o,t){const{primaryLanguage:a}=t.extract;console.log(e.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${e.bold(o.totalKeys)}`),console.log(`📚 Namespaces Found: ${e.bold(o.keysByNs.size)}`),console.log(`🌍 Locales: ${e.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${e.bold(a)}`),console.log("\nTranslation Progress:");for(const[e,t]of o.locales.entries()){const a=o.totalKeys>0?Math.round(t.totalTranslated/o.totalKeys*100):100,s=c(a);console.log(`- ${e}: ${s} ${a}% (${t.totalTranslated}/${o.totalKeys} keys)`)}i()}(o,t)}(o,u,y)}catch(e){d.fail("Failed to generate status report."),console.error(e)}};
1
+ "use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),a=require("./extractor/core/key-finder.js"),s=require("./utils/nested-object.js"),n=require("./utils/file-utils.js");function l(o,t,a){const s=a>0?Math.round(t/a*100):100,n=r(s);console.log(`${e.bold(o)}: ${n} ${s}% (${t}/${a})`)}function r(o){const t=Math.round(o/100*20),a=20-t;return`[${e.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function c(){console.log(e.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${e.cyan("npx i18next-cli locize-migrate")} to get started.`)}exports.runStatus=async function(i,u={}){i.extract.primaryLanguage||=i.locales[0]||"en",i.extract.secondaryLanguages||=i.locales.filter(e=>e!==i?.extract?.primaryLanguage);const d=o("Analyzing project localization status...\n").start();try{const o=await async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(o=>o!==e?.extract?.primaryLanguage);const{allKeys:o}=await a.findKeys(e),{secondaryLanguages:l,keySeparator:r=".",defaultNS:c="translation",mergeNamespaces:i=!1}=e.extract,u=new Map;for(const e of o.values()){const o=e.ns||c;u.has(o)||u.set(o,[]),u.get(o).push(e)}const d={totalKeys:o.size,keysByNs:u,locales:new Map};for(const o of l){let a=0;const l=new Map,c=i?await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o)))||{}:null;for(const[d,y]of u.entries()){const u=i?c?.[d]||{}:await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o,d)))||{};let g=0;const f=y.map(({key:e})=>{const o=!!s.getNestedValue(u,e,r??".");return o&&g++,{key:e,isTranslated:o}});l.set(d,{totalKeys:y.length,translatedKeys:g,keyDetails:f}),a+=g}d.locales.set(o,{totalTranslated:a,namespaces:l})}return d}(i);d.succeed("Analysis complete."),function(o,t,a){a.detail?function(o,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(e.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(e.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=o.locales.get(a);if(!n)return void console.error(e.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(e.bold(`\nKey Status for "${e.cyan(a)}":`));const r=Array.from(o.keysByNs.values()).flat().length;l("Overall",n.totalTranslated,r);const i=s?[s]:Array.from(n.namespaces.keys()).sort();for(const o of i){const t=n.namespaces.get(o);t&&(console.log(e.cyan.bold(`\nNamespace: ${o}`)),l("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:o,isTranslated:t})=>{const a=t?e.green("✓"):e.red("✗");console.log(` ${a} ${o}`)}))}const u=r-n.totalTranslated;u>0?console.log(e.yellow.bold(`\nSummary: Found ${u} missing translations for "${a}".`)):console.log(e.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));c()}(o,t,a.detail,a.namespace):a.namespace?function(o,t,a){const s=o.keysByNs.get(a);if(!s)return void console.error(e.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(e.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[e,t]of o.locales.entries()){const o=t.namespaces.get(a);if(o){const t=o.totalKeys>0?Math.round(o.translatedKeys/o.totalKeys*100):100,a=r(t);console.log(`- ${e}: ${a} ${t}% (${o.translatedKeys}/${o.totalKeys} keys)`)}}c()}(o,0,a.namespace):function(o,t){const{primaryLanguage:a}=t.extract;console.log(e.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${e.bold(o.totalKeys)}`),console.log(`📚 Namespaces Found: ${e.bold(o.keysByNs.size)}`),console.log(`🌍 Locales: ${e.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${e.bold(a)}`),console.log("\nTranslation Progress:");for(const[e,t]of o.locales.entries()){const a=o.totalKeys>0?Math.round(t.totalTranslated/o.totalKeys*100):100,s=r(a);console.log(`- ${e}: ${s} ${a}% (${t.totalTranslated}/${o.totalKeys} keys)`)}c()}(o,t)}(o,i,u)}catch(e){d.fail("Failed to generate status report."),console.error(e)}};
@@ -1 +1 @@
1
- "use strict";var e=require("node:fs/promises"),t=require("path"),r=require("chalk"),o=require("ora"),a=require("./utils/nested-object.js"),n=require("./utils/file-utils.js");exports.runSyncer=async function(c){const l=o("Running i18next locale synchronizer...\n").start();c.extract.primaryLanguage||=c.locales[0]||"en";const{primaryLanguage:s}=c.extract,i=c.locales.filter(e=>e!==s),u=c.extract.keySeparator??".",d=[];let y=!1;const g=c.extract.defaultNS??"translation",f=n.getOutputPath(c.extract.output,s,g),p=t.resolve(process.cwd(),f);let h;try{const t=await e.readFile(p,"utf-8");h=JSON.parse(t)}catch(e){return void console.error(`Primary language file not found at ${f}. Cannot sync.`)}const x=a.getNestedKeys(h,u);for(const o of i){const l=n.getOutputPath(c.extract.output,o,g),s=t.resolve(process.cwd(),l);let i={},f="";try{f=await e.readFile(s,"utf-8"),i=JSON.parse(f)}catch(e){}const p={};for(const e of x){const t=a.getNestedValue(i,e,u)??(c.extract?.defaultValue||"");a.setNestedValue(p,e,t,u)}const h=c.extract.indentation??2,w=JSON.stringify(p,null,h);w!==f?(y=!0,await e.mkdir(t.dirname(s),{recursive:!0}),await e.writeFile(s,w),d.push(` ${r.green("✓")} Synchronized: ${l}`)):d.push(` ${r.gray("-")} Already in sync: ${l}`)}l.succeed(r.bold("Synchronization complete!")),d.forEach(e=>console.log(e)),y?(console.log(r.green.bold("\n✅ Sync complete.")),console.log(r.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${r.cyan("npx i18next-cli locize-migrate")}`)):console.log(r.green.bold("\n✅ All locales are already in sync."))};
1
+ "use strict";var e=require("node:fs/promises"),o=require("path"),t=require("chalk"),n=require("ora"),r=require("glob"),a=require("./utils/nested-object.js"),i=require("./utils/file-utils.js");exports.runSyncer=async function(l){const s=n("Running i18next locale synchronizer...\n").start();try{const n=l.extract.primaryLanguage||l.locales[0]||"en",c=l.locales.filter(e=>e!==n),{output:u,keySeparator:d=".",outputFormat:f="json",indentation:y=2,defaultValue:g=""}=l.extract,h=[];let p=!1;const w=i.getOutputPath(u,n,"*"),m=await r.glob(w);if(0===m.length)return void s.warn(`No translation files found for primary language "${n}". Nothing to sync.`);for(const n of m){const r=o.basename(n).split(".")[0],l=await i.loadTranslationFile(n);if(!l){h.push(` ${t.yellow("-")} Could not read primary file: ${n}`);continue}const s=a.getNestedKeys(l,d??".");for(const n of c){const l=i.getOutputPath(u,n,r),c=o.resolve(process.cwd(),l),w=await i.loadTranslationFile(c)||{},m={};for(const e of s){const o=a.getNestedValue(w,e,d??".")??g;a.setNestedValue(m,e,o,d??".")}const S=JSON.stringify(w);if(JSON.stringify(m)!==S){p=!0;const n=i.serializeTranslationFile(m,f,y);await e.mkdir(o.dirname(c),{recursive:!0}),await e.writeFile(c,n),h.push(` ${t.green("✓")} Synchronized: ${l}`)}else h.push(` ${t.gray("-")} Already in sync: ${l}`)}}s.succeed(t.bold("Synchronization complete!")),h.forEach(e=>console.log(e)),p?(console.log(t.green.bold("\n✅ Sync complete.")),console.log(t.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${t.cyan("npx i18next-cli locize-migrate")}`)):console.log(t.green.bold("\n✅ All locales are already in sync."))}catch(e){s.fail(t.red("Synchronization failed.")),console.error(e)}};
@@ -1 +1 @@
1
- "use strict";require("node:fs/promises"),require("node:path"),exports.getOutputPath=function(e,r,a){return e.replace("{{language}}",r).replace("{{lng}}",r).replace("{{namespace}}",a).replace("{{ns}}",a)};
1
+ "use strict";var e=require("node:fs/promises");require("node:path");var t=require("jiti"),r="undefined"!=typeof document?document.currentScript:null;exports.getOutputPath=function(e,t,r=""){return e.replace("{{language}}",t).replace("{{lng}}",t).replace("{{namespace}}",r).replace("{{ns}}",r)},exports.loadTranslationFile=async function(n){try{if(n.endsWith(".json")){const t=await e.readFile(n,"utf-8");return JSON.parse(t)}if(n.endsWith(".ts")||n.endsWith(".js")){const e=t.createJiti("undefined"==typeof document?require("url").pathToFileURL(__filename).href:r&&"SCRIPT"===r.tagName.toUpperCase()&&r.src||new URL("utils/file-utils.js",document.baseURI).href);return await e.import(n,{default:!0})}}catch(e){return null}return null},exports.serializeTranslationFile=function(e,t="json",r=2){const n=JSON.stringify(e,null,r);switch(t){case"js":case"js-esm":return`export default ${n};\n`;case"js-cjs":return`module.exports = ${n};\n`;case"ts":return`export default ${n} as const;\n`;default:return n}};
package/dist/esm/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as o}from"commander";import e from"chokidar";import{glob as t}from"glob";import i from"chalk";import{ensureConfig as n,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:fs/promises";import"node:path";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.9"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async o=>{const a=await n(),c=async()=>{const e=await r(a);o.ci&&e&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");e.watch(await t(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(o,e)=>{let t=await a();if(!t){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),t=o}await f(t,{detail:o,namespace:e.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");e.watch(await t(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(p),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const e=await c();e||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=e}await d(o)}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async o=>{const e=await n();await g(e,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const e=await n();await u(e,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const e=await n();await y(e,o)}),w.parse(process.argv);
2
+ import{Command as o}from"commander";import t from"chokidar";import{glob as e}from"glob";import i from"chalk";import{ensureConfig as n,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.10"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async o=>{const a=await n(),c=async()=>{const t=await r(a);o.ci&&t&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");t.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(o,t)=>{let e=await a();if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=o}await f(e,{detail:o,namespace:t.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");t.watch(await e(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(p),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=t}await d(o)}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async o=>{const t=await n();await g(t,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const t=await n();await u(t,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const t=await n();await y(t,o)}),w.parse(process.argv);
@@ -1 +1 @@
1
- import t from"ora";import a from"chalk";import{parse as e}from"@swc/core";import{mkdir as r,writeFile as o,readFile as n}from"node:fs/promises";import{dirname as s}from"node:path";import{findKeys as i}from"./key-finder.js";import{getTranslations as c}from"./translation-manager.js";import{validateExtractorConfig as f,ExtractorError as l}from"../../utils/validation.js";import{createPluginContext as p}from"../plugin-manager.js";import{extractKeysFromComments as m}from"../parsers/comment-parser.js";import{ConsoleLogger as u}from"../../utils/logger.js";async function g(e,n=new u){e.extract.primaryLanguage||(e.extract.primaryLanguage=e.locales[0]||"en"),e.extract.secondaryLanguages||(e.extract.secondaryLanguages=e.locales.filter(t=>t!==e?.extract?.primaryLanguage)),f(e);const l=t("Running i18next key extractor...\n").start();try{const{allKeys:t,objectKeys:f}=await i(e,n);l.text=`Found ${t.size} unique keys. Updating translation files...`;const p=await c(t,f,e);let m=!1;for(const t of p)t.updated&&(m=!0,await r(s(t.path),{recursive:!0}),await o(t.path,JSON.stringify(t.newTranslations,null,2)),n.info(a.green(`Updated: ${t.path}`)));return l.succeed(a.bold("Extraction complete!")),m}catch(t){throw l.fail(a.red("Extraction failed.")),t}}async function y(t,a,r,o,s=new u){try{let i=await n(t,"utf-8");for(const e of a.plugins||[])i=await(e.onLoad?.(i,t))??i;const c=await e(i,{syntax:"typescript",tsx:!0,comments:!0}),f=p(r);m(i,a.extract.functions||["t"],f,a),o.visit(c),(a.plugins||[]).length>0&&d(c,a.plugins||[],f,s)}catch(a){throw new l("Failed to process file",t,a)}}function d(t,a,e,r=new u){if(t&&"object"==typeof t){for(const o of a)try{o.onVisitNode?.(t,e)}catch(t){r.warn(`Plugin ${o.name} onVisitNode failed:`,t)}for(const o of Object.keys(t)){const n=t[o];if(Array.isArray(n))for(const t of n)t&&"object"==typeof t&&d(t,a,e,r);else n&&"object"==typeof n&&d(n,a,e,r)}}}async function x(t){t.extract.primaryLanguage||(t.extract.primaryLanguage=t.locales[0]),t.extract.secondaryLanguages||(t.extract.secondaryLanguages=t.locales.filter(a=>a!==t?.extract?.primaryLanguage)),t.extract.functions||(t.extract.functions=["t"]),t.extract.transComponents||(t.extract.transComponents=["Trans"]);const{allKeys:a,objectKeys:e}=await i(t);return c(a,e,t)}export{x as extract,y as processFile,g as runExtractor};
1
+ import t from"ora";import o from"chalk";import{parse as a}from"@swc/core";import{mkdir as e,writeFile as r,readFile as n}from"node:fs/promises";import{dirname as i}from"node:path";import{findKeys as s}from"./key-finder.js";import{getTranslations as c}from"./translation-manager.js";import{validateExtractorConfig as f,ExtractorError as l}from"../../utils/validation.js";import{createPluginContext as p}from"../plugin-manager.js";import{extractKeysFromComments as m}from"../parsers/comment-parser.js";import{ConsoleLogger as u}from"../../utils/logger.js";import{serializeTranslationFile as y}from"../../utils/file-utils.js";async function g(a,n=new u){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage),f(a);const l=t("Running i18next key extractor...\n").start();try{const{allKeys:t,objectKeys:f}=await s(a,n);l.text=`Found ${t.size} unique keys. Updating translation files...`;const p=await c(t,f,a);let m=!1;for(const t of p)if(t.updated){m=!0;const s=y(t.newTranslations,a.extract.outputFormat,a.extract.indentation);await e(i(t.path),{recursive:!0}),await r(t.path,s),n.info(o.green(`Updated: ${t.path}`))}return l.succeed(o.bold("Extraction complete!")),m}catch(t){throw l.fail(o.red("Extraction failed.")),t}}async function d(t,o,e,r,i=new u){try{let s=await n(t,"utf-8");for(const a of o.plugins||[])s=await(a.onLoad?.(s,t))??s;const c=await a(s,{syntax:"typescript",tsx:!0,comments:!0}),f=p(e);m(s,o.extract.functions||["t"],f,o),r.visit(c),(o.plugins||[]).length>0&&x(c,o.plugins||[],f,i)}catch(o){throw new l("Failed to process file",t,o)}}function x(t,o,a,e=new u){if(t&&"object"==typeof t){for(const r of o)try{r.onVisitNode?.(t,a)}catch(t){e.warn(`Plugin ${r.name} onVisitNode failed:`,t)}for(const r of Object.keys(t)){const n=t[r];if(Array.isArray(n))for(const t of n)t&&"object"==typeof t&&x(t,o,a,e);else n&&"object"==typeof n&&x(n,o,a,e)}}}async function w(t){t.extract.primaryLanguage||=t.locales[0]||"en",t.extract.secondaryLanguages||=t.locales.filter(o=>o!==t?.extract?.primaryLanguage),t.extract.functions||=["t"],t.extract.transComponents||=["Trans"];const{allKeys:o,objectKeys:a}=await s(t);return c(o,a,t)}export{w as extract,d as processFile,g as runExtractor};
@@ -1 +1 @@
1
- import{readFile as t}from"node:fs/promises";import{resolve as e}from"node:path";import{getNestedKeys as a,getNestedValue as o,setNestedValue as r}from"../../utils/nested-object.js";import{getOutputPath as s}from"../../utils/file-utils.js";function n(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}async function c(c,i,p){const f=p.extract.defaultNS??"translation",l=p.extract.keySeparator??".",u=[...p.extract.preservePatterns||[]];for(const t of i)u.push(`${t}.*`);const g=u.map(n);p.extract.primaryLanguage||(p.extract.primaryLanguage=p.locales[0]||"en"),p.extract.secondaryLanguages||(p.extract.secondaryLanguages=p.locales.filter(t=>t!==p.extract.primaryLanguage));const m=new Map;for(const t of c.values()){const e=t.ns||f;m.has(e)||m.set(e,[]),m.get(e).push(t)}const x=[];for(const n of p.locales)for(const[c,i]of m.entries()){const f=s(p.extract.output,n,c),u=e(process.cwd(),f);let m="",y={};try{m=await t(u,"utf-8"),y=JSON.parse(m)}catch(t){}const d={},h=a(y,l);for(const t of h)if(g.some(e=>e.test(t))){const e=o(y,t,l);r(d,t,e,l)}const L=!1===p.extract.sort?i:i.sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:e}of L){const a=o(y,t,l)??(n===p.extract?.primaryLanguage?e:"");r(d,t,a,l)}const w=p.extract.indentation??2,$=JSON.stringify(d,null,w);x.push({path:u,updated:$!==m,newTranslations:d,existingTranslations:y})}return x}export{c as getTranslations};
1
+ import{resolve as t}from"node:path";import{getNestedKeys as e,getNestedValue as n,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as s,loadTranslationFile as o}from"../../utils/file-utils.js";function r(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}async function c(c,i,l){const u=l.extract.defaultNS??"translation",f=l.extract.keySeparator??".",p=[...l.extract.preservePatterns||[]],g=l.extract.mergeNamespaces??!1;for(const t of i)p.push(`${t}.*`);const x=p.map(r);l.extract.primaryLanguage||=l.locales[0]||"en",l.extract.secondaryLanguages||=l.locales.filter(t=>t!==l?.extract?.primaryLanguage);const d=l.extract.primaryLanguage,y=new Map;for(const t of c.values()){const e=t.ns||u;y.has(e)||y.set(e,[]),y.get(e).push(t)}const m=[];for(const r of l.locales){const c={},i={};for(const[u,p]of y.entries()){const y=s(l.extract.output,r,g?void 0:u),h=t(process.cwd(),y),w=await o(h)||{},k={},N=e(w,f);for(const t of N)if(x.some(e=>e.test(t))){const e=n(w,t,f);a(k,t,e,f)}const O=!1===l.extract.sort?p:[...p].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:e}of O){const s=n(w,t,f)??(r===d?e:l.extract.defaultValue??"");a(k,t,s,f)}if(g)c[u]=k,Object.keys(w).length>0&&(i[u]=w);else{const t=w?JSON.stringify(w,null,l.extract.indentation??2):"",e=JSON.stringify(k,null,l.extract.indentation??2);m.push({path:h,updated:e!==t,newTranslations:k,existingTranslations:w})}}if(g){const e=s(l.extract.output,r),n=t(process.cwd(),e),a=Object.keys(i).length>0?JSON.stringify(i,null,l.extract.indentation??2):"",o=JSON.stringify(c,null,l.extract.indentation??2);m.push({path:n,updated:o!==a,newTranslations:c,existingTranslations:i})}}return m}export{c as getTranslations};
@@ -1 +1 @@
1
- import{extractFromTransComponent as e}from"./jsx-parser.js";class t{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||[]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}}export{t as ASTVisitors};
1
+ import{extractFromTransComponent as e}from"./jsx-parser.js";class t{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||["t"]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}}export{t as ASTVisitors};
@@ -1 +1 @@
1
- import{glob as s}from"glob";import{readdir as e}from"node:fs/promises";import{dirname as n,join as o}from"node:path";const t=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","app/i18n/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json","app/i18n/locales/en/*.json"];async function l(){for(const l of t){const t=await s(l,{ignore:"node_modules/**"});if(t.length>0){const s=t[0],l=n(n(s));try{let s=(await e(l)).filter(s=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(s));if(s.length>0)return s.sort(),s.includes("dev")&&(s=["dev",...s.filter(s=>"dev"!==s)]),s.includes("en")&&(s=["en",...s.filter(s=>"en"!==s)]),{locales:s,extract:{input:["src/**/*.{js,jsx,ts,tsx}","app/**/*.{js,jsx,ts,tsx}","pages/**/*.{js,jsx,ts,tsx}","components/**/*.{js,jsx,ts,tsx}"],output:o(l,"{{language}}","{{namespace}}.json"),primaryLanguage:s.includes("en")?"en":s[0]}}}catch{continue}}}return null}export{l as detectConfig};
1
+ import{glob as s}from"glob";import{readdir as e}from"node:fs/promises";import{dirname as n,extname as o,join as t}from"node:path";const l=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","app/i18n/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json","app/i18n/locales/en/*.json"];async function a(){for(const a of l){const l=await s(a,{ignore:"node_modules/**"});if(l.length>0){const s=l[0],a=n(n(s)),c=o(s);let r="json";".ts"===c?r="ts":".js"===c&&(r="js");try{let s=(await e(a)).filter(s=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(s));if(s.length>0)return s.sort(),s.includes("dev")&&(s=["dev",...s.filter(s=>"dev"!==s)]),s.includes("en")&&(s=["en",...s.filter(s=>"en"!==s)]),{locales:s,extract:{input:["src/**/*.{js,jsx,ts,tsx}","app/**/*.{js,jsx,ts,tsx}","pages/**/*.{js,jsx,ts,tsx}","components/**/*.{js,jsx,ts,tsx}"],output:t(a,"{{language}}",`{{namespace}}${c}`),outputFormat:r,primaryLanguage:s.includes("en")?"en":s[0]}}}catch{continue}}}return null}export{a as detectConfig};
@@ -1 +1 @@
1
- import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{readFile as a}from"node:fs/promises";import{findKeys as s}from"./extractor/core/key-finder.js";import{getNestedValue as n}from"./utils/nested-object.js";import{getOutputPath as r}from"./utils/file-utils.js";async function l(l,d={}){l.extract.primaryLanguage||(l.extract.primaryLanguage=l.locales[0]||"en"),l.extract.secondaryLanguages||(l.extract.secondaryLanguages=l.locales.filter(o=>o!==l?.extract?.primaryLanguage));const g=e("Analyzing project localization status...\n").start();try{const e=await async function(o){const{allKeys:e}=await s(o),{primaryLanguage:l,keySeparator:c=".",defaultNS:i="translation"}=o.extract,y=o.locales.filter(o=>o!==l),d=new Map;for(const o of e.values()){const e=o.ns||i;d.has(e)||d.set(e,[]),d.get(e).push(o)}const g={totalKeys:e.size,keysByNs:d,locales:new Map};for(const e of y){let s=0;const l=new Map;for(const[i,y]of d.entries()){const d=r(o.extract.output,e,i);let g={};try{const o=await a(t(process.cwd(),d),"utf-8");g=JSON.parse(o)}catch{}let u=0;const f=y.map(({key:o})=>{const e=!!n(g,o,c??".");return e&&u++,{key:o,isTranslated:e}});l.set(i,{totalKeys:y.length,translatedKeys:u,keyDetails:f}),s+=u}g.locales.set(e,{totalTranslated:s,namespaces:l})}return g}(l);g.succeed("Analysis complete."),function(e,t,a){a.detail?function(e,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(o.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(o.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=e.locales.get(a);if(!n)return void console.error(o.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(o.bold(`\nKey Status for "${o.cyan(a)}":`));const r=Array.from(e.keysByNs.values()).flat().length;c("Overall",n.totalTranslated,r);const l=s?[s]:Array.from(n.namespaces.keys()).sort();for(const e of l){const t=n.namespaces.get(e);t&&(console.log(o.cyan.bold(`\nNamespace: ${e}`)),c("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:e,isTranslated:t})=>{const a=t?o.green("✓"):o.red("✗");console.log(` ${a} ${e}`)}))}const i=r-n.totalTranslated;i>0?console.log(o.yellow.bold(`\nSummary: Found ${i} missing translations for "${a}".`)):console.log(o.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));y()}(e,t,a.detail,a.namespace):a.namespace?function(e,t,a){const s=e.keysByNs.get(a);if(!s)return void console.error(o.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(o.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[o,t]of e.locales.entries()){const e=t.namespaces.get(a);if(e){const t=e.totalKeys>0?Math.round(e.translatedKeys/e.totalKeys*100):100,a=i(t);console.log(`- ${o}: ${a} ${t}% (${e.translatedKeys}/${e.totalKeys} keys)`)}}y()}(e,0,a.namespace):function(e,t){const{primaryLanguage:a}=t.extract;console.log(o.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${o.bold(e.totalKeys)}`),console.log(`📚 Namespaces Found: ${o.bold(e.keysByNs.size)}`),console.log(`🌍 Locales: ${o.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${o.bold(a)}`),console.log("\nTranslation Progress:");for(const[o,t]of e.locales.entries()){const a=e.totalKeys>0?Math.round(t.totalTranslated/e.totalKeys*100):100,s=i(a);console.log(`- ${o}: ${s} ${a}% (${t.totalTranslated}/${e.totalKeys} keys)`)}y()}(e,t)}(e,l,d)}catch(o){g.fail("Failed to generate status report."),console.error(o)}}function c(e,t,a){const s=a>0?Math.round(t/a*100):100,n=i(s);console.log(`${o.bold(e)}: ${n} ${s}% (${t}/${a})`)}function i(e){const t=Math.round(e/100*20),a=20-t;return`[${o.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function y(){console.log(o.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${o.cyan("npx i18next-cli locize-migrate")} to get started.`)}export{l as runStatus};
1
+ import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{findKeys as a}from"./extractor/core/key-finder.js";import{getNestedValue as s}from"./utils/nested-object.js";import{loadTranslationFile as n,getOutputPath as l}from"./utils/file-utils.js";async function r(r,d={}){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(o=>o!==r?.extract?.primaryLanguage);const g=e("Analyzing project localization status...\n").start();try{const e=await async function(o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(e=>e!==o?.extract?.primaryLanguage);const{allKeys:e}=await a(o),{secondaryLanguages:r,keySeparator:c=".",defaultNS:i="translation",mergeNamespaces:y=!1}=o.extract,d=new Map;for(const o of e.values()){const e=o.ns||i;d.has(e)||d.set(e,[]),d.get(e).push(o)}const g={totalKeys:e.size,keysByNs:d,locales:new Map};for(const e of r){let a=0;const r=new Map,i=y?await n(t(process.cwd(),l(o.extract.output,e)))||{}:null;for(const[g,u]of d.entries()){const d=y?i?.[g]||{}:await n(t(process.cwd(),l(o.extract.output,e,g)))||{};let f=0;const m=u.map(({key:o})=>{const e=!!s(d,o,c??".");return e&&f++,{key:o,isTranslated:e}});r.set(g,{totalKeys:u.length,translatedKeys:f,keyDetails:m}),a+=f}g.locales.set(e,{totalTranslated:a,namespaces:r})}return g}(r);g.succeed("Analysis complete."),function(e,t,a){a.detail?function(e,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(o.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(o.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=e.locales.get(a);if(!n)return void console.error(o.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(o.bold(`\nKey Status for "${o.cyan(a)}":`));const l=Array.from(e.keysByNs.values()).flat().length;c("Overall",n.totalTranslated,l);const r=s?[s]:Array.from(n.namespaces.keys()).sort();for(const e of r){const t=n.namespaces.get(e);t&&(console.log(o.cyan.bold(`\nNamespace: ${e}`)),c("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:e,isTranslated:t})=>{const a=t?o.green("✓"):o.red("✗");console.log(` ${a} ${e}`)}))}const i=l-n.totalTranslated;i>0?console.log(o.yellow.bold(`\nSummary: Found ${i} missing translations for "${a}".`)):console.log(o.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));y()}(e,t,a.detail,a.namespace):a.namespace?function(e,t,a){const s=e.keysByNs.get(a);if(!s)return void console.error(o.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(o.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[o,t]of e.locales.entries()){const e=t.namespaces.get(a);if(e){const t=e.totalKeys>0?Math.round(e.translatedKeys/e.totalKeys*100):100,a=i(t);console.log(`- ${o}: ${a} ${t}% (${e.translatedKeys}/${e.totalKeys} keys)`)}}y()}(e,0,a.namespace):function(e,t){const{primaryLanguage:a}=t.extract;console.log(o.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${o.bold(e.totalKeys)}`),console.log(`📚 Namespaces Found: ${o.bold(e.keysByNs.size)}`),console.log(`🌍 Locales: ${o.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${o.bold(a)}`),console.log("\nTranslation Progress:");for(const[o,t]of e.locales.entries()){const a=e.totalKeys>0?Math.round(t.totalTranslated/e.totalKeys*100):100,s=i(a);console.log(`- ${o}: ${s} ${a}% (${t.totalTranslated}/${e.totalKeys} keys)`)}y()}(e,t)}(e,r,d)}catch(o){g.fail("Failed to generate status report."),console.error(o)}}function c(e,t,a){const s=a>0?Math.round(t/a*100):100,n=i(s);console.log(`${o.bold(e)}: ${n} ${s}% (${t}/${a})`)}function i(e){const t=Math.round(e/100*20),a=20-t;return`[${o.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function y(){console.log(o.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${o.cyan("npx i18next-cli locize-migrate")} to get started.`)}export{r as runStatus};
@@ -1 +1 @@
1
- import{readFile as t,mkdir as o,writeFile as e}from"node:fs/promises";import{resolve as r,dirname as n}from"path";import a from"chalk";import c from"ora";import{getNestedKeys as l,getNestedValue as s,setNestedValue as i}from"./utils/nested-object.js";import{getOutputPath as f}from"./utils/file-utils.js";async function u(u){const p=c("Running i18next locale synchronizer...\n").start();u.extract.primaryLanguage||=u.locales[0]||"en";const{primaryLanguage:y}=u.extract,d=u.locales.filter(t=>t!==y),m=u.extract.keySeparator??".",g=[];let h=!1;const x=u.extract.defaultNS??"translation",w=f(u.extract.output,y,x),S=r(process.cwd(),w);let $;try{const o=await t(S,"utf-8");$=JSON.parse(o)}catch(t){return void console.error(`Primary language file not found at ${w}. Cannot sync.`)}const b=l($,m);for(const c of d){const l=f(u.extract.output,c,x),p=r(process.cwd(),l);let y={},d="";try{d=await t(p,"utf-8"),y=JSON.parse(d)}catch(t){}const w={};for(const t of b){const o=s(y,t,m)??(u.extract?.defaultValue||"");i(w,t,o,m)}const S=u.extract.indentation??2,$=JSON.stringify(w,null,S);$!==d?(h=!0,await o(n(p),{recursive:!0}),await e(p,$),g.push(` ${a.green("✓")} Synchronized: ${l}`)):g.push(` ${a.gray("-")} Already in sync: ${l}`)}p.succeed(a.bold("Synchronization complete!")),g.forEach(t=>console.log(t)),h?(console.log(a.green.bold("\n✅ Sync complete.")),console.log(a.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${a.cyan("npx i18next-cli locize-migrate")}`)):console.log(a.green.bold("\n✅ All locales are already in sync."))}export{u as runSyncer};
1
+ import{mkdir as o,writeFile as t}from"node:fs/promises";import{basename as e,resolve as n,dirname as r}from"path";import i from"chalk";import l from"ora";import{glob as a}from"glob";import{getNestedKeys as s,getNestedValue as c,setNestedValue as f}from"./utils/nested-object.js";import{getOutputPath as p,loadTranslationFile as u,serializeTranslationFile as y}from"./utils/file-utils.js";async function m(m){const d=l("Running i18next locale synchronizer...\n").start();try{const l=m.extract.primaryLanguage||m.locales[0]||"en",g=m.locales.filter(o=>o!==l),{output:h,keySeparator:w=".",outputFormat:S="json",indentation:$=2,defaultValue:x=""}=m.extract,b=[];let z=!1;const j=p(h,l,"*"),N=await a(j);if(0===N.length)return void d.warn(`No translation files found for primary language "${l}". Nothing to sync.`);for(const l of N){const a=e(l).split(".")[0],m=await u(l);if(!m){b.push(` ${i.yellow("-")} Could not read primary file: ${l}`);continue}const d=s(m,w??".");for(const e of g){const l=p(h,e,a),s=n(process.cwd(),l),m=await u(s)||{},g={};for(const o of d){const t=c(m,o,w??".");f(g,o,t??x,w??".")}const j=JSON.stringify(m);if(JSON.stringify(g)!==j){z=!0;const e=y(g,S,$);await o(r(s),{recursive:!0}),await t(s,e),b.push(` ${i.green("✓")} Synchronized: ${l}`)}else b.push(` ${i.gray("-")} Already in sync: ${l}`)}}d.succeed(i.bold("Synchronization complete!")),b.forEach(o=>console.log(o)),z?(console.log(i.green.bold("\n✅ Sync complete.")),console.log(i.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${i.cyan("npx i18next-cli locize-migrate")}`)):console.log(i.green.bold("\n✅ All locales are already in sync."))}catch(o){d.fail(i.red("Synchronization failed.")),console.error(o)}}export{m as runSyncer};
@@ -1 +1 @@
1
- import"node:fs/promises";import"node:path";function e(e,p,r){return e.replace("{{language}}",p).replace("{{lng}}",p).replace("{{namespace}}",r).replace("{{ns}}",r)}export{e as getOutputPath};
1
+ import{readFile as t}from"node:fs/promises";import"node:path";import{createJiti as e}from"jiti";function n(t,e,n=""){return t.replace("{{language}}",e).replace("{{lng}}",e).replace("{{namespace}}",n).replace("{{ns}}",n)}async function r(n){try{if(n.endsWith(".json")){const e=await t(n,"utf-8");return JSON.parse(e)}if(n.endsWith(".ts")||n.endsWith(".js")){const t=e(import.meta.url);return await t.import(n,{default:!0})}}catch(t){return null}return null}function s(t,e="json",n=2){const r=JSON.stringify(t,null,n);switch(e){case"js":case"js-esm":return`export default ${r};\n`;case"js-cjs":return`module.exports = ${r};\n`;case"ts":return`export default ${r} as const;\n`;default:return r}}export{n as getOutputPath,r as loadTranslationFile,s as serializeTranslationFile};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "0.9.9",
3
+ "version": "0.9.10",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -21,7 +21,7 @@ const program = new Command()
21
21
  program
22
22
  .name('i18next-cli')
23
23
  .description('A unified, high-performance i18next CLI.')
24
- .version('0.9.9')
24
+ .version('0.9.10')
25
25
 
26
26
  program
27
27
  .command('extract')
@@ -11,6 +11,7 @@ import { createPluginContext } from '../plugin-manager'
11
11
  import { extractKeysFromComments } from '../parsers/comment-parser'
12
12
  import { ASTVisitors } from '../parsers/ast-visitors'
13
13
  import { ConsoleLogger } from '../../utils/logger'
14
+ import { serializeTranslationFile } from '../../utils/file-utils'
14
15
 
15
16
  /**
16
17
  * Main extractor function that runs the complete key extraction and file generation process.
@@ -42,8 +43,8 @@ export async function runExtractor (
42
43
  config: I18nextToolkitConfig,
43
44
  logger: Logger = new ConsoleLogger()
44
45
  ): Promise<boolean> {
45
- if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0] || 'en'
46
- if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
46
+ config.extract.primaryLanguage ||= config.locales[0] || 'en'
47
+ config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
47
48
 
48
49
  validateExtractorConfig(config)
49
50
 
@@ -59,8 +60,13 @@ export async function runExtractor (
59
60
  for (const result of results) {
60
61
  if (result.updated) {
61
62
  anyFileUpdated = true
63
+ const fileContent = serializeTranslationFile(
64
+ result.newTranslations,
65
+ config.extract.outputFormat,
66
+ config.extract.indentation
67
+ )
62
68
  await mkdir(dirname(result.path), { recursive: true })
63
- await writeFile(result.path, JSON.stringify(result.newTranslations, null, 2))
69
+ await writeFile(result.path, fileContent)
64
70
  logger.info(chalk.green(`Updated: ${result.path}`))
65
71
  }
66
72
  }
@@ -180,10 +186,10 @@ function traverseEveryNode (node: any, plugins: any[], pluginContext: PluginCont
180
186
  * ```
181
187
  */
182
188
  export async function extract (config: I18nextToolkitConfig) {
183
- if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0]
184
- if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
185
- if (!config.extract.functions) config.extract.functions = ['t']
186
- if (!config.extract.transComponents) config.extract.transComponents = ['Trans']
189
+ config.extract.primaryLanguage ||= config.locales[0] || 'en'
190
+ config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
191
+ config.extract.functions ||= ['t']
192
+ config.extract.transComponents ||= ['Trans']
187
193
  const { allKeys, objectKeys } = await findKeys(config)
188
194
  return getTranslations(allKeys, objectKeys, config)
189
195
  }
@@ -1,8 +1,7 @@
1
1
  import { TranslationResult, ExtractedKey, I18nextToolkitConfig } from '../../types'
2
- import { readFile } from 'node:fs/promises'
3
2
  import { resolve } from 'node:path'
4
3
  import { getNestedValue, setNestedValue, getNestedKeys } from '../../utils/nested-object'
5
- import { getOutputPath } from '../../utils/file-utils'
4
+ import { getOutputPath, loadTranslationFile } from '../../utils/file-utils'
6
5
 
7
6
  /**
8
7
  * Converts a glob pattern to a regular expression for matching keys
@@ -51,13 +50,15 @@ export async function getTranslations (
51
50
  const defaultNS = config.extract.defaultNS ?? 'translation'
52
51
  const keySeparator = config.extract.keySeparator ?? '.'
53
52
  const patternsToPreserve = [...(config.extract.preservePatterns || [])]
53
+ const mergeNamespaces = config.extract.mergeNamespaces ?? false
54
54
  for (const key of objectKeys) {
55
55
  // Convert the object key to a glob pattern to preserve all its children
56
56
  patternsToPreserve.push(`${key}.*`)
57
57
  }
58
58
  const preservePatterns = patternsToPreserve.map(globToRegex)
59
- if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0] || 'en'
60
- if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config.extract.primaryLanguage)
59
+ config.extract.primaryLanguage ||= config.locales[0] || 'en'
60
+ config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
61
+ const primaryLanguage = config.extract.primaryLanguage
61
62
 
62
63
  // Group keys by namespace
63
64
  const keysByNS = new Map<string, ExtractedKey[]>()
@@ -72,21 +73,16 @@ export async function getTranslations (
72
73
  const results: TranslationResult[] = []
73
74
 
74
75
  for (const locale of config.locales) {
75
- for (const [ns, nsKeys] of keysByNS.entries()) {
76
- const outputPath = getOutputPath(config.extract.output, locale, ns)
76
+ const mergedTranslations: Record<string, any> = {}
77
+ const mergedExisting: Record<string, any> = {}
77
78
 
79
+ for (const [ns, nsKeys] of keysByNS.entries()) {
80
+ const outputPath = getOutputPath(config.extract.output, locale, mergeNamespaces ? undefined : ns)
78
81
  const fullPath = resolve(process.cwd(), outputPath)
79
82
 
80
- let oldContent = ''
81
- let existingTranslations: Record<string, any> = {}
82
- try {
83
- oldContent = await readFile(fullPath, 'utf-8')
84
- existingTranslations = JSON.parse(oldContent)
85
- } catch (e) { /* File doesn't exist, which is fine */ }
86
-
83
+ const existingTranslations = await loadTranslationFile(fullPath) || {}
87
84
  const newTranslations: Record<string, any> = {}
88
85
 
89
- // 1. Preserve keys from existing translations that match patterns
90
86
  const existingKeys = getNestedKeys(existingTranslations, keySeparator)
91
87
  for (const existingKey of existingKeys) {
92
88
  if (preservePatterns.some(re => re.test(existingKey))) {
@@ -95,24 +91,45 @@ export async function getTranslations (
95
91
  }
96
92
  }
97
93
 
98
- // 2. Merge in newly found keys for this namespace
99
94
  const sortedKeys = (config.extract.sort === false)
100
95
  ? nsKeys
101
- : nsKeys.sort((a, b) => a.key.localeCompare(b.key))
96
+ : [...nsKeys].sort((a, b) => a.key.localeCompare(b.key))
97
+
102
98
  for (const { key, defaultValue } of sortedKeys) {
103
99
  const existingValue = getNestedValue(existingTranslations, key, keySeparator)
104
- const valueToSet = existingValue ?? (locale === config.extract?.primaryLanguage ? defaultValue : '')
100
+ const valueToSet = existingValue ?? (locale === primaryLanguage ? defaultValue : (config.extract.defaultValue ?? ''))
105
101
  setNestedValue(newTranslations, key, valueToSet, keySeparator)
106
102
  }
107
103
 
108
- const indentation = config.extract.indentation ?? 2
109
- const newContent = JSON.stringify(newTranslations, null, indentation)
104
+ if (mergeNamespaces) {
105
+ mergedTranslations[ns] = newTranslations
106
+ if (Object.keys(existingTranslations).length > 0) {
107
+ mergedExisting[ns] = existingTranslations
108
+ }
109
+ } else {
110
+ const oldContent = existingTranslations ? JSON.stringify(existingTranslations, null, config.extract.indentation ?? 2) : ''
111
+ const newContent = JSON.stringify(newTranslations, null, config.extract.indentation ?? 2)
112
+
113
+ results.push({
114
+ path: fullPath,
115
+ updated: newContent !== oldContent,
116
+ newTranslations,
117
+ existingTranslations,
118
+ })
119
+ }
120
+ }
121
+
122
+ if (mergeNamespaces) {
123
+ const outputPath = getOutputPath(config.extract.output, locale)
124
+ const fullPath = resolve(process.cwd(), outputPath)
125
+ const oldContent = Object.keys(mergedExisting).length > 0 ? JSON.stringify(mergedExisting, null, config.extract.indentation ?? 2) : ''
126
+ const newContent = JSON.stringify(mergedTranslations, null, config.extract.indentation ?? 2)
110
127
 
111
128
  results.push({
112
129
  path: fullPath,
113
130
  updated: newContent !== oldContent,
114
- newTranslations,
115
- existingTranslations,
131
+ newTranslations: mergedTranslations,
132
+ existingTranslations: mergedExisting,
116
133
  })
117
134
  }
118
135
  }
@@ -337,7 +337,7 @@ export class ASTVisitors {
337
337
  if (callee.type !== 'Identifier') return
338
338
 
339
339
  const scopeInfo = this.getVarFromScope(callee.value)
340
- const isFunctionToParse = (this.config.extract.functions || []).includes(callee.value) || scopeInfo !== undefined
340
+ const isFunctionToParse = (this.config.extract.functions || ['t']).includes(callee.value) || scopeInfo !== undefined
341
341
  if (!isFunctionToParse || node.arguments.length === 0) return
342
342
 
343
343
  const firstArg = node.arguments[0].expression
@@ -1,6 +1,6 @@
1
1
  import { glob } from 'glob'
2
2
  import { readdir } from 'node:fs/promises'
3
- import { dirname, join } from 'node:path'
3
+ import { dirname, join, extname } from 'node:path'
4
4
  import type { I18nextToolkitConfig } from './types'
5
5
 
6
6
  // A list of common glob patterns for the primary language ('en') or ('dev') translation files.
@@ -32,10 +32,20 @@ export async function detectConfig (): Promise<Partial<I18nextToolkitConfig> | n
32
32
  if (files.length > 0) {
33
33
  const firstFile = files[0]
34
34
  const basePath = dirname(dirname(firstFile))
35
+ const extension = extname(firstFile)
36
+
37
+ // Infer outputFormat from the file extension
38
+ let outputFormat: I18nextToolkitConfig['extract']['outputFormat'] = 'json'
39
+ if (extension === '.ts') {
40
+ outputFormat = 'ts'
41
+ } else if (extension === '.js') {
42
+ // We can't know if it's ESM or CJS, so we default to a safe choice.
43
+ // The tool's file loaders can handle both.
44
+ outputFormat = 'js'
45
+ }
35
46
 
36
47
  try {
37
48
  const allDirs = await readdir(basePath)
38
- // CORRECTED REGEX: Now accepts 'dev' in addition to standard locale codes.
39
49
  let locales = allDirs.filter(dir => /^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(dir))
40
50
 
41
51
  if (locales.length > 0) {
@@ -57,7 +67,8 @@ export async function detectConfig (): Promise<Partial<I18nextToolkitConfig> | n
57
67
  'pages/**/*.{js,jsx,ts,tsx}',
58
68
  'components/**/*.{js,jsx,ts,tsx}'
59
69
  ],
60
- output: join(basePath, '{{language}}', '{{namespace}}.json'),
70
+ output: join(basePath, '{{language}}', `{{namespace}}${extension}`),
71
+ outputFormat,
61
72
  primaryLanguage: locales.includes('en') ? 'en' : locales[0],
62
73
  },
63
74
  }
package/src/status.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import chalk from 'chalk'
2
2
  import ora from 'ora'
3
3
  import { resolve } from 'node:path'
4
- import { readFile } from 'node:fs/promises'
5
4
  import { findKeys } from './extractor/core/key-finder'
6
5
  import { getNestedValue } from './utils/nested-object'
7
6
  import type { I18nextToolkitConfig, ExtractedKey } from './types'
8
- import { getOutputPath } from './utils/file-utils'
7
+ import { getOutputPath, loadTranslationFile } from './utils/file-utils'
9
8
 
10
9
  /**
11
10
  * Options for configuring the status report display.
@@ -56,8 +55,8 @@ interface StatusReport {
56
55
  * @throws {Error} When unable to extract keys or read translation files
57
56
  */
58
57
  export async function runStatus (config: I18nextToolkitConfig, options: StatusOptions = {}) {
59
- if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0] || 'en'
60
- if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
58
+ config.extract.primaryLanguage ||= config.locales[0] || 'en'
59
+ config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
61
60
  const spinner = ora('Analyzing project localization status...\n').start()
62
61
  try {
63
62
  const report = await generateStatusReport(config)
@@ -84,9 +83,16 @@ export async function runStatus (config: I18nextToolkitConfig, options: StatusOp
84
83
  * @throws {Error} When key extraction fails or configuration is invalid
85
84
  */
86
85
  async function generateStatusReport (config: I18nextToolkitConfig): Promise<StatusReport> {
86
+ config.extract.primaryLanguage ||= config.locales[0] || 'en'
87
+ config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
88
+
87
89
  const { allKeys: allExtractedKeys } = await findKeys(config)
88
- const { primaryLanguage, keySeparator = '.', defaultNS = 'translation' } = config.extract
89
- const secondaryLanguages = config.locales.filter(l => l !== primaryLanguage)
90
+ const {
91
+ secondaryLanguages,
92
+ keySeparator = '.',
93
+ defaultNS = 'translation',
94
+ mergeNamespaces = false,
95
+ } = config.extract
90
96
 
91
97
  const keysByNs = new Map<string, ExtractedKey[]>()
92
98
  for (const key of allExtractedKeys.values()) {
@@ -105,17 +111,19 @@ async function generateStatusReport (config: I18nextToolkitConfig): Promise<Stat
105
111
  let totalTranslatedForLocale = 0
106
112
  const namespaces = new Map<string, any>()
107
113
 
114
+ const mergedTranslations = mergeNamespaces
115
+ ? await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale))) || {}
116
+ : null
117
+
108
118
  for (const [ns, keysInNs] of keysByNs.entries()) {
109
- const langFilePath = getOutputPath(config.extract.output, locale, ns)
110
- let translations: Record<string, any> = {}
111
- try {
112
- const content = await readFile(resolve(process.cwd(), langFilePath), 'utf-8')
113
- translations = JSON.parse(content)
114
- } catch {}
119
+ const translationsForNs = mergeNamespaces
120
+ ? mergedTranslations?.[ns] || {}
121
+ : await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale, ns))) || {}
115
122
 
116
123
  let translatedInNs = 0
117
124
  const keyDetails = keysInNs.map(({ key }) => {
118
- const value = getNestedValue(translations, key, keySeparator ?? '.')
125
+ // Search for the key within the correct namespace object
126
+ const value = getNestedValue(translationsForNs, key, keySeparator ?? '.')
119
127
  const isTranslated = !!value
120
128
  if (isTranslated) translatedInNs++
121
129
  return { key, isTranslated }
package/src/syncer.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises'
2
- import { resolve, dirname } from 'path'
1
+ import { writeFile, mkdir } from 'node:fs/promises'
2
+ import { resolve, dirname, basename } from 'path'
3
3
  import chalk from 'chalk'
4
4
  import ora from 'ora'
5
+ import { glob } from 'glob'
5
6
  import type { I18nextToolkitConfig } from './types'
6
7
  import { getNestedKeys, getNestedValue, setNestedValue } from './utils/nested-object'
7
- import { getOutputPath } from './utils/file-utils'
8
+ import { getOutputPath, loadTranslationFile, serializeTranslationFile } from './utils/file-utils'
8
9
 
9
10
  /**
10
11
  * Synchronizes translation files across different locales by ensuring all secondary
@@ -38,77 +39,86 @@ import { getOutputPath } from './utils/file-utils'
38
39
  */
39
40
  export async function runSyncer (config: I18nextToolkitConfig) {
40
41
  const spinner = ora('Running i18next locale synchronizer...\n').start()
41
-
42
- config.extract.primaryLanguage ||= config.locales[0] || 'en'
43
- const { primaryLanguage } = config.extract
44
- const secondaryLanguages = config.locales.filter(l => l !== primaryLanguage)
45
- const keySeparator = config.extract.keySeparator ?? '.'
46
-
47
- const logMessages: string[] = []
48
- let wasAnythingSynced = false
49
-
50
- // Assume sync operates on the default namespace for simplicity
51
- const defaultNS = config.extract.defaultNS ?? 'translation'
52
-
53
- // 1. Get all keys from the primary language file
54
- const primaryPath = getOutputPath(config.extract.output, primaryLanguage, defaultNS)
55
-
56
- const fullPrimaryPath = resolve(process.cwd(), primaryPath)
57
-
58
- let primaryTranslations: Record<string, any>
59
42
  try {
60
- const primaryContent = await readFile(fullPrimaryPath, 'utf-8')
61
- primaryTranslations = JSON.parse(primaryContent)
62
- } catch (e) {
63
- console.error(`Primary language file not found at ${primaryPath}. Cannot sync.`)
64
- return
65
- }
66
-
67
- const primaryKeys = getNestedKeys(primaryTranslations, keySeparator)
68
-
69
- // 2. Iterate through secondary languages and sync them
70
- for (const lang of secondaryLanguages) {
71
- const secondaryPath = getOutputPath(config.extract.output, lang, defaultNS)
72
- const fullSecondaryPath = resolve(process.cwd(), secondaryPath)
73
-
74
- let secondaryTranslations: Record<string, any> = {}
75
- let oldContent = ''
76
- try {
77
- oldContent = await readFile(fullSecondaryPath, 'utf-8')
78
- secondaryTranslations = JSON.parse(oldContent)
79
- } catch (e) { /* File doesn't exist, will be created */ }
80
-
81
- const newSecondaryTranslations: Record<string, any> = {}
43
+ const primaryLanguage = config.extract.primaryLanguage || config.locales[0] || 'en'
44
+ const secondaryLanguages = config.locales.filter((l) => l !== primaryLanguage)
45
+ const {
46
+ output,
47
+ keySeparator = '.',
48
+ outputFormat = 'json',
49
+ indentation = 2,
50
+ defaultValue = '',
51
+ } = config.extract
52
+
53
+ const logMessages: string[] = []
54
+ let wasAnythingSynced = false
55
+
56
+ // 1. Find all namespace files for the primary language
57
+ const primaryNsPattern = getOutputPath(output, primaryLanguage, '*')
58
+ const primaryNsFiles = await glob(primaryNsPattern)
59
+
60
+ if (primaryNsFiles.length === 0) {
61
+ spinner.warn(`No translation files found for primary language "${primaryLanguage}". Nothing to sync.`)
62
+ return
63
+ }
82
64
 
83
- // Rebuild the secondary file based on the primary file's keys
84
- for (const key of primaryKeys) {
85
- const existingValue = getNestedValue(secondaryTranslations, key, keySeparator)
86
- // If value exists in old file, keep it. Otherwise, add as empty string.
87
- const valueToSet = existingValue ?? (config.extract?.defaultValue || '')
88
- setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator)
65
+ // 2. Loop through each primary namespace file
66
+ for (const primaryPath of primaryNsFiles) {
67
+ const ns = basename(primaryPath).split('.')[0]
68
+ const primaryTranslations = await loadTranslationFile(primaryPath)
69
+
70
+ if (!primaryTranslations) {
71
+ logMessages.push(` ${chalk.yellow('-')} Could not read primary file: ${primaryPath}`)
72
+ continue
73
+ }
74
+
75
+ const primaryKeys = getNestedKeys(primaryTranslations, keySeparator ?? '.')
76
+
77
+ // 3. For each secondary language, sync the current namespace
78
+ for (const lang of secondaryLanguages) {
79
+ const secondaryPath = getOutputPath(output, lang, ns)
80
+ const fullSecondaryPath = resolve(process.cwd(), secondaryPath)
81
+ const existingSecondaryTranslations = await loadTranslationFile(fullSecondaryPath) || {}
82
+ const newSecondaryTranslations: Record<string, any> = {}
83
+
84
+ for (const key of primaryKeys) {
85
+ const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.')
86
+ const valueToSet = existingValue ?? defaultValue
87
+ setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.')
88
+ }
89
+
90
+ // Use JSON.stringify for a reliable object comparison, regardless of format
91
+ const oldContent = JSON.stringify(existingSecondaryTranslations)
92
+ const newContent = JSON.stringify(newSecondaryTranslations)
93
+
94
+ if (newContent !== oldContent) {
95
+ wasAnythingSynced = true
96
+ const serializedContent = serializeTranslationFile(newSecondaryTranslations, outputFormat, indentation)
97
+ await mkdir(dirname(fullSecondaryPath), { recursive: true })
98
+ await writeFile(fullSecondaryPath, serializedContent)
99
+ logMessages.push(` ${chalk.green('✓')} Synchronized: ${secondaryPath}`)
100
+ } else {
101
+ logMessages.push(` ${chalk.gray('-')} Already in sync: ${secondaryPath}`)
102
+ }
103
+ }
89
104
  }
90
105
 
91
- const indentation = config.extract.indentation ?? 2
92
- const newContent = JSON.stringify(newSecondaryTranslations, null, indentation)
106
+ spinner.succeed(chalk.bold('Synchronization complete!'))
107
+ logMessages.forEach(msg => console.log(msg))
93
108
 
94
- if (newContent !== oldContent) {
95
- wasAnythingSynced = true
96
- await mkdir(dirname(fullSecondaryPath), { recursive: true })
97
- await writeFile(fullSecondaryPath, newContent)
98
- logMessages.push(` ${chalk.green('✓')} Synchronized: ${secondaryPath}`)
109
+ if (wasAnythingSynced) {
110
+ printLocizeFunnel()
99
111
  } else {
100
- logMessages.push(` ${chalk.gray('-')} Already in sync: ${secondaryPath}`)
112
+ console.log(chalk.green.bold('\n✅ All locales are already in sync.'))
101
113
  }
114
+ } catch (error) {
115
+ spinner.fail(chalk.red('Synchronization failed.'))
116
+ console.error(error)
102
117
  }
118
+ }
103
119
 
104
- spinner.succeed(chalk.bold('Synchronization complete!'))
105
- logMessages.forEach(msg => console.log(msg))
106
-
107
- if (wasAnythingSynced) {
108
- console.log(chalk.green.bold('\n✅ Sync complete.'))
109
- console.log(chalk.yellow('🚀 Ready to collaborate with translators? Move your files to the cloud.'))
110
- console.log(` Get started with the official TMS for i18next: ${chalk.cyan('npx i18next-cli locize-migrate')}`)
111
- } else {
112
- console.log(chalk.green.bold('\n✅ All locales are already in sync.'))
113
- }
120
+ function printLocizeFunnel () {
121
+ console.log(chalk.green.bold('\n✅ Sync complete.'))
122
+ console.log(chalk.yellow('🚀 Ready to collaborate with translators? Move your files to the cloud.'))
123
+ console.log(` Get started with the official TMS for i18next: ${chalk.cyan('npx i18next-cli locize-migrate')}`)
114
124
  }
package/src/types.ts CHANGED
@@ -83,6 +83,24 @@ export interface I18nextToolkitConfig {
83
83
 
84
84
  /** Secondary languages that get empty values initially */
85
85
  secondaryLanguages?: string[];
86
+
87
+ /**
88
+ * The format of the output translation files.
89
+ * 'json': Standard JSON file (default)
90
+ * 'js': JavaScript file with ES Module syntax (export default)
91
+ * 'js-esm': JavaScript file with ES Module syntax (export default)
92
+ * 'js-cjs': JavaScript file with CommonJS syntax (module.exports)
93
+ * 'ts': TypeScript file with ES Module syntax and `as const` for type safety
94
+ */
95
+ outputFormat?: 'json' | 'js' | 'js-esm' | 'js-esm' | 'js-cjs' | 'ts';
96
+
97
+ /**
98
+ * If true, all namespaces will be merged into a single file per language.
99
+ * The `output` path should not contain the `{{namespace}}` placeholder.
100
+ * Example output: `locales/en.js`
101
+ * (default: false)
102
+ */
103
+ mergeNamespaces?: boolean;
86
104
  };
87
105
 
88
106
  /** Configuration options for TypeScript type generation */
@@ -1,5 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
2
  import { dirname } from 'node:path'
3
+ import { createJiti } from 'jiti'
4
+ import type { I18nextToolkitConfig } from '../types'
3
5
 
4
6
  /**
5
7
  * Ensures that the directory for a given file path exists.
@@ -73,9 +75,60 @@ export async function writeFileAsync (filePath: string, data: string): Promise<v
73
75
  export function getOutputPath (
74
76
  template: string,
75
77
  locale: string,
76
- namespace: string
78
+ namespace: string = ''
77
79
  ): string {
78
80
  return template
79
81
  .replace('{{language}}', locale).replace('{{lng}}', locale)
80
82
  .replace('{{namespace}}', namespace).replace('{{ns}}', namespace)
81
83
  }
84
+
85
+ /**
86
+ * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
87
+ * @param filePath - The path to the translation file.
88
+ * @returns The parsed content of the file, or null if not found or failed to parse.
89
+ */
90
+ export async function loadTranslationFile (filePath: string): Promise<Record<string, any> | null> {
91
+ try {
92
+ if (filePath.endsWith('.json')) {
93
+ const content = await readFile(filePath, 'utf-8')
94
+ return JSON.parse(content)
95
+ } else if (filePath.endsWith('.ts') || filePath.endsWith('.js')) {
96
+ const jiti = createJiti(import.meta.url)
97
+ const module = await jiti.import(filePath, { default: true })
98
+ return module as Record<string, any> | null
99
+ }
100
+ } catch (e) {
101
+ // File not found or parse error
102
+ return null
103
+ }
104
+ return null
105
+ }
106
+
107
+ /**
108
+ * Serializes a translation object into a string based on the desired format.
109
+ * @param data - The translation data object.
110
+ * @param format - The desired output format ('json', 'js-esm', etc.).
111
+ * @param indentation - The number of spaces for indentation.
112
+ * @returns The serialized file content as a string.
113
+ */
114
+ export function serializeTranslationFile (
115
+ data: Record<string, any>,
116
+ format: I18nextToolkitConfig['extract']['outputFormat'] = 'json',
117
+ indentation: number = 2
118
+ ): string {
119
+ const jsonString = JSON.stringify(data, null, indentation)
120
+
121
+ switch (format) {
122
+ case 'js':
123
+ case 'js-esm':
124
+ return `export default ${jsonString};\n`
125
+ case 'js-cjs':
126
+ return `module.exports = ${jsonString};\n`
127
+ case 'ts':
128
+ // Using `as const` provides better type inference for TypeScript users
129
+ return `export default ${jsonString} as const;\n`
130
+ case 'json':
131
+ default:
132
+ return jsonString
133
+ }
134
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAM5F,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAGrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,OAAO,CAAC,CA+BlB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,WAAW,EACxB,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAmCD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,sDAO1D"}
1
+ {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAM5F,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,OAAO,CAAC,CAoClB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,WAAW,EACxB,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAmCD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,sDAO1D"}
@@ -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;AAiBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAuE9B"}
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;AAgBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyF9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"heuristic-config.d.ts","sourceRoot":"","sources":["../src/heuristic-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAiBnD;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAK,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC,CA4CnF"}
1
+ {"version":3,"file":"heuristic-config.d.ts","sourceRoot":"","sources":["../src/heuristic-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAiBnD;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAK,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC,CAuDnF"}
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,SAAS,CAAA;AAGjE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAYzF"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,SAAS,CAAA;AAGjE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAYzF"}
@@ -1 +1 @@
1
- {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAInD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,iBA2E5D"}
1
+ {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAInD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,iBA8E5D"}
package/types/types.d.ts CHANGED
@@ -63,6 +63,22 @@ export interface I18nextToolkitConfig {
63
63
  primaryLanguage?: string;
64
64
  /** Secondary languages that get empty values initially */
65
65
  secondaryLanguages?: string[];
66
+ /**
67
+ * The format of the output translation files.
68
+ * 'json': Standard JSON file (default)
69
+ * 'js': JavaScript file with ES Module syntax (export default)
70
+ * 'js-esm': JavaScript file with ES Module syntax (export default)
71
+ * 'js-cjs': JavaScript file with CommonJS syntax (module.exports)
72
+ * 'ts': TypeScript file with ES Module syntax and `as const` for type safety
73
+ */
74
+ outputFormat?: 'json' | 'js' | 'js-esm' | 'js-esm' | 'js-cjs' | 'ts';
75
+ /**
76
+ * If true, all namespaces will be merged into a single file per language.
77
+ * The `output` path should not contain the `{{namespace}}` placeholder.
78
+ * Example output: `locales/en.js`
79
+ * (default: false)
80
+ */
81
+ mergeNamespaces?: boolean;
66
82
  };
67
83
  /** Configuration options for TypeScript type generation */
68
84
  types?: {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import type { I18nextToolkitConfig } from '../types';
1
2
  /**
2
3
  * Ensures that the directory for a given file path exists.
3
4
  * Creates all necessary parent directories recursively if they don't exist.
@@ -57,5 +58,19 @@ export declare function writeFileAsync(filePath: string, data: string): Promise<
57
58
  * // Returns: 'locales/en/common.json'
58
59
  * ```
59
60
  */
60
- export declare function getOutputPath(template: string, locale: string, namespace: string): string;
61
+ export declare function getOutputPath(template: string, locale: string, namespace?: string): string;
62
+ /**
63
+ * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
64
+ * @param filePath - The path to the translation file.
65
+ * @returns The parsed content of the file, or null if not found or failed to parse.
66
+ */
67
+ export declare function loadTranslationFile(filePath: string): Promise<Record<string, any> | null>;
68
+ /**
69
+ * Serializes a translation object into a string based on the desired format.
70
+ * @param data - The translation data object.
71
+ * @param format - The desired output format ('json', 'js-esm', etc.).
72
+ * @param indentation - The number of spaces for indentation.
73
+ * @returns The serialized file content as a string.
74
+ */
75
+ export declare function serializeTranslationFile(data: Record<string, any>, format?: I18nextToolkitConfig['extract']['outputFormat'], indentation?: number): string;
61
76
  //# sourceMappingURL=file-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,MAAM,CAIR"}
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpD;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,MAAW,GACrB,MAAM,CAIR;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAehG;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,EAChE,WAAW,GAAE,MAAU,GACtB,MAAM,CAgBR"}