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 +9 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/ast-visitors.js +1 -1
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -1
- package/dist/cjs/extractor/parsers/scope-manager.js +1 -1
- package/dist/cjs/locize.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/ast-visitors.js +1 -1
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -1
- package/dist/esm/extractor/parsers/scope-manager.js +1 -1
- package/dist/esm/locize.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/extractor/core/ast-visitors.ts +25 -0
- package/src/extractor/parsers/jsx-parser.ts +233 -102
- package/src/extractor/parsers/scope-manager.ts +12 -0
- package/src/locize.ts +10 -2
- package/types/extractor/core/ast-visitors.d.ts.map +1 -1
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -1
- package/types/extractor/parsers/scope-manager.d.ts +8 -0
- package/types/extractor/parsers/scope-manager.d.ts.map +1 -1
- package/types/locize.d.ts.map +1 -1
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.
|
|
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(
|
|
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),
|
|
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
|
|
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})}};
|
package/dist/cjs/locize.js
CHANGED
|
@@ -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=
|
|
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.
|
|
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
|
|
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),
|
|
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
|
|
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};
|
package/dist/esm/locize.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execa as e}from"execa";import o from"chalk";import n from"ora";import t from"inquirer";import{resolve as
|
|
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
package/src/cli.ts
CHANGED
|
@@ -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
|
|
243
|
-
*
|
|
244
|
-
*
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
//
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
collapsed.push(n)
|
|
422
|
+
continue
|
|
308
423
|
}
|
|
309
|
-
}
|
|
310
424
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
430
|
+
// ignore unknown node types
|
|
431
|
+
}
|
|
320
432
|
}
|
|
321
433
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
if (child.opening.name.type === 'Identifier') {
|
|
358
|
-
tag = child.opening.name.value
|
|
359
|
-
}
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
360
476
|
|
|
361
|
-
|
|
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
|
-
|
|
365
|
-
out += `<${tag}>${
|
|
484
|
+
const inner = visitNodes(node.children || [])
|
|
485
|
+
out += `<${tag}>${inner}</${tag}>`
|
|
366
486
|
} else {
|
|
367
|
-
//
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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,
|
|
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"}
|
package/types/locize.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|