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