i18next-cli 1.18.1 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,10 @@ 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.19.0](https://github.com/i18next/i18next-cli/compare/v1.18.1...v1.19.0) - 2025-10-31
9
+
10
+ - feat(extractor): improve key extraction to detect translation keys stored in local variables, object properties and simple string expressions [#83](https://github.com/i18next/i18next-cli/issues/83)
11
+
8
12
  ## [1.18.1](https://github.com/i18next/i18next-cli/compare/v1.18.0...v1.18.1) - 2025-10-30
9
13
 
10
14
  - improve regression introduced in v1.17.2 [#82](https://github.com/i18next/i18next-cli/issues/82)
package/dist/cjs/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("minimatch"),i=require("chalk"),a=require("./config.js"),r=require("./heuristic-config.js"),c=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var s=require("./types-generator.js"),l=require("./syncer.js"),u=require("./migrator.js"),g=require("./init.js"),d=require("./linter.js"),p=require("./status.js"),f=require("./locize.js");const m=new e.Command;m.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.18.1"),m.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),m.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.").option("--sync-primary","Sync primary language values with default values from code.").action(async e=>{try{const o=m.opts().config,i=await a.ensureConfig(o),r=async()=>{const t=await c.runExtractor(i,{isWatchMode:!!e.watch,isDryRun:!!e.dryRun,syncPrimaryWithDefaults:!!e.syncPrimary});return e.ci&&!t?(console.log("✅ No files were updated."),process.exit(0)):e.ci&&t&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),t};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.extract.input),o=y(i.extract.ignore),a=h(i.extract.output),c=[...o,...a].filter(Boolean),s=e.filter(e=>!c.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}catch(e){console.error("Error running extractor:",e),process.exit(1)}}),m.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)=>{const o=m.opts().config;let n=await a.loadConfig(o);if(!n){console.log(i.blue("No config file found. Attempting to detect project structure..."));const e=await r.detectConfig();e||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),n=e}await p.runStatus(n,{detail:e,namespace:t.namespace})}),m.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=m.opts().config,i=await a.ensureConfig(o),r=()=>s.runTypesGenerator(i);if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.types?.input||[]),o=[...y(i.extract?.ignore)].filter(Boolean),a=e.filter(e=>!o.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}),m.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=m.opts().config,t=await a.ensureConfig(e);await l.runSyncer(t)}),m.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async e=>{await u.runMigrator(e)}),m.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(g.runInit),m.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 o=m.opts().config,c=async()=>{let e=await a.loadConfig(o);if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await r.detectConfig();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=t}await d.runLinterCli(e)};if(await c(),e.watch){console.log("\nWatching for changes...");const e=await a.loadConfig(o);if(e?.extract?.input){const o=await w(e.extract.input),i=[...y(e.extract.ignore),...h(e.extract.output)].filter(Boolean),a=o.filter(e=>!i.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}}),m.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=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeSync(o,e)}),m.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeDownload(o,e)}),m.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeMigrate(o,e)}),m.parse(process.argv);const y=e=>Array.isArray(e)?e:e?[e]:[],h=e=>e&&"string"==typeof e?[e.replace(/\{\{[^}]+\}\}/g,"*")]:[],w=async(e=[])=>{const t=y(e),n=await Promise.all(t.map(e=>o.glob(e||"",{nodir:!0})));return Array.from(new Set(n.flat()))};
2
+ "use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("minimatch"),i=require("chalk"),a=require("./config.js"),r=require("./heuristic-config.js"),c=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var s=require("./types-generator.js"),l=require("./syncer.js"),u=require("./migrator.js"),g=require("./init.js"),d=require("./linter.js"),p=require("./status.js"),f=require("./locize.js");const m=new e.Command;m.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.19.0"),m.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),m.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.").option("--sync-primary","Sync primary language values with default values from code.").action(async e=>{try{const o=m.opts().config,i=await a.ensureConfig(o),r=async()=>{const t=await c.runExtractor(i,{isWatchMode:!!e.watch,isDryRun:!!e.dryRun,syncPrimaryWithDefaults:!!e.syncPrimary});return e.ci&&!t?(console.log("✅ No files were updated."),process.exit(0)):e.ci&&t&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),t};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.extract.input),o=y(i.extract.ignore),a=h(i.extract.output),c=[...o,...a].filter(Boolean),s=e.filter(e=>!c.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}catch(e){console.error("Error running extractor:",e),process.exit(1)}}),m.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)=>{const o=m.opts().config;let n=await a.loadConfig(o);if(!n){console.log(i.blue("No config file found. Attempting to detect project structure..."));const e=await r.detectConfig();e||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),n=e}await p.runStatus(n,{detail:e,namespace:t.namespace})}),m.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=m.opts().config,i=await a.ensureConfig(o),r=()=>s.runTypesGenerator(i);if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.types?.input||[]),o=[...y(i.extract?.ignore)].filter(Boolean),a=e.filter(e=>!o.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}),m.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=m.opts().config,t=await a.ensureConfig(e);await l.runSyncer(t)}),m.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async e=>{await u.runMigrator(e)}),m.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(g.runInit),m.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 o=m.opts().config,c=async()=>{let e=await a.loadConfig(o);if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await r.detectConfig();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=t}await d.runLinterCli(e)};if(await c(),e.watch){console.log("\nWatching for changes...");const e=await a.loadConfig(o);if(e?.extract?.input){const o=await w(e.extract.input),i=[...y(e.extract.ignore),...h(e.extract.output)].filter(Boolean),a=o.filter(e=>!i.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}}),m.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=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeSync(o,e)}),m.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeDownload(o,e)}),m.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeMigrate(o,e)}),m.parse(process.argv);const y=e=>Array.isArray(e)?e:e?[e]:[],h=e=>e&&"string"==typeof e?[e.replace(/\{\{[^}]+\}\}/g,"*")]:[],w=async(e=[])=>{const t=y(e),n=await Promise.all(t.map(e=>o.glob(e||"",{nodir:!0})));return Array.from(new Set(n.flat()))};
@@ -1 +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(a,t,i,n){this.pluginContext=t,this.config=a,this.logger=i,this.hooks={onBeforeVisitNode:n?.onBeforeVisitNode,onAfterVisitNode:n?.onAfterVisitNode,resolvePossibleKeyStringValues:n?.resolvePossibleKeyStringValues,resolvePossibleContextStringValues:n?.resolvePossibleContextStringValues},this.scopeManager=new e.ScopeManager(a),this.expressionResolver=new s.ExpressionResolver(this.hooks),this.callExpressionHandler=new r.CallExpressionHandler(a,t,i,this.expressionResolver),this.jsxHandler=new o.JSXHandler(a,t,this.expressionResolver)}visit(e){this.scopeManager.reset(),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)if(e&&"object"==typeof e)if("VariableDeclarator"!==e.type){if("VariableDeclaration"===e.type&&Array.isArray(e.declarations))for(const s of e.declarations)s&&"object"==typeof s&&"VariableDeclarator"===s.type&&this.scopeManager.handleVariableDeclarator(s)}else this.scopeManager.handleVariableDeclarator(e);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
+ "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(a,t,i,n){this.pluginContext=t,this.config=a,this.logger=i,this.hooks={onBeforeVisitNode:n?.onBeforeVisitNode,onAfterVisitNode:n?.onAfterVisitNode,resolvePossibleKeyStringValues:n?.resolvePossibleKeyStringValues,resolvePossibleContextStringValues:n?.resolvePossibleContextStringValues},this.scopeManager=new e.ScopeManager(a),this.expressionResolver=new s.ExpressionResolver(this.hooks),this.callExpressionHandler=new r.CallExpressionHandler(a,t,i,this.expressionResolver),this.jsxHandler=new o.JSXHandler(a,t,this.expressionResolver)}visit(e){this.scopeManager.reset(),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),this.expressionResolver.captureVariableDeclarator(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)if(e&&"object"==typeof e)if("VariableDeclarator"!==e.type){if("VariableDeclaration"===e.type&&Array.isArray(e.declarations))for(const s of e.declarations)s&&"object"==typeof s&&"VariableDeclarator"===s.type&&(this.scopeManager.handleVariableDeclarator(s),this.expressionResolver.captureVariableDeclarator(s))}else this.scopeManager.handleVariableDeclarator(e),this.expressionResolver.captureVariableDeclarator(e);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";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??""])}};
1
+ "use strict";exports.ExpressionResolver=class{hooks;symbolTable=new Map;constructor(e){this.hooks=e}captureVariableDeclarator(e){try{if(!e||!e.id||!e.init)return;if("Identifier"!==e.id.type)return;const s=e.id.value,t=e.init;if("ObjectExpression"===t.type&&Array.isArray(t.properties)){const e={};for(const s of t.properties){if(!s||"KeyValueProperty"!==s.type)continue;const t=s.key,r="Identifier"===t?.type||"StringLiteral"===t?.type?t.value:void 0;if(!r)continue;const i=s.value,o=this.resolvePossibleStringValuesFromExpression(i);1===o.length&&(e[r]=o[0])}if(Object.keys(e).length>0)return void this.symbolTable.set(s,e)}const r=this.resolvePossibleStringValuesFromExpression(t);r.length>0&&this.symbolTable.set(s,r)}catch{}}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("MemberExpression"===e.type)try{const s=e.object,t=e.property;if("Identifier"===s.type){const e=this.symbolTable.get(s.value);if(e&&"string"!=typeof e&&!Array.isArray(e)){let s;if("Identifier"===t.type?s=t.value:"Computed"===t.type&&"StringLiteral"===t.expression?.type&&(s=t.expression.value),s&&void 0!==e[s])return[e[s]]}}}catch{}if(e.left&&e.right)try{const t=this.resolvePossibleStringValuesFromExpression(e.left,s),r=this.resolvePossibleStringValuesFromExpression(e.right,s);if(t.length>0&&r.length>0){const e=[];for(const s of t)for(const t of r)e.push(`${s}${t}`);return e}}catch{}if("NumericLiteral"===e.type||"BooleanLiteral"===e.type)return[`${e.value}`];if("TsSatisfiesExpression"===e.type||"TsAsExpression"===e.type){const t=e.typeAnnotation;return this.resolvePossibleStringValuesFromType(t,s)}if("Identifier"===e.type){const s=this.symbolTable.get(e.value);return s&&Array.isArray(s)?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,...t]=e.quasis;return e.expressions.reduce((e,s,r)=>e.flatMap(e=>{const i=t[r]?.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,...t]=e.quasis;return e.types.reduce((e,s,r)=>e.flatMap(e=>{const i=t[r]?.cooked??"";return this.resolvePossibleStringValuesFromType(s,!0).map(s=>`${e}${s}${i}`)}),[s.cooked??""])}};
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{minimatch as i}from"minimatch";import n from"chalk";import{ensureConfig as a,loadConfig as r}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as s}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as l}from"./types-generator.js";import{runSyncer as p}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as f}from"./init.js";import{runLinterCli as d}from"./linter.js";import{runStatus as g}from"./status.js";import{runLocizeSync as u,runLocizeDownload as y,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.18.1"),w.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),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.").option("--sync-primary","Sync primary language values with default values from code.").action(async t=>{try{const e=w.opts().config,n=await a(e),r=async()=>{const o=await s(n,{isWatchMode:!!t.watch,isDryRun:!!t.dryRun,syncPrimaryWithDefaults:!!t.syncPrimary});return t.ci&&!o?(console.log("✅ No files were updated."),process.exit(0)):t.ci&&o&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),o};if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.extract.input),e=x(n.extract.ignore),a=j(n.extract.output),c=[...e,...a].filter(Boolean),s=t.filter(t=>!c.some(o=>i(t,o,{dot:!0})));o.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}catch(t){console.error("Error running extractor:",t),process.exit(1)}}),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)=>{const e=w.opts().config;let i=await r(e);if(!i){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!")),i=t}await g(i,{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 e=w.opts().config,n=await a(e),r=()=>l(n);if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.types?.input||[]),e=[...x(n.extract?.ignore)].filter(Boolean),a=t.filter(t=>!e.some(o=>i(t,o,{dot:!0})));o.watch(a,{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=w.opts().config,o=await a(t);await p(o)}),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(f),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 e=w.opts().config,a=async()=>{let t=await r(e);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 a(),t.watch){console.log("\nWatching for changes...");const t=await r(e);if(t?.extract?.input){const e=await z(t.extract.input),n=[...x(t.extract.ignore),...j(t.extract.output)].filter(Boolean),r=e.filter(t=>!n.some(o=>i(t,o,{dot:!0})));o.watch(r,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}}),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=w.opts().config,e=await a(o);await u(e,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await y(e,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await h(e,t)}),w.parse(process.argv);const x=t=>Array.isArray(t)?t:t?[t]:[],j=t=>t&&"string"==typeof t?[t.replace(/\{\{[^}]+\}\}/g,"*")]:[],z=async(t=[])=>{const o=x(t),i=await Promise.all(o.map(t=>e(t||"",{nodir:!0})));return Array.from(new Set(i.flat()))};
2
+ import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import{minimatch as i}from"minimatch";import n from"chalk";import{ensureConfig as a,loadConfig as r}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as s}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as l}from"./types-generator.js";import{runSyncer as p}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as f}from"./init.js";import{runLinterCli as d}from"./linter.js";import{runStatus as g}from"./status.js";import{runLocizeSync as u,runLocizeDownload as y,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.19.0"),w.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),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.").option("--sync-primary","Sync primary language values with default values from code.").action(async t=>{try{const e=w.opts().config,n=await a(e),r=async()=>{const o=await s(n,{isWatchMode:!!t.watch,isDryRun:!!t.dryRun,syncPrimaryWithDefaults:!!t.syncPrimary});return t.ci&&!o?(console.log("✅ No files were updated."),process.exit(0)):t.ci&&o&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),o};if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.extract.input),e=x(n.extract.ignore),a=j(n.extract.output),c=[...e,...a].filter(Boolean),s=t.filter(t=>!c.some(o=>i(t,o,{dot:!0})));o.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}catch(t){console.error("Error running extractor:",t),process.exit(1)}}),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)=>{const e=w.opts().config;let i=await r(e);if(!i){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!")),i=t}await g(i,{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 e=w.opts().config,n=await a(e),r=()=>l(n);if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.types?.input||[]),e=[...x(n.extract?.ignore)].filter(Boolean),a=t.filter(t=>!e.some(o=>i(t,o,{dot:!0})));o.watch(a,{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=w.opts().config,o=await a(t);await p(o)}),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(f),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 e=w.opts().config,a=async()=>{let t=await r(e);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 a(),t.watch){console.log("\nWatching for changes...");const t=await r(e);if(t?.extract?.input){const e=await z(t.extract.input),n=[...x(t.extract.ignore),...j(t.extract.output)].filter(Boolean),r=e.filter(t=>!n.some(o=>i(t,o,{dot:!0})));o.watch(r,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}}),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=w.opts().config,e=await a(o);await u(e,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await y(e,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await h(e,t)}),w.parse(process.argv);const x=t=>Array.isArray(t)?t:t?[t]:[],j=t=>t&&"string"==typeof t?[t.replace(/\{\{[^}]+\}\}/g,"*")]:[],z=async(t=[])=>{const o=x(t),i=await Promise.all(o.map(t=>e(t||"",{nodir:!0})));return Array.from(new Set(i.flat()))};
@@ -1 +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,a,i,n){this.pluginContext=a,this.config=t,this.logger=i,this.hooks={onBeforeVisitNode:n?.onBeforeVisitNode,onAfterVisitNode:n?.onAfterVisitNode,resolvePossibleKeyStringValues:n?.resolvePossibleKeyStringValues,resolvePossibleContextStringValues:n?.resolvePossibleContextStringValues},this.scopeManager=new e(t),this.expressionResolver=new s(this.hooks),this.callExpressionHandler=new o(t,a,i,this.expressionResolver),this.jsxHandler=new r(t,a,this.expressionResolver)}visit(e){this.scopeManager.reset(),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)if(e&&"object"==typeof e)if("VariableDeclarator"!==e.type){if("VariableDeclaration"===e.type&&Array.isArray(e.declarations))for(const s of e.declarations)s&&"object"==typeof s&&"VariableDeclarator"===s.type&&this.scopeManager.handleVariableDeclarator(s)}else this.scopeManager.handleVariableDeclarator(e);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
+ import{ScopeManager as e}from"../parsers/scope-manager.js";import{ExpressionResolver as s}from"../parsers/expression-resolver.js";import{CallExpressionHandler as r}from"../parsers/call-expression-handler.js";import{JSXHandler as o}from"../parsers/jsx-handler.js";class a{pluginContext;config;logger;hooks;get objectKeys(){return this.callExpressionHandler.objectKeys}scopeManager;expressionResolver;callExpressionHandler;jsxHandler;constructor(a,t,i,n){this.pluginContext=t,this.config=a,this.logger=i,this.hooks={onBeforeVisitNode:n?.onBeforeVisitNode,onAfterVisitNode:n?.onAfterVisitNode,resolvePossibleKeyStringValues:n?.resolvePossibleKeyStringValues,resolvePossibleContextStringValues:n?.resolvePossibleContextStringValues},this.scopeManager=new e(a),this.expressionResolver=new s(this.hooks),this.callExpressionHandler=new r(a,t,i,this.expressionResolver),this.jsxHandler=new o(a,t,this.expressionResolver)}visit(e){this.scopeManager.reset(),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),this.expressionResolver.captureVariableDeclarator(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)if(e&&"object"==typeof e)if("VariableDeclarator"!==e.type){if("VariableDeclaration"===e.type&&Array.isArray(e.declarations))for(const s of e.declarations)s&&"object"==typeof s&&"VariableDeclarator"===s.type&&(this.scopeManager.handleVariableDeclarator(s),this.expressionResolver.captureVariableDeclarator(s))}else this.scopeManager.handleVariableDeclarator(e),this.expressionResolver.captureVariableDeclarator(e);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)}}export{a as ASTVisitors};
@@ -1 +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};
1
+ class e{hooks;symbolTable=new Map;constructor(e){this.hooks=e}captureVariableDeclarator(e){try{if(!e||!e.id||!e.init)return;if("Identifier"!==e.id.type)return;const s=e.id.value,t=e.init;if("ObjectExpression"===t.type&&Array.isArray(t.properties)){const e={};for(const s of t.properties){if(!s||"KeyValueProperty"!==s.type)continue;const t=s.key,r="Identifier"===t?.type||"StringLiteral"===t?.type?t.value:void 0;if(!r)continue;const i=s.value,o=this.resolvePossibleStringValuesFromExpression(i);1===o.length&&(e[r]=o[0])}if(Object.keys(e).length>0)return void this.symbolTable.set(s,e)}const r=this.resolvePossibleStringValuesFromExpression(t);r.length>0&&this.symbolTable.set(s,r)}catch{}}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("MemberExpression"===e.type)try{const s=e.object,t=e.property;if("Identifier"===s.type){const e=this.symbolTable.get(s.value);if(e&&"string"!=typeof e&&!Array.isArray(e)){let s;if("Identifier"===t.type?s=t.value:"Computed"===t.type&&"StringLiteral"===t.expression?.type&&(s=t.expression.value),s&&void 0!==e[s])return[e[s]]}}}catch{}if(e.left&&e.right)try{const t=this.resolvePossibleStringValuesFromExpression(e.left,s),r=this.resolvePossibleStringValuesFromExpression(e.right,s);if(t.length>0&&r.length>0){const e=[];for(const s of t)for(const t of r)e.push(`${s}${t}`);return e}}catch{}if("NumericLiteral"===e.type||"BooleanLiteral"===e.type)return[`${e.value}`];if("TsSatisfiesExpression"===e.type||"TsAsExpression"===e.type){const t=e.typeAnnotation;return this.resolvePossibleStringValuesFromType(t,s)}if("Identifier"===e.type){const s=this.symbolTable.get(e.value);return s&&Array.isArray(s)?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,...t]=e.quasis;return e.expressions.reduce((e,s,r)=>e.flatMap(e=>{const i=t[r]?.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,...t]=e.quasis;return e.types.reduce((e,s,r)=>e.flatMap(e=>{const i=t[r]?.cooked??"";return this.resolvePossibleStringValuesFromType(s,!0).map(s=>`${e}${s}${i}`)}),[s.cooked??""])}}export{e as ExpressionResolver};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -22,7 +22,7 @@ const program = new Command()
22
22
  program
23
23
  .name('i18next-cli')
24
24
  .description('A unified, high-performance i18next CLI.')
25
- .version('1.18.1')
25
+ .version('1.19.0')
26
26
 
27
27
  // new: global config override option
28
28
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)')
@@ -115,6 +115,9 @@ export class ASTVisitors {
115
115
  switch (node.type) {
116
116
  case 'VariableDeclarator':
117
117
  this.scopeManager.handleVariableDeclarator(node)
118
+ // Capture simple variable initializers so the expressionResolver can
119
+ // resolve identifiers / member expressions that reference them.
120
+ this.expressionResolver.captureVariableDeclarator(node)
118
121
  break
119
122
  case 'CallExpression':
120
123
  this.callExpressionHandler.handleCallExpression(node, this.scopeManager.getVarFromScope.bind(this.scopeManager))
@@ -146,6 +149,7 @@ export class ASTVisitors {
146
149
  // Direct declarator present in arrays (rare)
147
150
  if (item.type === 'VariableDeclarator') {
148
151
  this.scopeManager.handleVariableDeclarator(item)
152
+ this.expressionResolver.captureVariableDeclarator(item)
149
153
  continue
150
154
  }
151
155
 
@@ -154,6 +158,7 @@ export class ASTVisitors {
154
158
  for (const decl of item.declarations) {
155
159
  if (decl && typeof decl === 'object' && decl.type === 'VariableDeclarator') {
156
160
  this.scopeManager.handleVariableDeclarator(decl)
161
+ this.expressionResolver.captureVariableDeclarator(decl)
157
162
  }
158
163
  }
159
164
  }
@@ -3,11 +3,66 @@ import type { ASTVisitorHooks } from '../../types'
3
3
 
4
4
  export class ExpressionResolver {
5
5
  private hooks: ASTVisitorHooks
6
+ // Simple per-file symbol table for statically analyzable variables.
7
+ // Maps variableName -> either:
8
+ // - string[] (possible string values)
9
+ // - Record<string, string> (object of static string properties)
10
+ private symbolTable: Map<string, string[] | Record<string, string>> = new Map()
6
11
 
7
12
  constructor (hooks: ASTVisitorHooks) {
8
13
  this.hooks = hooks
9
14
  }
10
15
 
16
+ /**
17
+ * Capture a VariableDeclarator node to record simple statically analyzable
18
+ * initializers (string literals, object expressions of string literals,
19
+ * template literals and simple concatenations).
20
+ *
21
+ * This is called during AST traversal before deeper walking so later
22
+ * identifier/member-expression usage can be resolved.
23
+ *
24
+ * @param node - VariableDeclarator-like node (has .id and .init)
25
+ */
26
+ captureVariableDeclarator (node: any): void {
27
+ try {
28
+ if (!node || !node.id || !node.init) return
29
+ // only handle simple identifier bindings like `const x = ...`
30
+ if (node.id.type !== 'Identifier') return
31
+ const name = node.id.value
32
+ const init = node.init
33
+
34
+ // ObjectExpression -> map of string props
35
+ if (init.type === 'ObjectExpression' && Array.isArray(init.properties)) {
36
+ const map: Record<string, string> = {}
37
+ for (const p of init.properties as any[]) {
38
+ if (!p || p.type !== 'KeyValueProperty') continue
39
+ const keyNode = p.key
40
+ const keyName = keyNode?.type === 'Identifier' ? keyNode.value : keyNode?.type === 'StringLiteral' ? keyNode.value : undefined
41
+ if (!keyName) continue
42
+ const valExpr = p.value
43
+ const vals = this.resolvePossibleStringValuesFromExpression(valExpr)
44
+ // Only capture properties that we can statically resolve to a single string.
45
+ if (vals.length === 1) {
46
+ map[keyName] = vals[0]
47
+ }
48
+ }
49
+ // If at least one property was resolvable, record the partial map.
50
+ if (Object.keys(map).length > 0) {
51
+ this.symbolTable.set(name, map)
52
+ return
53
+ }
54
+ }
55
+
56
+ // For other initializers, try to resolve to one-or-more strings
57
+ const vals = this.resolvePossibleStringValuesFromExpression(init)
58
+ if (vals.length > 0) {
59
+ this.symbolTable.set(name, vals)
60
+ }
61
+ } catch {
62
+ // be silent - conservative only
63
+ }
64
+ }
65
+
11
66
  /**
12
67
  * Resolves an expression to one or more possible context string values that can be
13
68
  * determined statically from the AST. This is a wrapper around the plugin hook
@@ -73,6 +128,42 @@ export class ExpressionResolver {
73
128
  return this.resolvePossibleStringValuesFromTemplateString(expression)
74
129
  }
75
130
 
131
+ // MemberExpression: try to resolve object identifier to an object map in the symbol table
132
+ if (expression.type === 'MemberExpression') {
133
+ try {
134
+ const obj = expression.object
135
+ const prop = expression.property
136
+ // only handle simple identifier base + simple property (Identifier or computed StringLiteral)
137
+ if (obj.type === 'Identifier') {
138
+ const base = this.symbolTable.get(obj.value)
139
+ if (base && typeof base !== 'string' && !Array.isArray(base)) {
140
+ let propName: string | undefined
141
+ if (prop.type === 'Identifier') propName = prop.value
142
+ else if (prop.type === 'Computed' && prop.expression?.type === 'StringLiteral') propName = prop.expression.value
143
+ if (propName && base[propName] !== undefined) return [base[propName]]
144
+ }
145
+ }
146
+ } catch {}
147
+ }
148
+
149
+ // Binary concatenation support (e.g., a + '_' + b)
150
+ // SWC binary expr can be represented as `BinExpr` with left/right; be permissive:
151
+ if ((expression as any).left && (expression as any).right) {
152
+ try {
153
+ const leftVals = this.resolvePossibleStringValuesFromExpression((expression as any).left, returnEmptyStrings)
154
+ const rightVals = this.resolvePossibleStringValuesFromExpression((expression as any).right, returnEmptyStrings)
155
+ if (leftVals.length > 0 && rightVals.length > 0) {
156
+ const combos: string[] = []
157
+ for (const L of leftVals) {
158
+ for (const R of rightVals) {
159
+ combos.push(`${L}${R}`)
160
+ }
161
+ }
162
+ return combos
163
+ }
164
+ } catch {}
165
+ }
166
+
76
167
  if (expression.type === 'NumericLiteral' || expression.type === 'BooleanLiteral') {
77
168
  return [`${expression.value}`] // Handle literals like 5 or true
78
169
  }
@@ -84,6 +175,15 @@ export class ExpressionResolver {
84
175
  return this.resolvePossibleStringValuesFromType(annotation, returnEmptyStrings)
85
176
  }
86
177
 
178
+ // Identifier resolution via captured symbol table
179
+ if (expression.type === 'Identifier') {
180
+ const v = this.symbolTable.get(expression.value)
181
+ if (!v) return []
182
+ if (Array.isArray(v)) return v
183
+ // object map - cannot be used directly as key, so return empty
184
+ return []
185
+ }
186
+
87
187
  // We can't statically determine the value of other expressions (e.g., variables, function calls)
88
188
  return []
89
189
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAM1G;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IAEvC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe;IAkBzB;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IAkFZ;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;CAG7D"}
1
+ {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAM1G;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IAEvC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe;IAkBzB;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IAuFZ;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;CAG7D"}
@@ -2,7 +2,19 @@ import type { Expression } from '@swc/core';
2
2
  import type { ASTVisitorHooks } from '../../types';
3
3
  export declare class ExpressionResolver {
4
4
  private hooks;
5
+ private symbolTable;
5
6
  constructor(hooks: ASTVisitorHooks);
7
+ /**
8
+ * Capture a VariableDeclarator node to record simple statically analyzable
9
+ * initializers (string literals, object expressions of string literals,
10
+ * template literals and simple concatenations).
11
+ *
12
+ * This is called during AST traversal before deeper walking so later
13
+ * identifier/member-expression usage can be resolved.
14
+ *
15
+ * @param node - VariableDeclarator-like node (has .id and .init)
16
+ */
17
+ captureVariableDeclarator(node: any): void;
6
18
  /**
7
19
  * Resolves an expression to one or more possible context string values that can be
8
20
  * determined statically from the AST. This is a wrapper around the plugin hook
@@ -1 +1 @@
1
- {"version":3,"file":"expression-resolver.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/expression-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,WAAW,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAiB;gBAEjB,KAAK,EAAE,eAAe;IAInC;;;;;;;OAOG;IACH,kCAAkC,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKrE;;;;;;;OAOG;IACH,8BAA8B,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKjE;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,yCAAyC;IAmCjD,OAAO,CAAC,mCAAmC;IAwB3C;;;;;;OAMG;IACH,OAAO,CAAC,6CAA6C;IAyBrD;;;;;;OAMG;IACH,OAAO,CAAC,kDAAkD;CAwB3D"}
1
+ {"version":3,"file":"expression-resolver.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/expression-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,WAAW,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAiB;IAK9B,OAAO,CAAC,WAAW,CAA4D;gBAElE,KAAK,EAAE,eAAe;IAInC;;;;;;;;;OASG;IACH,yBAAyB,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAwC3C;;;;;;;OAOG;IACH,kCAAkC,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKrE;;;;;;;OAOG;IACH,8BAA8B,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKjE;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,yCAAyC;IAgFjD,OAAO,CAAC,mCAAmC;IAwB3C;;;;;;OAMG;IACH,OAAO,CAAC,6CAA6C;IAyBrD;;;;;;OAMG;IACH,OAAO,CAAC,kDAAkD;CAwB3D"}