i18next-cli 1.5.7 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ 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.5.9](https://github.com/i18next/i18next-cli/compare/v1.5.8...v1.5.9) - 2025-10-05
9
+
10
+ ### Added
11
+ - **Extractor (`<Trans>`):** Added support for plural-specific default values from `tOptions` prop (e.g., `defaultValue_other: "Items"`). The extractor now correctly uses these values when generating plural keys for Trans components. [#36](https://github.com/i18next/i18next-cli/pull/36)
12
+ - **Extractor:** Added support for dynamic expressions in `t()` function arguments. The extractor can now resolve ternary operators and other static expressions to extract all possible key variants (e.g., `t(isOpen ? 'open' : 'closed')`). [#37](https://github.com/i18next/i18next-cli/pull/37)
13
+
14
+ ### Enhanced
15
+ - **Extractor (`<Trans>`):** Improved namespace resolution consistency by prioritizing namespace from `i18nKey` prop over other sources. When a Trans component uses `i18nKey="ns:key"`, the namespace from the key now takes precedence over the `t` prop namespace, matching i18next's behavior. [#38](https://github.com/i18next/i18next-cli/pull/38)
16
+
17
+ ## [1.5.8](https://github.com/i18next/i18next-cli/compare/v1.5.7...v1.5.8) - 2025-10-05
18
+
19
+ - **Extractor:** Fixed namespace resolution/override order where explicitly passed `ns` options in `t()` calls were being incorrectly overridden by hook-level namespaces. The extractor now properly prioritizes explicit namespace options over inferred ones. [#32](https://github.com/i18next/i18next-cli/issues/32)
20
+ - **Extractor (`<Trans>`):** Fixed a bug where `context` and `count` props on Trans components were treated as mutually exclusive. The extractor now correctly generates all combinations of context and plural forms (e.g., `key_context_one`, `key_context_other`) to match i18next's behavior. [#33](https://github.com/i18next/i18next-cli/issues/33)
21
+ - **Extractor:** Fixed a bug where passing an empty string as a context value (e.g., `context: test ? 'male' : ''`) resulted in keys with trailing underscores. Empty strings are now treated as "no context" like i18next does, ensuring clean key generation. [#34](https://github.com/i18next/i18next-cli/issues/34)
22
+
8
23
  ## [1.5.7](https://github.com/i18next/i18next-cli/compare/v1.5.6...v1.5.7) - 2025-10-04
9
24
 
10
25
  ### Added
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.5.7"),f.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a,{isWatchMode:e.watch,isDryRun:e.dryRun});e.ci&&t&&(console.error(o.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(o.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${o.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),f.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(e,t)=>{let n=await i.loadConfig();if(!n){console.log(o.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),n=e}await g.runStatus(n,{detail:e,namespace:t.namespace})}),f.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const o=await i.ensureConfig(),a=()=>c.runTypesGenerator(o);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(o.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),f.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),f.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async e=>{await l.runMigrator(e)}),f.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),f.command("lint").description("Find potential issues like hardcoded strings in your codebase.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async e=>{const r=async()=>{let e=await i.loadConfig();if(!e){console.log(o.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),e=t}await d.runLinter(e)};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await i.loadConfig();if(e?.extract?.input){t.watch(await n.glob(e.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
2
+ "use strict";var e=require("commander"),t=require("chokidar"),n=require("glob"),o=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.5.9"),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);
@@ -1 +1 @@
1
- "use strict";function e(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}exports.getObjectPropValue=function(t,r){const y=e(t,r);if("KeyValueProperty"===y?.type){const e=y.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}},exports.getObjectProperty=e;
1
+ "use strict";function e(e,t){return e.properties.filter(e=>"KeyValueProperty"===e.type).find(e=>"Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t)}exports.getObjectPropValue=function(t,r){const i=e(t,r);if("KeyValueProperty"===i?.type){const e=i.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}},exports.getObjectProperty=e;
@@ -1 +1 @@
1
- "use strict";var e=require("./jsx-parser.js"),t=require("./ast-utils.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&this.walk(e);else n&&"object"==typeof n&&this.walk(n)}t&&this.exitScope()}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)}handleVariableDeclarator(e){const t=e.init;if(!t)return;const n="AwaitExpression"===t.type&&"CallExpression"===t.argument.type?t.argument:"CallExpression"===t.type?t:null;if(!n)return;const r=n.callee;if("Identifier"===r.type){const t=this.getUseTranslationConfig(r.value);if(t)return void this.handleUseTranslationDeclarator(e,n,t)}"MemberExpression"===r.type&&"Identifier"===r.property.type&&"getFixedT"===r.property.value&&this.handleGetFixedTDeclarator(e,n)}handleUseTranslationDeclarator(e,n,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=n.arguments?.[r.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=n.arguments?.[r.keyPrefixArg]?.expression;let l;if("ObjectExpression"===o?.type){const e=t.getObjectPropValue(o,"keyPrefix");l="string"==typeof e?e:void 0}this.setVarInScope(i,{defaultNs:a,keyPrefix:l})}handleGetFixedTDeclarator(e,t){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const n=e.id.value,r=t.arguments,i=r[1]?.expression,s=r[2]?.expression,a="StringLiteral"===i?.type?i.value:void 0,o="StringLiteral"===s?.type?s.value:void 0;(a||o)&&this.setVarInScope(n,{defaultNs:a,keyPrefix:o})}handleCallExpression(e){const n=this.getFunctionName(e.callee);if(!n)return;const r=this.getVarFromScope(n),i=this.config.extract.functions||["t","*.t"];let s=void 0!==r;if(!s)for(const e of i)if(e.startsWith("*.")){if(n.endsWith(e.substring(1))){s=!0;break}}else if(e===n){s=!0;break}if(!s||0===e.arguments.length)return;const a=e.arguments[0].expression;let o=[],l=!1;if("StringLiteral"===a.type)o.push(a.value);else if("ArrowFunctionExpression"===a.type){const e=this.extractKeyFromSelector(a);e&&(o.push(e),l=!0)}else if("ArrayExpression"===a.type)for(const e of a.elements)"StringLiteral"===e?.expression.type&&o.push(e.expression.value);if(o=o.filter(e=>!!e),0===o.length)return;let p=!1;const u=this.config.extract.pluralSeparator??"_";for(let e=0;e<o.length;e++)o[e].endsWith(`${u}ordinal`)&&(p=!0,o[e]=o[e].slice(0,-8));let c,f;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?f=t:"StringLiteral"===t.type&&(c=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(f=t)}const y=f?t.getObjectPropValue(f,"defaultValue"):void 0,g="string"==typeof y?y:c;for(let e=0;e<o.length;e++){let n,i=o[e];if(f){const e=t.getObjectPropValue(f,"ns");"string"==typeof e&&(n=e)}!n&&r?.defaultNs&&(n=r.defaultNs);const s=this.config.extract.nsSeparator??":";if(!n&&s&&i.includes(s)){const e=i.split(s);n=e.shift(),i=e.join(s)}n||(n=this.config.extract.defaultNS);let a=i;if(r?.keyPrefix){const e=this.config.extract.keySeparator??".";a=`${r.keyPrefix}${e}${i}`}const u=e===o.length-1&&g||i;if(f){const e=t.getObjectProperty(f,"context");if("ConditionalExpression"===e?.value?.type){const t=this.resolvePossibleStringValues(e.value),r=this.config.extract.contextSeparator??"_";if(t.length>0){t.forEach(e=>{this.pluginContext.addKey({key:`${a}${r}${e}`,ns:n,defaultValue:u})}),this.pluginContext.addKey({key:a,ns:n,defaultValue:u});continue}}const r=t.getObjectPropValue(f,"context");if("string"==typeof r&&r){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${a}${e}${r}`,ns:n,defaultValue:u});continue}const i=void 0!==t.getObjectPropValue(f,"count"),s=!0===t.getObjectPropValue(f,"ordinal");if(i||p){this.handlePluralKeys(a,n,f,s||p);continue}!0===t.getObjectPropValue(f,"returnObjects")&&this.objectKeys.add(a)}l&&this.objectKeys.add(a),this.pluginContext.addKey({key:a,ns:n,defaultValue:u})}}handlePluralKeys(e,n,r,i){try{const s=i?"ordinal":"cardinal",a=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:s}).resolvedOptions().pluralCategories,o=this.config.extract.pluralSeparator??"_",l=t.getObjectPropValue(r,"defaultValue"),p=t.getObjectPropValue(r,`defaultValue${o}other`),u=t.getObjectPropValue(r,`defaultValue${o}ordinal${o}other`);for(const s of a){const a=i?`defaultValue${o}ordinal${o}${s}`:`defaultValue${o}${s}`,c=t.getObjectPropValue(r,a);let f;f="string"==typeof c?c:"one"===s&&"string"==typeof l?l:i&&"string"==typeof u?u:i||"string"!=typeof p?"string"==typeof l?l:e:p;const y=i?`${e}${o}ordinal${o}${s}`:`${e}${o}${s}`;this.pluginContext.addKey({key:y,ns:n,defaultValue:f,hasCount:!0,isOrdinal:i})}}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const s=t.getObjectPropValue(r,"defaultValue");this.pluginContext.addKey({key:e,ns:n,defaultValue:"string"==typeof s?s:e})}}handleSimplePluralKeys(e,t,n){try{const r=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,i=this.config.extract.pluralSeparator??"_";for(const s of r)this.pluginContext.addKey({key:`${e}${i}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(r){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}".`),this.pluginContext.addKey({key:e,ns:n,defaultValue:t})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,r=this.getVarFromScope(t);r?.defaultNs&&(n.ns=r.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const r of e)this.pluginContext.addKey({key:`${n.key}${t}${r}`,ns:n.ns,defaultValue:n.defaultValue});"StringLiteral"!==n.contextExpression.type&&this.pluginContext.addKey(n)}}else if(n.hasCount){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),r=!!e,i=n.optionsNode??{type:"ObjectExpression",properties:[],span:{start:0,end:0,ctxt:0}};i.properties.push({type:"KeyValueProperty",key:{type:"Identifier",value:"defaultValue",optional:!1,span:{start:0,end:0,ctxt:0}},value:{type:"StringLiteral",value:n.defaultValue,span:{start:0,end:0,ctxt:0}}}),this.handlePluralKeys(n.key,n.ns,i,r)}else this.pluginContext.addKey(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(".")}}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 r=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)r.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;r.unshift(e.expression.value)}n=n.object}if(r.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return r.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getUseTranslationConfig(e){const t=this.config.extract.useTranslationNames||["useTranslation"];for(const n of t){if("string"==typeof n&&n===e)return{name:e,nsArg:0,keyPrefixArg:1};if("object"==typeof n&&n.name===e)return{name:n.name,nsArg:n.nsArg??0,keyPrefixArg:n.keyPrefixArg??1}}}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}};
1
+ "use strict";var e=require("./jsx-parser.js"),t=require("./ast-utils.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&this.walk(e);else n&&"object"==typeof n&&this.walk(n)}t&&this.exitScope()}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)}handleVariableDeclarator(e){const t=e.init;if(!t)return;const n="AwaitExpression"===t.type&&"CallExpression"===t.argument.type?t.argument:"CallExpression"===t.type?t:null;if(!n)return;const r=n.callee;if("Identifier"===r.type){const t=this.getUseTranslationConfig(r.value);if(t)return void this.handleUseTranslationDeclarator(e,n,t)}"MemberExpression"===r.type&&"Identifier"===r.property.type&&"getFixedT"===r.property.value&&this.handleGetFixedTDeclarator(e,n)}handleUseTranslationDeclarator(e,n,r){let s;if("Identifier"===e.id.type&&(s=e.id.value),"ArrayPattern"===e.id.type){const t=e.id.elements[0];"Identifier"===t?.type&&(s=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){s="t";break}if("KeyValuePatternProperty"===t.type&&"Identifier"===t.key.type&&"t"===t.key.value&&"Identifier"===t.value.type){s=t.value.value;break}}if(!s)return;const i=n.arguments?.[r.nsArg]?.expression;let a;"StringLiteral"===i?.type?a=i.value:"ArrayExpression"===i?.type&&"StringLiteral"===i.elements[0]?.expression.type&&(a=i.elements[0].expression.value);const o=n.arguments?.[r.keyPrefixArg]?.expression;let l;if("ObjectExpression"===o?.type){const e=t.getObjectPropValue(o,"keyPrefix");l="string"==typeof e?e:void 0}this.setVarInScope(s,{defaultNs:a,keyPrefix:l})}handleGetFixedTDeclarator(e,t){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const n=e.id.value,r=t.arguments,s=r[1]?.expression,i=r[2]?.expression,a="StringLiteral"===s?.type?s.value:void 0,o="StringLiteral"===i?.type?i.value:void 0;(a||o)&&this.setVarInScope(n,{defaultNs:a,keyPrefix:o})}handleCallExpression(e){const n=this.getFunctionName(e.callee);if(!n)return;const r=this.getVarFromScope(n),s=this.config.extract.functions||["t","*.t"];let i=void 0!==r;if(!i)for(const e of s)if(e.startsWith("*.")){if(n.endsWith(e.substring(1))){i=!0;break}}else if(e===n){i=!0;break}if(!i||0===e.arguments.length)return;const{keysToProcess:a,isSelectorAPI:o}=this.handleCallExpressionArgument(e,0);if(0===a.length)return;let l=!1;const u=this.config.extract.pluralSeparator??"_";for(let e=0;e<a.length;e++)a[e].endsWith(`${u}ordinal`)&&(l=!0,a[e]=a[e].slice(0,-8));let p,c;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?c=t:"StringLiteral"===t.type&&(p=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(c=t)}const f=c?t.getObjectPropValue(c,"defaultValue"):void 0,g="string"==typeof f?f:p;for(let e=0;e<a.length;e++){let n,s=a[e];if(c){const e=t.getObjectPropValue(c,"ns");"string"==typeof e&&(n=e)}const i=this.config.extract.nsSeparator??":";if(!n&&i&&s.includes(i)){const e=s.split(i);n=e.shift(),s=e.join(i)}!n&&r?.defaultNs&&(n=r.defaultNs),n||(n=this.config.extract.defaultNS);let u=s;if(r?.keyPrefix){const e=this.config.extract.keySeparator??".";u=`${r.keyPrefix}${e}${s}`}const p=e===a.length-1&&g||s;if(c){const e=t.getObjectProperty(c,"context"),r=[];if("ConditionalExpression"===e?.value?.type){const t=this.resolvePossibleStringValues(e.value),s=this.config.extract.contextSeparator??"_";t.length>0&&(t.forEach(e=>{r.push({key:`${u}${s}${e}`,ns:n,defaultValue:p})}),r.push({key:u,ns:n,defaultValue:p}))}else if("StringLiteral"===e?.value?.type){const t=e.value.value,s=this.config.extract.contextSeparator??"_";r.push({key:`${u}${s}${t}`,ns:n,defaultValue:p})}const s=void 0!==t.getObjectPropValue(c,"count"),i=!0===t.getObjectPropValue(c,"ordinal");if(s||l){if(r.length>0)for(const{key:e,ns:t}of r)this.handlePluralKeys(e,t,c,i||l);else this.handlePluralKeys(u,n,c,i||l);continue}if(r.length>0){r.forEach(this.pluginContext.addKey);continue}!0===t.getObjectPropValue(c,"returnObjects")&&this.objectKeys.add(u)}o&&this.objectKeys.add(u),this.pluginContext.addKey({key:u,ns:n,defaultValue:p})}}handleCallExpressionArgument(e,t){const n=e.arguments[t].expression,r=[];let s=!1;if("ArrowFunctionExpression"===n.type){const e=this.extractKeyFromSelector(n);e&&(r.push(e),s=!0)}else if("ArrayExpression"===n.type)for(const e of n.elements)e?.expression&&r.push(...this.resolvePossibleStringValues(e.expression));else r.push(...this.resolvePossibleStringValues(n));return{keysToProcess:r.filter(e=>!!e),isSelectorAPI:s}}handlePluralKeys(e,n,r,s){try{const i=s?"ordinal":"cardinal",a=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:i}).resolvedOptions().pluralCategories,o=this.config.extract.pluralSeparator??"_",l=t.getObjectPropValue(r,"defaultValue"),u=t.getObjectPropValue(r,`defaultValue${o}other`),p=t.getObjectPropValue(r,`defaultValue${o}ordinal${o}other`);for(const i of a){const a=s?`defaultValue${o}ordinal${o}${i}`:`defaultValue${o}${i}`,c=t.getObjectPropValue(r,a);let f;f="string"==typeof c?c:"one"===i&&"string"==typeof l?l:s&&"string"==typeof p?p:s||"string"!=typeof u?"string"==typeof l?l:e:u;const g=s?`${e}${o}ordinal${o}${i}`:`${e}${o}${i}`;this.pluginContext.addKey({key:g,ns:n,defaultValue:f,hasCount:!0,isOrdinal:s})}}catch(s){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const i=t.getObjectPropValue(r,"defaultValue");this.pluginContext.addKey({key:e,ns:n,defaultValue:"string"==typeof i?i:e})}}handleSimplePluralKeys(e,t,n){try{const r=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,s=this.config.extract.pluralSeparator??"_";for(const i of r)this.pluginContext.addKey({key:`${e}${s}${i}`,ns:n,defaultValue:t,hasCount:!0})}catch(r){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}".`),this.pluginContext.addKey({key:e,ns:n,defaultValue:t})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config),r=[];if(n){if(n.keyExpression){const e=this.resolvePossibleStringValues(n.keyExpression);r.push(...e)}else r.push(n.serializedChildren);let e;const{contextExpression:s,optionsNode:i,defaultValue:a,hasCount:o,isOrdinal:l,serializedChildren:u}=n;if(n.ns){const{ns:t}=n;e=r.map(e=>({key:e,ns:t,defaultValue:a||u,hasCount:o,isOrdinal:l}))}else{e=r.map(e=>{const t=this.config.extract.nsSeparator??":";let n;if(t&&e.includes(t)){let r;[n,...r]=e.split(t),e=r.join(t)}return{key:e,ns:n,defaultValue:a||u,hasCount:o,isOrdinal:l}});const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===n?.type&&"JSXExpressionContainer"===n.value?.type&&"Identifier"===n.value.expression.type){const t=n.value.expression.value,r=this.getVarFromScope(t);r?.defaultNs&&e.forEach(e=>{e.ns||(e.ns=r.defaultNs)})}}if(e.forEach(e=>{e.ns||(e.ns=this.config.extract.defaultNS)}),s&&o){const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),r=!!n,a=this.resolvePossibleStringValues(s),o=this.config.extract.contextSeparator??"_";if(a.length>0){e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,r,i));for(const t of a)for(const n of e){const e=`${n.key}${o}${t}`;this.generatePluralKeysForTrans(e,n.defaultValue,n.ns,r,i)}}else e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,r,i))}else if(s){const t=this.resolvePossibleStringValues(s),n=this.config.extract.contextSeparator??"_";if(t.length>0){for(const r of t)for(const{key:t,ns:s,defaultValue:i}of e)this.pluginContext.addKey({key:`${t}${n}${r}`,ns:s,defaultValue:i});"StringLiteral"!==s.type&&e.forEach(this.pluginContext.addKey)}}else if(o){const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),r=!!n;e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,r,i))}else e.forEach(this.pluginContext.addKey)}}}generatePluralKeysForTrans(e,n,r,s,i){try{const a=s?"ordinal":"cardinal",o=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:a}).resolvedOptions().pluralCategories,l=this.config.extract.pluralSeparator??"_";let u,p;i&&(u=t.getObjectPropValue(i,`defaultValue${l}other`),p=t.getObjectPropValue(i,`defaultValue${l}ordinal${l}other`));for(const a of o){const o=s?`defaultValue${l}ordinal${l}${a}`:`defaultValue${l}${a}`,c=i?t.getObjectPropValue(i,o):void 0;let f;f="string"==typeof c?c:"one"===a&&"string"==typeof n?n:s&&"string"==typeof p?p:s||"string"!=typeof u?"string"==typeof n?n:e:u;const g=s?`${e}${l}ordinal${l}${a}`:`${e}${l}${a}`;this.pluginContext.addKey({key:g,ns:r,defaultValue:f,hasCount:!0,isOrdinal:s})}}catch(t){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,ns:r,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(".")}}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 r=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)r.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;r.unshift(e.expression.value)}n=n.object}if(r.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return r.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return e.value?[e.value]:[];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getUseTranslationConfig(e){const t=this.config.extract.useTranslationNames||["useTranslation"];for(const n of t){if("string"==typeof n&&n===e)return{name:e,nsArg:0,keyPrefixArg:1};if("object"==typeof n&&n.name===e)return{name:n.name,nsArg:n.nsArg??0,keyPrefixArg:n.keyPrefixArg??1}}}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}};
@@ -1 +1 @@
1
- "use strict";var e=require("./ast-utils.js");function t(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);return function e(t){let i="";return t.forEach((t,r)=>{if("JSXText"===t.type)i+=t.value;else if("JSXExpressionContainer"===t.type){const e=t.expression;if("StringLiteral"===e.type)i+=e.value;else if("Identifier"===e.type)i+=`{{${e.value}}}`;else if("ObjectExpression"===e.type){const t=e.properties[0];t&&"Identifier"===t.type&&(i+=`{{${t.value}}}`)}}else if("JSXElement"===t.type){let a;"Identifier"===t.opening.name.type&&(a=t.opening.name.value);const u=e(t.children);a&&n.has(a)?i+=`<${a}>${u}</${a}>`:i+=`<${r}>${u}</${r}>`}else"JSXFragment"===t.type&&(i+=e(t.children))}),i}(e).trim().replace(/\s{2,}/g," ")}exports.extractFromTransComponent=function(n,i){const r=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),a=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),u=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),p=!!u,s=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),l="JSXAttribute"===s?.type&&"JSXExpressionContainer"===s.value?.type&&"ObjectExpression"===s.value.expression.type?s.value.expression:void 0,o=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let y,v="JSXAttribute"===o?.type&&"JSXExpressionContainer"===o.value?.type?o.value.expression:void 0;if(y="JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type?r.value.value:t(n.children,i),!y)return null;const f=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let c;if(c="JSXAttribute"===f?.type&&"StringLiteral"===f.value?.type?f.value.value:void 0,l&&(void 0===c&&(c=e.getObjectPropValue(l,"ns")),void 0===v)){const t=e.getObjectProperty(l,"context");t?.value&&(v=t.value)}let d=i.extract.defaultValue||"";return d="JSXAttribute"===a?.type&&"StringLiteral"===a.value?.type?a.value.value:t(n.children,i),{key:y,ns:c,defaultValue:d||y,hasCount:p,contextExpression:v,optionsNode:l}};
1
+ "use strict";var e=require("./ast-utils.js");exports.extractFromTransComponent=function(t,n){const i=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),r=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),a=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),s=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"values"===e.name.value);let p;a||"JSXAttribute"!==s?.type||"JSXExpressionContainer"!==s.value?.type||"ObjectExpression"!==s.value.expression.type||(p=e.getObjectProperty(s.value.expression,"count"));const u=!!a||!!p,o=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),l="JSXAttribute"===o?.type&&"JSXExpressionContainer"===o.value?.type&&"ObjectExpression"===o.value.expression.type?o.value.expression:void 0,y=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),v=!!y,d=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let c="JSXAttribute"===d?.type&&"JSXExpressionContainer"===d.value?.type?d.value.expression:void 0;const f=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let b;if(b="JSXAttribute"===f?.type&&"StringLiteral"===f.value?.type?f.value.value:void 0,l&&(void 0===b&&(b=e.getObjectPropValue(l,"ns")),void 0===c)){const t=e.getObjectProperty(l,"context");t?.value&&(c=t.value)}const S=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){let t="";return e.forEach((e,r)=>{if("JSXText"===e.type)t+=e.value;else if("JSXExpressionContainer"===e.type){const n=e.expression;if("StringLiteral"===n.type)t+=n.value;else if("Identifier"===n.type)t+=`{{${n.value}}}`;else if("ObjectExpression"===n.type){const e=n.properties[0];e&&"Identifier"===e.type&&(t+=`{{${e.value}}}`)}}else if("JSXElement"===e.type){let a;"Identifier"===e.opening.name.type&&(a=e.opening.name.value);const s=i(e.children);a&&n.has(a)?t+=`<${a}>${s}</${a}>`:t+=`<${r}>${s}</${r}>`}else"JSXFragment"===e.type&&(t+=i(e.children))}),t}return i(e).trim().replace(/\s{2,}/g," ")}(t.children,n);let x,m=n.extract.defaultValue||"";return"JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type&&(m=r.value.value),"JSXAttribute"!==i?.type||("StringLiteral"===i.value?.type?x=i.value:"JSXExpressionContainer"===i.value?.type&&"JSXEmptyExpression"!==i.value.expression.type&&(x=i.value.expression),x)?{keyExpression:x,serializedChildren:S,ns:b,defaultValue:m,hasCount:u,isOrdinal:v,contextExpression:c,optionsNode:l}:null};
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.5.7"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").action(async t=>{const a=await i(),c=async()=>{const o=await r(a,{isWatchMode:t.watch,isDryRun:t.dryRun});t.ci&&o&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),t.watch){console.log("\nWatching for changes...");o.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(t,o)=>{let e=await a();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await g(e,{detail:t,namespace:o.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async t=>{const n=await i(),a=()=>s(n);if(await a(),t.watch){console.log("\nWatching for changes...");o.watch(await e(n.types?.input||[]),{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=await i();await l(t)}),w.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async t=>{await m(t)}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(p),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async t=>{const i=async()=>{let t=await a();if(!t){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),t=o}await d(t)};if(await i(),t.watch){console.log("\nWatching for changes...");const t=await a();if(t?.extract?.input){o.watch(await e(t.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),i()})}}}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async t=>{const o=await i();await f(o,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=await i();await u(o,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=await i();await h(o,t)}),w.parse(process.argv);
2
+ import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import n from"chalk";import{ensureConfig as i,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as g}from"./status.js";import{runLocizeSync as f,runLocizeDownload as u,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.5.9"),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);
@@ -1 +1 @@
1
- function e(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}function t(t,r){const y=e(t,r);if("KeyValueProperty"===y?.type){const e=y.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}export{t as getObjectPropValue,e as getObjectProperty};
1
+ function e(e,t){return e.properties.filter(e=>"KeyValueProperty"===e.type).find(e=>"Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t)}function t(t,r){const i=e(t,r);if("KeyValueProperty"===i?.type){const e=i.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}export{t as getObjectPropValue,e as getObjectProperty};
@@ -1 +1 @@
1
- import{extractFromTransComponent as e}from"./jsx-parser.js";import{getObjectPropValue as t,getObjectProperty as n}from"./ast-utils.js";class i{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&this.walk(e);else n&&"object"==typeof n&&this.walk(n)}t&&this.exitScope()}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)}handleVariableDeclarator(e){const t=e.init;if(!t)return;const n="AwaitExpression"===t.type&&"CallExpression"===t.argument.type?t.argument:"CallExpression"===t.type?t:null;if(!n)return;const i=n.callee;if("Identifier"===i.type){const t=this.getUseTranslationConfig(i.value);if(t)return void this.handleUseTranslationDeclarator(e,n,t)}"MemberExpression"===i.type&&"Identifier"===i.property.type&&"getFixedT"===i.property.value&&this.handleGetFixedTDeclarator(e,n)}handleUseTranslationDeclarator(e,n,i){let r;if("Identifier"===e.id.type&&(r=e.id.value),"ArrayPattern"===e.id.type){const t=e.id.elements[0];"Identifier"===t?.type&&(r=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){r="t";break}if("KeyValuePatternProperty"===t.type&&"Identifier"===t.key.type&&"t"===t.key.value&&"Identifier"===t.value.type){r=t.value.value;break}}if(!r)return;const s=n.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=n.arguments?.[i.keyPrefixArg]?.expression;let l;if("ObjectExpression"===o?.type){const e=t(o,"keyPrefix");l="string"==typeof e?e:void 0}this.setVarInScope(r,{defaultNs:a,keyPrefix:l})}handleGetFixedTDeclarator(e,t){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const n=e.id.value,i=t.arguments,r=i[1]?.expression,s=i[2]?.expression,a="StringLiteral"===r?.type?r.value:void 0,o="StringLiteral"===s?.type?s.value:void 0;(a||o)&&this.setVarInScope(n,{defaultNs:a,keyPrefix:o})}handleCallExpression(e){const i=this.getFunctionName(e.callee);if(!i)return;const r=this.getVarFromScope(i),s=this.config.extract.functions||["t","*.t"];let a=void 0!==r;if(!a)for(const e of s)if(e.startsWith("*.")){if(i.endsWith(e.substring(1))){a=!0;break}}else if(e===i){a=!0;break}if(!a||0===e.arguments.length)return;const o=e.arguments[0].expression;let l=[],p=!1;if("StringLiteral"===o.type)l.push(o.value);else if("ArrowFunctionExpression"===o.type){const e=this.extractKeyFromSelector(o);e&&(l.push(e),p=!0)}else if("ArrayExpression"===o.type)for(const e of o.elements)"StringLiteral"===e?.expression.type&&l.push(e.expression.value);if(l=l.filter(e=>!!e),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,y;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?y=t:"StringLiteral"===t.type&&(f=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(y=t)}const g=y?t(y,"defaultValue"):void 0,d="string"==typeof g?g:f;for(let e=0;e<l.length;e++){let i,s=l[e];if(y){const e=t(y,"ns");"string"==typeof e&&(i=e)}!i&&r?.defaultNs&&(i=r.defaultNs);const a=this.config.extract.nsSeparator??":";if(!i&&a&&s.includes(a)){const e=s.split(a);i=e.shift(),s=e.join(a)}i||(i=this.config.extract.defaultNS);let o=s;if(r?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${r.keyPrefix}${e}${s}`}const c=e===l.length-1&&d||s;if(y){const e=n(y,"context");if("ConditionalExpression"===e?.value?.type){const t=this.resolvePossibleStringValues(e.value),n=this.config.extract.contextSeparator??"_";if(t.length>0){t.forEach(e=>{this.pluginContext.addKey({key:`${o}${n}${e}`,ns:i,defaultValue:c})}),this.pluginContext.addKey({key:o,ns:i,defaultValue:c});continue}}const r=t(y,"context");if("string"==typeof r&&r){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${r}`,ns:i,defaultValue:c});continue}const s=void 0!==t(y,"count"),a=!0===t(y,"ordinal");if(s||u){this.handlePluralKeys(o,i,y,a||u);continue}!0===t(y,"returnObjects")&&this.objectKeys.add(o)}p&&this.objectKeys.add(o),this.pluginContext.addKey({key:o,ns:i,defaultValue:c})}}handlePluralKeys(e,n,i,r){try{const s=r?"ordinal":"cardinal",a=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:s}).resolvedOptions().pluralCategories,o=this.config.extract.pluralSeparator??"_",l=t(i,"defaultValue"),p=t(i,`defaultValue${o}other`),u=t(i,`defaultValue${o}ordinal${o}other`);for(const s of a){const a=t(i,r?`defaultValue${o}ordinal${o}${s}`:`defaultValue${o}${s}`);let c;c="string"==typeof a?a:"one"===s&&"string"==typeof l?l:r&&"string"==typeof u?u:r||"string"!=typeof p?"string"==typeof l?l:e:p;const f=r?`${e}${o}ordinal${o}${s}`:`${e}${o}${s}`;this.pluginContext.addKey({key:f,ns:n,defaultValue:c,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 s=t(i,"defaultValue");this.pluginContext.addKey({key:e,ns:n,defaultValue:"string"==typeof s?s:e})}}handleSimplePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}".`),this.pluginContext.addKey({key:e,ns:n,defaultValue:t})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});"StringLiteral"!==n.contextExpression.type&&this.pluginContext.addKey(n)}}else if(n.hasCount){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),i=!!e,r=n.optionsNode??{type:"ObjectExpression",properties:[],span:{start:0,end:0,ctxt:0}};r.properties.push({type:"KeyValueProperty",key:{type:"Identifier",value:"defaultValue",optional:!1,span:{start:0,end:0,ctxt:0}},value:{type:"StringLiteral",value:n.defaultValue,span:{start:0,end:0,ctxt:0}}}),this.handlePluralKeys(n.key,n.ns,r,i)}else this.pluginContext.addKey(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(".")}}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 i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getUseTranslationConfig(e){const t=this.config.extract.useTranslationNames||["useTranslation"];for(const n of t){if("string"==typeof n&&n===e)return{name:e,nsArg:0,keyPrefixArg:1};if("object"==typeof n&&n.name===e)return{name:n.name,nsArg:n.nsArg??0,keyPrefixArg:n.keyPrefixArg??1}}}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{i as ASTVisitors};
1
+ import{extractFromTransComponent as e}from"./jsx-parser.js";import{getObjectPropValue as t,getObjectProperty as n}from"./ast-utils.js";class r{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&this.walk(e);else n&&"object"==typeof n&&this.walk(n)}t&&this.exitScope()}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)}handleVariableDeclarator(e){const t=e.init;if(!t)return;const n="AwaitExpression"===t.type&&"CallExpression"===t.argument.type?t.argument:"CallExpression"===t.type?t:null;if(!n)return;const r=n.callee;if("Identifier"===r.type){const t=this.getUseTranslationConfig(r.value);if(t)return void this.handleUseTranslationDeclarator(e,n,t)}"MemberExpression"===r.type&&"Identifier"===r.property.type&&"getFixedT"===r.property.value&&this.handleGetFixedTDeclarator(e,n)}handleUseTranslationDeclarator(e,n,r){let s;if("Identifier"===e.id.type&&(s=e.id.value),"ArrayPattern"===e.id.type){const t=e.id.elements[0];"Identifier"===t?.type&&(s=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){s="t";break}if("KeyValuePatternProperty"===t.type&&"Identifier"===t.key.type&&"t"===t.key.value&&"Identifier"===t.value.type){s=t.value.value;break}}if(!s)return;const i=n.arguments?.[r.nsArg]?.expression;let a;"StringLiteral"===i?.type?a=i.value:"ArrayExpression"===i?.type&&"StringLiteral"===i.elements[0]?.expression.type&&(a=i.elements[0].expression.value);const o=n.arguments?.[r.keyPrefixArg]?.expression;let l;if("ObjectExpression"===o?.type){const e=t(o,"keyPrefix");l="string"==typeof e?e:void 0}this.setVarInScope(s,{defaultNs:a,keyPrefix:l})}handleGetFixedTDeclarator(e,t){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const n=e.id.value,r=t.arguments,s=r[1]?.expression,i=r[2]?.expression,a="StringLiteral"===s?.type?s.value:void 0,o="StringLiteral"===i?.type?i.value:void 0;(a||o)&&this.setVarInScope(n,{defaultNs:a,keyPrefix:o})}handleCallExpression(e){const r=this.getFunctionName(e.callee);if(!r)return;const s=this.getVarFromScope(r),i=this.config.extract.functions||["t","*.t"];let a=void 0!==s;if(!a)for(const e of i)if(e.startsWith("*.")){if(r.endsWith(e.substring(1))){a=!0;break}}else if(e===r){a=!0;break}if(!a||0===e.arguments.length)return;const{keysToProcess:o,isSelectorAPI:l}=this.handleCallExpressionArgument(e,0);if(0===o.length)return;let u=!1;const p=this.config.extract.pluralSeparator??"_";for(let e=0;e<o.length;e++)o[e].endsWith(`${p}ordinal`)&&(u=!0,o[e]=o[e].slice(0,-8));let f,c;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?c=t:"StringLiteral"===t.type&&(f=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(c=t)}const y=c?t(c,"defaultValue"):void 0,g="string"==typeof y?y:f;for(let e=0;e<o.length;e++){let r,i=o[e];if(c){const e=t(c,"ns");"string"==typeof e&&(r=e)}const a=this.config.extract.nsSeparator??":";if(!r&&a&&i.includes(a)){const e=i.split(a);r=e.shift(),i=e.join(a)}!r&&s?.defaultNs&&(r=s.defaultNs),r||(r=this.config.extract.defaultNS);let p=i;if(s?.keyPrefix){const e=this.config.extract.keySeparator??".";p=`${s.keyPrefix}${e}${i}`}const f=e===o.length-1&&g||i;if(c){const e=n(c,"context"),s=[];if("ConditionalExpression"===e?.value?.type){const t=this.resolvePossibleStringValues(e.value),n=this.config.extract.contextSeparator??"_";t.length>0&&(t.forEach(e=>{s.push({key:`${p}${n}${e}`,ns:r,defaultValue:f})}),s.push({key:p,ns:r,defaultValue:f}))}else if("StringLiteral"===e?.value?.type){const t=e.value.value,n=this.config.extract.contextSeparator??"_";s.push({key:`${p}${n}${t}`,ns:r,defaultValue:f})}const i=void 0!==t(c,"count"),a=!0===t(c,"ordinal");if(i||u){if(s.length>0)for(const{key:e,ns:t}of s)this.handlePluralKeys(e,t,c,a||u);else this.handlePluralKeys(p,r,c,a||u);continue}if(s.length>0){s.forEach(this.pluginContext.addKey);continue}!0===t(c,"returnObjects")&&this.objectKeys.add(p)}l&&this.objectKeys.add(p),this.pluginContext.addKey({key:p,ns:r,defaultValue:f})}}handleCallExpressionArgument(e,t){const n=e.arguments[t].expression,r=[];let s=!1;if("ArrowFunctionExpression"===n.type){const e=this.extractKeyFromSelector(n);e&&(r.push(e),s=!0)}else if("ArrayExpression"===n.type)for(const e of n.elements)e?.expression&&r.push(...this.resolvePossibleStringValues(e.expression));else r.push(...this.resolvePossibleStringValues(n));return{keysToProcess:r.filter(e=>!!e),isSelectorAPI:s}}handlePluralKeys(e,n,r,s){try{const i=s?"ordinal":"cardinal",a=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:i}).resolvedOptions().pluralCategories,o=this.config.extract.pluralSeparator??"_",l=t(r,"defaultValue"),u=t(r,`defaultValue${o}other`),p=t(r,`defaultValue${o}ordinal${o}other`);for(const i of a){const a=t(r,s?`defaultValue${o}ordinal${o}${i}`:`defaultValue${o}${i}`);let f;f="string"==typeof a?a:"one"===i&&"string"==typeof l?l:s&&"string"==typeof p?p:s||"string"!=typeof u?"string"==typeof l?l:e:u;const c=s?`${e}${o}ordinal${o}${i}`:`${e}${o}${i}`;this.pluginContext.addKey({key:c,ns:n,defaultValue:f,hasCount:!0,isOrdinal:s})}}catch(s){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const i=t(r,"defaultValue");this.pluginContext.addKey({key:e,ns:n,defaultValue:"string"==typeof i?i:e})}}handleSimplePluralKeys(e,t,n){try{const r=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,s=this.config.extract.pluralSeparator??"_";for(const i of r)this.pluginContext.addKey({key:`${e}${s}${i}`,ns:n,defaultValue:t,hasCount:!0})}catch(r){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}".`),this.pluginContext.addKey({key:e,ns:n,defaultValue:t})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e(t,this.config),r=[];if(n){if(n.keyExpression){const e=this.resolvePossibleStringValues(n.keyExpression);r.push(...e)}else r.push(n.serializedChildren);let e;const{contextExpression:s,optionsNode:i,defaultValue:a,hasCount:o,isOrdinal:l,serializedChildren:u}=n;if(n.ns){const{ns:t}=n;e=r.map(e=>({key:e,ns:t,defaultValue:a||u,hasCount:o,isOrdinal:l}))}else{e=r.map(e=>{const t=this.config.extract.nsSeparator??":";let n;if(t&&e.includes(t)){let r;[n,...r]=e.split(t),e=r.join(t)}return{key:e,ns:n,defaultValue:a||u,hasCount:o,isOrdinal:l}});const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===n?.type&&"JSXExpressionContainer"===n.value?.type&&"Identifier"===n.value.expression.type){const t=n.value.expression.value,r=this.getVarFromScope(t);r?.defaultNs&&e.forEach(e=>{e.ns||(e.ns=r.defaultNs)})}}if(e.forEach(e=>{e.ns||(e.ns=this.config.extract.defaultNS)}),s&&o){const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),r=!!n,a=this.resolvePossibleStringValues(s),o=this.config.extract.contextSeparator??"_";if(a.length>0){e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,r,i));for(const t of a)for(const n of e){const e=`${n.key}${o}${t}`;this.generatePluralKeysForTrans(e,n.defaultValue,n.ns,r,i)}}else e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,r,i))}else if(s){const t=this.resolvePossibleStringValues(s),n=this.config.extract.contextSeparator??"_";if(t.length>0){for(const r of t)for(const{key:t,ns:s,defaultValue:i}of e)this.pluginContext.addKey({key:`${t}${n}${r}`,ns:s,defaultValue:i});"StringLiteral"!==s.type&&e.forEach(this.pluginContext.addKey)}}else if(o){const n=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),r=!!n;e.forEach(e=>this.generatePluralKeysForTrans(e.key,e.defaultValue,e.ns,r,i))}else e.forEach(this.pluginContext.addKey)}}}generatePluralKeysForTrans(e,n,r,s,i){try{const a=s?"ordinal":"cardinal",o=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:a}).resolvedOptions().pluralCategories,l=this.config.extract.pluralSeparator??"_";let u,p;i&&(u=t(i,`defaultValue${l}other`),p=t(i,`defaultValue${l}ordinal${l}other`));for(const a of o){const o=i?t(i,s?`defaultValue${l}ordinal${l}${a}`:`defaultValue${l}${a}`):void 0;let f;f="string"==typeof o?o:"one"===a&&"string"==typeof n?n:s&&"string"==typeof p?p:s||"string"!=typeof u?"string"==typeof n?n:e:u;const c=s?`${e}${l}ordinal${l}${a}`:`${e}${l}${a}`;this.pluginContext.addKey({key:c,ns:r,defaultValue:f,hasCount:!0,isOrdinal:s})}}catch(t){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,ns:r,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(".")}}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 r=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)r.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;r.unshift(e.expression.value)}n=n.object}if(r.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return r.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return e.value?[e.value]:[];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getUseTranslationConfig(e){const t=this.config.extract.useTranslationNames||["useTranslation"];for(const n of t){if("string"==typeof n&&n===e)return{name:e,nsArg:0,keyPrefixArg:1};if("object"==typeof n&&n.name===e)return{name:n.name,nsArg:n.nsArg??0,keyPrefixArg:n.keyPrefixArg??1}}}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{r as ASTVisitors};
@@ -1 +1 @@
1
- import{getObjectPropValue as e,getObjectProperty as t}from"./ast-utils.js";function n(n,r){const a=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),u=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),p=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),l=!!p,s=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),o="JSXAttribute"===s?.type&&"JSXExpressionContainer"===s.value?.type&&"ObjectExpression"===s.value.expression.type?s.value.expression:void 0,y=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let v,f="JSXAttribute"===y?.type&&"JSXExpressionContainer"===y.value?.type?y.value.expression:void 0;if(v="JSXAttribute"===a?.type&&"StringLiteral"===a.value?.type?a.value.value:i(n.children,r),!v)return null;const d=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let c;if(c="JSXAttribute"===d?.type&&"StringLiteral"===d.value?.type?d.value.value:void 0,o&&(void 0===c&&(c=e(o,"ns")),void 0===f)){const e=t(o,"context");e?.value&&(f=e.value)}let S=r.extract.defaultValue||"";return S="JSXAttribute"===u?.type&&"StringLiteral"===u.value?.type?u.value.value:i(n.children,r),{key:v,ns:c,defaultValue:S||v,hasCount:l,contextExpression:f,optionsNode:o}}function i(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);return function e(t){let i="";return t.forEach((t,r)=>{if("JSXText"===t.type)i+=t.value;else if("JSXExpressionContainer"===t.type){const e=t.expression;if("StringLiteral"===e.type)i+=e.value;else if("Identifier"===e.type)i+=`{{${e.value}}}`;else if("ObjectExpression"===e.type){const t=e.properties[0];t&&"Identifier"===t.type&&(i+=`{{${t.value}}}`)}}else if("JSXElement"===t.type){let a;"Identifier"===t.opening.name.type&&(a=t.opening.name.value);const u=e(t.children);a&&n.has(a)?i+=`<${a}>${u}</${a}>`:i+=`<${r}>${u}</${r}>`}else"JSXFragment"===t.type&&(i+=e(t.children))}),i}(e).trim().replace(/\s{2,}/g," ")}export{n as extractFromTransComponent};
1
+ import{getObjectProperty as e,getObjectPropValue as t}from"./ast-utils.js";function n(n,i){const r=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),a=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),p=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),s=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"values"===e.name.value);let u;p||"JSXAttribute"!==s?.type||"JSXExpressionContainer"!==s.value?.type||"ObjectExpression"!==s.value.expression.type||(u=e(s.value.expression,"count"));const o=!!p||!!u,l=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"tOptions"===e.name.value),y="JSXAttribute"===l?.type&&"JSXExpressionContainer"===l.value?.type&&"ObjectExpression"===l.value.expression.type?l.value.expression:void 0,v=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ordinal"===e.name.value),d=!!v,f=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"context"===e.name.value);let c="JSXAttribute"===f?.type&&"JSXExpressionContainer"===f.value?.type?f.value.expression:void 0;const S=n.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value);let b;if(b="JSXAttribute"===S?.type&&"StringLiteral"===S.value?.type?S.value.value:void 0,y&&(void 0===b&&(b=t(y,"ns")),void 0===c)){const t=e(y,"context");t?.value&&(c=t.value)}const x=function(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);function i(e){let t="";return e.forEach((e,r)=>{if("JSXText"===e.type)t+=e.value;else if("JSXExpressionContainer"===e.type){const n=e.expression;if("StringLiteral"===n.type)t+=n.value;else if("Identifier"===n.type)t+=`{{${n.value}}}`;else if("ObjectExpression"===n.type){const e=n.properties[0];e&&"Identifier"===e.type&&(t+=`{{${e.value}}}`)}}else if("JSXElement"===e.type){let a;"Identifier"===e.opening.name.type&&(a=e.opening.name.value);const p=i(e.children);a&&n.has(a)?t+=`<${a}>${p}</${a}>`:t+=`<${r}>${p}</${r}>`}else"JSXFragment"===e.type&&(t+=i(e.children))}),t}return i(e).trim().replace(/\s{2,}/g," ")}(n.children,i);let m,J=i.extract.defaultValue||"";return"JSXAttribute"===a?.type&&"StringLiteral"===a.value?.type&&(J=a.value.value),"JSXAttribute"!==r?.type||("StringLiteral"===r.value?.type?m=r.value:"JSXExpressionContainer"===r.value?.type&&"JSXEmptyExpression"!==r.value.expression.type&&(m=r.value.expression),m)?{keyExpression:m,serializedChildren:x,ns:b,defaultValue:J,hasCount:o,isOrdinal:d,contextExpression:c,optionsNode:y}:null}export{n as extractFromTransComponent};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -21,7 +21,7 @@ const program = new Command()
21
21
  program
22
22
  .name('i18next-cli')
23
23
  .description('A unified, high-performance i18next CLI.')
24
- .version('1.5.7')
24
+ .version('1.5.9')
25
25
 
26
26
  program
27
27
  .command('extract')
@@ -15,15 +15,16 @@ import type { ObjectExpression } from '@swc/core'
15
15
  * @param propName - The property name to locate
16
16
  * @returns The matching KeyValueProperty node if found, otherwise undefined.
17
17
  */
18
- export function getObjectProperty (object: ObjectExpression, propName: string): any {
19
- return (object.properties).find(
20
- (p) =>
21
- p.type === 'KeyValueProperty' &&
22
- (
23
- (p.key?.type === 'Identifier' && p.key.value === propName) ||
24
- (p.key?.type === 'StringLiteral' && p.key.value === propName)
25
- )
26
- )
18
+ export function getObjectProperty (object: ObjectExpression, propName: string) {
19
+ return (object.properties).filter(
20
+ (p) => p.type === 'KeyValueProperty')
21
+ .find(
22
+ (p) =>
23
+ (
24
+ (p.key?.type === 'Identifier' && p.key.value === propName) ||
25
+ (p.key?.type === 'StringLiteral' && p.key.value === propName)
26
+ )
27
+ )
27
28
  }
28
29
 
29
30
  /**
@@ -1,5 +1,5 @@
1
1
  import type { Module, Node, CallExpression, VariableDeclarator, JSXElement, ArrowFunctionExpression, ObjectExpression, Expression } from '@swc/core'
2
- import type { PluginContext, I18nextToolkitConfig, Logger } from '../../types'
2
+ import type { PluginContext, I18nextToolkitConfig, Logger, ExtractedKey } from '../../types'
3
3
  import { extractFromTransComponent } from './jsx-parser'
4
4
  import { getObjectProperty, getObjectPropValue } from './ast-utils'
5
5
 
@@ -218,11 +218,11 @@ export class ASTVisitors {
218
218
 
219
219
  // Determine the actual call expression, looking inside AwaitExpressions.
220
220
  const callExpr =
221
- init.type === 'AwaitExpression' && init.argument.type === 'CallExpression'
222
- ? init.argument
223
- : init.type === 'CallExpression'
224
- ? init
225
- : null
221
+ init.type === 'AwaitExpression' && init.argument.type === 'CallExpression'
222
+ ? init.argument
223
+ : init.type === 'CallExpression'
224
+ ? init
225
+ : null
226
226
 
227
227
  if (!callExpr) return
228
228
 
@@ -394,28 +394,8 @@ export class ASTVisitors {
394
394
  }
395
395
  if (!isFunctionToParse || node.arguments.length === 0) return
396
396
 
397
- const firstArg = node.arguments[0].expression
398
- let keysToProcess: string[] = []
399
- let isSelectorAPI = false
397
+ const { keysToProcess, isSelectorAPI } = this.handleCallExpressionArgument(node, 0)
400
398
 
401
- if (firstArg.type === 'StringLiteral') {
402
- keysToProcess.push(firstArg.value)
403
- } else if (firstArg.type === 'ArrowFunctionExpression') {
404
- const key = this.extractKeyFromSelector(firstArg)
405
- if (key) {
406
- keysToProcess.push(key)
407
- isSelectorAPI = true
408
- }
409
- } else if (firstArg.type === 'ArrayExpression') {
410
- for (const element of firstArg.elements) {
411
- // We only extract static string literals from the array
412
- if (element?.expression.type === 'StringLiteral') {
413
- keysToProcess.push(element.expression.value)
414
- }
415
- }
416
- }
417
-
418
- keysToProcess = keysToProcess.filter(key => !!key)
419
399
  if (keysToProcess.length === 0) return
420
400
 
421
401
  let isOrdinalByKey = false
@@ -454,12 +434,12 @@ export class ASTVisitors {
454
434
  let key = keysToProcess[i]
455
435
  let ns: string | undefined
456
436
 
457
- // Determine namespace (explicit ns > scope ns > ns:key > default)
437
+ // Determine namespace (explicit ns > ns:key > scope ns > default)
438
+ // See https://www.i18next.com/overview/api#getfixedt
458
439
  if (options) {
459
440
  const nsVal = getObjectPropValue(options, 'ns')
460
441
  if (typeof nsVal === 'string') ns = nsVal
461
442
  }
462
- if (!ns && scopeInfo?.defaultNs) ns = scopeInfo.defaultNs
463
443
 
464
444
  const nsSeparator = this.config.extract.nsSeparator ?? ':'
465
445
  if (!ns && nsSeparator && key.includes(nsSeparator)) {
@@ -467,6 +447,8 @@ export class ASTVisitors {
467
447
  ns = parts.shift()
468
448
  key = parts.join(nsSeparator)
469
449
  }
450
+
451
+ if (!ns && scopeInfo?.defaultNs) ns = scopeInfo.defaultNs
470
452
  if (!ns) ns = this.config.extract.defaultNS
471
453
 
472
454
  let finalKey = key
@@ -482,56 +464,106 @@ export class ASTVisitors {
482
464
  if (options) {
483
465
  const contextProp = getObjectProperty(options, 'context')
484
466
 
485
- // 1. Handle Dynamic Context (Ternary) first
467
+ const keysWithContext: ExtractedKey[] = []
468
+
469
+ // 1. Handle Context
486
470
  if (contextProp?.value?.type === 'ConditionalExpression') {
487
471
  const contextValues = this.resolvePossibleStringValues(contextProp.value)
488
472
  const contextSeparator = this.config.extract.contextSeparator ?? '_'
489
473
 
490
474
  if (contextValues.length > 0) {
491
475
  contextValues.forEach(context => {
492
- this.pluginContext.addKey({ key: `${finalKey}${contextSeparator}${context}`, ns, defaultValue: dv })
476
+ keysWithContext.push({ key: `${finalKey}${contextSeparator}${context}`, ns, defaultValue: dv })
493
477
  })
494
478
  // For dynamic context, also add the base key as a fallback
495
- this.pluginContext.addKey({ key: finalKey, ns, defaultValue: dv })
496
- continue // This key is fully handled, move to the next in the array
479
+ keysWithContext.push({ key: finalKey, ns, defaultValue: dv })
497
480
  }
498
- }
481
+ } else if (contextProp?.value?.type === 'StringLiteral') {
482
+ const contextValue = contextProp.value.value
499
483
 
500
- // 2. Handle Static Context
501
- const contextValue = getObjectPropValue(options, 'context')
502
- if (typeof contextValue === 'string' && contextValue) {
503
484
  const contextSeparator = this.config.extract.contextSeparator ?? '_'
504
- this.pluginContext.addKey({ key: `${finalKey}${contextSeparator}${contextValue}`, ns, defaultValue: dv })
505
- continue // This key is fully handled
485
+ keysWithContext.push({ key: `${finalKey}${contextSeparator}${contextValue}`, ns, defaultValue: dv })
506
486
  }
507
487
 
508
- // 3. Handle Plurals
488
+ // 2. Handle Plurals
509
489
  const hasCount = getObjectPropValue(options, 'count') !== undefined
510
490
  const isOrdinalByOption = getObjectPropValue(options, 'ordinal') === true
511
491
  if (hasCount || isOrdinalByKey) {
512
- // Pass the combined ordinal flag to the handler
513
- this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey)
514
- continue
492
+ // If we have keys with context pluralize them
493
+ if (keysWithContext.length > 0) {
494
+ for (const { key, ns } of keysWithContext) {
495
+ // Pass the combined ordinal flag to the handler
496
+ this.handlePluralKeys(key, ns, options, isOrdinalByOption || isOrdinalByKey)
497
+ }
498
+ } else {
499
+ // Otherwise pluralize the base key
500
+ this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey)
501
+ }
502
+
503
+ continue // This key is fully handled
504
+ }
505
+
506
+ if (keysWithContext.length > 0) {
507
+ keysWithContext.forEach(this.pluginContext.addKey)
508
+
509
+ continue // This key is now fully handled
515
510
  }
516
511
 
517
- // 4. Handle returnObjects
512
+ // 3. Handle returnObjects
518
513
  if (getObjectPropValue(options, 'returnObjects') === true) {
519
514
  this.objectKeys.add(finalKey)
520
515
  // Fall through to add the base key itself
521
516
  }
522
517
  }
523
518
 
524
- // 5. Handle selector API as implicit returnObjects
519
+ // 4. Handle selector API as implicit returnObjects
525
520
  if (isSelectorAPI) {
526
521
  this.objectKeys.add(finalKey)
527
522
  // Fall through to add the base key itself
528
523
  }
529
524
 
530
- // 6. Default case: Add the simple key
525
+ // 5. Default case: Add the simple key
531
526
  this.pluginContext.addKey({ key: finalKey, ns, defaultValue: dv })
532
527
  }
533
528
  }
534
529
 
530
+ /**
531
+ * Processed a call expression to extract keys from the specified argument.
532
+ *
533
+ * @param node - The call expression node
534
+ * @param argIndex - The index of the argument to process
535
+ * @returns An object containing the keys to process and a flag indicating if the selector API is used
536
+ */
537
+ private handleCallExpressionArgument (
538
+ node: CallExpression,
539
+ argIndex: number
540
+ ): { keysToProcess: string[]; isSelectorAPI: boolean } {
541
+ const firstArg = node.arguments[argIndex].expression
542
+ const keysToProcess: string[] = []
543
+ let isSelectorAPI = false
544
+
545
+ if (firstArg.type === 'ArrowFunctionExpression') {
546
+ const key = this.extractKeyFromSelector(firstArg)
547
+ if (key) {
548
+ keysToProcess.push(key)
549
+ isSelectorAPI = true
550
+ }
551
+ } else if (firstArg.type === 'ArrayExpression') {
552
+ for (const element of firstArg.elements) {
553
+ if (element?.expression) {
554
+ keysToProcess.push(...this.resolvePossibleStringValues(element.expression))
555
+ }
556
+ }
557
+ } else {
558
+ keysToProcess.push(...this.resolvePossibleStringValues(firstArg))
559
+ }
560
+
561
+ return {
562
+ keysToProcess: keysToProcess.filter((key) => !!key),
563
+ isSelectorAPI,
564
+ }
565
+ }
566
+
535
567
  /**
536
568
  * Generates plural form keys based on the primary language's plural rules.
537
569
  *
@@ -647,10 +679,41 @@ export class ASTVisitors {
647
679
  const elementName = this.getElementName(node)
648
680
 
649
681
  if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
650
- const extractedKey = extractFromTransComponent(node, this.config)
651
- if (extractedKey) {
652
- // If ns is not explicitly set on the component, try to find it from the `t` prop
653
- if (!extractedKey.ns) {
682
+ const extractedAttributes = extractFromTransComponent(node, this.config)
683
+
684
+ const keysToProcess: string[] = []
685
+
686
+ if (extractedAttributes) {
687
+ if (extractedAttributes.keyExpression) {
688
+ const keyValues = this.resolvePossibleStringValues(extractedAttributes.keyExpression)
689
+ keysToProcess.push(...keyValues)
690
+ } else {
691
+ keysToProcess.push(extractedAttributes.serializedChildren)
692
+ }
693
+
694
+ let extractedKeys: ExtractedKey[]
695
+
696
+ const { contextExpression, optionsNode, defaultValue, hasCount, isOrdinal, serializedChildren } = extractedAttributes
697
+
698
+ // If ns is not explicitly set on the component, try to find it from the key
699
+ // or the `t` prop
700
+ if (!extractedAttributes.ns) {
701
+ extractedKeys = keysToProcess.map(key => {
702
+ const nsSeparator = this.config.extract.nsSeparator ?? ':'
703
+ let ns: string | undefined
704
+
705
+ // If the key contains a namespace separator, it takes precedence
706
+ // over the default t ns value
707
+ if (nsSeparator && key.includes(nsSeparator)) {
708
+ let parts: string[]
709
+ ([ns, ...parts] = key.split(nsSeparator))
710
+
711
+ key = parts.join(nsSeparator)
712
+ }
713
+
714
+ return { key, ns, defaultValue: defaultValue || serializedChildren, hasCount, isOrdinal }
715
+ })
716
+
654
717
  const tProp = node.opening.attributes?.find(
655
718
  attr =>
656
719
  attr.type === 'JSXAttribute' &&
@@ -667,54 +730,160 @@ export class ASTVisitors {
667
730
  const tIdentifier = tProp.value.expression.value
668
731
  const scopeInfo = this.getVarFromScope(tIdentifier)
669
732
  if (scopeInfo?.defaultNs) {
670
- extractedKey.ns = scopeInfo.defaultNs
733
+ extractedKeys.forEach(key => {
734
+ if (!key.ns) {
735
+ key.ns = scopeInfo.defaultNs
736
+ }
737
+ })
671
738
  }
672
739
  }
740
+ } else {
741
+ const { ns } = extractedAttributes
742
+ extractedKeys = keysToProcess.map(key => {
743
+ return { key, ns, defaultValue: defaultValue || serializedChildren, hasCount, isOrdinal, }
744
+ })
673
745
  }
674
746
 
675
- // Apply defaultNS from config if no namespace was found on the component
676
- if (!extractedKey.ns) {
677
- extractedKey.ns = this.config.extract.defaultNS
678
- }
747
+ extractedKeys.forEach(key => {
748
+ // Apply defaultNS from config if no namespace was found on the component and
749
+ // the key does not contain a namespace prefix
750
+ if (!key.ns) {
751
+ key.ns = this.config.extract.defaultNS
752
+ }
753
+ })
754
+
755
+ // Handle the combination of context and count
756
+ if (contextExpression && hasCount) {
757
+ // Find isOrdinal prop on the <Trans> component
758
+ const ordinalAttr = node.opening.attributes?.find(
759
+ (attr) =>
760
+ attr.type === 'JSXAttribute' &&
761
+ attr.name.type === 'Identifier' &&
762
+ attr.name.value === 'ordinal'
763
+ )
764
+ const isOrdinal = !!ordinalAttr
679
765
 
680
- if (extractedKey.contextExpression) {
681
- const contextValues = this.resolvePossibleStringValues(extractedKey.contextExpression)
766
+ const contextValues = this.resolvePossibleStringValues(contextExpression)
682
767
  const contextSeparator = this.config.extract.contextSeparator ?? '_'
683
768
 
769
+ // Generate all combinations of context and plural forms
684
770
  if (contextValues.length > 0) {
771
+ // Generate base plural forms (no context)
772
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
773
+
774
+ // Generate context + plural combinations
685
775
  for (const context of contextValues) {
686
- this.pluginContext.addKey({ key: `${extractedKey.key}${contextSeparator}${context}`, ns: extractedKey.ns, defaultValue: extractedKey.defaultValue })
776
+ for (const extractedKey of extractedKeys) {
777
+ const contextKey = `${extractedKey.key}${contextSeparator}${context}`
778
+ this.generatePluralKeysForTrans(contextKey, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode)
779
+ }
780
+ }
781
+ } else {
782
+ // Fallback to just plural forms if context resolution fails
783
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
784
+ }
785
+ } else if (contextExpression) {
786
+ const contextValues = this.resolvePossibleStringValues(contextExpression)
787
+ const contextSeparator = this.config.extract.contextSeparator ?? '_'
788
+
789
+ if (contextValues.length > 0) {
790
+ for (const context of contextValues) {
791
+ for (const { key, ns, defaultValue } of extractedKeys) {
792
+ this.pluginContext.addKey({ key: `${key}${contextSeparator}${context}`, ns, defaultValue })
793
+ }
687
794
  }
688
795
  // Only add the base key as a fallback if the context is dynamic (i.e., not a simple string).
689
- if (extractedKey.contextExpression.type !== 'StringLiteral') {
690
- this.pluginContext.addKey(extractedKey)
796
+ if (contextExpression.type !== 'StringLiteral') {
797
+ extractedKeys.forEach(this.pluginContext.addKey)
691
798
  }
692
799
  }
693
- } else if (extractedKey.hasCount) {
800
+ } else if (hasCount) {
694
801
  // Find isOrdinal prop on the <Trans> component
695
802
  const ordinalAttr = node.opening.attributes?.find(
696
803
  (attr) =>
697
804
  attr.type === 'JSXAttribute' &&
698
- attr.name.type === 'Identifier' &&
699
- attr.name.value === 'ordinal'
805
+ attr.name.type === 'Identifier' &&
806
+ attr.name.value === 'ordinal'
700
807
  )
701
808
  const isOrdinal = !!ordinalAttr
702
809
 
703
- // If tOptions are provided, use the advanced plural handler
704
- const optionsNode = extractedKey.optionsNode ?? { type: 'ObjectExpression', properties: [], span: { start: 0, end: 0, ctxt: 0 } }
810
+ extractedKeys.forEach(extractedKey => this.generatePluralKeysForTrans(extractedKey.key, extractedKey.defaultValue, extractedKey.ns, isOrdinal, optionsNode))
811
+ } else {
812
+ extractedKeys.forEach(this.pluginContext.addKey)
813
+ }
814
+ }
815
+ }
816
+ }
705
817
 
706
- // Inject the defaultValue from children into the options object
707
- optionsNode.properties.push({
708
- type: 'KeyValueProperty',
709
- key: { type: 'Identifier', value: 'defaultValue', optional: false, span: { start: 0, end: 0, ctxt: 0 } },
710
- value: { type: 'StringLiteral', value: extractedKey.defaultValue!, span: { start: 0, end: 0, ctxt: 0 } }
711
- })
818
+ /**
819
+ * Generates plural keys for Trans components, with support for tOptions plural defaults.
820
+ *
821
+ * @param key - Base key name for pluralization
822
+ * @param defaultValue - Default value for the keys
823
+ * @param ns - Namespace for the keys
824
+ * @param isOrdinal - Whether to generate ordinal plural forms
825
+ * @param optionsNode - Optional tOptions object expression for plural-specific defaults
826
+ *
827
+ * @private
828
+ */
829
+ private generatePluralKeysForTrans (key: string, defaultValue: string | undefined, ns: string | undefined, isOrdinal: boolean, optionsNode?: ObjectExpression): void {
830
+ try {
831
+ const type = isOrdinal ? 'ordinal' : 'cardinal'
832
+ const pluralCategories = new Intl.PluralRules(this.config.extract?.primaryLanguage, { type }).resolvedOptions().pluralCategories
833
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
834
+
835
+ // Get plural-specific default values from tOptions if available
836
+ let otherDefault: string | undefined
837
+ let ordinalOtherDefault: string | undefined
838
+
839
+ if (optionsNode) {
840
+ otherDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}other`) as string | undefined
841
+ ordinalOtherDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`) as string | undefined
842
+ }
843
+
844
+ for (const category of pluralCategories) {
845
+ // Look for the most specific default value (e.g., defaultValue_ordinal_one)
846
+ const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`
847
+ const specificDefault = optionsNode ? getObjectPropValue(optionsNode, specificDefaultKey) as string | undefined : undefined
712
848
 
713
- this.handlePluralKeys(extractedKey.key, extractedKey.ns, optionsNode, isOrdinal)
849
+ // Determine the final default value using a clear fallback chain
850
+ let finalDefaultValue: string | undefined
851
+ if (typeof specificDefault === 'string') {
852
+ // 1. Use the most specific default if it exists (e.g., defaultValue_one)
853
+ finalDefaultValue = specificDefault
854
+ } else if (category === 'one' && typeof defaultValue === 'string') {
855
+ // 2. SPECIAL CASE: The 'one' category falls back to the main default value (children content)
856
+ finalDefaultValue = defaultValue
857
+ } else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
858
+ // 3a. Other ordinal categories fall back to 'defaultValue_ordinal_other'
859
+ finalDefaultValue = ordinalOtherDefault
860
+ } else if (!isOrdinal && typeof otherDefault === 'string') {
861
+ // 3b. Other cardinal categories fall back to 'defaultValue_other'
862
+ finalDefaultValue = otherDefault
863
+ } else if (typeof defaultValue === 'string') {
864
+ // 4. If no '_other' is found, all categories can fall back to the main default value
865
+ finalDefaultValue = defaultValue
714
866
  } else {
715
- this.pluginContext.addKey(extractedKey)
867
+ // 5. Final fallback to the base key itself
868
+ finalDefaultValue = key
716
869
  }
870
+
871
+ const finalKey = isOrdinal
872
+ ? `${key}${pluralSeparator}ordinal${pluralSeparator}${category}`
873
+ : `${key}${pluralSeparator}${category}`
874
+
875
+ this.pluginContext.addKey({
876
+ key: finalKey,
877
+ ns,
878
+ defaultValue: finalDefaultValue,
879
+ hasCount: true,
880
+ isOrdinal
881
+ })
717
882
  }
883
+ } catch (e) {
884
+ this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`)
885
+ // Fallback to a simple key if Intl API fails
886
+ this.pluginContext.addKey({ key, ns, defaultValue })
718
887
  }
719
888
  }
720
889
 
@@ -810,7 +979,7 @@ export class ASTVisitors {
810
979
  * determined statically from the AST.
811
980
  *
812
981
  * Supports:
813
- * - StringLiteral -> single value
982
+ * - StringLiteral -> single value (filtered to exclude empty strings for context)
814
983
  * - ConditionalExpression (ternary) -> union of consequent and alternate resolved values
815
984
  * - The identifier `undefined` -> empty array
816
985
  *
@@ -823,7 +992,8 @@ export class ASTVisitors {
823
992
  */
824
993
  private resolvePossibleStringValues (expression: Expression): string[] {
825
994
  if (expression.type === 'StringLiteral') {
826
- return [expression.value]
995
+ // Filter out empty strings as they should be treated as "no context" like i18next does
996
+ return expression.value ? [expression.value] : []
827
997
  }
828
998
 
829
999
  if (expression.type === 'ConditionalExpression') { // This is a ternary operator
@@ -1,7 +1,33 @@
1
- import type { JSXElement } from '@swc/core'
2
- import type { ExtractedKey, I18nextToolkitConfig } from '../../types'
1
+ import type { Expression, JSXElement, ObjectExpression, Property } from '@swc/core'
2
+ import type { I18nextToolkitConfig } from '../../types'
3
3
  import { getObjectProperty, getObjectPropValue } from './ast-utils'
4
4
 
5
+ export interface ExtractedJSXAttributes {
6
+ /** holds the raw key expression from the AST */
7
+ keyExpression?: Expression;
8
+
9
+ /** holds the serialized JSX children from the AST */
10
+ serializedChildren: string;
11
+
12
+ /** Default value to use in the primary language */
13
+ defaultValue?: string;
14
+
15
+ /** Namespace this key belongs to (if defined on <Trans />) */
16
+ ns?: string;
17
+
18
+ /** Whether this key is used with pluralization (count parameter) */
19
+ hasCount?: boolean;
20
+
21
+ /** Whether this key is used with ordinal pluralization */
22
+ isOrdinal?: boolean;
23
+
24
+ /** AST node for options object, used for advanced plural handling in Trans */
25
+ optionsNode?: ObjectExpression;
26
+
27
+ /** hold the raw context expression from the AST */
28
+ contextExpression?: Expression;
29
+ }
30
+
5
31
  /**
6
32
  * Extracts translation keys from JSX Trans components.
7
33
  *
@@ -27,13 +53,14 @@ import { getObjectProperty, getObjectPropValue } from './ast-utils'
27
53
  * const result = extractFromTransComponent(jsxNode, config)
28
54
  * // Returns: {
29
55
  * // key: 'welcome.title',
56
+ * // keyExpression: { ... },
30
57
  * // ns: 'home',
31
58
  * // defaultValue: 'Welcome!',
32
59
  * // hasCount: false
33
60
  * // }
34
61
  * ```
35
62
  */
36
- export function extractFromTransComponent (node: JSXElement, config: I18nextToolkitConfig): ExtractedKey | null {
63
+ export function extractFromTransComponent (node: JSXElement, config: I18nextToolkitConfig): ExtractedJSXAttributes | null {
37
64
  const i18nKeyAttr = node.opening.attributes?.find(
38
65
  (attr) =>
39
66
  attr.type === 'JSXAttribute' &&
@@ -54,7 +81,23 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
54
81
  attr.name.type === 'Identifier' &&
55
82
  attr.name.value === 'count'
56
83
  )
57
- const hasCount = !!countAttr
84
+
85
+ const valuesAttr = node.opening.attributes?.find(
86
+ (attr) => attr.type === 'JSXAttribute' && attr.name.type === 'Identifier' && attr.name.value === 'values'
87
+ )
88
+
89
+ // Find the 'count' property in the 'values' object if count={...} is not defined
90
+ let valuesCountProperty: Property | undefined
91
+ if (
92
+ !countAttr &&
93
+ valuesAttr?.type === 'JSXAttribute' &&
94
+ valuesAttr.value?.type === 'JSXExpressionContainer' &&
95
+ valuesAttr.value.expression.type === 'ObjectExpression'
96
+ ) {
97
+ valuesCountProperty = getObjectProperty(valuesAttr.value.expression, 'count')
98
+ }
99
+
100
+ const hasCount = !!countAttr || !!valuesCountProperty
58
101
 
59
102
  const tOptionsAttr = node.opening.attributes?.find(
60
103
  (attr) =>
@@ -66,27 +109,25 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
66
109
  ? tOptionsAttr.value.expression
67
110
  : undefined
68
111
 
112
+ // Find isOrdinal prop on the <Trans> component
113
+ const ordinalAttr = node.opening.attributes?.find(
114
+ (attr) =>
115
+ attr.type === 'JSXAttribute' &&
116
+ attr.name.type === 'Identifier' &&
117
+ attr.name.value === 'ordinal'
118
+ )
119
+ const isOrdinal = !!ordinalAttr
120
+
69
121
  const contextAttr = node.opening.attributes?.find(
70
122
  (attr) =>
71
123
  attr.type === 'JSXAttribute' &&
72
- attr.name.type === 'Identifier' &&
73
- attr.name.value === 'context'
124
+ attr.name.type === 'Identifier' &&
125
+ attr.name.value === 'context'
74
126
  )
75
127
  let contextExpression = (contextAttr?.type === 'JSXAttribute' && contextAttr.value?.type === 'JSXExpressionContainer')
76
128
  ? contextAttr.value.expression
77
129
  : undefined
78
130
 
79
- let key: string
80
- if (i18nKeyAttr?.type === 'JSXAttribute' && i18nKeyAttr.value?.type === 'StringLiteral') {
81
- key = i18nKeyAttr.value.value
82
- } else {
83
- key = serializeJSXChildren(node.children, config)
84
- }
85
-
86
- if (!key) {
87
- return null
88
- }
89
-
90
131
  // 1. Prioritize direct props for 'ns' and 'context'
91
132
  const nsAttr = node.opening.attributes?.find(attr => attr.type === 'JSXAttribute' && attr.name.type === 'Identifier' && attr.name.value === 'ns')
92
133
  let ns: string | undefined
@@ -109,14 +150,37 @@ export function extractFromTransComponent (node: JSXElement, config: I18nextTool
109
150
  }
110
151
  }
111
152
 
153
+ const serialized = serializeJSXChildren(node.children, config)
154
+
112
155
  let defaultValue = config.extract.defaultValue || ''
113
156
  if (defaultsAttr?.type === 'JSXAttribute' && defaultsAttr.value?.type === 'StringLiteral') {
114
157
  defaultValue = defaultsAttr.value.value
115
- } else {
116
- defaultValue = serializeJSXChildren(node.children, config)
117
158
  }
118
159
 
119
- return { key, ns, defaultValue: defaultValue || key, hasCount, contextExpression, optionsNode }
160
+ let keyExpression: Expression | undefined
161
+ if (i18nKeyAttr?.type === 'JSXAttribute') {
162
+ if (i18nKeyAttr.value?.type === 'StringLiteral') {
163
+ keyExpression = i18nKeyAttr.value
164
+ } else if (
165
+ i18nKeyAttr.value?.type === 'JSXExpressionContainer' &&
166
+ i18nKeyAttr.value.expression.type !== 'JSXEmptyExpression'
167
+ ) {
168
+ keyExpression = i18nKeyAttr.value.expression
169
+ }
170
+
171
+ if (!keyExpression) return null
172
+ }
173
+
174
+ return {
175
+ keyExpression,
176
+ serializedChildren: serialized,
177
+ ns,
178
+ defaultValue,
179
+ hasCount,
180
+ isOrdinal,
181
+ contextExpression,
182
+ optionsNode,
183
+ }
120
184
  }
121
185
 
122
186
  /**
@@ -14,7 +14,7 @@ import type { ObjectExpression } from '@swc/core';
14
14
  * @param propName - The property name to locate
15
15
  * @returns The matching KeyValueProperty node if found, otherwise undefined.
16
16
  */
17
- export declare function getObjectProperty(object: ObjectExpression, propName: string): any;
17
+ export declare function getObjectProperty(object: ObjectExpression, propName: string): import("@swc/types").KeyValueProperty | undefined;
18
18
  /**
19
19
  * Extracts string value from object property.
20
20
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAEjD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,CASlF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAWrH"}
1
+ {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/ast-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAEjD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,qDAU5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAWrH"}
@@ -152,6 +152,14 @@ export declare class ASTVisitors {
152
152
  * @private
153
153
  */
154
154
  private handleCallExpression;
155
+ /**
156
+ * Processed a call expression to extract keys from the specified argument.
157
+ *
158
+ * @param node - The call expression node
159
+ * @param argIndex - The index of the argument to process
160
+ * @returns An object containing the keys to process and a flag indicating if the selector API is used
161
+ */
162
+ private handleCallExpressionArgument;
155
163
  /**
156
164
  * Generates plural form keys based on the primary language's plural rules.
157
165
  *
@@ -188,6 +196,18 @@ export declare class ASTVisitors {
188
196
  * @private
189
197
  */
190
198
  private handleJSXElement;
199
+ /**
200
+ * Generates plural keys for Trans components, with support for tOptions plural defaults.
201
+ *
202
+ * @param key - Base key name for pluralization
203
+ * @param defaultValue - Default value for the keys
204
+ * @param ns - Namespace for the keys
205
+ * @param isOrdinal - Whether to generate ordinal plural forms
206
+ * @param optionsNode - Optional tOptions object expression for plural-specific defaults
207
+ *
208
+ * @private
209
+ */
210
+ private generatePluralKeysForTrans;
191
211
  /**
192
212
  * Extracts element name from JSX opening tag.
193
213
  *
@@ -223,7 +243,7 @@ export declare class ASTVisitors {
223
243
  * determined statically from the AST.
224
244
  *
225
245
  * Supports:
226
- * - StringLiteral -> single value
246
+ * - StringLiteral -> single value (filtered to exclude empty strings for context)
227
247
  * - ConditionalExpression (ternary) -> union of consequent and alternate resolved values
228
248
  * - The identifier `undefined` -> empty array
229
249
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAA+G,MAAM,WAAW,CAAA;AACpJ,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAqB9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,UAAU,CAAoC;IAE/C,UAAU,cAAoB;IAErC;;;;;;OAMG;gBAED,MAAM,EAAE,oBAAoB,EAC5B,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM;IAOhB;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAMjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IAsDZ;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAIlB;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAIjB;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAMrB;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IASvB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,wBAAwB;IAmChC;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,8BAA8B;IAwDtC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IAoBjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,oBAAoB;IAqK5B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,gBAAgB;IA4DxB;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;IA2ExB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,2BAA2B;IAmBnC;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,OAAO,CAAC,eAAe;CA2BxB"}
1
+ {"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAA+G,MAAM,WAAW,CAAA;AACpJ,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAgB,MAAM,aAAa,CAAA;AAqB5F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,UAAU,CAAoC;IAE/C,UAAU,cAAoB;IAErC;;;;;;OAMG;gBAED,MAAM,EAAE,oBAAoB,EAC5B,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM;IAOhB;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAMjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IAsDZ;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAIlB;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAIjB;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAMrB;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IASvB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,wBAAwB;IAmChC;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,8BAA8B;IAwDtC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IAoBjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,oBAAoB;IAgK5B;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,gBAAgB;IA4DxB;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;IA4IxB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,0BAA0B;IA6DlC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,2BAA2B;IAoBnC;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,OAAO,CAAC,eAAe;CA2BxB"}
@@ -1,5 +1,23 @@
1
- import type { JSXElement } from '@swc/core';
2
- import type { ExtractedKey, I18nextToolkitConfig } from '../../types';
1
+ import type { Expression, JSXElement, ObjectExpression } from '@swc/core';
2
+ import type { I18nextToolkitConfig } from '../../types';
3
+ export interface ExtractedJSXAttributes {
4
+ /** holds the raw key expression from the AST */
5
+ keyExpression?: Expression;
6
+ /** holds the serialized JSX children from the AST */
7
+ serializedChildren: string;
8
+ /** Default value to use in the primary language */
9
+ defaultValue?: string;
10
+ /** Namespace this key belongs to (if defined on <Trans />) */
11
+ ns?: string;
12
+ /** Whether this key is used with pluralization (count parameter) */
13
+ hasCount?: boolean;
14
+ /** Whether this key is used with ordinal pluralization */
15
+ isOrdinal?: boolean;
16
+ /** AST node for options object, used for advanced plural handling in Trans */
17
+ optionsNode?: ObjectExpression;
18
+ /** hold the raw context expression from the AST */
19
+ contextExpression?: Expression;
20
+ }
3
21
  /**
4
22
  * Extracts translation keys from JSX Trans components.
5
23
  *
@@ -25,11 +43,12 @@ import type { ExtractedKey, I18nextToolkitConfig } from '../../types';
25
43
  * const result = extractFromTransComponent(jsxNode, config)
26
44
  * // Returns: {
27
45
  * // key: 'welcome.title',
46
+ * // keyExpression: { ... },
28
47
  * // ns: 'home',
29
48
  * // defaultValue: 'Welcome!',
30
49
  * // hasCount: false
31
50
  * // }
32
51
  * ```
33
52
  */
34
- export declare function extractFromTransComponent(node: JSXElement, config: I18nextToolkitConfig): ExtractedKey | null;
53
+ export declare function extractFromTransComponent(node: JSXElement, config: I18nextToolkitConfig): ExtractedJSXAttributes | null;
35
54
  //# sourceMappingURL=jsx-parser.d.ts.map
@@ -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,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,yBAAyB,CAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,oBAAoB,GAAG,YAAY,GAAG,IAAI,CAoF9G"}
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,CAyHxH"}
package/vitest.config.ts DELETED
@@ -1,17 +0,0 @@
1
- // eslint-disable-next-line import/no-unresolved
2
- import swc from 'unplugin-swc'
3
- import { defineConfig } from 'vitest/config'
4
-
5
- export default defineConfig({
6
- test: {
7
- // Make vitest globals like `describe` and `it` available without importing
8
- globals: true,
9
- // Look for test files in the entire project
10
- root: './',
11
- coverage: {
12
- include: ['src/**/*'],
13
- exclude: ['src/types.ts', 'src/index.ts', 'src/extractor/index.ts']
14
- }
15
- },
16
- plugins: [swc.vite()]
17
- })