i18next-cli 1.10.1 → 1.10.3

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 (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -0
  3. package/dist/cjs/cli.js +1 -1
  4. package/dist/cjs/extractor/core/ast-visitors.js +1 -0
  5. package/dist/cjs/extractor/core/key-finder.js +1 -1
  6. package/dist/cjs/extractor/core/translation-manager.js +1 -1
  7. package/dist/cjs/extractor/parsers/call-expression-handler.js +1 -0
  8. package/dist/cjs/extractor/parsers/expression-resolver.js +1 -0
  9. package/dist/cjs/extractor/parsers/jsx-handler.js +1 -0
  10. package/dist/cjs/extractor/parsers/jsx-parser.js +1 -1
  11. package/dist/cjs/extractor/parsers/scope-manager.js +1 -0
  12. package/dist/esm/cli.js +1 -1
  13. package/dist/esm/extractor/core/ast-visitors.js +1 -0
  14. package/dist/esm/extractor/core/key-finder.js +1 -1
  15. package/dist/esm/extractor/core/translation-manager.js +1 -1
  16. package/dist/esm/extractor/parsers/call-expression-handler.js +1 -0
  17. package/dist/esm/extractor/parsers/expression-resolver.js +1 -0
  18. package/dist/esm/extractor/parsers/jsx-handler.js +1 -0
  19. package/dist/esm/extractor/parsers/jsx-parser.js +1 -1
  20. package/dist/esm/extractor/parsers/scope-manager.js +1 -0
  21. package/package.json +1 -1
  22. package/src/cli.ts +1 -1
  23. package/src/extractor/core/ast-visitors.ts +170 -0
  24. package/src/extractor/core/extractor.ts +1 -1
  25. package/src/extractor/core/key-finder.ts +2 -2
  26. package/src/extractor/core/translation-manager.ts +88 -8
  27. package/src/extractor/index.ts +1 -1
  28. package/src/extractor/parsers/call-expression-handler.ts +506 -0
  29. package/src/extractor/parsers/expression-resolver.ts +178 -0
  30. package/src/extractor/parsers/jsx-handler.ts +358 -0
  31. package/src/extractor/parsers/jsx-parser.ts +5 -1
  32. package/src/extractor/parsers/scope-manager.ts +327 -0
  33. package/src/extractor.ts +1 -1
  34. package/src/types.ts +82 -0
  35. package/types/extractor/core/ast-visitors.d.ts +75 -0
  36. package/types/extractor/core/ast-visitors.d.ts.map +1 -0
  37. package/types/extractor/core/extractor.d.ts +1 -1
  38. package/types/extractor/core/extractor.d.ts.map +1 -1
  39. package/types/extractor/core/key-finder.d.ts.map +1 -1
  40. package/types/extractor/core/translation-manager.d.ts.map +1 -1
  41. package/types/extractor/index.d.ts +1 -1
  42. package/types/extractor/index.d.ts.map +1 -1
  43. package/types/extractor/parsers/call-expression-handler.d.ts +74 -0
  44. package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -0
  45. package/types/extractor/parsers/expression-resolver.d.ts +62 -0
  46. package/types/extractor/parsers/expression-resolver.d.ts.map +1 -0
  47. package/types/extractor/parsers/jsx-handler.d.ts +44 -0
  48. package/types/extractor/parsers/jsx-handler.d.ts.map +1 -0
  49. package/types/extractor/parsers/scope-manager.d.ts +99 -0
  50. package/types/extractor/parsers/scope-manager.d.ts.map +1 -0
  51. package/types/extractor.d.ts +1 -1
  52. package/types/extractor.d.ts.map +1 -1
  53. package/types/types.d.ts +77 -0
  54. package/types/types.d.ts.map +1 -1
  55. package/dist/cjs/extractor/parsers/ast-visitors.js +0 -1
  56. package/dist/esm/extractor/parsers/ast-visitors.js +0 -1
  57. package/src/extractor/parsers/ast-visitors.ts +0 -1510
  58. package/types/extractor/parsers/ast-visitors.d.ts +0 -352
  59. package/types/extractor/parsers/ast-visitors.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.10.3](https://github.com/i18next/i18next-cli/compare/v1.10.2...v1.10.3) - 2025-10-08
9
+
10
+ - **Extractor (`<Trans>`):** Fixed variable placeholder extraction from object expressions in Trans components. Previously, expressions like `<Trans>Hello {{name: userName}}</Trans>` would lose the variable placeholder and extract as `"Hello "` instead of `"Hello {{name}}"`. The JSX children serializer now correctly handles both simple identifiers (`{{name}}`) and object expressions (`{{name: value}}`) to preserve variable placeholders in the extracted translation keys. [#58](https://github.com/i18next/i18next-cli/issues/58)
11
+
12
+ ## [1.10.2](https://github.com/i18next/i18next-cli/compare/v1.10.1...v1.10.2) - 2025-10-08
13
+
14
+ ### Fixed
15
+ - **Extractor:** Fixed plural form sorting to follow canonical i18next order (zero, one, two, few, many, other) instead of alphabetical sorting. Plural keys are now properly grouped by their base key and sorted within each group according to i18next's pluralization rules, ensuring consistent and predictable translation file structure. For example, `item_other`, `item_one`, `item_zero` now correctly sorts to `item_zero`, `item_one`, `item_other`. [#57](https://github.com/i18next/i18next-cli/issues/57)
16
+
17
+ ### Enhanced
18
+ - **Extractor:** Added intelligent handling for the optional `_zero` suffix in plural forms. The extractor now preserves existing `_zero` keys when related plural forms are present in the extracted keys, but removes them when no related plurals exist. This aligns with i18next's special handling of `count: 0` scenarios where `_zero` provides more natural language expressions (e.g., "No items" instead of "0 items"), while ensuring unused `_zero` forms don't accumulate in translation files.
19
+
8
20
  ## [1.10.1](https://github.com/i18next/i18next-cli/compare/v1.10.0...v1.10.1) - 2025-10-07
9
21
 
10
22
  - **Extractor:** Fixed incorrect behavior of the `preservePatterns` option where keys matching the specified patterns were being extracted instead of being excluded from extraction. The option now correctly skips keys that match the glob patterns during both AST-based extraction and comment parsing, preventing re-extraction of keys that already exist in other translation files (e.g., when `BUILDINGS.*` keys exist in `assets.json` but shouldn't be duplicated in `app.json`). This resolves issues where dynamic key references were incorrectly creating duplicate entries in extracted translation files, allowing developers to use patterns like `t('BUILDINGS.ACADEMY.NAME')` directly without workarounds. [#53](https://github.com/i18next/i18next-cli/issues/53)
package/README.md CHANGED
@@ -647,6 +647,9 @@ This will:
647
647
  - Preserve custom settings where possible
648
648
  - Create a new `i18next.config.ts` file
649
649
 
650
+ Important: File Management Differences <br/>
651
+ Unlike `i18next-parser`, `i18next-cli` takes full ownership of translation files in the output directory. If you have manually managed translation files that should not be modified, place them in a separate directory or use different naming patterns to avoid conflicts.
652
+
650
653
  ## CI/CD Integration
651
654
 
652
655
  Use the `--ci` flag to fail builds when translations are outdated:
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"),n=require("glob"),o=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("1.10.1"),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.").option("--dry-run","Run the extractor without writing any files to disk.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a,{isWatchMode:e.watch,isDryRun:e.dryRun});e.ci&&t&&(console.error(o.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(o.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${o.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await n.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 n=await i.loadConfig();if(!n){console.log(o.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),n=e}await g.runStatus(n,{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 o=await i.ensureConfig(),a=()=>c.runTypesGenerator(o);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(o.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 [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async e=>{await l.runMigrator(e)}),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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async e=>{const r=async()=>{let e=await i.loadConfig();if(!e){console.log(o.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),e=t}await d.runLinter(e)};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await i.loadConfig();if(e?.extract?.input){t.watch(await n.glob(e.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}}),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"),n=require("glob"),o=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("1.10.3"),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.").option("--dry-run","Run the extractor without writing any files to disk.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a,{isWatchMode:e.watch,isDryRun:e.dryRun});e.ci&&t&&(console.error(o.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(o.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${o.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await n.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 n=await i.loadConfig();if(!n){console.log(o.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),n=e}await g.runStatus(n,{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 o=await i.ensureConfig(),a=()=>c.runTypesGenerator(o);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(o.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 [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async e=>{await l.runMigrator(e)}),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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async e=>{const r=async()=>{let e=await i.loadConfig();if(!e){console.log(o.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),e=t}await d.runLinter(e)};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await i.loadConfig();if(e?.extract?.input){t.watch(await n.glob(e.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}}),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);
@@ -0,0 +1 @@
1
+ "use strict";var e=require("../parsers/scope-manager.js"),s=require("../parsers/expression-resolver.js"),r=require("../parsers/call-expression-handler.js"),o=require("../parsers/jsx-handler.js");exports.ASTVisitors=class{pluginContext;config;logger;hooks;get objectKeys(){return this.callExpressionHandler.objectKeys}scopeManager;expressionResolver;callExpressionHandler;jsxHandler;constructor(i,t,n,a){this.pluginContext=t,this.config=i,this.logger=n,this.hooks={onBeforeVisitNode:a?.onBeforeVisitNode,onAfterVisitNode:a?.onAfterVisitNode,resolvePossibleKeyStringValues:a?.resolvePossibleKeyStringValues,resolvePossibleContextStringValues:a?.resolvePossibleContextStringValues},this.scopeManager=new e.ScopeManager(i),this.expressionResolver=new s.ExpressionResolver(this.hooks),this.callExpressionHandler=new r.CallExpressionHandler(i,t,n,this.expressionResolver),this.jsxHandler=new o.JSXHandler(i,t,this.expressionResolver)}visit(e){this.scopeManager.enterScope(),this.walk(e),this.scopeManager.exitScope()}walk(e){if(!e)return;let s=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.scopeManager.enterScope(),s=!0),this.hooks.onBeforeVisitNode?.(e),e.type){case"VariableDeclarator":this.scopeManager.handleVariableDeclarator(e);break;case"CallExpression":this.callExpressionHandler.handleCallExpression(e,this.scopeManager.getVarFromScope.bind(this.scopeManager));break;case"JSXElement":this.jsxHandler.handleJSXElement(e,this.scopeManager.getVarFromScope.bind(this.scopeManager))}this.hooks.onAfterVisitNode?.(e);for(const s in e){if("span"===s)continue;const r=e[s];if(Array.isArray(r))for(const e of r)e&&"object"==typeof e&&this.walk(e);else r&&"object"==typeof r&&this.walk(r)}s&&this.scopeManager.exitScope()}getVarFromScope(e){return this.scopeManager.getVarFromScope(e)}};
@@ -1 +1 @@
1
- "use strict";var e=require("glob"),r=require("./extractor.js"),t=require("../../utils/logger.js"),o=require("../plugin-manager.js"),n=require("../parsers/ast-visitors.js");exports.findKeys=async function(i,s=new t.ConsoleLogger){const{plugins:a,...c}=i,l=a||[],u=await async function(r){const t=["node_modules/**"],o=Array.isArray(r.extract.ignore)?r.extract.ignore:r.extract.ignore?[r.extract.ignore]:[];return await e.glob(r.extract.input,{ignore:[...t,...o],cwd:process.cwd()})}(i),g=new Map,x=o.createPluginContext(g,l,c,s),p={onBeforeVisitNode:e=>{for(const r of l)try{r.onVisitNode?.(e,x)}catch(e){s.warn(`Plugin ${r.name} onVisitNode failed:`,e)}},resolvePossibleKeyStringValues:e=>l.flatMap(r=>{try{return r.extractKeysFromExpression?.(e,i,s)??[]}catch(e){return s.warn(`Plugin ${r.name} extractKeysFromExpression failed:`,e),[]}}),resolvePossibleContextStringValues:e=>l.flatMap(r=>{try{return r.extractContextFromExpression?.(e,i,s)??[]}catch(e){return s.warn(`Plugin ${r.name} extractContextFromExpression failed:`,e),[]}})},f=new n.ASTVisitors(c,x,s,p);x.getVarFromScope=f.getVarFromScope.bind(f),await o.initializePlugins(l);for(const e of u)await r.processFile(e,l,f,x,c,s);for(const e of l)await(e.onEnd?.(g));return{allKeys:g,objectKeys:f.objectKeys}};
1
+ "use strict";var e=require("glob"),r=require("./extractor.js"),t=require("../../utils/logger.js"),o=require("../plugin-manager.js"),n=require("./ast-visitors.js");exports.findKeys=async function(i,s=new t.ConsoleLogger){const{plugins:a,...c}=i,l=a||[],u=await async function(r){const t=["node_modules/**"],o=Array.isArray(r.extract.ignore)?r.extract.ignore:r.extract.ignore?[r.extract.ignore]:[];return await e.glob(r.extract.input,{ignore:[...t,...o],cwd:process.cwd()})}(i),g=new Map,x=o.createPluginContext(g,l,c,s),f={onBeforeVisitNode:e=>{for(const r of l)try{r.onVisitNode?.(e,x)}catch(e){s.warn(`Plugin ${r.name} onVisitNode failed:`,e)}},resolvePossibleKeyStringValues:e=>l.flatMap(r=>{try{return r.extractKeysFromExpression?.(e,i,s)??[]}catch(e){return s.warn(`Plugin ${r.name} extractKeysFromExpression failed:`,e),[]}}),resolvePossibleContextStringValues:e=>l.flatMap(r=>{try{return r.extractContextFromExpression?.(e,i,s)??[]}catch(e){return s.warn(`Plugin ${r.name} extractContextFromExpression failed:`,e),[]}})},p=new n.ASTVisitors(c,x,s,f);x.getVarFromScope=p.getVarFromScope.bind(p),await o.initializePlugins(l);for(const e of u)await r.processFile(e,l,p,x,c,s);for(const e of l)await(e.onEnd?.(g));return{allKeys:g,objectKeys:p.objectKeys}};
@@ -1 +1 @@
1
- "use strict";var e=require("node:path"),t=require("glob"),s=require("../../utils/nested-object.js"),r=require("../../utils/file-utils.js"),n=require("../../utils/default-value.js");function a(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}function o(e){if("object"!=typeof e||null===e||Array.isArray(e))return e;const t={},s=Object.keys(e).sort((e,t)=>{const s=e.localeCompare(t,void 0,{sensitivity:"base"});return 0===s?e.localeCompare(t,void 0,{sensitivity:"case"}):s});for(const r of s)t[r]=o(e[r]);return t}function l(e,t,r,a,l,i,c){const{keySeparator:u=".",sort:f=!0,removeUnusedKeys:p=!0,primaryLanguage:d,defaultValue:g="",pluralSeparator:y="_"}=r.extract,h=new Set;try{const e=new Intl.PluralRules(a,{type:"cardinal"}),t=new Intl.PluralRules(a,{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(e=>h.add(e)),t.resolvedOptions().pluralCategories.forEach(e=>h.add(`ordinal_${e}`))}catch(e){const t=new Intl.PluralRules(d||"en",{type:"cardinal"}),s=new Intl.PluralRules(d||"en",{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(e=>h.add(e)),s.resolvedOptions().pluralCategories.forEach(e=>h.add(`ordinal_${e}`))}const x=e.filter(({key:e,hasCount:t,isOrdinal:s})=>{if(i.some(t=>t.test(e)))return!1;if(!t)return!0;const r=e.split(y);if(s&&r.includes("ordinal")){const e=r[r.length-1];return h.has(`ordinal_${e}`)}if(t){const e=r[r.length-1];return h.has(e)}return!0});let v=p?{}:JSON.parse(JSON.stringify(t));const w=s.getNestedKeys(t,u??".");for(const e of w)if(i.some(t=>t.test(e))){const r=s.getNestedValue(t,e,u??".");s.setNestedValue(v,e,r,u??".")}for(const{key:e,defaultValue:r}of x){const o=s.getNestedValue(t,e,u??"."),i=!x.some(t=>t.key.startsWith(`${e}${u}`)&&t.key!==e),f="object"==typeof o&&null!==o&&(c.has(e)||!r||r===e),p="object"==typeof o&&null!==o&&i&&!c.has(e)&&!f;if(f){s.setNestedValue(v,e,o,u??".");continue}let y;y=void 0===o||p?a===d?r||e:n.resolveDefaultValue(g,e,l,a):o,s.setNestedValue(v,e,y,u??".")}if(!0===f)return o(v);if("function"==typeof f){const e={},t=Object.keys(v),s=new Map;for(const e of x){const t=!1===u?e.key:e.key.split(u)[0];s.has(t)||s.set(t,e)}t.sort((e,t)=>{if("function"==typeof f){const r=s.get(e),n=s.get(t);if(r&&n)return f(r,n)}return e.localeCompare(t,void 0,{sensitivity:"base"})});for(const s of t)e[s]=v[s];v=e}return v}exports.getTranslations=async function(s,n,o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(e=>e!==o?.extract?.primaryLanguage);const i=o.extract.defaultNS??"translation",c=[...o.extract.preservePatterns||[]],u=o.extract.indentation??2;for(const e of n)c.push(`${e}.*`);const f=c.map(a),p=new Map;for(const e of s.values()){const t=e.ns||i;p.has(t)||p.set(t,[]),p.get(t).push(e)}const d=[],g=Array.isArray(o.extract.ignore)?o.extract.ignore:o.extract.ignore?[o.extract.ignore]:[];for(const s of o.locales){if(o.extract.mergeNamespaces||!o.extract.output.includes("{{namespace}}")){const t={},a=r.getOutputPath(o.extract.output,s),i=e.resolve(process.cwd(),a),c=await r.loadTranslationFile(i)||{},g=new Set([...p.keys(),...Object.keys(c)]);for(const e of g){const r=p.get(e)||[],a=c[e]||{};t[e]=l(r,a,o,s,e,f,n)}const y=JSON.stringify(c,null,u),h=JSON.stringify(t,null,u);d.push({path:i,updated:h!==y,newTranslations:t,existingTranslations:c})}else{const a=new Set(p.keys()),i=r.getOutputPath(o.extract.output,s,"*"),c=await t.glob(i,{ignore:g});for(const t of c)a.add(e.basename(t,e.extname(t)));for(const t of a){const a=p.get(t)||[],i=r.getOutputPath(o.extract.output,s,t),c=e.resolve(process.cwd(),i),g=await r.loadTranslationFile(c)||{},y=l(a,g,o,s,t,f,n),h=JSON.stringify(g,null,u),x=JSON.stringify(y,null,u);d.push({path:c,updated:x!==h,newTranslations:y,existingTranslations:g})}}}return d};
1
+ "use strict";var e=require("node:path"),t=require("glob"),s=require("../../utils/nested-object.js"),r=require("../../utils/file-utils.js"),n=require("../../utils/default-value.js");function o(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}function a(e,t){if("object"!=typeof e||null===e||Array.isArray(e))return e;const s={},r=t?.extract?.pluralSeparator??"_",n=["zero","one","two","few","many","other"],o=n.map(e=>`ordinal_${e}`),l=Object.keys(e).sort((e,t)=>{const s=e=>{for(const t of o)if(e.endsWith(`${r}${t}`)){return{base:e.slice(0,-(r.length+t.length)),form:t,isOrdinal:!0,isPlural:!0,fullKey:e}}for(const t of n)if(e.endsWith(`${r}${t}`)){return{base:e.slice(0,-(r.length+t.length)),form:t,isOrdinal:!1,isPlural:!0,fullKey:e}}return{base:e,form:"",isOrdinal:!1,isPlural:!1,fullKey:e}},a=s(e),l=s(t);if(a.isPlural&&l.isPlural){const e=a.base.localeCompare(l.base,void 0,{sensitivity:"base"});if(0!==e)return e;if(a.isOrdinal!==l.isOrdinal)return a.isOrdinal?1:-1;const t=a.isOrdinal?o:n,s=t.indexOf(a.form),r=t.indexOf(l.form);return-1!==s&&-1!==r?s-r:a.form.localeCompare(l.form)}const i=e.localeCompare(t,void 0,{sensitivity:"base"});return 0===i?e.localeCompare(t,void 0,{sensitivity:"case"}):i});for(const r of l)s[r]=a(e[r],t);return s}function l(e,t,r,o,l,i,c){const{keySeparator:u=".",sort:f=!0,removeUnusedKeys:d=!0,primaryLanguage:p,defaultValue:g="",pluralSeparator:y="_"}=r.extract,h=new Set;try{const e=new Intl.PluralRules(o,{type:"cardinal"}),t=new Intl.PluralRules(o,{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(e=>h.add(e)),t.resolvedOptions().pluralCategories.forEach(e=>h.add(`ordinal_${e}`))}catch(e){const t=new Intl.PluralRules(p||"en",{type:"cardinal"}),s=new Intl.PluralRules(p||"en",{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(e=>h.add(e)),s.resolvedOptions().pluralCategories.forEach(e=>h.add(`ordinal_${e}`))}const m=e.filter(({key:e,hasCount:t,isOrdinal:s})=>{if(i.some(t=>t.test(e)))return!1;if(!t)return!0;const r=e.split(y);if(s&&r.includes("ordinal")){const e=r[r.length-1];return h.has(`ordinal_${e}`)}if(t){const e=r[r.length-1];return h.has(e)}return!0});let O=d?{}:JSON.parse(JSON.stringify(t));const x=s.getNestedKeys(t,u??".");for(const e of x)if(i.some(t=>t.test(e))){const r=s.getNestedValue(t,e,u??".");s.setNestedValue(O,e,r,u??".")}if(d){const e=s.getNestedKeys(t,u??".");for(const r of e){const e=r.split(y);if("zero"===e[e.length-1]){const n=e.slice(0,-1).join(y);if(m.some(({key:e})=>e.split(y).slice(0,-1).join(y)===n)){const e=s.getNestedValue(t,r,u??".");s.setNestedValue(O,r,e,u??".")}}}}for(const{key:e,defaultValue:r}of m){const a=s.getNestedValue(t,e,u??"."),i=!m.some(t=>t.key.startsWith(`${e}${u}`)&&t.key!==e),f="object"==typeof a&&null!==a&&(c.has(e)||!r||r===e),d="object"==typeof a&&null!==a&&i&&!c.has(e)&&!f;if(f){s.setNestedValue(O,e,a,u??".");continue}let y;y=void 0===a||d?o===p?r||e:n.resolveDefaultValue(g,e,l,o):a,s.setNestedValue(O,e,y,u??".")}if(!0===f)return a(O,r);if("function"==typeof f){const e={},t=Object.keys(O),s=new Map;for(const e of m){const t=!1===u?e.key:e.key.split(u)[0];s.has(t)||s.set(t,e)}t.sort((e,t)=>{if("function"==typeof f){const r=s.get(e),n=s.get(t);if(r&&n)return f(r,n)}return e.localeCompare(t,void 0,{sensitivity:"base"})});for(const s of t)e[s]=a(O[s],r);O=e}return O}exports.getTranslations=async function(s,n,a){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(e=>e!==a?.extract?.primaryLanguage);const i=a.extract.defaultNS??"translation",c=[...a.extract.preservePatterns||[]],u=a.extract.indentation??2;for(const e of n)c.push(`${e}.*`);const f=c.map(o),d=new Map;for(const e of s.values()){const t=e.ns||i;d.has(t)||d.set(t,[]),d.get(t).push(e)}const p=[],g=Array.isArray(a.extract.ignore)?a.extract.ignore:a.extract.ignore?[a.extract.ignore]:[];for(const s of a.locales){if(a.extract.mergeNamespaces||!a.extract.output.includes("{{namespace}}")){const t={},o=r.getOutputPath(a.extract.output,s),i=e.resolve(process.cwd(),o),c=await r.loadTranslationFile(i)||{},g=new Set([...d.keys(),...Object.keys(c)]);for(const e of g){const r=d.get(e)||[],o=c[e]||{};t[e]=l(r,o,a,s,e,f,n)}const y=JSON.stringify(c,null,u),h=JSON.stringify(t,null,u);p.push({path:i,updated:h!==y,newTranslations:t,existingTranslations:c})}else{const o=new Set(d.keys()),i=r.getOutputPath(a.extract.output,s,"*"),c=await t.glob(i,{ignore:g});for(const t of c)o.add(e.basename(t,e.extname(t)));for(const t of o){const o=d.get(t)||[],i=r.getOutputPath(a.extract.output,s,t),c=e.resolve(process.cwd(),i),g=await r.loadTranslationFile(c)||{},y=l(o,g,a,s,t,f,n),h=JSON.stringify(g,null,u),m=JSON.stringify(y,null,u);p.push({path:c,updated:m!==h,newTranslations:y,existingTranslations:g})}}}return p};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./ast-utils.js");exports.CallExpressionHandler=class{pluginContext;config;logger;expressionResolver;objectKeys=new Set;constructor(e,t,s,n){this.config=e,this.pluginContext=t,this.logger=s,this.expressionResolver=n}handleCallExpression(t,s){const n=this.getFunctionName(t.callee);if(!n)return;const r=s(n),o=this.config.extract.functions||["t","*.t"];let i=void 0!==r;if(!i)for(const e of o)if(e.startsWith("*.")){if(n.endsWith(e.substring(1))){i=!0;break}}else if(e===n){i=!0;break}if(!i||0===t.arguments.length)return;const{keysToProcess:l,isSelectorAPI:a}=this.handleCallExpressionArgument(t,0);if(0===l.length)return;let u=!1;const c=this.config.extract.pluralSeparator??"_";for(let e=0;e<l.length;e++)l[e].endsWith(`${c}ordinal`)&&(u=!0,l[e]=l[e].slice(0,-8));let f,p;if(t.arguments.length>1){const e=t.arguments[1].expression;"ObjectExpression"===e.type?p=e:"StringLiteral"===e.type&&(f=e.value)}if(t.arguments.length>2){const e=t.arguments[2].expression;"ObjectExpression"===e.type&&(p=e)}const g=p?e.getObjectPropValue(p,"defaultValue"):void 0,h="string"==typeof g?g:f;for(let t=0;t<l.length;t++){let s,n=l[t];if(p){const t=e.getObjectPropValue(p,"ns");"string"==typeof t&&(s=t)}const o=this.config.extract.nsSeparator??":";if(!s&&o&&n.includes(o)){const e=n.split(o);if(s=e.shift(),n=e.join(o),!n||""===n.trim()){this.logger.warn(`Skipping key that became empty after namespace removal: '${s}${o}'`);continue}}!s&&r?.defaultNs&&(s=r.defaultNs),s||(s=this.config.extract.defaultNS);let i=n;if(r?.keyPrefix){const e=this.config.extract.keySeparator??".";if(i=!1!==e?r.keyPrefix.endsWith(e)?`${r.keyPrefix}${n}`:`${r.keyPrefix}${e}${n}`:`${r.keyPrefix}${n}`,!1!==e){if(i.split(e).some(e=>""===e.trim())){this.logger.warn(`Skipping key with empty segments: '${i}' (keyPrefix: '${r.keyPrefix}', key: '${n}')`);continue}}}const c=t===l.length-1&&h||n;if(p){const t=e.getObjectProperty(p,"context"),n=[];if("StringLiteral"===t?.value?.type||"NumericLiteral"===t?.value.type||"BooleanLiteral"===t?.value.type){const e=`${t.value.value}`,r=this.config.extract.contextSeparator??"_";""!==e&&n.push({key:`${i}${r}${e}`,ns:s,defaultValue:c})}else if(t?.value){const e=this.expressionResolver.resolvePossibleContextStringValues(t.value),r=this.config.extract.contextSeparator??"_";e.length>0&&(e.forEach(e=>{n.push({key:`${i}${r}${e}`,ns:s,defaultValue:c})}),n.push({key:i,ns:s,defaultValue:c}))}const r=void 0!==e.getObjectPropValue(p,"count"),o=!0===e.getObjectPropValue(p,"ordinal");if(r||u){this.config.extract.disablePlurals?n.length>0?n.forEach(this.pluginContext.addKey):this.pluginContext.addKey({key:i,ns:s,defaultValue:c}):this.handlePluralKeys(i,s,p,o||u,h);continue}if(n.length>0){n.forEach(this.pluginContext.addKey);continue}!0===e.getObjectPropValue(p,"returnObjects")&&this.objectKeys.add(i)}a&&this.objectKeys.add(i),this.pluginContext.addKey({key:i,ns:s,defaultValue:c})}}handleCallExpressionArgument(e,t){const s=e.arguments[t].expression,n=[];let r=!1;if("ArrowFunctionExpression"===s.type){const e=this.extractKeyFromSelector(s);e&&(n.push(e),r=!0)}else if("ArrayExpression"===s.type)for(const e of s.elements)e?.expression&&n.push(...this.expressionResolver.resolvePossibleKeyStringValues(e.expression));else n.push(...this.expressionResolver.resolvePossibleKeyStringValues(s));return{keysToProcess:n.filter(e=>!!e),isSelectorAPI:r}}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 s=t;const n=[];for(;"MemberExpression"===s.type;){const e=s.property;if("Identifier"===e.type)n.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;n.unshift(e.expression.value)}s=s.object}if(n.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return n.join(t)}return null}handlePluralKeys(t,s,n,r,o){try{const i=r?"ordinal":"cardinal",l=new Set;for(const e of this.config.locales)try{const t=new Intl.PluralRules(e,{type:i});t.resolvedOptions().pluralCategories.forEach(e=>l.add(e))}catch(e){const t=new Intl.PluralRules("en",{type:i});t.resolvedOptions().pluralCategories.forEach(e=>l.add(e))}const a=Array.from(l).sort(),u=this.config.extract.pluralSeparator??"_",c=e.getObjectPropValue(n,"defaultValue"),f=e.getObjectPropValue(n,`defaultValue${u}other`),p=e.getObjectPropValue(n,`defaultValue${u}ordinal${u}other`),g=e.getObjectPropValue(n,"count");let h;if("number"==typeof g)try{const e=this.config.extract?.primaryLanguage||this.config.locales[0]||"en";h=new Intl.PluralRules(e,{type:i}).select(g)}catch(e){}const y=e.getObjectProperty(n,"context"),d=[];if(y?.value){const e=this.expressionResolver.resolvePossibleContextStringValues(y.value);if(e.length>0){for(const s of e)s.length>0&&d.push({key:t,context:s});!1!==this.config.extract?.generateBasePluralForms&&d.push({key:t})}else d.push({key:t})}else d.push({key:t});for(const{key:t,context:i}of d)for(const l of a){const a=r?`defaultValue${u}ordinal${u}${l}`:`defaultValue${u}${l}`,g=e.getObjectPropValue(n,a);let y,d;if(y="string"==typeof g?g:"one"===l&&"string"==typeof c?c:r&&"string"==typeof p?p:r||"string"!=typeof f?"string"==typeof c?c:o&&h===l?o:t:f,i){const e=this.config.extract.contextSeparator??"_";d=r?`${t}${e}${i}${u}ordinal${u}${l}`:`${t}${e}${i}${u}${l}`}else d=r?`${t}${u}ordinal${u}${l}`:`${t}${u}${l}`;this.pluginContext.addKey({key:d,ns:s,defaultValue:y,hasCount:!0,isOrdinal:r})}}catch(r){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const i=o||e.getObjectPropValue(n,"defaultValue");this.pluginContext.addKey({key:t,ns:s,defaultValue:"string"==typeof i?i:t})}}getFunctionName(e){if("Identifier"===e.type)return e.value;if("MemberExpression"===e.type){const t=[];let s=e;for(;"MemberExpression"===s.type;){if("Identifier"!==s.property.type)return null;t.unshift(s.property.value),s=s.object}if("ThisExpression"===s.type)t.unshift("this");else{if("Identifier"!==s.type)return null;t.unshift(s.value)}return t.join(".")}return null}};
@@ -0,0 +1 @@
1
+ "use strict";exports.ExpressionResolver=class{hooks;constructor(e){this.hooks=e}resolvePossibleContextStringValues(e){return[...this.hooks.resolvePossibleContextStringValues?.(e)??[],...this.resolvePossibleStringValuesFromExpression(e)]}resolvePossibleKeyStringValues(e){return[...this.hooks.resolvePossibleKeyStringValues?.(e)??[],...this.resolvePossibleStringValuesFromExpression(e)]}resolvePossibleStringValuesFromExpression(e,s=!1){if("StringLiteral"===e.type)return e.value||s?[e.value]:[];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValuesFromExpression(e.consequent,s),...this.resolvePossibleStringValuesFromExpression(e.alternate,s)]}if("Identifier"===e.type&&"undefined"===e.value)return[];if("TemplateLiteral"===e.type)return this.resolvePossibleStringValuesFromTemplateString(e);if("NumericLiteral"===e.type||"BooleanLiteral"===e.type)return[`${e.value}`];if("TsSatisfiesExpression"===e.type||"TsAsExpression"===e.type){const r=e.typeAnnotation;return this.resolvePossibleStringValuesFromType(r,s)}return[]}resolvePossibleStringValuesFromType(e,s=!1){if("TsUnionType"===e.type)return e.types.flatMap(e=>this.resolvePossibleStringValuesFromType(e,s));if("TsLiteralType"===e.type){if("StringLiteral"===e.literal.type)return e.literal.value||s?[e.literal.value]:[];if("TemplateLiteral"===e.literal.type)return this.resolvePossibleStringValuesFromTemplateLiteralType(e.literal);if("NumericLiteral"===e.literal.type||"BooleanLiteral"===e.literal.type)return[`${e.literal.value}`]}return[]}resolvePossibleStringValuesFromTemplateString(e){if(1===e.quasis.length&&0===e.expressions.length)return[e.quasis[0].cooked||""];const[s,...r]=e.quasis;return e.expressions.reduce((e,s,t)=>e.flatMap(e=>{const i=r[t]?.cooked??"";return this.resolvePossibleStringValuesFromExpression(s,!0).map(s=>`${e}${s}${i}`)}),[s.cooked??""])}resolvePossibleStringValuesFromTemplateLiteralType(e){if(1===e.quasis.length&&0===e.types.length)return[e.quasis[0].cooked||""];const[s,...r]=e.quasis;return e.types.reduce((e,s,t)=>e.flatMap(e=>{const i=r[t]?.cooked??"";return this.resolvePossibleStringValuesFromType(s,!0).map(s=>`${e}${s}${i}`)}),[s.cooked??""])}};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./jsx-parser.js"),t=require("./ast-utils.js");exports.JSXHandler=class{config;pluginContext;expressionResolver;constructor(e,t,n){this.config=e,this.pluginContext=t,this.expressionResolver=n}handleJSXElement(t,n){const s=this.getElementName(t);if(s&&(this.config.extract.transComponents||["Trans"]).includes(s)){const s=e.extractFromTransComponent(t,this.config),a=[];if(s){if(s.keyExpression){const e=this.expressionResolver.resolvePossibleKeyStringValues(s.keyExpression);a.push(...e)}else a.push(s.serializedChildren);let e;const{contextExpression:l,optionsNode:i,defaultValue:o,hasCount:r,isOrdinal:u,serializedChildren:f}=s;if(s.ns){const{ns:t}=s;e=a.map(e=>({key:e,ns:t,defaultValue:o||f,hasCount:r,isOrdinal:u}))}else{e=a.map(e=>{const t=this.config.extract.nsSeparator??":";let n;if(t&&e.includes(t)){let s;[n,...s]=e.split(t),e=s.join(t)}return{key:e,ns:n,defaultValue:o||f,hasCount:r,isOrdinal:u}});const s=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===s?.type&&"JSXExpressionContainer"===s.value?.type&&"Identifier"===s.value.expression.type){const t=n(s.value.expression.value);t?.defaultNs&&e.forEach(e=>{e.ns||(e.ns=t.defaultNs)})}}if(e.forEach(e=>{e.ns||(e.ns=this.config.extract.defaultNS)}),l&&r)if(this.config.extract.disablePlurals){const t=this.expressionResolver.resolvePossibleContextStringValues(l),n=this.config.extract.contextSeparator??"_";if(t.length>0)if("StringLiteral"===l.type)for(const s of t)for(const t of e){const e=`${t.key}${n}${s}`;this.pluginContext.addKey({key:e,ns:t.ns,defaultValue:t.defaultValue})}else{e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})});for(const s of t)for(const t of e){const e=`${t.key}${n}${s}`;this.pluginContext.addKey({key:e,ns:t.ns,defaultValue:t.defaultValue})}}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}else{const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),s=!!n,a=this.expressionResolver.resolvePossibleContextStringValues(l),o=this.config.extract.contextSeparator??"_";if(a.length>0){e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,s,i));for(const t of a)for(const n of e){const e=`${n.key}${o}${t}`;this.generatePluralKeysForTrans(e,n.defaultValue,n.ns,s,i)}}else e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,s,i))}else if(l){const t=this.expressionResolver.resolvePossibleContextStringValues(l),n=this.config.extract.contextSeparator??"_";if(t.length>0){for(const s of t)for(const{key:t,ns:a,defaultValue:l}of e)this.pluginContext.addKey({key:`${t}${n}${s}`,ns:a,defaultValue:l});"StringLiteral"!==l.type&&e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}else if(r)if(this.config.extract.disablePlurals)e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})});else{const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),s=!!n;e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,s,i))}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}}}generatePluralKeysForTrans(e,n,s,a,l){try{const i=a?"ordinal":"cardinal",o=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:i}).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";let u,f;l&&(u=t.getObjectPropValue(l,`defaultValue${r}other`),f=t.getObjectPropValue(l,`defaultValue${r}ordinal${r}other`));for(const i of o){const o=a?`defaultValue${r}ordinal${r}${i}`:`defaultValue${r}${i}`,d=l?t.getObjectPropValue(l,o):void 0;let p;p="string"==typeof d?d:"one"===i&&"string"==typeof n?n:a&&"string"==typeof f?f:a||"string"!=typeof u?"string"==typeof n?n:e:u;const c=a?`${e}${r}ordinal${r}${i}`:`${e}${r}${i}`;this.pluginContext.addKey({key:c,ns:s,defaultValue:p,hasCount:!0,isOrdinal:a})}}catch(t){this.pluginContext.addKey({key:e,ns:s,defaultValue: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(".")}}};
@@ -1 +1 @@
1
- "use strict";var e=require("./ast-utils.js");exports.extractFromTransComponent=function(t,n){const i=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),r=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),a=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),s=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"values"===e.name.value);let p;a||"JSXAttribute"!==s?.type||"JSXExpressionContainer"!==s.value?.type||"ObjectExpression"!==s.value.expression.type||(p=e.getObjectProperty(s.value.expression,"count"));const o=!!a||!!p,l=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),u="JSXAttribute"===l?.type&&"JSXExpressionContainer"===l.value?.type&&"ObjectExpression"===l.value.expression.type?l.value.expression:void 0,y=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),v=!!y,c=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let f="JSXAttribute"===c?.type&&"JSXExpressionContainer"===c.value?.type?c.value.expression:"JSXAttribute"===c?.type&&"StringLiteral"===c.value?.type?c.value:void 0;const d=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let m;if(m="JSXAttribute"===d?.type&&"StringLiteral"===d.value?.type?d.value.value:void 0,u&&(void 0===m&&(m=e.getObjectPropValue(u,"ns")),void 0===f)){const t=e.getObjectProperty(u,"context");t?.value&&(f=t.value)}const S=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){let t="";return e.forEach((e,r)=>{if("JSXText"===e.type)t+=e.value;else if("JSXExpressionContainer"===e.type){const n=e.expression;if("StringLiteral"===n.type)t+=n.value;else if("Identifier"===n.type)t+=`{{${n.value}}}`;else if("ObjectExpression"===n.type){const e=n.properties[0];e&&"Identifier"===e.type&&(t+=`{{${e.value}}}`)}}else if("JSXElement"===e.type){let a;"Identifier"===e.opening.name.type&&(a=e.opening.name.value);const s=i(e.children);a&&n.has(a)?t+=`<${a}>${s}</${a}>`:t+=`<${r}>${s}</${r}>`}else"JSXFragment"===e.type&&(t+=i(e.children))}),t}return i(e).trim().replace(/\s{2,}/g," ")}(t.children,n);let b,g,x;if("JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type)b=r.value.value;else{const e=n.extract.defaultValue;b="string"==typeof e?e:""}if("JSXAttribute"===i?.type){if("StringLiteral"===i.value?.type){if(g=i.value,x=g.value,!x||""===x.trim())return console.warn("Ignoring Trans component with empty i18nKey"),null;if(m&&"StringLiteral"===g.type){const e=n.extract.nsSeparator??":",t=g.value;if(e&&t.startsWith(`${m}${e}`)){if(x=t.slice(`${m}${e}`.length),!x||""===x.trim())return console.warn("Ignoring Trans component with i18nKey that becomes empty after namespace removal"),null;g={...g,value:x}}}}else"JSXExpressionContainer"===i.value?.type&&"JSXEmptyExpression"!==i.value.expression.type&&(g=i.value.expression);if(!g)return null}return r||!x||S.trim()?!r&&S.trim()&&(b=S):b=x,{keyExpression:g,serializedChildren:S,ns:m,defaultValue:b,hasCount:o,isOrdinal:v,contextExpression:f,optionsNode:u}};
1
+ "use strict";var e=require("./ast-utils.js");exports.extractFromTransComponent=function(t,n){const i=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),r=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),a=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),s=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"values"===e.name.value);let p;a||"JSXAttribute"!==s?.type||"JSXExpressionContainer"!==s.value?.type||"ObjectExpression"!==s.value.expression.type||(p=e.getObjectProperty(s.value.expression,"count"));const o=!!a||!!p,l=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),u="JSXAttribute"===l?.type&&"JSXExpressionContainer"===l.value?.type&&"ObjectExpression"===l.value.expression.type?l.value.expression:void 0,y=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),v=!!y,f=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let c="JSXAttribute"===f?.type&&"JSXExpressionContainer"===f.value?.type?f.value.expression:"JSXAttribute"===f?.type&&"StringLiteral"===f.value?.type?f.value:void 0;const d=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let m;if(m="JSXAttribute"===d?.type&&"StringLiteral"===d.value?.type?d.value.value:void 0,u&&(void 0===m&&(m=e.getObjectPropValue(u,"ns")),void 0===c)){const t=e.getObjectProperty(u,"context");t?.value&&(c=t.value)}const S=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){let t="";return e.forEach((e,r)=>{if("JSXText"===e.type)t+=e.value;else if("JSXExpressionContainer"===e.type){const n=e.expression;if("StringLiteral"===n.type)t+=n.value;else if("Identifier"===n.type)t+=`{{${n.value}}}`;else if("ObjectExpression"===n.type){const e=n.properties[0];e&&"KeyValueProperty"===e.type&&e.key&&"Identifier"===e.key.type?t+=`{{${e.key.value}}}`:e&&"Identifier"===e.type&&(t+=`{{${e.value}}}`)}}else if("JSXElement"===e.type){let a;"Identifier"===e.opening.name.type&&(a=e.opening.name.value);const s=i(e.children);a&&n.has(a)?t+=`<${a}>${s}</${a}>`:t+=`<${r}>${s}</${r}>`}else"JSXFragment"===e.type&&(t+=i(e.children))}),t}return i(e).trim().replace(/\s{2,}/g," ")}(t.children,n);let b,g,x;if("JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type)b=r.value.value;else{const e=n.extract.defaultValue;b="string"==typeof e?e:""}if("JSXAttribute"===i?.type){if("StringLiteral"===i.value?.type){if(g=i.value,x=g.value,!x||""===x.trim())return console.warn("Ignoring Trans component with empty i18nKey"),null;if(m&&"StringLiteral"===g.type){const e=n.extract.nsSeparator??":",t=g.value;if(e&&t.startsWith(`${m}${e}`)){if(x=t.slice(`${m}${e}`.length),!x||""===x.trim())return console.warn("Ignoring Trans component with i18nKey that becomes empty after namespace removal"),null;g={...g,value:x}}}}else"JSXExpressionContainer"===i.value?.type&&"JSXEmptyExpression"!==i.value.expression.type&&(g=i.value.expression);if(!g)return null}return r||!x||S.trim()?!r&&S.trim()&&(b=S):b=x,{keyExpression:g,serializedChildren:S,ns:m,defaultValue:b,hasCount:o,isOrdinal:v,contextExpression:c,optionsNode:u}};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./ast-utils.js");exports.ScopeManager=class{scopeStack=[];config;scope=new Map;constructor(e){this.config=e}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)}const t=this.scope.get(e);if(t)return t}handleVariableDeclarator(e){const t=e.init;if(!t)return;const r="AwaitExpression"===t.type&&"CallExpression"===t.argument.type?t.argument:"CallExpression"===t.type?t:null;if(!r)return;const i=r.callee;if("Identifier"===i.type){const t=this.getUseTranslationConfig(i.value);if(t)return this.handleUseTranslationDeclarator(e,r,t),void this.handleUseTranslationForComments(e,r,t)}"MemberExpression"===i.type&&"Identifier"===i.property.type&&"getFixedT"===i.property.value&&this.handleGetFixedTDeclarator(e,r)}getUseTranslationConfig(e){const t=this.config.extract.useTranslationNames||["useTranslation"];for(const r of t){if("string"==typeof r&&r===e)return{name:e,nsArg:0,keyPrefixArg:1};if("object"==typeof r&&r.name===e)return{name:r.name,nsArg:r.nsArg??0,keyPrefixArg:r.keyPrefixArg??1}}}handleUseTranslationForComments(e,t,r){let i;if("Identifier"===e.id.type&&(i=e.id.value),"ArrayPattern"===e.id.type){const t=e.id.elements[0];"Identifier"===t?.type&&(i=t.value)}if("ObjectPattern"===e.id.type)for(const t of e.id.properties){if("AssignmentPatternProperty"===t.type&&"Identifier"===t.key.type&&"t"===t.key.value){i="t";break}if("KeyValuePatternProperty"===t.type&&"Identifier"===t.key.type&&"t"===t.key.value&&"Identifier"===t.value.type){i=t.value.value;break}}if(!i)return;const n=t.arguments?.[r.nsArg]?.expression,s=t.arguments?.[r.keyPrefixArg]?.expression;let a,o;if("StringLiteral"===n?.type?a=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(a=n.elements[0].expression.value),"ObjectExpression"===s?.type){const e=s.properties.find(e=>"KeyValueProperty"===e.type&&"Identifier"===e.key.type&&"keyPrefix"===e.key.value);"KeyValueProperty"===e?.type&&"StringLiteral"===e.value.type&&(o=e.value.value)}(a||o)&&this.scope.set(i,{defaultNs:a,keyPrefix:o})}handleUseTranslationDeclarator(t,r,i){let n;if("Identifier"===t.id.type&&(n=t.id.value),"ArrayPattern"===t.id.type){const e=t.id.elements[0];"Identifier"===e?.type&&(n=e.value)}if("ObjectPattern"===t.id.type)for(const e of t.id.properties){if("AssignmentPatternProperty"===e.type&&"Identifier"===e.key.type&&"t"===e.key.value){n="t";break}if("KeyValuePatternProperty"===e.type&&"Identifier"===e.key.type&&"t"===e.key.value&&"Identifier"===e.value.type){n=e.value.value;break}}if(!n)return;const s=r.arguments?.[i.nsArg]?.expression;let a;"StringLiteral"===s?.type?a=s.value:"ArrayExpression"===s?.type&&"StringLiteral"===s.elements[0]?.expression.type&&(a=s.elements[0].expression.value);const o=r.arguments?.[i.keyPrefixArg]?.expression;let p;if("ObjectExpression"===o?.type){const t=e.getObjectPropValue(o,"keyPrefix");p="string"==typeof t?t:void 0}this.setVarInScope(n,{defaultNs:a,keyPrefix:p})}handleGetFixedTDeclarator(e,t){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const r=e.id.value,i=t.arguments,n=i[1]?.expression,s=i[2]?.expression,a="StringLiteral"===n?.type?n.value:void 0,o="StringLiteral"===s?.type?s.value:void 0;(a||o)&&this.setVarInScope(r,{defaultNs:a,keyPrefix:o})}};
package/dist/esm/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import n from"chalk";import{ensureConfig as i,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 g}from"./status.js";import{runLocizeSync as f,runLocizeDownload as u,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.10.1"),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.").option("--dry-run","Run the extractor without writing any files to disk.").action(async t=>{const a=await i(),c=async()=>{const o=await r(a,{isWatchMode:t.watch,isDryRun:t.dryRun});t.ci&&o&&(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(),t.watch){console.log("\nWatching for changes...");o.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),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(t,o)=>{let e=await a();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await c();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 i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await g(e,{detail:t,namespace:o.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 t=>{const n=await i(),a=()=>s(n);if(await a(),t.watch){console.log("\nWatching for changes...");o.watch(await e(n.types?.input||[]),{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=await i();await l(t)}),w.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async t=>{await m(t)}),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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async t=>{const i=async()=>{let t=await a();if(!t){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(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!")),t=o}await d(t)};if(await i(),t.watch){console.log("\nWatching for changes...");const t=await a();if(t?.extract?.input){o.watch(await e(t.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),i()})}}}),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 t=>{const o=await i();await f(o,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=await i();await u(o,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=await i();await h(o,t)}),w.parse(process.argv);
2
+ import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import n from"chalk";import{ensureConfig as i,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 g}from"./status.js";import{runLocizeSync as f,runLocizeDownload as u,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.10.3"),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.").option("--dry-run","Run the extractor without writing any files to disk.").action(async t=>{const a=await i(),c=async()=>{const o=await r(a,{isWatchMode:t.watch,isDryRun:t.dryRun});t.ci&&o&&(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(),t.watch){console.log("\nWatching for changes...");o.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),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(t,o)=>{let e=await a();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await c();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 i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await g(e,{detail:t,namespace:o.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 t=>{const n=await i(),a=()=>s(n);if(await a(),t.watch){console.log("\nWatching for changes...");o.watch(await e(n.types?.input||[]),{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=await i();await l(t)}),w.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async t=>{await m(t)}),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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async t=>{const i=async()=>{let t=await a();if(!t){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(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!")),t=o}await d(t)};if(await i(),t.watch){console.log("\nWatching for changes...");const t=await a();if(t?.extract?.input){o.watch(await e(t.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),i()})}}}),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 t=>{const o=await i();await f(o,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=await i();await u(o,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=await i();await h(o,t)}),w.parse(process.argv);
@@ -0,0 +1 @@
1
+ import{ScopeManager as e}from"../parsers/scope-manager.js";import{ExpressionResolver as s}from"../parsers/expression-resolver.js";import{CallExpressionHandler as o}from"../parsers/call-expression-handler.js";import{JSXHandler as r}from"../parsers/jsx-handler.js";class t{pluginContext;config;logger;hooks;get objectKeys(){return this.callExpressionHandler.objectKeys}scopeManager;expressionResolver;callExpressionHandler;jsxHandler;constructor(t,i,n,a){this.pluginContext=i,this.config=t,this.logger=n,this.hooks={onBeforeVisitNode:a?.onBeforeVisitNode,onAfterVisitNode:a?.onAfterVisitNode,resolvePossibleKeyStringValues:a?.resolvePossibleKeyStringValues,resolvePossibleContextStringValues:a?.resolvePossibleContextStringValues},this.scopeManager=new e(t),this.expressionResolver=new s(this.hooks),this.callExpressionHandler=new o(t,i,n,this.expressionResolver),this.jsxHandler=new r(t,i,this.expressionResolver)}visit(e){this.scopeManager.enterScope(),this.walk(e),this.scopeManager.exitScope()}walk(e){if(!e)return;let s=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.scopeManager.enterScope(),s=!0),this.hooks.onBeforeVisitNode?.(e),e.type){case"VariableDeclarator":this.scopeManager.handleVariableDeclarator(e);break;case"CallExpression":this.callExpressionHandler.handleCallExpression(e,this.scopeManager.getVarFromScope.bind(this.scopeManager));break;case"JSXElement":this.jsxHandler.handleJSXElement(e,this.scopeManager.getVarFromScope.bind(this.scopeManager))}this.hooks.onAfterVisitNode?.(e);for(const s in e){if("span"===s)continue;const o=e[s];if(Array.isArray(o))for(const e of o)e&&"object"==typeof e&&this.walk(e);else o&&"object"==typeof o&&this.walk(o)}s&&this.scopeManager.exitScope()}getVarFromScope(e){return this.scopeManager.getVarFromScope(e)}}export{t as ASTVisitors};
@@ -1 +1 @@
1
- import{glob as r}from"glob";import{processFile as t}from"./extractor.js";import{ConsoleLogger as e}from"../../utils/logger.js";import{createPluginContext as o,initializePlugins as n}from"../plugin-manager.js";import{ASTVisitors as a}from"../parsers/ast-visitors.js";async function s(s,i=new e){const{plugins:c,...l}=s,m=c||[],p=await async function(t){const e=["node_modules/**"],o=Array.isArray(t.extract.ignore)?t.extract.ignore:t.extract.ignore?[t.extract.ignore]:[];return await r(t.extract.input,{ignore:[...e,...o],cwd:process.cwd()})}(s),f=new Map,g=o(f,m,l,i),u={onBeforeVisitNode:r=>{for(const t of m)try{t.onVisitNode?.(r,g)}catch(r){i.warn(`Plugin ${t.name} onVisitNode failed:`,r)}},resolvePossibleKeyStringValues:r=>m.flatMap(t=>{try{return t.extractKeysFromExpression?.(r,s,i)??[]}catch(r){return i.warn(`Plugin ${t.name} extractKeysFromExpression failed:`,r),[]}}),resolvePossibleContextStringValues:r=>m.flatMap(t=>{try{return t.extractContextFromExpression?.(r,s,i)??[]}catch(r){return i.warn(`Plugin ${t.name} extractContextFromExpression failed:`,r),[]}})},x=new a(l,g,i,u);g.getVarFromScope=x.getVarFromScope.bind(x),await n(m);for(const r of p)await t(r,m,x,g,l,i);for(const r of m)await(r.onEnd?.(f));return{allKeys:f,objectKeys:x.objectKeys}}export{s as findKeys};
1
+ import{glob as r}from"glob";import{processFile as t}from"./extractor.js";import{ConsoleLogger as e}from"../../utils/logger.js";import{createPluginContext as o,initializePlugins as n}from"../plugin-manager.js";import{ASTVisitors as a}from"./ast-visitors.js";async function i(i,s=new e){const{plugins:c,...l}=i,m=c||[],f=await async function(t){const e=["node_modules/**"],o=Array.isArray(t.extract.ignore)?t.extract.ignore:t.extract.ignore?[t.extract.ignore]:[];return await r(t.extract.input,{ignore:[...e,...o],cwd:process.cwd()})}(i),p=new Map,g=o(p,m,l,s),u={onBeforeVisitNode:r=>{for(const t of m)try{t.onVisitNode?.(r,g)}catch(r){s.warn(`Plugin ${t.name} onVisitNode failed:`,r)}},resolvePossibleKeyStringValues:r=>m.flatMap(t=>{try{return t.extractKeysFromExpression?.(r,i,s)??[]}catch(r){return s.warn(`Plugin ${t.name} extractKeysFromExpression failed:`,r),[]}}),resolvePossibleContextStringValues:r=>m.flatMap(t=>{try{return t.extractContextFromExpression?.(r,i,s)??[]}catch(r){return s.warn(`Plugin ${t.name} extractContextFromExpression failed:`,r),[]}})},x=new a(l,g,s,u);g.getVarFromScope=x.getVarFromScope.bind(x),await n(m);for(const r of f)await t(r,m,x,g,l,s);for(const r of m)await(r.onEnd?.(p));return{allKeys:p,objectKeys:x.objectKeys}}export{i as findKeys};
@@ -1 +1 @@
1
- import{resolve as t,basename as e,extname as o}from"node:path";import{glob as r}from"glob";import{getNestedKeys as n,getNestedValue as s,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as i,loadTranslationFile as l}from"../../utils/file-utils.js";import{resolveDefaultValue as c}from"../../utils/default-value.js";function u(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function f(t){if("object"!=typeof t||null===t||Array.isArray(t))return t;const e={},o=Object.keys(t).sort((t,e)=>{const o=t.localeCompare(e,void 0,{sensitivity:"base"});return 0===o?t.localeCompare(e,void 0,{sensitivity:"case"}):o});for(const r of o)e[r]=f(t[r]);return e}function p(t,e,o,r,i,l,u){const{keySeparator:p=".",sort:d=!0,removeUnusedKeys:y=!0,primaryLanguage:g,defaultValue:m="",pluralSeparator:h="_"}=o.extract,x=new Set;try{const t=new Intl.PluralRules(r,{type:"cardinal"}),e=new Intl.PluralRules(r,{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(t=>x.add(t)),e.resolvedOptions().pluralCategories.forEach(t=>x.add(`ordinal_${t}`))}catch(t){const e=new Intl.PluralRules(g||"en",{type:"cardinal"}),o=new Intl.PluralRules(g||"en",{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(t=>x.add(t)),o.resolvedOptions().pluralCategories.forEach(t=>x.add(`ordinal_${t}`))}const w=t.filter(({key:t,hasCount:e,isOrdinal:o})=>{if(l.some(e=>e.test(t)))return!1;if(!e)return!0;const r=t.split(h);if(o&&r.includes("ordinal")){const t=r[r.length-1];return x.has(`ordinal_${t}`)}if(e){const t=r[r.length-1];return x.has(t)}return!0});let v=y?{}:JSON.parse(JSON.stringify(e));const O=n(e,p??".");for(const t of O)if(l.some(e=>e.test(t))){const o=s(e,t,p??".");a(v,t,o,p??".")}for(const{key:t,defaultValue:o}of w){const n=s(e,t,p??"."),l=!w.some(e=>e.key.startsWith(`${t}${p}`)&&e.key!==t),f="object"==typeof n&&null!==n&&(u.has(t)||!o||o===t),d="object"==typeof n&&null!==n&&l&&!u.has(t)&&!f;if(f){a(v,t,n,p??".");continue}let y;y=void 0===n||d?r===g?o||t:c(m,t,i,r):n,a(v,t,y,p??".")}if(!0===d)return f(v);if("function"==typeof d){const t={},e=Object.keys(v),o=new Map;for(const t of w){const e=!1===p?t.key:t.key.split(p)[0];o.has(e)||o.set(e,t)}e.sort((t,e)=>{if("function"==typeof d){const r=o.get(t),n=o.get(e);if(r&&n)return d(r,n)}return t.localeCompare(e,void 0,{sensitivity:"base"})});for(const o of e)t[o]=v[o];v=t}return v}async function d(n,s,a){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage);const c=a.extract.defaultNS??"translation",f=[...a.extract.preservePatterns||[]],d=a.extract.indentation??2;for(const t of s)f.push(`${t}.*`);const y=f.map(u),g=new Map;for(const t of n.values()){const e=t.ns||c;g.has(e)||g.set(e,[]),g.get(e).push(t)}const m=[],h=Array.isArray(a.extract.ignore)?a.extract.ignore:a.extract.ignore?[a.extract.ignore]:[];for(const n of a.locales){if(a.extract.mergeNamespaces||!a.extract.output.includes("{{namespace}}")){const e={},o=i(a.extract.output,n),r=t(process.cwd(),o),c=await l(r)||{},u=new Set([...g.keys(),...Object.keys(c)]);for(const t of u){const o=g.get(t)||[],r=c[t]||{};e[t]=p(o,r,a,n,t,y,s)}const f=JSON.stringify(c,null,d),h=JSON.stringify(e,null,d);m.push({path:r,updated:h!==f,newTranslations:e,existingTranslations:c})}else{const c=new Set(g.keys()),u=i(a.extract.output,n,"*"),f=await r(u,{ignore:h});for(const t of f)c.add(e(t,o(t)));for(const e of c){const o=g.get(e)||[],r=i(a.extract.output,n,e),c=t(process.cwd(),r),u=await l(c)||{},f=p(o,u,a,n,e,y,s),h=JSON.stringify(u,null,d),x=JSON.stringify(f,null,d);m.push({path:c,updated:x!==h,newTranslations:f,existingTranslations:u})}}}return m}export{d as getTranslations};
1
+ import{resolve as t,basename as e,extname as r}from"node:path";import{glob as o}from"glob";import{getNestedKeys as n,getNestedValue as s,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as i,loadTranslationFile as l}from"../../utils/file-utils.js";import{resolveDefaultValue as c}from"../../utils/default-value.js";function f(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function u(t,e){if("object"!=typeof t||null===t||Array.isArray(t))return t;const r={},o=e?.extract?.pluralSeparator??"_",n=["zero","one","two","few","many","other"],s=n.map(t=>`ordinal_${t}`),a=Object.keys(t).sort((t,e)=>{const r=t=>{for(const e of s)if(t.endsWith(`${o}${e}`)){return{base:t.slice(0,-(o.length+e.length)),form:e,isOrdinal:!0,isPlural:!0,fullKey:t}}for(const e of n)if(t.endsWith(`${o}${e}`)){return{base:t.slice(0,-(o.length+e.length)),form:e,isOrdinal:!1,isPlural:!0,fullKey:t}}return{base:t,form:"",isOrdinal:!1,isPlural:!1,fullKey:t}},a=r(t),i=r(e);if(a.isPlural&&i.isPlural){const t=a.base.localeCompare(i.base,void 0,{sensitivity:"base"});if(0!==t)return t;if(a.isOrdinal!==i.isOrdinal)return a.isOrdinal?1:-1;const e=a.isOrdinal?s:n,r=e.indexOf(a.form),o=e.indexOf(i.form);return-1!==r&&-1!==o?r-o:a.form.localeCompare(i.form)}const l=t.localeCompare(e,void 0,{sensitivity:"base"});return 0===l?t.localeCompare(e,void 0,{sensitivity:"case"}):l});for(const o of a)r[o]=u(t[o],e);return r}function p(t,e,r,o,i,l,f){const{keySeparator:p=".",sort:d=!0,removeUnusedKeys:y=!0,primaryLanguage:g,defaultValue:m="",pluralSeparator:h="_"}=r.extract,x=new Set;try{const t=new Intl.PluralRules(o,{type:"cardinal"}),e=new Intl.PluralRules(o,{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(t=>x.add(t)),e.resolvedOptions().pluralCategories.forEach(t=>x.add(`ordinal_${t}`))}catch(t){const e=new Intl.PluralRules(g||"en",{type:"cardinal"}),r=new Intl.PluralRules(g||"en",{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(t=>x.add(t)),r.resolvedOptions().pluralCategories.forEach(t=>x.add(`ordinal_${t}`))}const O=t.filter(({key:t,hasCount:e,isOrdinal:r})=>{if(l.some(e=>e.test(t)))return!1;if(!e)return!0;const o=t.split(h);if(r&&o.includes("ordinal")){const t=o[o.length-1];return x.has(`ordinal_${t}`)}if(e){const t=o[o.length-1];return x.has(t)}return!0});let w=y?{}:JSON.parse(JSON.stringify(e));const v=n(e,p??".");for(const t of v)if(l.some(e=>e.test(t))){const r=s(e,t,p??".");a(w,t,r,p??".")}if(y){const t=n(e,p??".");for(const r of t){const t=r.split(h);if("zero"===t[t.length-1]){const o=t.slice(0,-1).join(h);if(O.some(({key:t})=>t.split(h).slice(0,-1).join(h)===o)){const t=s(e,r,p??".");a(w,r,t,p??".")}}}}for(const{key:t,defaultValue:r}of O){const n=s(e,t,p??"."),l=!O.some(e=>e.key.startsWith(`${t}${p}`)&&e.key!==t),u="object"==typeof n&&null!==n&&(f.has(t)||!r||r===t),d="object"==typeof n&&null!==n&&l&&!f.has(t)&&!u;if(u){a(w,t,n,p??".");continue}let y;y=void 0===n||d?o===g?r||t:c(m,t,i,o):n,a(w,t,y,p??".")}if(!0===d)return u(w,r);if("function"==typeof d){const t={},e=Object.keys(w),o=new Map;for(const t of O){const e=!1===p?t.key:t.key.split(p)[0];o.has(e)||o.set(e,t)}e.sort((t,e)=>{if("function"==typeof d){const r=o.get(t),n=o.get(e);if(r&&n)return d(r,n)}return t.localeCompare(e,void 0,{sensitivity:"base"})});for(const o of e)t[o]=u(w[o],r);w=t}return w}async function d(n,s,a){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage);const c=a.extract.defaultNS??"translation",u=[...a.extract.preservePatterns||[]],d=a.extract.indentation??2;for(const t of s)u.push(`${t}.*`);const y=u.map(f),g=new Map;for(const t of n.values()){const e=t.ns||c;g.has(e)||g.set(e,[]),g.get(e).push(t)}const m=[],h=Array.isArray(a.extract.ignore)?a.extract.ignore:a.extract.ignore?[a.extract.ignore]:[];for(const n of a.locales){if(a.extract.mergeNamespaces||!a.extract.output.includes("{{namespace}}")){const e={},r=i(a.extract.output,n),o=t(process.cwd(),r),c=await l(o)||{},f=new Set([...g.keys(),...Object.keys(c)]);for(const t of f){const r=g.get(t)||[],o=c[t]||{};e[t]=p(r,o,a,n,t,y,s)}const u=JSON.stringify(c,null,d),h=JSON.stringify(e,null,d);m.push({path:o,updated:h!==u,newTranslations:e,existingTranslations:c})}else{const c=new Set(g.keys()),f=i(a.extract.output,n,"*"),u=await o(f,{ignore:h});for(const t of u)c.add(e(t,r(t)));for(const e of c){const r=g.get(e)||[],o=i(a.extract.output,n,e),c=t(process.cwd(),o),f=await l(c)||{},u=p(r,f,a,n,e,y,s),h=JSON.stringify(f,null,d),x=JSON.stringify(u,null,d);m.push({path:c,updated:x!==h,newTranslations:u,existingTranslations:f})}}}return m}export{d as getTranslations};
@@ -0,0 +1 @@
1
+ import{getObjectPropValue as e,getObjectProperty as t}from"./ast-utils.js";class n{pluginContext;config;logger;expressionResolver;objectKeys=new Set;constructor(e,t,n,s){this.config=e,this.pluginContext=t,this.logger=n,this.expressionResolver=s}handleCallExpression(n,s){const r=this.getFunctionName(n.callee);if(!r)return;const i=s(r),o=this.config.extract.functions||["t","*.t"];let l=void 0!==i;if(!l)for(const e of o)if(e.startsWith("*.")){if(r.endsWith(e.substring(1))){l=!0;break}}else if(e===r){l=!0;break}if(!l||0===n.arguments.length)return;const{keysToProcess:a,isSelectorAPI:u}=this.handleCallExpressionArgument(n,0);if(0===a.length)return;let c=!1;const f=this.config.extract.pluralSeparator??"_";for(let e=0;e<a.length;e++)a[e].endsWith(`${f}ordinal`)&&(c=!0,a[e]=a[e].slice(0,-8));let p,h;if(n.arguments.length>1){const e=n.arguments[1].expression;"ObjectExpression"===e.type?h=e:"StringLiteral"===e.type&&(p=e.value)}if(n.arguments.length>2){const e=n.arguments[2].expression;"ObjectExpression"===e.type&&(h=e)}const g=h?e(h,"defaultValue"):void 0,y="string"==typeof g?g:p;for(let n=0;n<a.length;n++){let s,r=a[n];if(h){const t=e(h,"ns");"string"==typeof t&&(s=t)}const o=this.config.extract.nsSeparator??":";if(!s&&o&&r.includes(o)){const e=r.split(o);if(s=e.shift(),r=e.join(o),!r||""===r.trim()){this.logger.warn(`Skipping key that became empty after namespace removal: '${s}${o}'`);continue}}!s&&i?.defaultNs&&(s=i.defaultNs),s||(s=this.config.extract.defaultNS);let l=r;if(i?.keyPrefix){const e=this.config.extract.keySeparator??".";if(l=!1!==e?i.keyPrefix.endsWith(e)?`${i.keyPrefix}${r}`:`${i.keyPrefix}${e}${r}`:`${i.keyPrefix}${r}`,!1!==e){if(l.split(e).some(e=>""===e.trim())){this.logger.warn(`Skipping key with empty segments: '${l}' (keyPrefix: '${i.keyPrefix}', key: '${r}')`);continue}}}const f=n===a.length-1&&y||r;if(h){const n=t(h,"context"),r=[];if("StringLiteral"===n?.value?.type||"NumericLiteral"===n?.value.type||"BooleanLiteral"===n?.value.type){const e=`${n.value.value}`,t=this.config.extract.contextSeparator??"_";""!==e&&r.push({key:`${l}${t}${e}`,ns:s,defaultValue:f})}else if(n?.value){const e=this.expressionResolver.resolvePossibleContextStringValues(n.value),t=this.config.extract.contextSeparator??"_";e.length>0&&(e.forEach(e=>{r.push({key:`${l}${t}${e}`,ns:s,defaultValue:f})}),r.push({key:l,ns:s,defaultValue:f}))}const i=void 0!==e(h,"count"),o=!0===e(h,"ordinal");if(i||c){this.config.extract.disablePlurals?r.length>0?r.forEach(this.pluginContext.addKey):this.pluginContext.addKey({key:l,ns:s,defaultValue:f}):this.handlePluralKeys(l,s,h,o||c,y);continue}if(r.length>0){r.forEach(this.pluginContext.addKey);continue}!0===e(h,"returnObjects")&&this.objectKeys.add(l)}u&&this.objectKeys.add(l),this.pluginContext.addKey({key:l,ns:s,defaultValue:f})}}handleCallExpressionArgument(e,t){const n=e.arguments[t].expression,s=[];let r=!1;if("ArrowFunctionExpression"===n.type){const e=this.extractKeyFromSelector(n);e&&(s.push(e),r=!0)}else if("ArrayExpression"===n.type)for(const e of n.elements)e?.expression&&s.push(...this.expressionResolver.resolvePossibleKeyStringValues(e.expression));else s.push(...this.expressionResolver.resolvePossibleKeyStringValues(n));return{keysToProcess:s.filter(e=>!!e),isSelectorAPI:r}}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 s=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)s.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;s.unshift(e.expression.value)}n=n.object}if(s.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return s.join(t)}return null}handlePluralKeys(n,s,r,i,o){try{const l=i?"ordinal":"cardinal",a=new Set;for(const e of this.config.locales)try{const t=new Intl.PluralRules(e,{type:l});t.resolvedOptions().pluralCategories.forEach(e=>a.add(e))}catch(e){const t=new Intl.PluralRules("en",{type:l});t.resolvedOptions().pluralCategories.forEach(e=>a.add(e))}const u=Array.from(a).sort(),c=this.config.extract.pluralSeparator??"_",f=e(r,"defaultValue"),p=e(r,`defaultValue${c}other`),h=e(r,`defaultValue${c}ordinal${c}other`),g=e(r,"count");let y;if("number"==typeof g)try{const e=this.config.extract?.primaryLanguage||this.config.locales[0]||"en";y=new Intl.PluralRules(e,{type:l}).select(g)}catch(e){}const d=t(r,"context"),x=[];if(d?.value){const e=this.expressionResolver.resolvePossibleContextStringValues(d.value);if(e.length>0){for(const t of e)t.length>0&&x.push({key:n,context:t});!1!==this.config.extract?.generateBasePluralForms&&x.push({key:n})}else x.push({key:n})}else x.push({key:n});for(const{key:t,context:n}of x)for(const l of u){const a=e(r,i?`defaultValue${c}ordinal${c}${l}`:`defaultValue${c}${l}`);let u,g;if(u="string"==typeof a?a:"one"===l&&"string"==typeof f?f:i&&"string"==typeof h?h:i||"string"!=typeof p?"string"==typeof f?f:o&&y===l?o:t:p,n){const e=this.config.extract.contextSeparator??"_";g=i?`${t}${e}${n}${c}ordinal${c}${l}`:`${t}${e}${n}${c}${l}`}else g=i?`${t}${c}ordinal${c}${l}`:`${t}${c}${l}`;this.pluginContext.addKey({key:g,ns:s,defaultValue:u,hasCount:!0,isOrdinal:i})}}catch(t){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const i=o||e(r,"defaultValue");this.pluginContext.addKey({key:n,ns:s,defaultValue:"string"==typeof i?i:n})}}getFunctionName(e){if("Identifier"===e.type)return e.value;if("MemberExpression"===e.type){const t=[];let n=e;for(;"MemberExpression"===n.type;){if("Identifier"!==n.property.type)return null;t.unshift(n.property.value),n=n.object}if("ThisExpression"===n.type)t.unshift("this");else{if("Identifier"!==n.type)return null;t.unshift(n.value)}return t.join(".")}return null}}export{n as CallExpressionHandler};
@@ -0,0 +1 @@
1
+ class e{hooks;constructor(e){this.hooks=e}resolvePossibleContextStringValues(e){return[...this.hooks.resolvePossibleContextStringValues?.(e)??[],...this.resolvePossibleStringValuesFromExpression(e)]}resolvePossibleKeyStringValues(e){return[...this.hooks.resolvePossibleKeyStringValues?.(e)??[],...this.resolvePossibleStringValuesFromExpression(e)]}resolvePossibleStringValuesFromExpression(e,s=!1){if("StringLiteral"===e.type)return e.value||s?[e.value]:[];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValuesFromExpression(e.consequent,s),...this.resolvePossibleStringValuesFromExpression(e.alternate,s)]}if("Identifier"===e.type&&"undefined"===e.value)return[];if("TemplateLiteral"===e.type)return this.resolvePossibleStringValuesFromTemplateString(e);if("NumericLiteral"===e.type||"BooleanLiteral"===e.type)return[`${e.value}`];if("TsSatisfiesExpression"===e.type||"TsAsExpression"===e.type){const r=e.typeAnnotation;return this.resolvePossibleStringValuesFromType(r,s)}return[]}resolvePossibleStringValuesFromType(e,s=!1){if("TsUnionType"===e.type)return e.types.flatMap(e=>this.resolvePossibleStringValuesFromType(e,s));if("TsLiteralType"===e.type){if("StringLiteral"===e.literal.type)return e.literal.value||s?[e.literal.value]:[];if("TemplateLiteral"===e.literal.type)return this.resolvePossibleStringValuesFromTemplateLiteralType(e.literal);if("NumericLiteral"===e.literal.type||"BooleanLiteral"===e.literal.type)return[`${e.literal.value}`]}return[]}resolvePossibleStringValuesFromTemplateString(e){if(1===e.quasis.length&&0===e.expressions.length)return[e.quasis[0].cooked||""];const[s,...r]=e.quasis;return e.expressions.reduce((e,s,t)=>e.flatMap(e=>{const i=r[t]?.cooked??"";return this.resolvePossibleStringValuesFromExpression(s,!0).map(s=>`${e}${s}${i}`)}),[s.cooked??""])}resolvePossibleStringValuesFromTemplateLiteralType(e){if(1===e.quasis.length&&0===e.types.length)return[e.quasis[0].cooked||""];const[s,...r]=e.quasis;return e.types.reduce((e,s,t)=>e.flatMap(e=>{const i=r[t]?.cooked??"";return this.resolvePossibleStringValuesFromType(s,!0).map(s=>`${e}${s}${i}`)}),[s.cooked??""])}}export{e as ExpressionResolver};
@@ -0,0 +1 @@
1
+ import{extractFromTransComponent as e}from"./jsx-parser.js";import{getObjectPropValue as t}from"./ast-utils.js";class n{config;pluginContext;expressionResolver;constructor(e,t,n){this.config=e,this.pluginContext=t,this.expressionResolver=n}handleJSXElement(t,n){const s=this.getElementName(t);if(s&&(this.config.extract.transComponents||["Trans"]).includes(s)){const s=e(t,this.config),a=[];if(s){if(s.keyExpression){const e=this.expressionResolver.resolvePossibleKeyStringValues(s.keyExpression);a.push(...e)}else a.push(s.serializedChildren);let e;const{contextExpression:i,optionsNode:l,defaultValue:o,hasCount:r,isOrdinal:u,serializedChildren:f}=s;if(s.ns){const{ns:t}=s;e=a.map(e=>({key:e,ns:t,defaultValue:o||f,hasCount:r,isOrdinal:u}))}else{e=a.map(e=>{const t=this.config.extract.nsSeparator??":";let n;if(t&&e.includes(t)){let s;[n,...s]=e.split(t),e=s.join(t)}return{key:e,ns:n,defaultValue:o||f,hasCount:r,isOrdinal:u}});const s=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===s?.type&&"JSXExpressionContainer"===s.value?.type&&"Identifier"===s.value.expression.type){const t=n(s.value.expression.value);t?.defaultNs&&e.forEach(e=>{e.ns||(e.ns=t.defaultNs)})}}if(e.forEach(e=>{e.ns||(e.ns=this.config.extract.defaultNS)}),i&&r)if(this.config.extract.disablePlurals){const t=this.expressionResolver.resolvePossibleContextStringValues(i),n=this.config.extract.contextSeparator??"_";if(t.length>0)if("StringLiteral"===i.type)for(const s of t)for(const t of e){const e=`${t.key}${n}${s}`;this.pluginContext.addKey({key:e,ns:t.ns,defaultValue:t.defaultValue})}else{e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})});for(const s of t)for(const t of e){const e=`${t.key}${n}${s}`;this.pluginContext.addKey({key:e,ns:t.ns,defaultValue:t.defaultValue})}}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}else{const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),s=!!n,a=this.expressionResolver.resolvePossibleContextStringValues(i),o=this.config.extract.contextSeparator??"_";if(a.length>0){e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,s,l));for(const t of a)for(const n of e){const e=`${n.key}${o}${t}`;this.generatePluralKeysForTrans(e,n.defaultValue,n.ns,s,l)}}else e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,s,l))}else if(i){const t=this.expressionResolver.resolvePossibleContextStringValues(i),n=this.config.extract.contextSeparator??"_";if(t.length>0){for(const s of t)for(const{key:t,ns:a,defaultValue:i}of e)this.pluginContext.addKey({key:`${t}${n}${s}`,ns:a,defaultValue:i});"StringLiteral"!==i.type&&e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}else if(r)if(this.config.extract.disablePlurals)e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})});else{const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),s=!!n;e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,s,l))}else e.forEach(e=>{this.pluginContext.addKey({key:e.key,ns:e.ns,defaultValue:e.defaultValue})})}}}generatePluralKeysForTrans(e,n,s,a,i){try{const l=a?"ordinal":"cardinal",o=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:l}).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";let u,f;i&&(u=t(i,`defaultValue${r}other`),f=t(i,`defaultValue${r}ordinal${r}other`));for(const l of o){const o=i?t(i,a?`defaultValue${r}ordinal${r}${l}`:`defaultValue${r}${l}`):void 0;let d;d="string"==typeof o?o:"one"===l&&"string"==typeof n?n:a&&"string"==typeof f?f:a||"string"!=typeof u?"string"==typeof n?n:e:u;const p=a?`${e}${r}ordinal${r}${l}`:`${e}${r}${l}`;this.pluginContext.addKey({key:p,ns:s,defaultValue:d,hasCount:!0,isOrdinal:a})}}catch(t){this.pluginContext.addKey({key:e,ns:s,defaultValue: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(".")}}}export{n as JSXHandler};
@@ -1 +1 @@
1
- import{getObjectProperty as e,getObjectPropValue as t}from"./ast-utils.js";function n(n,i){const r=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),a=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),s=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),p=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"values"===e.name.value);let l;s||"JSXAttribute"!==p?.type||"JSXExpressionContainer"!==p.value?.type||"ObjectExpression"!==p.value.expression.type||(l=e(p.value.expression,"count"));const o=!!s||!!l,u=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),y="JSXAttribute"===u?.type&&"JSXExpressionContainer"===u.value?.type&&"ObjectExpression"===u.value.expression.type?u.value.expression:void 0,v=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),f=!!v,c=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let d="JSXAttribute"===c?.type&&"JSXExpressionContainer"===c.value?.type?c.value.expression:"JSXAttribute"===c?.type&&"StringLiteral"===c.value?.type?c.value:void 0;const m=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let S;if(S="JSXAttribute"===m?.type&&"StringLiteral"===m.value?.type?m.value.value:void 0,y&&(void 0===S&&(S=t(y,"ns")),void 0===d)){const t=e(y,"context");t?.value&&(d=t.value)}const b=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){let t="";return e.forEach((e,r)=>{if("JSXText"===e.type)t+=e.value;else if("JSXExpressionContainer"===e.type){const n=e.expression;if("StringLiteral"===n.type)t+=n.value;else if("Identifier"===n.type)t+=`{{${n.value}}}`;else if("ObjectExpression"===n.type){const e=n.properties[0];e&&"Identifier"===e.type&&(t+=`{{${e.value}}}`)}}else if("JSXElement"===e.type){let a;"Identifier"===e.opening.name.type&&(a=e.opening.name.value);const s=i(e.children);a&&n.has(a)?t+=`<${a}>${s}</${a}>`:t+=`<${r}>${s}</${r}>`}else"JSXFragment"===e.type&&(t+=i(e.children))}),t}return i(e).trim().replace(/\s{2,}/g," ")}(n.children,i);let x,g,J;if("JSXAttribute"===a?.type&&"StringLiteral"===a.value?.type)x=a.value.value;else{const e=i.extract.defaultValue;x="string"==typeof e?e:""}if("JSXAttribute"===r?.type){if("StringLiteral"===r.value?.type){if(g=r.value,J=g.value,!J||""===J.trim())return console.warn("Ignoring Trans component with empty i18nKey"),null;if(S&&"StringLiteral"===g.type){const e=i.extract.nsSeparator??":",t=g.value;if(e&&t.startsWith(`${S}${e}`)){if(J=t.slice(`${S}${e}`.length),!J||""===J.trim())return console.warn("Ignoring Trans component with i18nKey that becomes empty after namespace removal"),null;g={...g,value:J}}}}else"JSXExpressionContainer"===r.value?.type&&"JSXEmptyExpression"!==r.value.expression.type&&(g=r.value.expression);if(!g)return null}return a||!J||b.trim()?!a&&b.trim()&&(x=b):x=J,{keyExpression:g,serializedChildren:b,ns:S,defaultValue:x,hasCount:o,isOrdinal:f,contextExpression:d,optionsNode:y}}export{n as extractFromTransComponent};
1
+ import{getObjectProperty as e,getObjectPropValue as t}from"./ast-utils.js";function n(n,i){const r=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),a=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),s=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),p=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"values"===e.name.value);let l;s||"JSXAttribute"!==p?.type||"JSXExpressionContainer"!==p.value?.type||"ObjectExpression"!==p.value.expression.type||(l=e(p.value.expression,"count"));const o=!!s||!!l,u=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),y="JSXAttribute"===u?.type&&"JSXExpressionContainer"===u.value?.type&&"ObjectExpression"===u.value.expression.type?u.value.expression:void 0,v=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),f=!!v,c=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let d="JSXAttribute"===c?.type&&"JSXExpressionContainer"===c.value?.type?c.value.expression:"JSXAttribute"===c?.type&&"StringLiteral"===c.value?.type?c.value:void 0;const m=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let S;if(S="JSXAttribute"===m?.type&&"StringLiteral"===m.value?.type?m.value.value:void 0,y&&(void 0===S&&(S=t(y,"ns")),void 0===d)){const t=e(y,"context");t?.value&&(d=t.value)}const b=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){let t="";return e.forEach((e,r)=>{if("JSXText"===e.type)t+=e.value;else if("JSXExpressionContainer"===e.type){const n=e.expression;if("StringLiteral"===n.type)t+=n.value;else if("Identifier"===n.type)t+=`{{${n.value}}}`;else if("ObjectExpression"===n.type){const e=n.properties[0];e&&"KeyValueProperty"===e.type&&e.key&&"Identifier"===e.key.type?t+=`{{${e.key.value}}}`:e&&"Identifier"===e.type&&(t+=`{{${e.value}}}`)}}else if("JSXElement"===e.type){let a;"Identifier"===e.opening.name.type&&(a=e.opening.name.value);const s=i(e.children);a&&n.has(a)?t+=`<${a}>${s}</${a}>`:t+=`<${r}>${s}</${r}>`}else"JSXFragment"===e.type&&(t+=i(e.children))}),t}return i(e).trim().replace(/\s{2,}/g," ")}(n.children,i);let x,g,J;if("JSXAttribute"===a?.type&&"StringLiteral"===a.value?.type)x=a.value.value;else{const e=i.extract.defaultValue;x="string"==typeof e?e:""}if("JSXAttribute"===r?.type){if("StringLiteral"===r.value?.type){if(g=r.value,J=g.value,!J||""===J.trim())return console.warn("Ignoring Trans component with empty i18nKey"),null;if(S&&"StringLiteral"===g.type){const e=i.extract.nsSeparator??":",t=g.value;if(e&&t.startsWith(`${S}${e}`)){if(J=t.slice(`${S}${e}`.length),!J||""===J.trim())return console.warn("Ignoring Trans component with i18nKey that becomes empty after namespace removal"),null;g={...g,value:J}}}}else"JSXExpressionContainer"===r.value?.type&&"JSXEmptyExpression"!==r.value.expression.type&&(g=r.value.expression);if(!g)return null}return a||!J||b.trim()?!a&&b.trim()&&(x=b):x=J,{keyExpression:g,serializedChildren:b,ns:S,defaultValue:x,hasCount:o,isOrdinal:f,contextExpression:d,optionsNode:y}}export{n as extractFromTransComponent};
@@ -0,0 +1 @@
1
+ import{getObjectPropValue as e}from"./ast-utils.js";class t{scopeStack=[];config;scope=new Map;constructor(e){this.config=e}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)}const t=this.scope.get(e);if(t)return t}handleVariableDeclarator(e){const t=e.init;if(!t)return;const r="AwaitExpression"===t.type&&"CallExpression"===t.argument.type?t.argument:"CallExpression"===t.type?t:null;if(!r)return;const i=r.callee;if("Identifier"===i.type){const t=this.getUseTranslationConfig(i.value);if(t)return this.handleUseTranslationDeclarator(e,r,t),void this.handleUseTranslationForComments(e,r,t)}"MemberExpression"===i.type&&"Identifier"===i.property.type&&"getFixedT"===i.property.value&&this.handleGetFixedTDeclarator(e,r)}getUseTranslationConfig(e){const t=this.config.extract.useTranslationNames||["useTranslation"];for(const r of t){if("string"==typeof r&&r===e)return{name:e,nsArg:0,keyPrefixArg:1};if("object"==typeof r&&r.name===e)return{name:r.name,nsArg:r.nsArg??0,keyPrefixArg:r.keyPrefixArg??1}}}handleUseTranslationForComments(e,t,r){let i;if("Identifier"===e.id.type&&(i=e.id.value),"ArrayPattern"===e.id.type){const t=e.id.elements[0];"Identifier"===t?.type&&(i=t.value)}if("ObjectPattern"===e.id.type)for(const t of e.id.properties){if("AssignmentPatternProperty"===t.type&&"Identifier"===t.key.type&&"t"===t.key.value){i="t";break}if("KeyValuePatternProperty"===t.type&&"Identifier"===t.key.type&&"t"===t.key.value&&"Identifier"===t.value.type){i=t.value.value;break}}if(!i)return;const n=t.arguments?.[r.nsArg]?.expression,s=t.arguments?.[r.keyPrefixArg]?.expression;let a,o;if("StringLiteral"===n?.type?a=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(a=n.elements[0].expression.value),"ObjectExpression"===s?.type){const e=s.properties.find(e=>"KeyValueProperty"===e.type&&"Identifier"===e.key.type&&"keyPrefix"===e.key.value);"KeyValueProperty"===e?.type&&"StringLiteral"===e.value.type&&(o=e.value.value)}(a||o)&&this.scope.set(i,{defaultNs:a,keyPrefix:o})}handleUseTranslationDeclarator(t,r,i){let n;if("Identifier"===t.id.type&&(n=t.id.value),"ArrayPattern"===t.id.type){const e=t.id.elements[0];"Identifier"===e?.type&&(n=e.value)}if("ObjectPattern"===t.id.type)for(const e of t.id.properties){if("AssignmentPatternProperty"===e.type&&"Identifier"===e.key.type&&"t"===e.key.value){n="t";break}if("KeyValuePatternProperty"===e.type&&"Identifier"===e.key.type&&"t"===e.key.value&&"Identifier"===e.value.type){n=e.value.value;break}}if(!n)return;const s=r.arguments?.[i.nsArg]?.expression;let a;"StringLiteral"===s?.type?a=s.value:"ArrayExpression"===s?.type&&"StringLiteral"===s.elements[0]?.expression.type&&(a=s.elements[0].expression.value);const o=r.arguments?.[i.keyPrefixArg]?.expression;let p;if("ObjectExpression"===o?.type){const t=e(o,"keyPrefix");p="string"==typeof t?t:void 0}this.setVarInScope(n,{defaultNs:a,keyPrefix:p})}handleGetFixedTDeclarator(e,t){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const r=e.id.value,i=t.arguments,n=i[1]?.expression,s=i[2]?.expression,a="StringLiteral"===n?.type?n.value:void 0,o="StringLiteral"===s?.type?s.value:void 0;(a||o)&&this.setVarInScope(r,{defaultNs:a,keyPrefix:o})}}export{t as ScopeManager};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.10.1",
3
+ "version": "1.10.3",
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('1.10.1')
24
+ .version('1.10.3')
25
25
 
26
26
  program
27
27
  .command('extract')
@@ -0,0 +1,170 @@
1
+ import type { Module, Node } from '@swc/core'
2
+ import type { PluginContext, I18nextToolkitConfig, Logger, ASTVisitorHooks, ScopeInfo } from '../../types'
3
+ import { ScopeManager } from '../parsers/scope-manager'
4
+ import { ExpressionResolver } from '../parsers/expression-resolver'
5
+ import { CallExpressionHandler } from '../parsers/call-expression-handler'
6
+ import { JSXHandler } from '../parsers/jsx-handler'
7
+
8
+ /**
9
+ * AST visitor class that traverses JavaScript/TypeScript syntax trees to extract translation keys.
10
+ *
11
+ * This class implements a manual recursive walker that:
12
+ * - Maintains scope information for tracking useTranslation and getFixedT calls
13
+ * - Extracts keys from t() function calls with various argument patterns
14
+ * - Handles JSX Trans components with complex children serialization
15
+ * - Supports both string literals and selector API for type-safe keys
16
+ * - Processes pluralization and context variants
17
+ * - Manages namespace resolution from multiple sources
18
+ *
19
+ * The visitor respects configuration options for separators, function names,
20
+ * component names, and other extraction settings.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const visitors = new ASTVisitors(config, pluginContext, logger)
25
+ * visitors.visit(parsedAST)
26
+ *
27
+ * // The pluginContext will now contain all extracted keys
28
+ * ```
29
+ */
30
+ export class ASTVisitors {
31
+ private readonly pluginContext: PluginContext
32
+ private readonly config: Omit<I18nextToolkitConfig, 'plugins'>
33
+ private readonly logger: Logger
34
+ private hooks: ASTVisitorHooks
35
+
36
+ public get objectKeys () {
37
+ return this.callExpressionHandler.objectKeys
38
+ }
39
+
40
+ private readonly scopeManager: ScopeManager
41
+ private readonly expressionResolver: ExpressionResolver
42
+ private readonly callExpressionHandler: CallExpressionHandler
43
+ private readonly jsxHandler: JSXHandler
44
+
45
+ /**
46
+ * Creates a new AST visitor instance.
47
+ *
48
+ * @param config - Toolkit configuration with extraction settings
49
+ * @param pluginContext - Context for adding discovered translation keys
50
+ * @param logger - Logger for warnings and debug information
51
+ */
52
+ constructor (
53
+ config: Omit<I18nextToolkitConfig, 'plugins'>,
54
+ pluginContext: PluginContext,
55
+ logger: Logger,
56
+ hooks?: ASTVisitorHooks
57
+ ) {
58
+ this.pluginContext = pluginContext
59
+ this.config = config
60
+ this.logger = logger
61
+ this.hooks = {
62
+ onBeforeVisitNode: hooks?.onBeforeVisitNode,
63
+ onAfterVisitNode: hooks?.onAfterVisitNode,
64
+ resolvePossibleKeyStringValues: hooks?.resolvePossibleKeyStringValues,
65
+ resolvePossibleContextStringValues: hooks?.resolvePossibleContextStringValues
66
+ }
67
+
68
+ this.scopeManager = new ScopeManager(config)
69
+ this.expressionResolver = new ExpressionResolver(this.hooks)
70
+ this.callExpressionHandler = new CallExpressionHandler(config, pluginContext, logger, this.expressionResolver)
71
+ this.jsxHandler = new JSXHandler(config, pluginContext, this.expressionResolver)
72
+ }
73
+
74
+ /**
75
+ * Main entry point for AST traversal.
76
+ * Creates a root scope and begins the recursive walk through the syntax tree.
77
+ *
78
+ * @param node - The root module node to traverse
79
+ */
80
+ public visit (node: Module): void {
81
+ this.scopeManager.enterScope() // Create the root scope for the file
82
+ this.walk(node)
83
+ this.scopeManager.exitScope() // Clean up the root scope
84
+ }
85
+
86
+ /**
87
+ * Recursively walks through AST nodes, handling scoping and visiting logic.
88
+ *
89
+ * This is the core traversal method that:
90
+ * 1. Manages function scopes (enter/exit)
91
+ * 2. Dispatches to specific handlers based on node type
92
+ * 3. Recursively processes child nodes
93
+ * 4. Maintains proper scope cleanup
94
+ *
95
+ * @param node - The current AST node to process
96
+ *
97
+ * @private
98
+ */
99
+ private walk (node: Node | any): void {
100
+ if (!node) return
101
+
102
+ let isNewScope = false
103
+ // ENTER SCOPE for functions
104
+ if (node.type === 'Function' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
105
+ this.scopeManager.enterScope()
106
+ isNewScope = true
107
+ }
108
+
109
+ this.hooks.onBeforeVisitNode?.(node)
110
+
111
+ // --- VISIT LOGIC ---
112
+ // Handle specific node types
113
+ switch (node.type) {
114
+ case 'VariableDeclarator':
115
+ this.scopeManager.handleVariableDeclarator(node)
116
+ break
117
+ case 'CallExpression':
118
+ this.callExpressionHandler.handleCallExpression(node, this.scopeManager.getVarFromScope.bind(this.scopeManager))
119
+ break
120
+ case 'JSXElement':
121
+ this.jsxHandler.handleJSXElement(node, this.scopeManager.getVarFromScope.bind(this.scopeManager))
122
+ break
123
+ }
124
+
125
+ this.hooks.onAfterVisitNode?.(node)
126
+
127
+ // --- END VISIT LOGIC ---
128
+
129
+ // --- RECURSION ---
130
+ // Recurse into the children of the current node
131
+ for (const key in node) {
132
+ if (key === 'span') continue
133
+
134
+ const child = node[key]
135
+ if (Array.isArray(child)) {
136
+ for (const item of child) {
137
+ // Be less strict: if it's a non-null object, walk it.
138
+ // This allows traversal into nodes that might not have a `.type` property
139
+ // but still contain other valid AST nodes.
140
+ if (item && typeof item === 'object') {
141
+ this.walk(item)
142
+ }
143
+ }
144
+ } else if (child && typeof child === 'object') {
145
+ // The condition for single objects should be the same as for array items.
146
+ // Do not require `child.type`. This allows traversal into class method bodies.
147
+ this.walk(child)
148
+ }
149
+ }
150
+ // --- END RECURSION ---
151
+
152
+ // LEAVE SCOPE for functions
153
+ if (isNewScope) {
154
+ this.scopeManager.exitScope()
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Retrieves variable information from the scope chain.
160
+ * Searches from innermost to outermost scope.
161
+ *
162
+ * @param name - Variable name to look up
163
+ * @returns Scope information if found, undefined otherwise
164
+ *
165
+ * @private
166
+ */
167
+ public getVarFromScope (name: string): ScopeInfo | undefined {
168
+ return this.scopeManager.getVarFromScope(name)
169
+ }
170
+ }
@@ -8,7 +8,7 @@ import { findKeys } from './key-finder'
8
8
  import { getTranslations } from './translation-manager'
9
9
  import { validateExtractorConfig, ExtractorError } from '../../utils/validation'
10
10
  import { extractKeysFromComments } from '../parsers/comment-parser'
11
- import { ASTVisitors } from '../parsers/ast-visitors'
11
+ import { ASTVisitors } from './ast-visitors'
12
12
  import { ConsoleLogger } from '../../utils/logger'
13
13
  import { serializeTranslationFile } from '../../utils/file-utils'
14
14
  import { shouldShowFunnel, recordFunnelShown } from '../../utils/funnel-msg-tracker'
@@ -1,10 +1,10 @@
1
1
  import { glob } from 'glob'
2
2
  import type { Expression } from '@swc/core'
3
- import type { ExtractedKey, Logger, I18nextToolkitConfig } from '../../types'
3
+ import type { ExtractedKey, Logger, I18nextToolkitConfig, ASTVisitorHooks } from '../../types'
4
4
  import { processFile } from './extractor'
5
5
  import { ConsoleLogger } from '../../utils/logger'
6
6
  import { initializePlugins, createPluginContext } from '../plugin-manager'
7
- import { type ASTVisitorHooks, ASTVisitors } from '../parsers/ast-visitors'
7
+ import { ASTVisitors } from './ast-visitors'
8
8
 
9
9
  /**
10
10
  * Main function for finding translation keys across all source files in a project.