i18next-cli 1.11.4 → 1.11.6

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,15 @@ 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.11.6](https://github.com/i18next/i18next-cli/compare/v1.11.5...v1.11.6) - 2025-10-19
9
+
10
+ - Fix Trans child indexing to match react-i18next and handle layout-only whitespace correctly [#63](https://github.com/i18next/i18next-cli/issues/63)
11
+
12
+ ## [1.11.5](https://github.com/i18next/i18next-cli/compare/v1.11.4...v1.11.5) - 2025-10-17
13
+
14
+ - **Extractor:** ignore formatting-only JSXText nodes when serializing <Trans> children to avoid inflated component placeholder indexes and incorrect keys caused by indentation/newlines — fixes [#66](https://github.com/i18next/i18next-cli/issues/66)
15
+ - **Extractor:** prevent scope leakage and ensure correct namespace resolution for t()/getFixedT() when functions reference `t` declared later in the same file; restores idempotent extraction regardless of input file ordering — fixes [#65](https://github.com/i18next/i18next-cli/issues/65)
16
+
8
17
  ## [1.11.4](https://github.com/i18next/i18next-cli/compare/v1.11.3...v1.11.4) - 2025-10-15
9
18
 
10
19
  - **Extractor:** Ignore formatting-only JSXText nodes that contain only whitespace/newlines when serializing `<Trans>` children. This prevents inflated component placeholder indexes caused by indentation/newlines while preserving intentional spaces (e.g. `{' '}`). Resulting keys now match react-i18next runtime normalization. See [#63](https://github.com/i18next/i18next-cli/issues/63).
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.11.4"),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.").option("--sync-primary","Sync primary language values with default values from code.").action(async e=>{try{const o=await i.ensureConfig(),a=async()=>{const t=await r.runExtractor(o,{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 a(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(o.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}catch(e){console.error("Error running extractor:",e),process.exit(1)}}),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.11.6"),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.").option("--sync-primary","Sync primary language values with default values from code.").action(async e=>{try{const o=await i.ensureConfig(),a=async()=>{const t=await r.runExtractor(o,{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 a(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(o.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}catch(e){console.error("Error running extractor:",e),process.exit(1)}}),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);
@@ -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(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
+ "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 +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 l=!!a||!!p,u=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),o="JSXAttribute"===u?.type&&"JSXExpressionContainer"===u.value?.type&&"ObjectExpression"===u.value.expression.type?u.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 S=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let d;if(d="JSXAttribute"===S?.type&&"StringLiteral"===S.value?.type?S.value.value:void 0,o&&(void 0===d&&(d=e.getObjectPropValue(o,"ns")),void 0===c)){const t=e.getObjectProperty(o,"context");t?.value&&(c=t.value)}const m=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){if(!e||!e.length)return[];const t=e.filter(e=>!!e&&("JSXText"!==e.type||!/^\s*$/.test(e.value)||!e.value.includes("\n"))).map(e=>"JSXExpressionContainer"===e.type&&"StringLiteral"===e.expression?.type?{type:"JSXText",value:e.expression.value}:e),n=[];for(const e of t)if("JSXText"===e.type){const t=n[n.length-1];t&&"JSXText"===t.type?t.value=t.value+e.value:n.push({type:"JSXText",value:String(e.value)})}else n.push(e);for(const e of n)"JSXText"===e.type&&(e.value=String(e.value).replace(/\s+/g," "));return n}function r(e){e=i(e);let t="";return e.forEach((e,i)=>{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=r(e.children);a&&n.has(a)?t+=`<${a}>${s}</${a}>`:t+=`<${i}>${s}</${i}>`}else"JSXFragment"===e.type&&(t+=r(e.children))}),t}return r(e=i(e)).trim().replace(/\s{2,}/g," ")}(t.children,n);let x,g,b;if("JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type)x=r.value.value;else{const e=n.extract.defaultValue;x="string"==typeof e?e:""}if("JSXAttribute"===i?.type){if("StringLiteral"===i.value?.type){if(g=i.value,b=g.value,!b||""===b.trim())return console.warn("Ignoring Trans component with empty i18nKey"),null;if(d&&"StringLiteral"===g.type){const e=n.extract.nsSeparator??":",t=g.value;if(e&&t.startsWith(`${d}${e}`)){if(b=t.slice(`${d}${e}`.length),!b||""===b.trim())return console.warn("Ignoring Trans component with i18nKey that becomes empty after namespace removal"),null;g={...g,value:b}}}}else"JSXExpressionContainer"===i.value?.type&&"JSXEmptyExpression"!==i.value.expression.type&&(g=i.value.expression);if(!g)return null}return r||!b||m.trim()?!r&&m.trim()&&(x=m):x=b,{keyExpression:g,serializedChildren:m,ns:d,defaultValue:x,hasCount:l,isOrdinal:v,contextExpression:c,optionsNode:o}};
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),p=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 o;p||"JSXAttribute"!==s?.type||"JSXExpressionContainer"!==s.value?.type||"ObjectExpression"!==s.value.expression.type||(o=e.getObjectProperty(s.value.expression,"count"));const a=!!p||!!o,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),f=!!y,c=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let v="JSXAttribute"===c?.type&&"JSXExpressionContainer"===c.value?.type?c.value.expression:"JSXAttribute"===c?.type&&"StringLiteral"===c.value?.type?c.value:void 0;const S=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let x;if(x="JSXAttribute"===S?.type&&"StringLiteral"===S.value?.type?S.value.value:void 0,u&&(void 0===x&&(x=e.getObjectPropValue(u,"ns")),void 0===v)){const t=e.getObjectProperty(u,"context");t?.value&&(v=t.value)}const d=function(e,t){if(!e||0===e.length)return"";const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]),i=e=>e&&"JSXText"===e.type&&/^\s*$/.test(e.value)&&e.value.includes("\n");function r(e,t,p=!1){if(!e||!e.length)return;let s=0,o=e.length-1;for(;s<=o&&i(e[s]);)s++;for(;o>=s&&i(e[o]);)o--;const a=s<=o?e.slice(s,o+1):[];for(let e=0;e<a.length;e++){const s=a[e];if(s)if("JSXText"!==s.type)if("JSXExpressionContainer"!==s.type){if("JSXElement"===s.type){const e=s.opening&&s.opening.name&&"Identifier"===s.opening.name.type?s.opening.name.value:void 0;e&&n.has(e)?r(s.children||[],t,!1):(t.push(s),r(s.children||[],t,!0));continue}"JSXFragment"!==s.type||r(s.children||[],t,p)}else{if(p&&s.expression&&"ObjectExpression"===s.expression.type){const e=s.expression.properties&&s.expression.properties[0];if(e&&"KeyValueProperty"===e.type)continue}if(s.expression&&"StringLiteral"===s.expression.type){const n=String(s.expression.value||""),r=/^\s*$/.test(n)&&!n.includes("\n"),p=a[e-1],o=a[e+1];if(r){const n=a[e+2];if(o&&"JSXText"===o.type&&i(o)&&n&&("JSXElement"===n.type||"JSXFragment"===n.type)){const t=a[e-1],n=a[e-2];if(!t||"JSXText"!==t.type||n&&"JSXExpressionContainer"===n.type)continue}if(p&&("JSXElement"===p.type||"JSXFragment"===p.type)&&o&&"JSXText"===o.type&&i(o))continue;const r=!o||"JSXText"===o.type&&!i(o);if(p&&"JSXText"===p.type&&r){const e=t[t.length-1];if(e&&"JSXText"===e.type){e.value=String(e.value)+s.expression.value;continue}}if(p&&("JSXElement"===p.type||"JSXFragment"===p.type)&&o&&"JSXText"===o.type&&i(o))continue}}t.push(s)}else{if(i(s)){const n=t[t.length-1],i=a[e-1];if(n){if(i&&"JSXExpressionContainer"===i.type)continue;if("JSXText"===n.type&&i&&"JSXText"===i.type){n.value=String(n.value)+s.value;continue}}}t.push(s)}}}const p=[];function s(e){if(!e||0===e.length)return"";let t="";for(const r of e)if(r)if("JSXText"!==r.type){if("JSXExpressionContainer"===r.type){const e=r.expression;if(!e)continue;if("StringLiteral"===e.type)t+=e.value;else if("Identifier"===e.type)t+=`{{${e.value}}}`;else if("ObjectExpression"===e.type){const n=e.properties[0];n&&"KeyValueProperty"===n.type&&n.key&&"Identifier"===n.key.type?t+=`{{${n.key.value}}}`:n&&"Identifier"===n.type?t+=`{{${n.value}}}`:t+="{{value}}"}else"MemberExpression"===e.type&&e.property&&"Identifier"===e.property.type?t+=`{{${e.property.value}}}`:"CallExpression"===e.type&&"Identifier"===e.callee?.type?t+=`{{${e.callee.value}}}`:t+="{{value}}";continue}if("JSXElement"===r.type){let e;if(r.opening&&r.opening.name&&"Identifier"===r.opening.name.type&&(e=r.opening.name.value),e&&n.has(e)){t+=`<${e}>${s(r.children||[])}</${e}>`}else{const e=p.indexOf(r);t+=`<${e}>${s(r.children||[])}</${e}>`}continue}"JSXFragment"!==r.type||(t+=s(r.children||[]))}else i(r)||(t+=r.value);return t}r(e,p,!1);const o=s(e);return String(o).replace(/\s+/g," ").trim()}(t.children,n);let g,J,X;if("JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type)g=r.value.value;else{const e=n.extract.defaultValue;g="string"==typeof e?e:""}if("JSXAttribute"===i?.type){if("StringLiteral"===i.value?.type){if(J=i.value,X=J.value,!X||""===X.trim())return null;if(x&&"StringLiteral"===J.type){const e=n.extract.nsSeparator??":",t=J.value;if(e&&t.startsWith(`${x}${e}`)){if(X=t.slice(`${x}${e}`.length),!X||""===X.trim())return null;J={...J,value:X}}}}else"JSXExpressionContainer"===i.value?.type&&"JSXEmptyExpression"!==i.value.expression.type&&(J=i.value.expression);if(!J)return null}return r||!X||d.trim()?!r&&d.trim()&&(g=d):g=X,{keyExpression:J,serializedChildren:d,ns:x,defaultValue:g,hasCount:a,isOrdinal:f,contextExpression:v,optionsNode:u}};
@@ -1 +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})}};
1
+ "use strict";var e=require("./ast-utils.js");exports.ScopeManager=class{scopeStack=[];config;scope=new Map;constructor(e){this.config=e}reset(){this.scopeStack=[],this.scope=new Map}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 s=t.arguments?.[r.nsArg]?.expression,n=t.arguments?.[r.keyPrefixArg]?.expression;let a,o;if("StringLiteral"===s?.type?a=s.value:"ArrayExpression"===s?.type&&"StringLiteral"===s.elements[0]?.expression.type&&(a=s.elements[0].expression.value),"ObjectExpression"===n?.type){const e=n.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 s;if("Identifier"===t.id.type&&(s=t.id.value),"ArrayPattern"===t.id.type){const e=t.id.elements[0];"Identifier"===e?.type&&(s=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){s="t";break}if("KeyValuePatternProperty"===e.type&&"Identifier"===e.key.type&&"t"===e.key.value&&"Identifier"===e.value.type){s=e.value.value;break}}if(!s)return;const n=r.arguments?.[i.nsArg]?.expression;let a;"StringLiteral"===n?.type?a=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(a=n.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(s,{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,s=i[1]?.expression,n=i[2]?.expression,a="StringLiteral"===s?.type?s.value:void 0,o="StringLiteral"===n?.type?n.value:void 0;(a||o)&&this.setVarInScope(r,{defaultNs:a,keyPrefix:o})}};
@@ -1 +1 @@
1
- "use strict";var e=require("execa"),o=require("chalk"),n=require("ora"),t=require("inquirer"),i=require("node:path");function r(e,o,n){const{locize:t={},extract:r}=o,{projectId:s,apiKey:c,version:a}=t,l=[e];if(s&&l.push("--project-id",s),c&&l.push("--api-key",c),a&&l.push("--ver",a),"sync"===e){(n.updateValues??t.updateValues)&&l.push("--update-values","true");(n.srcLngOnly??t.sourceLanguageOnly)&&l.push("--reference-language-only","true");(n.compareMtime??t.compareModificationTime)&&l.push("--compare-modification-time","true");(n.dryRun??t.dryRun)&&l.push("--dry","true")}const u=i.resolve(process.cwd(),r.output.split("/{{language}}/")[0]);return l.push("--path",u),l}async function s(i,s,c={}){await async function(){try{await e.execa("locize",["--version"])}catch(e){"ENOENT"===e.code&&(console.error(o.red("Error: `locize-cli` command not found.")),console.log(o.yellow("Please install it globally to use the locize integration:")),console.log(o.cyan("npm install -g locize-cli")),process.exit(1))}}();const a=n(`Running 'locize ${i}'...\n`).start();let l=s;try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green(`'locize ${i}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(n){const s=n.stderr||"";if(s.includes("missing required argument")){const n=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await t.prompt([{type:"input",name:"projectId",message:"What is your locize Project ID? (Find this in your project settings on www.locize.app)",validate:e=>!!e||"Project ID cannot be empty."},{type:"password",name:"apiKey",message:'What is your locize API key? (Create or use one in your project settings > "API Keys")',validate:e=>!!e||"API Key cannot be empty."},{type:"input",name:"version",message:"What version do you want to sync with?",default:"latest"}]);if(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:n}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(n){const n=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.version}',\n },`;console.log(o.cyan("\nGreat! For the best security, we recommend using environment variables for your API key.")),console.log(o.bold("\nRecommended approach (.env file):")),console.log(o.green(n)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(n){l={...l,locize:n},a.start("Retrying with new credentials...");try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${i}'.`)),console.error(s||n.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${i}' completed successfully.`))}exports.runLocizeDownload=(e,o)=>s("download",e,o),exports.runLocizeMigrate=(e,o)=>s("migrate",e,o),exports.runLocizeSync=(e,o)=>s("sync",e,o);
1
+ "use strict";var e=require("execa"),o=require("chalk"),n=require("ora"),t=require("inquirer"),i=require("node:path");function r(e,o,n){const{locize:t={},extract:r}=o,{projectId:s,apiKey:c,version:a}=t,l=[e];if(s&&l.push("--project-id",s),c&&l.push("--api-key",c),a&&l.push("--ver",a),"sync"===e){(n.updateValues??t.updateValues)&&l.push("--update-values","true");(n.srcLngOnly??t.sourceLanguageOnly)&&l.push("--reference-language-only","true");(n.compareMtime??t.compareModificationTime)&&l.push("--compare-modification-time","true");(n.dryRun??t.dryRun)&&l.push("--dry","true")}const u=String(r.output||"").replace(/\\/g,"/"),p=(u.includes("/{{language}}/")?u.split("/{{language}}/")[0]:u.replace("{{language}}","")).split("/").join(i.sep),d=i.resolve(process.cwd(),p);return l.push("--path",d),l}async function s(i,s,c={}){await async function(){try{await e.execa("locize",["--version"])}catch(e){"ENOENT"===e.code&&(console.error(o.red("Error: `locize-cli` command not found.")),console.log(o.yellow("Please install it globally to use the locize integration:")),console.log(o.cyan("npm install -g locize-cli")),process.exit(1))}}();const a=n(`Running 'locize ${i}'...\n`).start();let l=s;try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green(`'locize ${i}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(n){const s=n.stderr||"";if(s.includes("missing required argument")){const n=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await t.prompt([{type:"input",name:"projectId",message:"What is your locize Project ID? (Find this in your project settings on www.locize.app)",validate:e=>!!e||"Project ID cannot be empty."},{type:"password",name:"apiKey",message:'What is your locize API key? (Create or use one in your project settings > "API Keys")',validate:e=>!!e||"API Key cannot be empty."},{type:"input",name:"version",message:"What version do you want to sync with?",default:"latest"}]);if(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:n}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(n){const n=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.version}',\n },`;console.log(o.cyan("\nGreat! For the best security, we recommend using environment variables for your API key.")),console.log(o.bold("\nRecommended approach (.env file):")),console.log(o.green(n)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(n){l={...l,locize:n},a.start("Retrying with new credentials...");try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${i}'.`)),console.error(s||n.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${i}' completed successfully.`))}exports.runLocizeDownload=(e,o)=>s("download",e,o),exports.runLocizeMigrate=(e,o)=>s("migrate",e,o),exports.runLocizeSync=(e,o)=>s("sync",e,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 e from"chokidar";import{glob as o}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 p}from"./migrator.js";import{runInit as m}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as u}from"./status.js";import{runLocizeSync as f,runLocizeDownload as g,runLocizeMigrate as h}from"./locize.js";const y=new t;y.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.11.4"),y.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 n=await i(),a=async()=>{const e=await r(n,{isWatchMode:!!t.watch,isDryRun:!!t.dryRun,syncPrimaryWithDefaults:!!t.syncPrimary});return t.ci&&!e?(console.log("✅ No files were updated."),process.exit(0)):t.ci&&e&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),e};if(await a(),t.watch){console.log("\nWatching for changes...");e.watch(await o(n.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}catch(t){console.error("Error running extractor:",t),process.exit(1)}}),y.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,e)=>{let o=await a();if(!o){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!")),o=t}await u(o,{detail:t,namespace:e.namespace})}),y.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...");e.watch(await o(n.types?.input||[]),{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}),y.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=await i();await l(t)}),y.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async t=>{await p(t)}),y.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(m),y.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 e=await c();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),t=e}await d(t)};if(await i(),t.watch){console.log("\nWatching for changes...");const t=await a();if(t?.extract?.input){e.watch(await o(t.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),i()})}}}),y.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 e=await i();await f(e,t)}),y.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const e=await i();await g(e,t)}),y.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const e=await i();await h(e,t)}),y.parse(process.argv);
2
+ import{Command as t}from"commander";import e from"chokidar";import{glob as o}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 p}from"./migrator.js";import{runInit as m}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as u}from"./status.js";import{runLocizeSync as f,runLocizeDownload as g,runLocizeMigrate as h}from"./locize.js";const y=new t;y.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.11.6"),y.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 n=await i(),a=async()=>{const e=await r(n,{isWatchMode:!!t.watch,isDryRun:!!t.dryRun,syncPrimaryWithDefaults:!!t.syncPrimary});return t.ci&&!e?(console.log("✅ No files were updated."),process.exit(0)):t.ci&&e&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),e};if(await a(),t.watch){console.log("\nWatching for changes...");e.watch(await o(n.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}catch(t){console.error("Error running extractor:",t),process.exit(1)}}),y.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,e)=>{let o=await a();if(!o){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!")),o=t}await u(o,{detail:t,namespace:e.namespace})}),y.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...");e.watch(await o(n.types?.input||[]),{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}),y.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=await i();await l(t)}),y.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async t=>{await p(t)}),y.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(m),y.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 e=await c();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),t=e}await d(t)};if(await i(),t.watch){console.log("\nWatching for changes...");const t=await a();if(t?.extract?.input){e.watch(await o(t.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),i()})}}}),y.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 e=await i();await f(e,t)}),y.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const e=await i();await g(e,t)}),y.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const e=await i();await h(e,t)}),y.parse(process.argv);
@@ -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,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
+ 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 +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 u=!!s||!!l,o=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),y="JSXAttribute"===o?.type&&"JSXExpressionContainer"===o.value?.type&&"ObjectExpression"===o.value.expression.type?o.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 S="JSXAttribute"===c?.type&&"JSXExpressionContainer"===c.value?.type?c.value.expression:"JSXAttribute"===c?.type&&"StringLiteral"===c.value?.type?c.value:void 0;const d=n.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,y&&(void 0===m&&(m=t(y,"ns")),void 0===S)){const t=e(y,"context");t?.value&&(S=t.value)}const x=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){if(!e||!e.length)return[];const t=e.filter(e=>!!e&&("JSXText"!==e.type||!/^\s*$/.test(e.value)||!e.value.includes("\n"))).map(e=>"JSXExpressionContainer"===e.type&&"StringLiteral"===e.expression?.type?{type:"JSXText",value:e.expression.value}:e),n=[];for(const e of t)if("JSXText"===e.type){const t=n[n.length-1];t&&"JSXText"===t.type?t.value=t.value+e.value:n.push({type:"JSXText",value:String(e.value)})}else n.push(e);for(const e of n)"JSXText"===e.type&&(e.value=String(e.value).replace(/\s+/g," "));return n}function r(e){e=i(e);let t="";return e.forEach((e,i)=>{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=r(e.children);a&&n.has(a)?t+=`<${a}>${s}</${a}>`:t+=`<${i}>${s}</${i}>`}else"JSXFragment"===e.type&&(t+=r(e.children))}),t}return r(e=i(e)).trim().replace(/\s{2,}/g," ")}(n.children,i);let g,J,X;if("JSXAttribute"===a?.type&&"StringLiteral"===a.value?.type)g=a.value.value;else{const e=i.extract.defaultValue;g="string"==typeof e?e:""}if("JSXAttribute"===r?.type){if("StringLiteral"===r.value?.type){if(J=r.value,X=J.value,!X||""===X.trim())return console.warn("Ignoring Trans component with empty i18nKey"),null;if(m&&"StringLiteral"===J.type){const e=i.extract.nsSeparator??":",t=J.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;J={...J,value:X}}}}else"JSXExpressionContainer"===r.value?.type&&"JSXEmptyExpression"!==r.value.expression.type&&(J=r.value.expression);if(!J)return null}return a||!X||x.trim()?!a&&x.trim()&&(g=x):g=X,{keyExpression:J,serializedChildren:x,ns:m,defaultValue:g,hasCount:u,isOrdinal:f,contextExpression:S,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),p=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),o=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"values"===e.name.value);let a;s||"JSXAttribute"!==o?.type||"JSXExpressionContainer"!==o.value?.type||"ObjectExpression"!==o.value.expression.type||(a=e(o.value.expression,"count"));const l=!!s||!!a,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,f=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),c=!!f,v=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let S="JSXAttribute"===v?.type&&"JSXExpressionContainer"===v.value?.type?v.value.expression:"JSXAttribute"===v?.type&&"StringLiteral"===v.value?.type?v.value:void 0;const x=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let d;if(d="JSXAttribute"===x?.type&&"StringLiteral"===x.value?.type?x.value.value:void 0,y&&(void 0===d&&(d=t(y,"ns")),void 0===S)){const t=e(y,"context");t?.value&&(S=t.value)}const J=function(e,t){if(!e||0===e.length)return"";const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]),i=e=>e&&"JSXText"===e.type&&/^\s*$/.test(e.value)&&e.value.includes("\n");function r(e,t,p=!1){if(!e||!e.length)return;let s=0,o=e.length-1;for(;s<=o&&i(e[s]);)s++;for(;o>=s&&i(e[o]);)o--;const a=s<=o?e.slice(s,o+1):[];for(let e=0;e<a.length;e++){const s=a[e];if(s)if("JSXText"!==s.type)if("JSXExpressionContainer"!==s.type){if("JSXElement"===s.type){const e=s.opening&&s.opening.name&&"Identifier"===s.opening.name.type?s.opening.name.value:void 0;e&&n.has(e)?r(s.children||[],t,!1):(t.push(s),r(s.children||[],t,!0));continue}"JSXFragment"!==s.type||r(s.children||[],t,p)}else{if(p&&s.expression&&"ObjectExpression"===s.expression.type){const e=s.expression.properties&&s.expression.properties[0];if(e&&"KeyValueProperty"===e.type)continue}if(s.expression&&"StringLiteral"===s.expression.type){const n=String(s.expression.value||""),r=/^\s*$/.test(n)&&!n.includes("\n"),p=a[e-1],o=a[e+1];if(r){const n=a[e+2];if(o&&"JSXText"===o.type&&i(o)&&n&&("JSXElement"===n.type||"JSXFragment"===n.type)){const t=a[e-1],n=a[e-2];if(!t||"JSXText"!==t.type||n&&"JSXExpressionContainer"===n.type)continue}if(p&&("JSXElement"===p.type||"JSXFragment"===p.type)&&o&&"JSXText"===o.type&&i(o))continue;const r=!o||"JSXText"===o.type&&!i(o);if(p&&"JSXText"===p.type&&r){const e=t[t.length-1];if(e&&"JSXText"===e.type){e.value=String(e.value)+s.expression.value;continue}}if(p&&("JSXElement"===p.type||"JSXFragment"===p.type)&&o&&"JSXText"===o.type&&i(o))continue}}t.push(s)}else{if(i(s)){const n=t[t.length-1],i=a[e-1];if(n){if(i&&"JSXExpressionContainer"===i.type)continue;if("JSXText"===n.type&&i&&"JSXText"===i.type){n.value=String(n.value)+s.value;continue}}}t.push(s)}}}const p=[];function s(e){if(!e||0===e.length)return"";let t="";for(const r of e)if(r)if("JSXText"!==r.type){if("JSXExpressionContainer"===r.type){const e=r.expression;if(!e)continue;if("StringLiteral"===e.type)t+=e.value;else if("Identifier"===e.type)t+=`{{${e.value}}}`;else if("ObjectExpression"===e.type){const n=e.properties[0];n&&"KeyValueProperty"===n.type&&n.key&&"Identifier"===n.key.type?t+=`{{${n.key.value}}}`:n&&"Identifier"===n.type?t+=`{{${n.value}}}`:t+="{{value}}"}else"MemberExpression"===e.type&&e.property&&"Identifier"===e.property.type?t+=`{{${e.property.value}}}`:"CallExpression"===e.type&&"Identifier"===e.callee?.type?t+=`{{${e.callee.value}}}`:t+="{{value}}";continue}if("JSXElement"===r.type){let e;if(r.opening&&r.opening.name&&"Identifier"===r.opening.name.type&&(e=r.opening.name.value),e&&n.has(e)){t+=`<${e}>${s(r.children||[])}</${e}>`}else{const e=p.indexOf(r);t+=`<${e}>${s(r.children||[])}</${e}>`}continue}"JSXFragment"!==r.type||(t+=s(r.children||[]))}else i(r)||(t+=r.value);return t}r(e,p,!1);const o=s(e);return String(o).replace(/\s+/g," ").trim()}(n.children,i);let X,g,m;if("JSXAttribute"===p?.type&&"StringLiteral"===p.value?.type)X=p.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,m=g.value,!m||""===m.trim())return null;if(d&&"StringLiteral"===g.type){const e=i.extract.nsSeparator??":",t=g.value;if(e&&t.startsWith(`${d}${e}`)){if(m=t.slice(`${d}${e}`.length),!m||""===m.trim())return null;g={...g,value:m}}}}else"JSXExpressionContainer"===r.value?.type&&"JSXEmptyExpression"!==r.value.expression.type&&(g=r.value.expression);if(!g)return null}return p||!m||J.trim()?!p&&J.trim()&&(X=J):X=m,{keyExpression:g,serializedChildren:J,ns:d,defaultValue:X,hasCount:l,isOrdinal:c,contextExpression:S,optionsNode:y}}export{n as extractFromTransComponent};
@@ -1 +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};
1
+ import{getObjectPropValue as e}from"./ast-utils.js";class t{scopeStack=[];config;scope=new Map;constructor(e){this.config=e}reset(){this.scopeStack=[],this.scope=new Map}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 s=t.arguments?.[r.nsArg]?.expression,n=t.arguments?.[r.keyPrefixArg]?.expression;let a,o;if("StringLiteral"===s?.type?a=s.value:"ArrayExpression"===s?.type&&"StringLiteral"===s.elements[0]?.expression.type&&(a=s.elements[0].expression.value),"ObjectExpression"===n?.type){const e=n.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 s;if("Identifier"===t.id.type&&(s=t.id.value),"ArrayPattern"===t.id.type){const e=t.id.elements[0];"Identifier"===e?.type&&(s=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){s="t";break}if("KeyValuePatternProperty"===e.type&&"Identifier"===e.key.type&&"t"===e.key.value&&"Identifier"===e.value.type){s=e.value.value;break}}if(!s)return;const n=r.arguments?.[i.nsArg]?.expression;let a;"StringLiteral"===n?.type?a=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(a=n.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(s,{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,s=i[1]?.expression,n=i[2]?.expression,a="StringLiteral"===s?.type?s.value:void 0,o="StringLiteral"===n?.type?n.value:void 0;(a||o)&&this.setVarInScope(r,{defaultNs:a,keyPrefix:o})}}export{t as ScopeManager};
@@ -1 +1 @@
1
- import{execa as e}from"execa";import o from"chalk";import n from"ora";import t from"inquirer";import{resolve as i}from"node:path";function s(e,o,n){const{locize:t={},extract:s}=o,{projectId:r,apiKey:c,version:a}=t,l=[e];if(r&&l.push("--project-id",r),c&&l.push("--api-key",c),a&&l.push("--ver",a),"sync"===e){(n.updateValues??t.updateValues)&&l.push("--update-values","true");(n.srcLngOnly??t.sourceLanguageOnly)&&l.push("--reference-language-only","true");(n.compareMtime??t.compareModificationTime)&&l.push("--compare-modification-time","true");(n.dryRun??t.dryRun)&&l.push("--dry","true")}const u=i(process.cwd(),s.output.split("/{{language}}/")[0]);return l.push("--path",u),l}async function r(i,r,c={}){await async function(){try{await e("locize",["--version"])}catch(e){"ENOENT"===e.code&&(console.error(o.red("Error: `locize-cli` command not found.")),console.log(o.yellow("Please install it globally to use the locize integration:")),console.log(o.cyan("npm install -g locize-cli")),process.exit(1))}}();const a=n(`Running 'locize ${i}'...\n`).start();let l=r;try{const n=s(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e("locize",n,{stdio:"pipe"});a.succeed(o.green(`'locize ${i}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(n){const r=n.stderr||"";if(r.includes("missing required argument")){const n=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await t.prompt([{type:"input",name:"projectId",message:"What is your locize Project ID? (Find this in your project settings on www.locize.app)",validate:e=>!!e||"Project ID cannot be empty."},{type:"password",name:"apiKey",message:'What is your locize API key? (Create or use one in your project settings > "API Keys")',validate:e=>!!e||"API Key cannot be empty."},{type:"input",name:"version",message:"What version do you want to sync with?",default:"latest"}]);if(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:n}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(n){const n=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.version}',\n },`;console.log(o.cyan("\nGreat! For the best security, we recommend using environment variables for your API key.")),console.log(o.bold("\nRecommended approach (.env file):")),console.log(o.green(n)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(n){l={...l,locize:n},a.start("Retrying with new credentials...");try{const n=s(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e("locize",n,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${i}'.`)),console.error(r||n.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${i}' completed successfully.`))}const c=(e,o)=>r("sync",e,o),a=(e,o)=>r("download",e,o),l=(e,o)=>r("migrate",e,o);export{a as runLocizeDownload,l as runLocizeMigrate,c as runLocizeSync};
1
+ import{execa as e}from"execa";import o from"chalk";import n from"ora";import t from"inquirer";import{sep as i,resolve as s}from"node:path";function r(e,o,n){const{locize:t={},extract:r}=o,{projectId:c,apiKey:a,version:l}=t,u=[e];if(c&&u.push("--project-id",c),a&&u.push("--api-key",a),l&&u.push("--ver",l),"sync"===e){(n.updateValues??t.updateValues)&&u.push("--update-values","true");(n.srcLngOnly??t.sourceLanguageOnly)&&u.push("--reference-language-only","true");(n.compareMtime??t.compareModificationTime)&&u.push("--compare-modification-time","true");(n.dryRun??t.dryRun)&&u.push("--dry","true")}const p=String(r.output||"").replace(/\\/g,"/"),d=(p.includes("/{{language}}/")?p.split("/{{language}}/")[0]:p.replace("{{language}}","")).split("/").join(i),g=s(process.cwd(),d);return u.push("--path",g),u}async function c(i,s,c={}){await async function(){try{await e("locize",["--version"])}catch(e){"ENOENT"===e.code&&(console.error(o.red("Error: `locize-cli` command not found.")),console.log(o.yellow("Please install it globally to use the locize integration:")),console.log(o.cyan("npm install -g locize-cli")),process.exit(1))}}();const a=n(`Running 'locize ${i}'...\n`).start();let l=s;try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e("locize",n,{stdio:"pipe"});a.succeed(o.green(`'locize ${i}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(n){const s=n.stderr||"";if(s.includes("missing required argument")){const n=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await t.prompt([{type:"input",name:"projectId",message:"What is your locize Project ID? (Find this in your project settings on www.locize.app)",validate:e=>!!e||"Project ID cannot be empty."},{type:"password",name:"apiKey",message:'What is your locize API key? (Create or use one in your project settings > "API Keys")',validate:e=>!!e||"API Key cannot be empty."},{type:"input",name:"version",message:"What version do you want to sync with?",default:"latest"}]);if(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:n}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(n){const n=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.version}',\n },`;console.log(o.cyan("\nGreat! For the best security, we recommend using environment variables for your API key.")),console.log(o.bold("\nRecommended approach (.env file):")),console.log(o.green(n)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(n){l={...l,locize:n},a.start("Retrying with new credentials...");try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e("locize",n,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${i}'.`)),console.error(s||n.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${i}' completed successfully.`))}const a=(e,o)=>c("sync",e,o),l=(e,o)=>c("download",e,o),u=(e,o)=>c("migrate",e,o);export{l as runLocizeDownload,u as runLocizeMigrate,a as runLocizeSync};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
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.11.4')
24
+ .version('1.11.6')
25
25
 
26
26
  program
27
27
  .command('extract')
@@ -78,6 +78,8 @@ export class ASTVisitors {
78
78
  * @param node - The root module node to traverse
79
79
  */
80
80
  public visit (node: Module): void {
81
+ // Reset any per-file scope state to avoid leaking scopes between files.
82
+ this.scopeManager.reset()
81
83
  this.scopeManager.enterScope() // Create the root scope for the file
82
84
  this.walk(node)
83
85
  this.scopeManager.exitScope() // Clean up the root scope
@@ -133,6 +135,29 @@ export class ASTVisitors {
133
135
 
134
136
  const child = node[key]
135
137
  if (Array.isArray(child)) {
138
+ // Pre-scan array children to register VariableDeclarator-based scopes
139
+ // (e.g., `const { t } = useTranslation(...)`) before walking the rest
140
+ // of the items. This ensures that functions/arrow-functions defined
141
+ // earlier in the same block that reference t will resolve to the
142
+ // correct scope even if the `useTranslation` declarator appears later.
143
+ for (const item of child) {
144
+ if (!item || typeof item !== 'object') continue
145
+
146
+ // Direct declarator present in arrays (rare)
147
+ if (item.type === 'VariableDeclarator') {
148
+ this.scopeManager.handleVariableDeclarator(item)
149
+ continue
150
+ }
151
+
152
+ // Common case: VariableDeclaration which contains .declarations (VariableDeclarator[])
153
+ if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
154
+ for (const decl of item.declarations) {
155
+ if (decl && typeof decl === 'object' && decl.type === 'VariableDeclarator') {
156
+ this.scopeManager.handleVariableDeclarator(decl)
157
+ }
158
+ }
159
+ }
160
+ }
136
161
  for (const item of child) {
137
162
  // Be less strict: if it's a non-null object, walk it.
138
163
  // This allows traversal into nodes that might not have a `.type` property
@@ -182,7 +182,6 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
182
182
 
183
183
  // Validate that the key is not empty
184
184
  if (!processedKeyValue || processedKeyValue.trim() === '') {
185
- console.warn('Ignoring Trans component with empty i18nKey')
186
185
  return null
187
186
  }
188
187
 
@@ -197,7 +196,6 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
197
196
 
198
197
  // Validate processed key is not empty
199
198
  if (!processedKeyValue || processedKeyValue.trim() === '') {
200
- console.warn('Ignoring Trans component with i18nKey that becomes empty after namespace removal')
201
199
  return null
202
200
  }
203
201
 
@@ -239,140 +237,273 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
239
237
  }
240
238
 
241
239
  /**
242
- * Serializes JSX children into a string representation suitable for i18next.
243
- *
244
- * This function converts JSX children into the format expected by i18next:
245
- * - Text nodes are preserved as-is
246
- * - HTML elements are converted to indexed placeholders or preserved if allowed
247
- * - JSX expressions become interpolation placeholders: `{{variable}}`
248
- * - Fragments are flattened
249
- * - Whitespace is normalized
250
- *
251
- * The serialization respects the `transKeepBasicHtmlNodesFor` configuration
252
- * to determine which HTML tags should be preserved vs. converted to indexed placeholders.
253
- *
254
- * @param children - Array of JSX child nodes to serialize
255
- * @param config - Configuration containing HTML preservation settings
256
- * @returns Serialized string representation
257
- *
258
- * @example
259
- * ```typescript
260
- * // JSX: Hello <strong>{{name}}</strong>, you have <Link to="/msgs">{{count}} messages</Link>.
261
- * // With transKeepBasicHtmlNodesFor: ['strong']
262
- * // Returns: "Hello <strong>{{name}}</strong>, you have <1>{{count}} messages</1>."
263
- * // (strong preserved, Link becomes indexed placeholder <1>)
264
- *
265
- * const serialized = serializeJSXChildren(children, config)
266
- * ```
267
- *
268
- * @internal
240
+ * Serializes JSX children into a string, correctly indexing component placeholders.
241
+ * This version correctly calculates an element's index based on its position
242
+ * among its sibling *elements*, ignoring text nodes for indexing purposes.
269
243
  */
270
244
  function serializeJSXChildren (children: any[], config: I18nextToolkitConfig): string {
245
+ if (!children || children.length === 0) return ''
246
+
271
247
  const allowedTags = new Set(config.extract.transKeepBasicHtmlNodesFor ?? ['br', 'strong', 'i', 'p'])
272
248
 
273
- // Remove formatting-only JSXText nodes (those that are entirely whitespace and
274
- // contain newlines). These are introduced by pretty-printing/indentation and
275
- // should not affect component indexing. Keep single-space text and explicit
276
- // {' '} expression containers.
277
- function normalizeChildren (nodes: any[]): any[] {
278
- if (!nodes || !nodes.length) return []
279
-
280
- // 1) Remove purely formatting whitespace nodes (those that are whitespace-only and include newlines)
281
- const filtered = nodes.filter(n => {
282
- if (!n) return false
283
- if (n.type === 'JSXText' && /^\s*$/.test(n.value) && n.value.includes('\n')) return false
284
- return true
285
- })
286
-
287
- // 2) Convert explicit {' '} expression containers into text nodes so they can be merged
288
- const converted = filtered.map(n => {
289
- if (n.type === 'JSXExpressionContainer' && n.expression?.type === 'StringLiteral') {
290
- return { type: 'JSXText', value: n.expression.value }
291
- }
292
- return n
293
- })
249
+ const isFormattingWhitespace = (n: any) =>
250
+ n &&
251
+ n.type === 'JSXText' &&
252
+ /^\s*$/.test(n.value) &&
253
+ n.value.includes('\n')
254
+
255
+ // Build deterministic global slot list (pre-order)
256
+ function collectSlots (nodes: any[], slots: any[], parentIsNonPreserved = false) {
257
+ if (!nodes || !nodes.length) return
258
+
259
+ // First, identify boundary whitespace nodes (start and end of sibling list)
260
+ // We trim ONLY pure-whitespace JSXText nodes from the boundaries
261
+ let startIdx = 0
262
+ let endIdx = nodes.length - 1
263
+
264
+ // Skip leading boundary whitespace (pure whitespace JSXText only)
265
+ while (startIdx <= endIdx && isFormattingWhitespace(nodes[startIdx])) {
266
+ startIdx++
267
+ }
268
+
269
+ // Skip trailing boundary whitespace (pure whitespace JSXText only)
270
+ while (endIdx >= startIdx && isFormattingWhitespace(nodes[endIdx])) {
271
+ endIdx--
272
+ }
273
+
274
+ // Now process all nodes in the range [startIdx, endIdx] - this includes interior whitespace
275
+ const meaningfulNodes = startIdx <= endIdx ? nodes.slice(startIdx, endIdx + 1) : []
276
+
277
+ for (let i = 0; i < meaningfulNodes.length; i++) {
278
+ const n = meaningfulNodes[i]
279
+ if (!n) continue
294
280
 
295
- // 3) Collapse consecutive text-like nodes into a single JSXText node and normalize whitespace
296
- const collapsed: any[] = []
297
- for (const n of converted) {
298
281
  if (n.type === 'JSXText') {
299
- const last = collapsed[collapsed.length - 1]
300
- if (last && last.type === 'JSXText') {
301
- last.value = last.value + n.value
282
+ // Skip/merge pure-whitespace JSXText according to context:
283
+ // - If it immediately follows an expression container (and that expression
284
+ // wasn't originally a JSXText sibling), skip it (it's formatting).
285
+ // - If it follows an original JSXText sibling, merge it into that text.
286
+ if (isFormattingWhitespace(n)) {
287
+ const prevSlot = slots[slots.length - 1]
288
+ const prevOriginal = meaningfulNodes[i - 1]
289
+
290
+ if (prevSlot) {
291
+ // If the previous original sibling is an expression container, treat
292
+ // this formatting whitespace as formatting after an expression and skip it.
293
+ if (prevOriginal && prevOriginal.type === 'JSXExpressionContainer') {
294
+ continue
295
+ }
296
+ // Only merge into previous text when the previous original sibling was also JSXText.
297
+ if (prevSlot.type === 'JSXText' && prevOriginal && prevOriginal.type === 'JSXText') {
298
+ prevSlot.value = String(prevSlot.value) + n.value
299
+ continue
300
+ }
301
+ }
302
+ }
303
+ // Add all other JSXText nodes
304
+ slots.push(n)
305
+ continue
306
+ }
307
+
308
+ if (n.type === 'JSXExpressionContainer') {
309
+ // If this expression is an object-expression placeholder like
310
+ // {{ key: value }} and we're inside a non-preserved parent element,
311
+ // treat it as part of the parent (do NOT add as a sibling/global slot).
312
+ if (parentIsNonPreserved && n.expression && n.expression.type === 'ObjectExpression') {
313
+ const prop = n.expression.properties && n.expression.properties[0]
314
+ if (prop && prop.type === 'KeyValueProperty') {
315
+ continue
316
+ }
317
+ }
318
+
319
+ // Handle pure-string expression containers (e.g. {" "}):
320
+ // - If it's pure space (no newline) and it directly follows a non-text sibling
321
+ // (element/fragment), treat it as formatting and skip it.
322
+ // - Otherwise, count it as a slot (do NOT merge it into previous JSXText).
323
+ if (n.expression && n.expression.type === 'StringLiteral') {
324
+ const textVal = String(n.expression.value || '')
325
+ const isPureSpaceNoNewline = /^\s*$/.test(textVal) && !textVal.includes('\n')
326
+ const prevOriginal = meaningfulNodes[i - 1]
327
+ const nextOriginal = meaningfulNodes[i + 1]
328
+
329
+ if (isPureSpaceNoNewline) {
330
+ // If the explicit {" "} is followed by a newline-only JSXText which
331
+ // itself is followed by an element/fragment, treat the {" "}
332
+ // as layout-only and skip it. This covers cases where the space
333
+ // precedes a newline-only separator before an element (fixes index
334
+ // shifting in object-expression span tests).
335
+ const nextNextOriginal = meaningfulNodes[i + 2]
336
+ if (
337
+ nextOriginal &&
338
+ nextOriginal.type === 'JSXText' &&
339
+ isFormattingWhitespace(nextOriginal) &&
340
+ nextNextOriginal &&
341
+ (nextNextOriginal.type === 'JSXElement' || nextNextOriginal.type === 'JSXFragment')
342
+ ) {
343
+ // Only skip this pure-space when it appears in one of these cases:
344
+ // - there is no previous original sibling (leading)
345
+ // - the previous original sibling is not a JSXText (e.g. element/expr)
346
+ // - OR the previous original is JSXText but it itself follows an expression
347
+ // (pattern: <expr>, JSXText, {" "}, newline-only JSXText, <element>)
348
+ const prevOriginal = meaningfulNodes[i - 1]
349
+ const prevPrevOriginal = meaningfulNodes[i - 2]
350
+ const shouldSkip =
351
+ !prevOriginal ||
352
+ prevOriginal.type !== 'JSXText' ||
353
+ (prevPrevOriginal && prevPrevOriginal.type === 'JSXExpressionContainer')
354
+
355
+ if (shouldSkip) {
356
+ continue
357
+ }
358
+ }
359
+
360
+ // Only treat {" "} as pure formatting to skip when it sits between
361
+ // an element/fragment and a newline-only JSXText. In that specific
362
+ // boundary case the explicit space is merely layout and must be ignored.
363
+ if (
364
+ prevOriginal &&
365
+ (prevOriginal.type === 'JSXElement' || prevOriginal.type === 'JSXFragment') &&
366
+ nextOriginal &&
367
+ nextOriginal.type === 'JSXText' &&
368
+ isFormattingWhitespace(nextOriginal)
369
+ ) {
370
+ continue
371
+ }
372
+
373
+ // 1) Merge into previous text when the previous original sibling is JSXText
374
+ // and the next original sibling is either missing or a non-formatting JSXText.
375
+ // This preserves "foo{' '}bar" as a single text node but avoids merging
376
+ // when the {" "} is followed by newline-only formatting before an element.
377
+ const nextIsTextNonFormatting = !nextOriginal || (nextOriginal.type === 'JSXText' && !isFormattingWhitespace(nextOriginal))
378
+ if (prevOriginal && prevOriginal.type === 'JSXText' && nextIsTextNonFormatting) {
379
+ const prevSlot = slots[slots.length - 1]
380
+ if (prevSlot && prevSlot.type === 'JSXText') {
381
+ prevSlot.value = String(prevSlot.value) + n.expression.value
382
+ continue
383
+ }
384
+ }
385
+
386
+ // 2) Skip when this explicit space sits between an element/fragment
387
+ // and a newline-only formatting JSXText (boundary formatting).
388
+ if (
389
+ prevOriginal &&
390
+ (prevOriginal.type === 'JSXElement' || prevOriginal.type === 'JSXFragment') &&
391
+ nextOriginal &&
392
+ nextOriginal.type === 'JSXText' &&
393
+ isFormattingWhitespace(nextOriginal)
394
+ ) {
395
+ continue
396
+ }
397
+ // 3) Otherwise fallthrough and count this expression as a slot.
398
+ }
399
+ }
400
+
401
+ // All JSXExpressionContainers count as slots for indexing.
402
+ slots.push(n)
403
+ continue
404
+ }
405
+
406
+ if (n.type === 'JSXElement') {
407
+ const tagName = n.opening && n.opening.name && n.opening.name.type === 'Identifier'
408
+ ? n.opening.name.value
409
+ : undefined
410
+ if (tagName && allowedTags.has(tagName)) {
411
+ // preserved tag: descend into children (tag itself is not a slot)
412
+ collectSlots(n.children || [], slots, false)
302
413
  } else {
303
- // clone to avoid mutating original AST nodes
304
- collapsed.push({ type: 'JSXText', value: String(n.value) })
414
+ // non-preserved element: the element itself is a single slot.
415
+ // Pre-order: allocate the parent's slot first, then descend into its
416
+ // children. While descending, mark parentIsNonPreserved so
417
+ // KeyValueProperty-style object-expression placeholders are not added
418
+ // as separate sibling slots.
419
+ slots.push(n)
420
+ collectSlots(n.children || [], slots, true)
305
421
  }
306
- } else {
307
- collapsed.push(n)
422
+ continue
308
423
  }
309
- }
310
424
 
311
- // 4) Normalize whitespace inside text nodes: collapse runs of whitespace/newlines into a single space
312
- for (const item of collapsed) {
313
- if (item.type === 'JSXText') {
314
- // keep intentional single spaces, collapse multi whitespace/newlines
315
- item.value = String(item.value).replace(/\s+/g, ' ')
425
+ if (n.type === 'JSXFragment') {
426
+ collectSlots(n.children || [], slots, parentIsNonPreserved)
427
+ continue
316
428
  }
317
- }
318
429
 
319
- return collapsed
430
+ // ignore unknown node types
431
+ }
320
432
  }
321
433
 
322
- children = normalizeChildren(children)
323
-
324
- /**
325
- * Recursively processes JSX children and converts them to string format.
326
- *
327
- * @param children - Array of child nodes to process
328
- * @returns Serialized string content
329
- */
330
- function serializeChildren (children: any[]): string {
331
- // Normalize at each recursion level so formatting whitespace inside
332
- // elements doesn't produce extra text nodes or surrounding spaces.
333
- children = normalizeChildren(children)
434
+ // prepare the global slot list for the whole subtree
435
+ const globalSlots: any[] = []
436
+ collectSlots(children, globalSlots, false)
437
+
438
+ function visitNodes (nodes: any[]): string {
439
+ if (!nodes || nodes.length === 0) return ''
334
440
  let out = ''
335
- // Use forEach to get the direct index of each child in the (normalized) array
336
- children.forEach((child, index) => {
337
- if (child.type === 'JSXText') {
338
- out += child.value
339
- } else if (child.type === 'JSXExpressionContainer') {
340
- const expr = child.expression
441
+
442
+ for (const node of nodes) {
443
+ if (!node) continue
444
+
445
+ if (node.type === 'JSXText') {
446
+ if (!isFormattingWhitespace(node)) out += node.value
447
+ continue
448
+ }
449
+
450
+ if (node.type === 'JSXExpressionContainer') {
451
+ const expr = node.expression
452
+ if (!expr) continue
453
+
341
454
  if (expr.type === 'StringLiteral') {
342
455
  out += expr.value
343
456
  } else if (expr.type === 'Identifier') {
344
457
  out += `{{${expr.value}}}`
345
458
  } else if (expr.type === 'ObjectExpression') {
346
- // Handle object expressions like {{bar: 1}} -> {{bar}}
347
459
  const prop = expr.properties[0]
348
460
  if (prop && prop.type === 'KeyValueProperty' && prop.key && prop.key.type === 'Identifier') {
349
461
  out += `{{${prop.key.value}}}`
350
462
  } else if (prop && prop.type === 'Identifier') {
351
- // Handle the case where properties[0] is directly an Identifier
352
463
  out += `{{${prop.value}}}`
464
+ } else {
465
+ out += '{{value}}'
353
466
  }
467
+ } else if (expr.type === 'MemberExpression' && expr.property && expr.property.type === 'Identifier') {
468
+ out += `{{${expr.property.value}}}`
469
+ } else if (expr.type === 'CallExpression' && expr.callee?.type === 'Identifier') {
470
+ out += `{{${expr.callee.value}}}`
471
+ } else {
472
+ out += '{{value}}'
354
473
  }
355
- } else if (child.type === 'JSXElement') {
356
- let tag
357
- if (child.opening.name.type === 'Identifier') {
358
- tag = child.opening.name.value
359
- }
474
+ continue
475
+ }
360
476
 
361
- const innerContent = serializeChildren(child.children)
477
+ if (node.type === 'JSXElement') {
478
+ let tag: string | undefined
479
+ if (node.opening && node.opening.name && node.opening.name.type === 'Identifier') {
480
+ tag = node.opening.name.value
481
+ }
362
482
 
363
483
  if (tag && allowedTags.has(tag)) {
364
- // If the tag is in the allowed list, preserve it
365
- out += `<${tag}>${innerContent}</${tag}>`
484
+ const inner = visitNodes(node.children || [])
485
+ out += `<${tag}>${inner}</${tag}>`
366
486
  } else {
367
- // Otherwise, replace it with ITS INDEX IN THE CHILDREN ARRAY
368
- out += `<${index}>${innerContent}</${index}>`
487
+ // Use the pre-order globalSlots index so placeholder numbers reflect
488
+ // the global ordering (including nested slots collected earlier).
489
+ const idx = globalSlots.indexOf(node)
490
+ const inner = visitNodes(node.children || [])
491
+ out += `<${idx}>${inner}</${idx}>`
369
492
  }
370
- } else if (child.type === 'JSXFragment') {
371
- out += serializeChildren(child.children)
493
+ continue
494
+ }
495
+
496
+ if (node.type === 'JSXFragment') {
497
+ out += visitNodes(node.children || [])
498
+ continue
372
499
  }
373
- })
500
+
501
+ // unknown node types: ignore
502
+ }
503
+
374
504
  return out
375
505
  }
376
506
 
377
- return serializeChildren(children).trim().replace(/\s{2,}/g, ' ')
507
+ const result = visitNodes(children)
508
+ return String(result).replace(/\s+/g, ' ').trim()
378
509
  }
@@ -11,6 +11,18 @@ export class ScopeManager {
11
11
  this.config = config
12
12
  }
13
13
 
14
+ /**
15
+ * Reset per-file scope state.
16
+ *
17
+ * This clears both the scope stack and the legacy scope map. It should be
18
+ * called at the start of processing each file so that scope info does not
19
+ * leak between files.
20
+ */
21
+ public reset (): void {
22
+ this.scopeStack = []
23
+ this.scope = new Map()
24
+ }
25
+
14
26
  /**
15
27
  * Enters a new variable scope by pushing a new scope map onto the stack.
16
28
  * Used when entering functions to isolate variable declarations.
package/src/locize.ts CHANGED
@@ -2,7 +2,7 @@ import { execa } from 'execa'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
4
  import inquirer from 'inquirer'
5
- import { resolve } from 'node:path'
5
+ import { resolve, sep } from 'node:path'
6
6
  import type { I18nextToolkitConfig } from './types'
7
7
 
8
8
  /**
@@ -143,7 +143,15 @@ function buildArgs (command: string, config: I18nextToolkitConfig, cliOptions: a
143
143
  if (dryRun) commandArgs.push('--dry', 'true')
144
144
  }
145
145
 
146
- const basePath = resolve(process.cwd(), extract.output.split('/{{language}}/')[0])
146
+ // Normalize path separators in the configured output so splitting works on both POSIX and Windows
147
+ const outputNormalized = String(extract.output || '').replace(/\\/g, '/')
148
+ const baseCandidate = outputNormalized.includes('/{{language}}/')
149
+ ? outputNormalized.split('/{{language}}/')[0]
150
+ : outputNormalized.replace('{{language}}', '')
151
+ // Convert to OS-specific separators before resolving so the resulting path uses path.sep
152
+ const baseCandidateWithSep = baseCandidate.split('/').join(sep)
153
+ const basePath = resolve(process.cwd(), baseCandidateWithSep)
154
+
147
155
  commandArgs.push('--path', basePath)
148
156
 
149
157
  return commandArgs
@@ -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;IAMjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IA2DZ;;;;;;;;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;IAkFZ;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;CAG7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-parser.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAY,MAAM,WAAW,CAAA;AACnF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGvD,MAAM,WAAW,sBAAsB;IACrC,gDAAgD;IAChD,aAAa,CAAC,EAAE,UAAU,CAAC;IAE3B,qDAAqD;IACrD,kBAAkB,EAAE,MAAM,CAAC;IAE3B,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,8DAA8D;IAC9D,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAE/B,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,UAAU,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,yBAAyB,CAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,oBAAoB,GAAG,sBAAsB,GAAG,IAAI,CAgLxH"}
1
+ {"version":3,"file":"jsx-parser.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAY,MAAM,WAAW,CAAA;AACnF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGvD,MAAM,WAAW,sBAAsB;IACrC,gDAAgD;IAChD,aAAa,CAAC,EAAE,UAAU,CAAC;IAE3B,qDAAqD;IACrD,kBAAkB,EAAE,MAAM,CAAC;IAE3B,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,8DAA8D;IAC9D,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAE/B,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,UAAU,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,yBAAyB,CAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,oBAAoB,GAAG,sBAAsB,GAAG,IAAI,CA8KxH"}
@@ -5,6 +5,14 @@ export declare class ScopeManager {
5
5
  private config;
6
6
  private scope;
7
7
  constructor(config: Omit<I18nextToolkitConfig, 'plugins'>);
8
+ /**
9
+ * Reset per-file scope state.
10
+ *
11
+ * This clears both the scope stack and the legacy scope map. It should be
12
+ * called at the start of processing each file so that scope info does not
13
+ * leak between files.
14
+ */
15
+ reset(): void;
8
16
  /**
9
17
  * Enters a new variable scope by pushing a new scope map onto the stack.
10
18
  * Used when entering functions to isolate variable declarations.
@@ -1 +1 @@
1
- {"version":3,"file":"scope-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/scope-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,WAAW,CAAA;AACnE,OAAO,KAAK,EAAE,SAAS,EAA4B,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAG5F,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,KAAK,CAAqE;gBAErE,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC;IAI1D;;;OAGG;IACH,UAAU,IAAK,IAAI;IAInB;;;OAGG;IACH,SAAS,IAAK,IAAI;IAIlB;;;;;;OAMG;IACH,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAMnD;;;;;;OAMG;IACH,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAkBrD;;;;;;;;;;OAUG;IACH,wBAAwB,CAAE,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAsCzD;;;;;;;;OAQG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,+BAA+B;IAmEvC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,8BAA8B;IAwDtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,yBAAyB;CAmBlC"}
1
+ {"version":3,"file":"scope-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/scope-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,WAAW,CAAA;AACnE,OAAO,KAAK,EAAE,SAAS,EAA4B,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAG5F,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,KAAK,CAAqE;gBAErE,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC;IAI1D;;;;;;OAMG;IACI,KAAK,IAAK,IAAI;IAKrB;;;OAGG;IACH,UAAU,IAAK,IAAI;IAInB;;;OAGG;IACH,SAAS,IAAK,IAAI;IAIlB;;;;;;OAMG;IACH,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAMnD;;;;;;OAMG;IACH,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAkBrD;;;;;;;;;;OAUG;IACH,wBAAwB,CAAE,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAsCzD;;;;;;;;OAQG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,+BAA+B;IAmEvC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,8BAA8B;IAwDtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,yBAAyB;CAmBlC"}
@@ -1 +1 @@
1
- {"version":3,"file":"locize.d.ts","sourceRoot":"","sources":["../src/locize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAgOnD,eAAO,MAAM,aAAa,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAiD,CAAA;AAC7H,eAAO,MAAM,iBAAiB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAqD,CAAA;AACrI,eAAO,MAAM,gBAAgB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAoD,CAAA"}
1
+ {"version":3,"file":"locize.d.ts","sourceRoot":"","sources":["../src/locize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAwOnD,eAAO,MAAM,aAAa,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAiD,CAAA;AAC7H,eAAO,MAAM,iBAAiB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAqD,CAAA;AACrI,eAAO,MAAM,gBAAgB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAoD,CAAA"}