i18next-cli 0.9.17 → 0.9.19

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,10 +5,20 @@ 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.0.0](https://github.com/i18next/i18next-cli/compare/v0.9.17...v1.0.0) - 2025-xx-yy
8
+ ## [1.0.0](https://github.com/i18next/i18next-cli/compare/v0.9.19...v1.0.0) - 2025-xx-yy
9
9
 
10
10
  - not yet released
11
11
 
12
+ ## [0.9.19](https://github.com/i18next/i18next-cli/compare/v0.9.18...v0.9.19) - 2025-09-30
13
+
14
+ - **Status Command:** Greatly improved the accuracy of the translation status report for plural keys. The command now calculates the total number of required keys for each language based on that specific language's pluralization rules (e.g., 2 forms for English, 6 for Arabic), rather than incorrectly using the primary language's rules for all locales.
15
+ - **Extractor:** Corrected the logic for ordinal plurals and default value fallbacks. The extractor now recognizes keys with an `_ordinal` suffix as ordinal plurals. The fallback hierarchy for all plural default values (e.g., `defaultValue_one`, `defaultValue_other`) now correctly matches i18next's behavior.
16
+
17
+ ## [0.9.18](https://github.com/i18next/i18next-cli/compare/v0.9.17...v0.9.18) - 2025-09-30
18
+
19
+ - **Extractor:** Fixed a bug where translation keys were not found in custom functions that were part of an object (e.g., `i18n.t(...)`). The `functions` configuration option now correctly handles member expressions in addition to simple function names. [#10](https://github.com/i18next/i18next-cli/issues/10)
20
+ - **Extractor:** Fixed a critical bug where the `extract` command would incorrectly overwrite existing translations in secondary languages when using the `mergeNamespaces: true` option. The fix also resolves a related issue where unused keys were not being correctly pruned from the primary language file in the same scenario. The translation manager logic is now more robust for both merged and non-merged configurations. [#11](https://github.com/i18next/i18next-cli/issues/11)
21
+
12
22
  ## [0.9.17](https://github.com/i18next/i18next-cli/compare/v0.9.16...v0.9.17) - 2025-09-29
13
23
 
14
24
  - **Extractor:** Fixed a bug where namespace and `keyPrefix` information from custom `useTranslationNames` hooks was ignored when the `t` function was assigned directly to a variable (e.g., `let t = myHook()`). The extractor now correctly handles this pattern in addition to destructuring assignments. [#9](https://github.com/i18next/i18next-cli/issues/9)
package/README.md CHANGED
@@ -244,6 +244,8 @@ npx i18next-cli locize-sync [options]
244
244
 
245
245
  The configuration file supports both TypeScript (`.ts`) and JavaScript (`.js`) formats. Use the `defineConfig` helper for type safety and IntelliSense.
246
246
 
247
+ > **💡 No Installation Required?** If you don't want to install `i18next-cli` as a dependency, you can skip the `defineConfig` helper and return a plain JavaScript object or JSON instead. The `defineConfig` function is purely for TypeScript support and doesn't affect functionality.
248
+
247
249
  ### Basic Configuration
248
250
 
249
251
  ```typescript
@@ -259,6 +261,19 @@ export default defineConfig({
259
261
  });
260
262
  ```
261
263
 
264
+ **Alternative without local installation:**
265
+
266
+ ```javascript
267
+ // i18next.config.js
268
+ export default {
269
+ locales: ['en', 'de', 'fr'],
270
+ extract: {
271
+ input: ['src/**/*.{ts,tsx,js,jsx}'],
272
+ output: 'locales/{{language}}/{{namespace}}.json',
273
+ },
274
+ };
275
+ ```
276
+
262
277
  ### Advanced Configuration
263
278
 
264
279
  ```typescript
package/dist/cjs/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("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("0.9.17"),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.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a);e.ci&&t&&(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(),e.watch){console.log("\nWatching for changes...");t.watch(await o.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 o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{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 n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(n.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").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),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.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();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 i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await d.runLinter(e)}),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"),o=require("glob"),n=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("0.9.19"),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.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a);e.ci&&t&&(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(),e.watch){console.log("\nWatching for changes...");t.watch(await o.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 o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{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 n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(n.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").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),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.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();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 i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await d.runLinter(e)}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
@@ -1 +1 @@
1
- "use strict";var t=require("node:path"),e=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function a(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}exports.getTranslations=async function(n,r,o){const c=o.extract.defaultNS??"translation",i=o.extract.keySeparator??".",l=[...o.extract.preservePatterns||[]],u=o.extract.mergeNamespaces??!1;for(const t of r)l.push(`${t}.*`);const p=l.map(a);o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(t=>t!==o?.extract?.primaryLanguage);const g=o.extract.primaryLanguage,f=new Map;for(const t of n.values()){const e=t.ns||c;f.has(e)||f.set(e,[]),f.get(e).push(t)}const d=[];for(const a of o.locales){const n={},r={};for(const[c,l]of f.entries()){const f=s.getOutputPath(o.extract.output,a,u?void 0:c),x=t.resolve(process.cwd(),f),y=await s.loadTranslationFile(x)||{},h={},N=e.getNestedKeys(y,i);for(const t of N)if(p.some(e=>e.test(t))){const s=e.getNestedValue(y,t,i);e.setNestedValue(h,t,s,i)}const m=!1===o.extract.sort?l:[...l].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:s}of m){const n=e.getNestedValue(y,t,i)??(a===g?s:o.extract.defaultValue??"");e.setNestedValue(h,t,n,i)}if(u)n[c]=h,Object.keys(y).length>0&&(r[c]=y);else{const t=y?JSON.stringify(y,null,o.extract.indentation??2):"",e=JSON.stringify(h,null,o.extract.indentation??2);d.push({path:x,updated:e!==t,newTranslations:h,existingTranslations:y})}}if(u){const e=s.getOutputPath(o.extract.output,a),c=t.resolve(process.cwd(),e),i=Object.keys(r).length>0?JSON.stringify(r,null,o.extract.indentation??2):"",l=JSON.stringify(n,null,o.extract.indentation??2);d.push({path:c,updated:l!==i,newTranslations:n,existingTranslations:r})}}return d};
1
+ "use strict";var t=require("node:path"),e=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function a(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}exports.getTranslations=async function(r,n,o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(t=>t!==o?.extract?.primaryLanguage);const c=o.extract.primaryLanguage,l=o.extract.defaultNS??"translation",u=o.extract.keySeparator??".",i=[...o.extract.preservePatterns||[]],p=o.extract.mergeNamespaces??!1,f=o.extract.indentation??2;for(const t of n)i.push(`${t}.*`);const g=i.map(a),d=new Map;for(const t of r.values()){const e=t.ns||l;d.has(e)||d.set(e,[]),d.get(e).push(t)}const x=[];for(const a of o.locales){const r=p?await s.loadTranslationFile(t.resolve(process.cwd(),s.getOutputPath(o.extract.output,a)))||{}:null,n={};for(const[l,i]of d.entries()){const d=s.getOutputPath(o.extract.output,a,p?void 0:l),y=t.resolve(process.cwd(),d),h=p?r?.[l]||{}:await s.loadTranslationFile(y)||{},N={},w=e.getNestedKeys(h,u);for(const t of w)if(g.some(e=>e.test(t))){const s=e.getNestedValue(h,t,u);e.setNestedValue(N,t,s,u)}const m=!1===o.extract.sort?i:[...i].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:s}of m){const r=e.getNestedValue(h,t,u)??(a===c?s:o.extract.defaultValue??"");e.setNestedValue(N,t,r,u)}if(p)n[l]=N;else{const t=JSON.stringify(h,null,f),e=JSON.stringify(N,null,f);x.push({path:y,updated:e!==t,newTranslations:N,existingTranslations:h})}}if(p){const e=s.getOutputPath(o.extract.output,a),c=t.resolve(process.cwd(),e),l=JSON.stringify(r,null,f),u=JSON.stringify(n,null,f);x.push({path:c,updated:u!==l,newTranslations:n,existingTranslations:r||{}})}}return x};
@@ -1 +1 @@
1
- "use strict";var e=require("./jsx-parser.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&&n.type&&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,t,n){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 i=t.arguments?.[n.nsArg]?.expression;let s;"StringLiteral"===i?.type?s=i.value:"ArrayExpression"===i?.type&&"StringLiteral"===i.elements[0]?.expression.type&&(s=i.elements[0].expression.value);const a=t.arguments?.[n.keyPrefixArg]?.expression;let o;if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"keyPrefix");o="string"==typeof e?e:void 0}this.setVarInScope(r,{defaultNs:s,keyPrefix:o})}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 t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||["t"]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const r=e.arguments[0].expression,i=[];if("StringLiteral"===r.type)i.push(r.value);else if("ArrowFunctionExpression"===r.type){const e=this.extractKeyFromSelector(r);e&&i.push(e)}else if("ArrayExpression"===r.type)for(const e of r.elements)"StringLiteral"===e?.expression.type&&i.push(e.expression.value);if(0===i.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<i.length;e++){let t,r=i[e];if(a){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&r.includes(s)){const e=r.split(s);t=e.shift(),r=e.join(s)}t||(t=this.config.extract.defaultNS);let o=r;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${r}`}const p=e===i.length-1&&l||r;if(a){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),r=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${r}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,t,a);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const r=!0===this.getObjectPropValue(n,"ordinal"),i=r?"ordinal":"cardinal",s=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:i}).resolvedOptions().pluralCategories,a=this.config.extract.pluralSeparator??"_",o=this.getObjectPropValue(n,"defaultValue"),l=this.getObjectPropValue(n,"defaultValue_other");for(const i of s){const s=r?`defaultValue_ordinal_${i}`:`defaultValue_${i}`,p=this.getObjectPropValue(n,s);let u;u="string"==typeof p?p:"one"===i&&"string"==typeof o?o:"string"==typeof l?l:"string"==typeof o?o:e;const c=r?`${e}${a}ordinal${a}${i}`:`${e}${a}${i}`;this.pluginContext.addKey({key:c,ns:t,defaultValue:u,hasCount:!0})}}catch(r){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const i=this.getObjectPropValue(n,"defaultValue");this.pluginContext.addKey({key:e,ns:t,defaultValue:"string"==typeof i?i: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});this.pluginContext.addKey(n)}}else n.hasCount?this.handleSimplePluralKeys(n.key,n.defaultValue,n.ns):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(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}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,[]}getObjectProperty(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))}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}}}};
1
+ "use strict";var e=require("./jsx-parser.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&&n.type&&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,t,n){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 i=t.arguments?.[n.nsArg]?.expression;let s;"StringLiteral"===i?.type?s=i.value:"ArrayExpression"===i?.type&&"StringLiteral"===i.elements[0]?.expression.type&&(s=i.elements[0].expression.value);const a=t.arguments?.[n.keyPrefixArg]?.expression;let o;if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"keyPrefix");o="string"==typeof e?e:void 0}this.setVarInScope(r,{defaultNs:s,keyPrefix:o})}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 t=this.getFunctionName(e.callee);if(!t)return;const n=this.getVarFromScope(t);if(!((this.config.extract.functions||["t"]).includes(t)||void 0!==n)||0===e.arguments.length)return;const r=e.arguments[0].expression,i=[];if("StringLiteral"===r.type)i.push(r.value);else if("ArrowFunctionExpression"===r.type){const e=this.extractKeyFromSelector(r);e&&i.push(e)}else if("ArrayExpression"===r.type)for(const e of r.elements)"StringLiteral"===e?.expression.type&&i.push(e.expression.value);if(0===i.length)return;let s=!1;const a=this.config.extract.pluralSeparator??"_";for(let e=0;e<i.length;e++)i[e].endsWith(`${a}ordinal`)&&(s=!0,i[e]=i[e].slice(0,-8));let o,l;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?l=t:"StringLiteral"===t.type&&(o=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(l=t)}const p=l?this.getObjectPropValue(l,"defaultValue"):void 0,u="string"==typeof p?p:o;for(let e=0;e<i.length;e++){let t,r=i[e];if(l){const e=this.getObjectPropValue(l,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const a=this.config.extract.nsSeparator??":";if(!t&&a&&r.includes(a)){const e=r.split(a);t=e.shift(),r=e.join(a)}t||(t=this.config.extract.defaultNS);let o=r;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${r}`}const p=e===i.length-1&&u||r;if(l){const e=this.getObjectProperty(l,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),r=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${r}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(l,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}const r=void 0!==this.getObjectPropValue(l,"count"),i=!0===this.getObjectPropValue(l,"ordinal");if(r||s){this.handlePluralKeys(o,t,l,i||s);continue}!0===this.getObjectPropValue(l,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n,r){try{const i=r?"ordinal":"cardinal",s=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:i}).resolvedOptions().pluralCategories,a=this.config.extract.pluralSeparator??"_",o=this.getObjectPropValue(n,"defaultValue"),l=this.getObjectPropValue(n,"defaultValue_other"),p=this.getObjectPropValue(n,"defaultValue_ordinal_other");for(const i of s){const s=r?`defaultValue_ordinal_${i}`:`defaultValue_${i}`,u=this.getObjectPropValue(n,s);let c;c="string"==typeof u?u:"one"===i&&"string"==typeof o?o:r&&"string"==typeof p?p:r||"string"!=typeof l?"string"==typeof o?o:e:l;const f=r?`${e}${a}ordinal${a}${i}`:`${e}${a}${i}`;this.pluginContext.addKey({key:f,ns:t,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 i=this.getObjectPropValue(n,"defaultValue");this.pluginContext.addKey({key:e,ns:t,defaultValue:"string"==typeof i?i: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});this.pluginContext.addKey(n)}}else n.hasCount?this.handleSimplePluralKeys(n.key,n.defaultValue,n.ns):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(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}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,[]}getObjectProperty(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))}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}return"Identifier"!==n.type?null:(t.unshift(n.value),t.join("."))}return null}};
@@ -1 +1 @@
1
- "use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),a=require("./extractor/core/key-finder.js"),s=require("./utils/nested-object.js"),n=require("./utils/file-utils.js");function l(o,t,a){const s=a>0?Math.round(t/a*100):100,n=r(s);console.log(`${e.bold(o)}: ${n} ${s}% (${t}/${a})`)}function r(o){const t=Math.round(o/100*20),a=20-t;return`[${e.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function c(){console.log(e.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${e.cyan("npx i18next-cli locize-migrate")} to get started.`)}exports.runStatus=async function(i,u={}){i.extract.primaryLanguage||=i.locales[0]||"en",i.extract.secondaryLanguages||=i.locales.filter(e=>e!==i?.extract?.primaryLanguage);const d=o("Analyzing project localization status...\n").start();try{const o=await async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(o=>o!==e?.extract?.primaryLanguage);const{allKeys:o}=await a.findKeys(e),{secondaryLanguages:l,keySeparator:r=".",defaultNS:c="translation",mergeNamespaces:i=!1}=e.extract,u=new Map;for(const e of o.values()){const o=e.ns||c;u.has(o)||u.set(o,[]),u.get(o).push(e)}const d={totalKeys:o.size,keysByNs:u,locales:new Map};for(const o of l){let a=0;const l=new Map,c=i?await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o)))||{}:null;for(const[d,y]of u.entries()){const u=i?c?.[d]||{}:await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o,d)))||{};let g=0;const f=y.map(({key:e})=>{const o=!!s.getNestedValue(u,e,r??".");return o&&g++,{key:e,isTranslated:o}});l.set(d,{totalKeys:y.length,translatedKeys:g,keyDetails:f}),a+=g}d.locales.set(o,{totalTranslated:a,namespaces:l})}return d}(i);d.succeed("Analysis complete."),function(o,t,a){a.detail?function(o,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(e.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(e.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=o.locales.get(a);if(!n)return void console.error(e.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(e.bold(`\nKey Status for "${e.cyan(a)}":`));const r=Array.from(o.keysByNs.values()).flat().length;l("Overall",n.totalTranslated,r);const i=s?[s]:Array.from(n.namespaces.keys()).sort();for(const o of i){const t=n.namespaces.get(o);t&&(console.log(e.cyan.bold(`\nNamespace: ${o}`)),l("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:o,isTranslated:t})=>{const a=t?e.green("✓"):e.red("✗");console.log(` ${a} ${o}`)}))}const u=r-n.totalTranslated;u>0?console.log(e.yellow.bold(`\nSummary: Found ${u} missing translations for "${a}".`)):console.log(e.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));c()}(o,t,a.detail,a.namespace):a.namespace?function(o,t,a){const s=o.keysByNs.get(a);if(!s)return void console.error(e.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(e.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[e,t]of o.locales.entries()){const o=t.namespaces.get(a);if(o){const t=o.totalKeys>0?Math.round(o.translatedKeys/o.totalKeys*100):100,a=r(t);console.log(`- ${e}: ${a} ${t}% (${o.translatedKeys}/${o.totalKeys} keys)`)}}c()}(o,0,a.namespace):function(o,t){const{primaryLanguage:a}=t.extract;console.log(e.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${e.bold(o.totalKeys)}`),console.log(`📚 Namespaces Found: ${e.bold(o.keysByNs.size)}`),console.log(`🌍 Locales: ${e.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${e.bold(a)}`),console.log("\nTranslation Progress:");for(const[e,t]of o.locales.entries()){const a=o.totalKeys>0?Math.round(t.totalTranslated/o.totalKeys*100):100,s=r(a);console.log(`- ${e}: ${s} ${a}% (${t.totalTranslated}/${o.totalKeys} keys)`)}c()}(o,t)}(o,i,u)}catch(e){d.fail("Failed to generate status report."),console.error(e)}};
1
+ "use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),a=require("./extractor/core/key-finder.js"),s=require("./utils/nested-object.js"),n=require("./utils/file-utils.js");function l(o,t,a){const s=a>0?Math.round(t/a*100):100,n=r(s);console.log(`${e.bold(o)}: ${n} ${s}% (${t}/${a})`)}function r(o){const t=Math.floor(o/100*20),a=20-t;return`[${e.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function c(){console.log(e.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${e.cyan("npx i18next-cli locize-migrate")} to get started.`)}exports.runStatus=async function(i,u={}){i.extract.primaryLanguage||=i.locales[0]||"en",i.extract.secondaryLanguages||=i.locales.filter(e=>e!==i?.extract?.primaryLanguage);const d=o("Analyzing project localization status...\n").start();try{const o=await async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(o=>o!==e?.extract?.primaryLanguage);const{allKeys:o}=await a.findKeys(e),{secondaryLanguages:l,keySeparator:r=".",defaultNS:c="translation",mergeNamespaces:i=!1,pluralSeparator:u="_"}=e.extract,d=new Map;for(const e of o.values()){const o=e.ns||c;d.has(o)||d.set(o,[]),d.get(o).push(e)}const y={totalBaseKeys:o.size,keysByNs:d,locales:new Map};for(const o of l){let a=0,l=0;const c=new Map,g=i?await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o)))||{}:null;for(const[y,f]of d.entries()){const d=i?g?.[y]||{}:await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o,y)))||{};let p=0,$=0;const m=[];for(const{key:e,hasCount:t,isOrdinal:a}of f)if(t){const t=a?"ordinal":"cardinal",n=new Intl.PluralRules(o,{type:t}).resolvedOptions().pluralCategories;for(const o of n){$++;const t=a?`${e}${u}ordinal${u}${o}`:`${e}${u}${o}`,n=!!s.getNestedValue(d,t,r??".");n&&p++,m.push({key:t,isTranslated:n})}}else{$++;const o=!!s.getNestedValue(d,e,r??".");o&&p++,m.push({key:e,isTranslated:o})}c.set(y,{totalKeys:$,translatedKeys:p,keyDetails:m}),a+=p,l+=$}y.locales.set(o,{totalKeys:l,totalTranslated:a,namespaces:c})}return y}(i);d.succeed("Analysis complete."),function(o,t,a){a.detail?function(o,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(e.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(e.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=o.locales.get(a);if(!n)return void console.error(e.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(e.bold(`\nKey Status for "${e.cyan(a)}":`));const r=Array.from(o.keysByNs.values()).flat().length;l("Overall",n.totalTranslated,n.totalKeys);const i=s?[s]:Array.from(n.namespaces.keys()).sort();for(const o of i){const t=n.namespaces.get(o);t&&(console.log(e.cyan.bold(`\nNamespace: ${o}`)),l("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:o,isTranslated:t})=>{const a=t?e.green("✓"):e.red("✗");console.log(` ${a} ${o}`)}))}const u=r-n.totalTranslated;u>0?console.log(e.yellow.bold(`\nSummary: Found ${u} missing translations for "${a}".`)):console.log(e.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));c()}(o,t,a.detail,a.namespace):a.namespace?function(o,t,a){const s=o.keysByNs.get(a);if(!s)return void console.error(e.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(e.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[e,t]of o.locales.entries()){const o=t.namespaces.get(a);if(o){const t=o.totalKeys>0?Math.round(o.translatedKeys/o.totalKeys*100):100,a=r(t);console.log(`- ${e}: ${a} ${t}% (${o.translatedKeys}/${o.totalKeys} keys)`)}}c()}(o,0,a.namespace):function(o,t){const{primaryLanguage:a}=t.extract;console.log(e.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${e.bold(o.totalBaseKeys)}`),console.log(`📚 Namespaces Found: ${e.bold(o.keysByNs.size)}`),console.log(`🌍 Locales: ${e.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${e.bold(a)}`),console.log("\nTranslation Progress:");for(const[e,t]of o.locales.entries()){const o=t.totalKeys>0?Math.round(t.totalTranslated/t.totalKeys*100):100,a=r(o);console.log(`- ${e}: ${a} ${o}% (${t.totalTranslated}/${t.totalKeys} keys)`)}c()}(o,t)}(o,i,u)}catch(e){d.fail("Failed to generate status report."),console.error(e)}};
package/dist/esm/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as o}from"commander";import t from"chokidar";import{glob as e}from"glob";import i from"chalk";import{ensureConfig as n,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 f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.17"),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.").action(async o=>{const a=await n(),c=async()=>{const t=await r(a);o.ci&&t&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");t.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),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(o,t)=>{let e=await a();if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=o}await f(e,{detail:o,namespace:t.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 o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");t.watch(await e(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),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.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=t}await d(o)}),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 o=>{const t=await n();await g(t,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const t=await n();await u(t,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const t=await n();await y(t,o)}),w.parse(process.argv);
2
+ import{Command as o}from"commander";import t from"chokidar";import{glob as e}from"glob";import i from"chalk";import{ensureConfig as n,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 f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.19"),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.").action(async o=>{const a=await n(),c=async()=>{const t=await r(a);o.ci&&t&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");t.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),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(o,t)=>{let e=await a();if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=o}await f(e,{detail:o,namespace:t.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 o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");t.watch(await e(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),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.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=t}await d(o)}),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 o=>{const t=await n();await g(t,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const t=await n();await u(t,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const t=await n();await y(t,o)}),w.parse(process.argv);
@@ -1 +1 @@
1
- import{resolve as t}from"node:path";import{getNestedKeys as e,getNestedValue as n,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as s,loadTranslationFile as o}from"../../utils/file-utils.js";function r(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}async function c(c,i,l){const u=l.extract.defaultNS??"translation",f=l.extract.keySeparator??".",p=[...l.extract.preservePatterns||[]],g=l.extract.mergeNamespaces??!1;for(const t of i)p.push(`${t}.*`);const x=p.map(r);l.extract.primaryLanguage||=l.locales[0]||"en",l.extract.secondaryLanguages||=l.locales.filter(t=>t!==l?.extract?.primaryLanguage);const d=l.extract.primaryLanguage,y=new Map;for(const t of c.values()){const e=t.ns||u;y.has(e)||y.set(e,[]),y.get(e).push(t)}const m=[];for(const r of l.locales){const c={},i={};for(const[u,p]of y.entries()){const y=s(l.extract.output,r,g?void 0:u),h=t(process.cwd(),y),w=await o(h)||{},k={},N=e(w,f);for(const t of N)if(x.some(e=>e.test(t))){const e=n(w,t,f);a(k,t,e,f)}const O=!1===l.extract.sort?p:[...p].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:e}of O){const s=n(w,t,f)??(r===d?e:l.extract.defaultValue??"");a(k,t,s,f)}if(g)c[u]=k,Object.keys(w).length>0&&(i[u]=w);else{const t=w?JSON.stringify(w,null,l.extract.indentation??2):"",e=JSON.stringify(k,null,l.extract.indentation??2);m.push({path:h,updated:e!==t,newTranslations:k,existingTranslations:w})}}if(g){const e=s(l.extract.output,r),n=t(process.cwd(),e),a=Object.keys(i).length>0?JSON.stringify(i,null,l.extract.indentation??2):"",o=JSON.stringify(c,null,l.extract.indentation??2);m.push({path:n,updated:o!==a,newTranslations:c,existingTranslations:i})}}return m}export{c as getTranslations};
1
+ import{resolve as t}from"node:path";import{getNestedKeys as e,getNestedValue as s,setNestedValue as a}from"../../utils/nested-object.js";import{loadTranslationFile as n,getOutputPath as o}from"../../utils/file-utils.js";function r(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}async function c(c,i,l){l.extract.primaryLanguage||=l.locales[0]||"en",l.extract.secondaryLanguages||=l.locales.filter(t=>t!==l?.extract?.primaryLanguage);const u=l.extract.primaryLanguage,p=l.extract.defaultNS??"translation",f=l.extract.keySeparator??".",g=[...l.extract.preservePatterns||[]],x=l.extract.mergeNamespaces??!1,d=l.extract.indentation??2;for(const t of i)g.push(`${t}.*`);const m=g.map(r),y=new Map;for(const t of c.values()){const e=t.ns||p;y.has(e)||y.set(e,[]),y.get(e).push(t)}const w=[];for(const r of l.locales){const c=x?await n(t(process.cwd(),o(l.extract.output,r)))||{}:null,i={};for(const[p,g]of y.entries()){const y=o(l.extract.output,r,x?void 0:p),h=t(process.cwd(),y),N=x?c?.[p]||{}:await n(h)||{},S={},$=e(N,f);for(const t of $)if(m.some(e=>e.test(t))){const e=s(N,t,f);a(S,t,e,f)}const k=!1===l.extract.sort?g:[...g].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:e}of k){const n=s(N,t,f)??(r===u?e:l.extract.defaultValue??"");a(S,t,n,f)}if(x)i[p]=S;else{const t=JSON.stringify(N,null,d),e=JSON.stringify(S,null,d);w.push({path:h,updated:e!==t,newTranslations:S,existingTranslations:N})}}if(x){const e=o(l.extract.output,r),s=t(process.cwd(),e),a=JSON.stringify(c,null,d),n=JSON.stringify(i,null,d);w.push({path:s,updated:n!==a,newTranslations:i,existingTranslations:c||{}})}}return w}export{c as getTranslations};
@@ -1 +1 @@
1
- import{extractFromTransComponent as e}from"./jsx-parser.js";class t{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&&n.type&&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,t,n){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 r=t.arguments?.[n.nsArg]?.expression;let s;"StringLiteral"===r?.type?s=r.value:"ArrayExpression"===r?.type&&"StringLiteral"===r.elements[0]?.expression.type&&(s=r.elements[0].expression.value);const a=t.arguments?.[n.keyPrefixArg]?.expression;let o;if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"keyPrefix");o="string"==typeof e?e:void 0}this.setVarInScope(i,{defaultNs:s,keyPrefix:o})}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 t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||["t"]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if(a){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if(a){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,t,a);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=!0===this.getObjectPropValue(n,"ordinal"),r=i?"ordinal":"cardinal",s=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:r}).resolvedOptions().pluralCategories,a=this.config.extract.pluralSeparator??"_",o=this.getObjectPropValue(n,"defaultValue"),l=this.getObjectPropValue(n,"defaultValue_other");for(const r of s){const s=i?`defaultValue_ordinal_${r}`:`defaultValue_${r}`,p=this.getObjectPropValue(n,s);let u;u="string"==typeof p?p:"one"===r&&"string"==typeof o?o:"string"==typeof l?l:"string"==typeof o?o:e;const c=i?`${e}${a}ordinal${a}${r}`:`${e}${a}${r}`;this.pluginContext.addKey({key:c,ns:t,defaultValue:u,hasCount:!0})}}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);const r=this.getObjectPropValue(n,"defaultValue");this.pluginContext.addKey({key:e,ns:t,defaultValue:"string"==typeof r?r: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});this.pluginContext.addKey(n)}}else n.hasCount?this.handleSimplePluralKeys(n.key,n.defaultValue,n.ns):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(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}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,[]}getObjectProperty(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))}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}}}}export{t as ASTVisitors};
1
+ import{extractFromTransComponent as e}from"./jsx-parser.js";class t{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&&n.type&&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,t,n){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 i=t.arguments?.[n.nsArg]?.expression;let s;"StringLiteral"===i?.type?s=i.value:"ArrayExpression"===i?.type&&"StringLiteral"===i.elements[0]?.expression.type&&(s=i.elements[0].expression.value);const a=t.arguments?.[n.keyPrefixArg]?.expression;let o;if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"keyPrefix");o="string"==typeof e?e:void 0}this.setVarInScope(r,{defaultNs:s,keyPrefix:o})}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 t=this.getFunctionName(e.callee);if(!t)return;const n=this.getVarFromScope(t);if(!((this.config.extract.functions||["t"]).includes(t)||void 0!==n)||0===e.arguments.length)return;const r=e.arguments[0].expression,i=[];if("StringLiteral"===r.type)i.push(r.value);else if("ArrowFunctionExpression"===r.type){const e=this.extractKeyFromSelector(r);e&&i.push(e)}else if("ArrayExpression"===r.type)for(const e of r.elements)"StringLiteral"===e?.expression.type&&i.push(e.expression.value);if(0===i.length)return;let s=!1;const a=this.config.extract.pluralSeparator??"_";for(let e=0;e<i.length;e++)i[e].endsWith(`${a}ordinal`)&&(s=!0,i[e]=i[e].slice(0,-8));let o,l;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?l=t:"StringLiteral"===t.type&&(o=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(l=t)}const p=l?this.getObjectPropValue(l,"defaultValue"):void 0,u="string"==typeof p?p:o;for(let e=0;e<i.length;e++){let t,r=i[e];if(l){const e=this.getObjectPropValue(l,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const a=this.config.extract.nsSeparator??":";if(!t&&a&&r.includes(a)){const e=r.split(a);t=e.shift(),r=e.join(a)}t||(t=this.config.extract.defaultNS);let o=r;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${r}`}const p=e===i.length-1&&u||r;if(l){const e=this.getObjectProperty(l,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),r=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${r}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(l,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}const r=void 0!==this.getObjectPropValue(l,"count"),i=!0===this.getObjectPropValue(l,"ordinal");if(r||s){this.handlePluralKeys(o,t,l,i||s);continue}!0===this.getObjectPropValue(l,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n,r){try{const i=r?"ordinal":"cardinal",s=new Intl.PluralRules(this.config.extract?.primaryLanguage,{type:i}).resolvedOptions().pluralCategories,a=this.config.extract.pluralSeparator??"_",o=this.getObjectPropValue(n,"defaultValue"),l=this.getObjectPropValue(n,"defaultValue_other"),p=this.getObjectPropValue(n,"defaultValue_ordinal_other");for(const i of s){const s=r?`defaultValue_ordinal_${i}`:`defaultValue_${i}`,u=this.getObjectPropValue(n,s);let c;c="string"==typeof u?u:"one"===i&&"string"==typeof o?o:r&&"string"==typeof p?p:r||"string"!=typeof l?"string"==typeof o?o:e:l;const f=r?`${e}${a}ordinal${a}${i}`:`${e}${a}${i}`;this.pluginContext.addKey({key:f,ns:t,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 i=this.getObjectPropValue(n,"defaultValue");this.pluginContext.addKey({key:e,ns:t,defaultValue:"string"==typeof i?i: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(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});this.pluginContext.addKey(n)}}else n.hasCount?this.handleSimplePluralKeys(n.key,n.defaultValue,n.ns):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(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}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,[]}getObjectProperty(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))}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}return"Identifier"!==n.type?null:(t.unshift(n.value),t.join("."))}return null}}export{t as ASTVisitors};
@@ -1 +1 @@
1
- import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{findKeys as a}from"./extractor/core/key-finder.js";import{getNestedValue as s}from"./utils/nested-object.js";import{loadTranslationFile as n,getOutputPath as l}from"./utils/file-utils.js";async function r(r,d={}){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(o=>o!==r?.extract?.primaryLanguage);const g=e("Analyzing project localization status...\n").start();try{const e=await async function(o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(e=>e!==o?.extract?.primaryLanguage);const{allKeys:e}=await a(o),{secondaryLanguages:r,keySeparator:c=".",defaultNS:i="translation",mergeNamespaces:y=!1}=o.extract,d=new Map;for(const o of e.values()){const e=o.ns||i;d.has(e)||d.set(e,[]),d.get(e).push(o)}const g={totalKeys:e.size,keysByNs:d,locales:new Map};for(const e of r){let a=0;const r=new Map,i=y?await n(t(process.cwd(),l(o.extract.output,e)))||{}:null;for(const[g,u]of d.entries()){const d=y?i?.[g]||{}:await n(t(process.cwd(),l(o.extract.output,e,g)))||{};let f=0;const m=u.map(({key:o})=>{const e=!!s(d,o,c??".");return e&&f++,{key:o,isTranslated:e}});r.set(g,{totalKeys:u.length,translatedKeys:f,keyDetails:m}),a+=f}g.locales.set(e,{totalTranslated:a,namespaces:r})}return g}(r);g.succeed("Analysis complete."),function(e,t,a){a.detail?function(e,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(o.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(o.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=e.locales.get(a);if(!n)return void console.error(o.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(o.bold(`\nKey Status for "${o.cyan(a)}":`));const l=Array.from(e.keysByNs.values()).flat().length;c("Overall",n.totalTranslated,l);const r=s?[s]:Array.from(n.namespaces.keys()).sort();for(const e of r){const t=n.namespaces.get(e);t&&(console.log(o.cyan.bold(`\nNamespace: ${e}`)),c("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:e,isTranslated:t})=>{const a=t?o.green("✓"):o.red("✗");console.log(` ${a} ${e}`)}))}const i=l-n.totalTranslated;i>0?console.log(o.yellow.bold(`\nSummary: Found ${i} missing translations for "${a}".`)):console.log(o.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));y()}(e,t,a.detail,a.namespace):a.namespace?function(e,t,a){const s=e.keysByNs.get(a);if(!s)return void console.error(o.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(o.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[o,t]of e.locales.entries()){const e=t.namespaces.get(a);if(e){const t=e.totalKeys>0?Math.round(e.translatedKeys/e.totalKeys*100):100,a=i(t);console.log(`- ${o}: ${a} ${t}% (${e.translatedKeys}/${e.totalKeys} keys)`)}}y()}(e,0,a.namespace):function(e,t){const{primaryLanguage:a}=t.extract;console.log(o.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${o.bold(e.totalKeys)}`),console.log(`📚 Namespaces Found: ${o.bold(e.keysByNs.size)}`),console.log(`🌍 Locales: ${o.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${o.bold(a)}`),console.log("\nTranslation Progress:");for(const[o,t]of e.locales.entries()){const a=e.totalKeys>0?Math.round(t.totalTranslated/e.totalKeys*100):100,s=i(a);console.log(`- ${o}: ${s} ${a}% (${t.totalTranslated}/${e.totalKeys} keys)`)}y()}(e,t)}(e,r,d)}catch(o){g.fail("Failed to generate status report."),console.error(o)}}function c(e,t,a){const s=a>0?Math.round(t/a*100):100,n=i(s);console.log(`${o.bold(e)}: ${n} ${s}% (${t}/${a})`)}function i(e){const t=Math.round(e/100*20),a=20-t;return`[${o.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function y(){console.log(o.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${o.cyan("npx i18next-cli locize-migrate")} to get started.`)}export{r as runStatus};
1
+ import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{findKeys as a}from"./extractor/core/key-finder.js";import{getNestedValue as s}from"./utils/nested-object.js";import{loadTranslationFile as n,getOutputPath as l}from"./utils/file-utils.js";async function r(r,d={}){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(o=>o!==r?.extract?.primaryLanguage);const u=e("Analyzing project localization status...\n").start();try{const e=await async function(o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(e=>e!==o?.extract?.primaryLanguage);const{allKeys:e}=await a(o),{secondaryLanguages:r,keySeparator:c=".",defaultNS:i="translation",mergeNamespaces:y=!1,pluralSeparator:d="_"}=o.extract,u=new Map;for(const o of e.values()){const e=o.ns||i;u.has(e)||u.set(e,[]),u.get(e).push(o)}const g={totalBaseKeys:e.size,keysByNs:u,locales:new Map};for(const e of r){let a=0,r=0;const i=new Map,f=y?await n(t(process.cwd(),l(o.extract.output,e)))||{}:null;for(const[g,p]of u.entries()){const u=y?f?.[g]||{}:await n(t(process.cwd(),l(o.extract.output,e,g)))||{};let m=0,$=0;const h=[];for(const{key:o,hasCount:t,isOrdinal:a}of p)if(t){const t=a?"ordinal":"cardinal",n=new Intl.PluralRules(e,{type:t}).resolvedOptions().pluralCategories;for(const e of n){$++;const t=a?`${o}${d}ordinal${d}${e}`:`${o}${d}${e}`,n=!!s(u,t,c??".");n&&m++,h.push({key:t,isTranslated:n})}}else{$++;const e=!!s(u,o,c??".");e&&m++,h.push({key:o,isTranslated:e})}i.set(g,{totalKeys:$,translatedKeys:m,keyDetails:h}),a+=m,r+=$}g.locales.set(e,{totalKeys:r,totalTranslated:a,namespaces:i})}return g}(r);u.succeed("Analysis complete."),function(e,t,a){a.detail?function(e,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(o.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(o.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=e.locales.get(a);if(!n)return void console.error(o.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(o.bold(`\nKey Status for "${o.cyan(a)}":`));const l=Array.from(e.keysByNs.values()).flat().length;c("Overall",n.totalTranslated,n.totalKeys);const r=s?[s]:Array.from(n.namespaces.keys()).sort();for(const e of r){const t=n.namespaces.get(e);t&&(console.log(o.cyan.bold(`\nNamespace: ${e}`)),c("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:e,isTranslated:t})=>{const a=t?o.green("✓"):o.red("✗");console.log(` ${a} ${e}`)}))}const i=l-n.totalTranslated;i>0?console.log(o.yellow.bold(`\nSummary: Found ${i} missing translations for "${a}".`)):console.log(o.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));y()}(e,t,a.detail,a.namespace):a.namespace?function(e,t,a){const s=e.keysByNs.get(a);if(!s)return void console.error(o.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(o.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[o,t]of e.locales.entries()){const e=t.namespaces.get(a);if(e){const t=e.totalKeys>0?Math.round(e.translatedKeys/e.totalKeys*100):100,a=i(t);console.log(`- ${o}: ${a} ${t}% (${e.translatedKeys}/${e.totalKeys} keys)`)}}y()}(e,0,a.namespace):function(e,t){const{primaryLanguage:a}=t.extract;console.log(o.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${o.bold(e.totalBaseKeys)}`),console.log(`📚 Namespaces Found: ${o.bold(e.keysByNs.size)}`),console.log(`🌍 Locales: ${o.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${o.bold(a)}`),console.log("\nTranslation Progress:");for(const[o,t]of e.locales.entries()){const e=t.totalKeys>0?Math.round(t.totalTranslated/t.totalKeys*100):100,a=i(e);console.log(`- ${o}: ${a} ${e}% (${t.totalTranslated}/${t.totalKeys} keys)`)}y()}(e,t)}(e,r,d)}catch(o){u.fail("Failed to generate status report."),console.error(o)}}function c(e,t,a){const s=a>0?Math.round(t/a*100):100,n=i(s);console.log(`${o.bold(e)}: ${n} ${s}% (${t}/${a})`)}function i(e){const t=Math.floor(e/100*20),a=20-t;return`[${o.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function y(){console.log(o.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${o.cyan("npx i18next-cli locize-migrate")} to get started.`)}export{r as runStatus};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "0.9.17",
3
+ "version": "0.9.19",
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('0.9.17')
24
+ .version('0.9.19')
25
25
 
26
26
  program
27
27
  .command('extract')
@@ -47,40 +47,46 @@ export async function getTranslations (
47
47
  objectKeys: Set<string>,
48
48
  config: I18nextToolkitConfig
49
49
  ): Promise<TranslationResult[]> {
50
+ config.extract.primaryLanguage ||= config.locales[0] || 'en'
51
+ config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
52
+ const primaryLanguage = config.extract.primaryLanguage
50
53
  const defaultNS = config.extract.defaultNS ?? 'translation'
51
54
  const keySeparator = config.extract.keySeparator ?? '.'
52
55
  const patternsToPreserve = [...(config.extract.preservePatterns || [])]
53
56
  const mergeNamespaces = config.extract.mergeNamespaces ?? false
57
+ const indentation = config.extract.indentation ?? 2
58
+
54
59
  for (const key of objectKeys) {
55
60
  // Convert the object key to a glob pattern to preserve all its children
56
61
  patternsToPreserve.push(`${key}.*`)
57
62
  }
58
63
  const preservePatterns = patternsToPreserve.map(globToRegex)
59
- config.extract.primaryLanguage ||= config.locales[0] || 'en'
60
- config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
61
- const primaryLanguage = config.extract.primaryLanguage
62
64
 
63
65
  // Group keys by namespace
64
66
  const keysByNS = new Map<string, ExtractedKey[]>()
65
67
  for (const key of keys.values()) {
66
68
  const ns = key.ns || defaultNS
67
- if (!keysByNS.has(ns)) {
68
- keysByNS.set(ns, [])
69
- }
69
+ if (!keysByNS.has(ns)) keysByNS.set(ns, [])
70
70
  keysByNS.get(ns)!.push(key)
71
71
  }
72
72
 
73
73
  const results: TranslationResult[] = []
74
74
 
75
75
  for (const locale of config.locales) {
76
- const mergedTranslations: Record<string, any> = {}
77
- const mergedExisting: Record<string, any> = {}
76
+ const existingMergedFile = mergeNamespaces
77
+ ? await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale))) || {}
78
+ : null
79
+
80
+ const newMergedTranslations: Record<string, any> = {}
78
81
 
79
82
  for (const [ns, nsKeys] of keysByNS.entries()) {
80
83
  const outputPath = getOutputPath(config.extract.output, locale, mergeNamespaces ? undefined : ns)
81
84
  const fullPath = resolve(process.cwd(), outputPath)
82
85
 
83
- const existingTranslations = await loadTranslationFile(fullPath) || {}
86
+ const existingTranslations = mergeNamespaces
87
+ ? existingMergedFile?.[ns] || {}
88
+ : await loadTranslationFile(fullPath) || {}
89
+
84
90
  const newTranslations: Record<string, any> = {}
85
91
 
86
92
  const existingKeys = getNestedKeys(existingTranslations, keySeparator)
@@ -91,10 +97,7 @@ export async function getTranslations (
91
97
  }
92
98
  }
93
99
 
94
- const sortedKeys = (config.extract.sort === false)
95
- ? nsKeys
96
- : [...nsKeys].sort((a, b) => a.key.localeCompare(b.key))
97
-
100
+ const sortedKeys = (config.extract.sort === false) ? nsKeys : [...nsKeys].sort((a, b) => a.key.localeCompare(b.key))
98
101
  for (const { key, defaultValue } of sortedKeys) {
99
102
  const existingValue = getNestedValue(existingTranslations, key, keySeparator)
100
103
  const valueToSet = existingValue ?? (locale === primaryLanguage ? defaultValue : (config.extract.defaultValue ?? ''))
@@ -102,35 +105,20 @@ export async function getTranslations (
102
105
  }
103
106
 
104
107
  if (mergeNamespaces) {
105
- mergedTranslations[ns] = newTranslations
106
- if (Object.keys(existingTranslations).length > 0) {
107
- mergedExisting[ns] = existingTranslations
108
- }
108
+ newMergedTranslations[ns] = newTranslations
109
109
  } else {
110
- const oldContent = existingTranslations ? JSON.stringify(existingTranslations, null, config.extract.indentation ?? 2) : ''
111
- const newContent = JSON.stringify(newTranslations, null, config.extract.indentation ?? 2)
112
-
113
- results.push({
114
- path: fullPath,
115
- updated: newContent !== oldContent,
116
- newTranslations,
117
- existingTranslations,
118
- })
110
+ const oldContent = JSON.stringify(existingTranslations, null, indentation)
111
+ const newContent = JSON.stringify(newTranslations, null, indentation)
112
+ results.push({ path: fullPath, updated: newContent !== oldContent, newTranslations, existingTranslations })
119
113
  }
120
114
  }
121
115
 
122
116
  if (mergeNamespaces) {
123
117
  const outputPath = getOutputPath(config.extract.output, locale)
124
118
  const fullPath = resolve(process.cwd(), outputPath)
125
- const oldContent = Object.keys(mergedExisting).length > 0 ? JSON.stringify(mergedExisting, null, config.extract.indentation ?? 2) : ''
126
- const newContent = JSON.stringify(mergedTranslations, null, config.extract.indentation ?? 2)
127
-
128
- results.push({
129
- path: fullPath,
130
- updated: newContent !== oldContent,
131
- newTranslations: mergedTranslations,
132
- existingTranslations: mergedExisting,
133
- })
119
+ const oldContent = JSON.stringify(existingMergedFile, null, indentation)
120
+ const newContent = JSON.stringify(newMergedTranslations, null, indentation)
121
+ results.push({ path: fullPath, updated: newContent !== oldContent, newTranslations: newMergedTranslations, existingTranslations: existingMergedFile || {} })
134
122
  }
135
123
  }
136
124
 
@@ -365,11 +365,12 @@ export class ASTVisitors {
365
365
  * @private
366
366
  */
367
367
  private handleCallExpression (node: CallExpression): void {
368
- const callee = node.callee
369
- if (callee.type !== 'Identifier') return
368
+ const functionName = this.getFunctionName(node.callee)
369
+ if (!functionName) return
370
370
 
371
- const scopeInfo = this.getVarFromScope(callee.value)
372
- const isFunctionToParse = (this.config.extract.functions || ['t']).includes(callee.value) || scopeInfo !== undefined
371
+ // The scope lookup will only work for simple identifiers, which is okay for this fix.
372
+ const scopeInfo = this.getVarFromScope(functionName)
373
+ const isFunctionToParse = (this.config.extract.functions || ['t']).includes(functionName) || scopeInfo !== undefined
373
374
  if (!isFunctionToParse || node.arguments.length === 0) return
374
375
 
375
376
  const firstArg = node.arguments[0].expression
@@ -391,6 +392,17 @@ export class ASTVisitors {
391
392
 
392
393
  if (keysToProcess.length === 0) return
393
394
 
395
+ let isOrdinalByKey = false
396
+ const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
397
+
398
+ for (let i = 0; i < keysToProcess.length; i++) {
399
+ if (keysToProcess[i].endsWith(`${pluralSeparator}ordinal`)) {
400
+ isOrdinalByKey = true
401
+ // Normalize the key by stripping the suffix
402
+ keysToProcess[i] = keysToProcess[i].slice(0, -8)
403
+ }
404
+ }
405
+
394
406
  let defaultValue: string | undefined
395
407
  let options: ObjectExpression | undefined
396
408
 
@@ -468,8 +480,11 @@ export class ASTVisitors {
468
480
  }
469
481
 
470
482
  // 3. Handle Plurals
471
- if (this.getObjectPropValue(options, 'count') !== undefined) {
472
- this.handlePluralKeys(finalKey, ns, options)
483
+ const hasCount = this.getObjectPropValue(options, 'count') !== undefined
484
+ const isOrdinalByOption = this.getObjectPropValue(options, 'ordinal') === true
485
+ if (hasCount || isOrdinalByKey) {
486
+ // Pass the combined ordinal flag to the handler
487
+ this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey)
473
488
  continue
474
489
  }
475
490
 
@@ -495,43 +510,50 @@ export class ASTVisitors {
495
510
  * @param key - Base key name for pluralization
496
511
  * @param ns - Namespace for the keys
497
512
  * @param options - object expression options
513
+ * @param isOrdinal - isOrdinal flag
498
514
  *
499
515
  * @private
500
516
  */
501
- private handlePluralKeys (key: string, ns: string | undefined, options: ObjectExpression): void {
517
+ private handlePluralKeys (key: string, ns: string | undefined, options: ObjectExpression, isOrdinal: boolean): void {
502
518
  try {
503
- const isOrdinal = this.getObjectPropValue(options, 'ordinal') === true
504
519
  const type = isOrdinal ? 'ordinal' : 'cardinal'
505
520
 
506
521
  const pluralCategories = new Intl.PluralRules(this.config.extract?.primaryLanguage, { type }).resolvedOptions().pluralCategories
507
522
  const pluralSeparator = this.config.extract.pluralSeparator ?? '_'
508
523
 
524
+ // Get all possible default values once at the start
509
525
  const defaultValue = this.getObjectPropValue(options, 'defaultValue')
510
- const defaultValueOther = this.getObjectPropValue(options, 'defaultValue_other')
526
+ const otherDefault = this.getObjectPropValue(options, 'defaultValue_other')
527
+ const ordinalOtherDefault = this.getObjectPropValue(options, 'defaultValue_ordinal_other')
511
528
 
512
529
  for (const category of pluralCategories) {
513
- // Construct the specific defaultValue key, e.g., `defaultValue_ordinal_one` or `defaultValue_one`
530
+ // 1. Look for the most specific default value (e.g., defaultValue_ordinal_one)
514
531
  const specificDefaultKey = isOrdinal ? `defaultValue_ordinal_${category}` : `defaultValue_${category}`
515
532
  const specificDefault = this.getObjectPropValue(options, specificDefaultKey)
516
533
 
534
+ // 2. Determine the final default value using a clear fallback chain
517
535
  let finalDefaultValue: string | undefined
518
-
519
536
  if (typeof specificDefault === 'string') {
537
+ // 1. Use the most specific default if it exists (e.g., defaultValue_one)
520
538
  finalDefaultValue = specificDefault
521
539
  } else if (category === 'one' && typeof defaultValue === 'string') {
522
- // 'one' specifically falls back to the main `defaultValue` prop
540
+ // 2. SPECIAL CASE: The 'one' category falls back to the main 'defaultValue' prop
523
541
  finalDefaultValue = defaultValue
524
- } else if (typeof defaultValueOther === 'string') {
525
- // All other categories fall back to `defaultValue_other`
526
- finalDefaultValue = defaultValueOther
542
+ } else if (isOrdinal && typeof ordinalOtherDefault === 'string') {
543
+ // 3a. Other ordinal categories fall back to 'defaultValue_ordinal_other'
544
+ finalDefaultValue = ordinalOtherDefault
545
+ } else if (!isOrdinal && typeof otherDefault === 'string') {
546
+ // 3b. Other cardinal categories fall back to 'defaultValue_other'
547
+ finalDefaultValue = otherDefault
527
548
  } else if (typeof defaultValue === 'string') {
528
- // If all else fails, use the main `defaultValue` as a last resort
549
+ // 4. If no '_other' is found, all categories can fall back to the main 'defaultValue'
529
550
  finalDefaultValue = defaultValue
530
551
  } else {
531
- finalDefaultValue = key // Fallback to the key itself
552
+ // 5. Final fallback to the base key itself
553
+ finalDefaultValue = key
532
554
  }
533
555
 
534
- // Construct the final key, e.g., `key_ordinal_one` or `key_one`
556
+ // 3. Construct the final plural key
535
557
  const finalKey = isOrdinal
536
558
  ? `${key}${pluralSeparator}ordinal${pluralSeparator}${category}`
537
559
  : `${key}${pluralSeparator}${category}`
@@ -541,6 +563,7 @@ export class ASTVisitors {
541
563
  ns,
542
564
  defaultValue: finalDefaultValue,
543
565
  hasCount: true,
566
+ isOrdinal
544
567
  })
545
568
  }
546
569
  } catch (e) {
@@ -858,4 +881,58 @@ export class ASTVisitors {
858
881
  }
859
882
  return undefined
860
883
  }
884
+
885
+ /**
886
+ * Serializes a callee node (Identifier or MemberExpression) into a string.
887
+ *
888
+ * Produces a dotted name for simple callees that can be used for scope lookups
889
+ * or configuration matching.
890
+ *
891
+ * Supported inputs:
892
+ * - Identifier: returns the identifier name (e.g., `t` -> "t")
893
+ * - MemberExpression with Identifier parts: returns a dotted path of identifiers
894
+ * (e.g., `i18n.t` -> "i18n.t", `i18n.getFixedT` -> "i18n.getFixedT")
895
+ *
896
+ * Behavior notes:
897
+ * - Computed properties are not supported and cause this function to return null
898
+ * (e.g., `i18n['t']` -> null).
899
+ * - The base of a MemberExpression must be a simple Identifier. More complex bases
900
+ * (other expressions, `this`, etc.) will result in null.
901
+ * - This function does not attempt to resolve or evaluate expressions — it only
902
+ * serializes static identifier/member chains.
903
+ *
904
+ * Examples:
905
+ * - Identifier callee: { type: 'Identifier', value: 't' } -> "t"
906
+ * - Member callee: { type: 'MemberExpression', object: { type: 'Identifier', value: 'i18n' }, property: { type: 'Identifier', value: 't' } } -> "i18n.t"
907
+ *
908
+ * @param callee - The CallExpression callee node to serialize
909
+ * @returns A dotted string name for supported callees, or null when the callee
910
+ * is a computed/unsupported expression.
911
+ *
912
+ * @private
913
+ */
914
+ private getFunctionName (callee: CallExpression['callee']): string | null {
915
+ if (callee.type === 'Identifier') {
916
+ return callee.value
917
+ }
918
+ if (callee.type === 'MemberExpression') {
919
+ const parts: string[] = []
920
+ let current: any = callee
921
+ while (current.type === 'MemberExpression') {
922
+ if (current.property.type === 'Identifier') {
923
+ parts.unshift(current.property.value)
924
+ } else {
925
+ return null // Cannot handle computed properties like i18n['t']
926
+ }
927
+ current = current.object
928
+ }
929
+ if (current.type === 'Identifier') {
930
+ parts.unshift(current.value)
931
+ } else {
932
+ return null // Base of the expression is not a simple identifier
933
+ }
934
+ return parts.join('.')
935
+ }
936
+ return null
937
+ }
861
938
  }
package/src/status.ts CHANGED
@@ -21,11 +21,13 @@ interface StatusOptions {
21
21
  */
22
22
  interface StatusReport {
23
23
  /** Total number of extracted keys across all namespaces */
24
- totalKeys: number;
24
+ totalBaseKeys: number;
25
25
  /** Map of namespace names to their extracted keys */
26
26
  keysByNs: Map<string, ExtractedKey[]>;
27
27
  /** Map of locale codes to their translation status data */
28
28
  locales: Map<string, {
29
+ /** Total number of extracted keys per locale */
30
+ totalKeys: number;
29
31
  /** Total number of translated keys for this locale */
30
32
  totalTranslated: number;
31
33
  /** Map of namespace names to their translation details for this locale */
@@ -87,12 +89,7 @@ async function generateStatusReport (config: I18nextToolkitConfig): Promise<Stat
87
89
  config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
88
90
 
89
91
  const { allKeys: allExtractedKeys } = await findKeys(config)
90
- const {
91
- secondaryLanguages,
92
- keySeparator = '.',
93
- defaultNS = 'translation',
94
- mergeNamespaces = false,
95
- } = config.extract
92
+ const { secondaryLanguages, keySeparator = '.', defaultNS = 'translation', mergeNamespaces = false, pluralSeparator = '_' } = config.extract
96
93
 
97
94
  const keysByNs = new Map<string, ExtractedKey[]>()
98
95
  for (const key of allExtractedKeys.values()) {
@@ -102,13 +99,14 @@ async function generateStatusReport (config: I18nextToolkitConfig): Promise<Stat
102
99
  }
103
100
 
104
101
  const report: StatusReport = {
105
- totalKeys: allExtractedKeys.size,
102
+ totalBaseKeys: allExtractedKeys.size,
106
103
  keysByNs,
107
104
  locales: new Map(),
108
105
  }
109
106
 
110
107
  for (const locale of secondaryLanguages) {
111
108
  let totalTranslatedForLocale = 0
109
+ let totalKeysForLocale = 0
112
110
  const namespaces = new Map<string, any>()
113
111
 
114
112
  const mergedTranslations = mergeNamespaces
@@ -121,24 +119,41 @@ async function generateStatusReport (config: I18nextToolkitConfig): Promise<Stat
121
119
  : await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale, ns))) || {}
122
120
 
123
121
  let translatedInNs = 0
124
- const keyDetails = keysInNs.map(({ key }) => {
125
- // Search for the key within the correct namespace object
126
- const value = getNestedValue(translationsForNs, key, keySeparator ?? '.')
127
- const isTranslated = !!value
128
- if (isTranslated) translatedInNs++
129
- return { key, isTranslated }
130
- })
131
-
132
- namespaces.set(ns, {
133
- totalKeys: keysInNs.length,
134
- translatedKeys: translatedInNs,
135
- keyDetails,
136
- })
122
+ let totalInNs = 0
123
+ const keyDetails: Array<{ key: string; isTranslated: boolean }> = []
124
+
125
+ // This is the new, language-aware logic loop
126
+ for (const { key: baseKey, hasCount, isOrdinal } of keysInNs) {
127
+ if (hasCount) {
128
+ const type = isOrdinal ? 'ordinal' : 'cardinal'
129
+ // It's a plural key: expand it based on the current locale's rules
130
+ const pluralCategories = new Intl.PluralRules(locale, { type }).resolvedOptions().pluralCategories
131
+ for (const category of pluralCategories) {
132
+ totalInNs++
133
+ const pluralKey = isOrdinal
134
+ ? `${baseKey}${pluralSeparator}ordinal${pluralSeparator}${category}`
135
+ : `${baseKey}${pluralSeparator}${category}`
136
+ const value = getNestedValue(translationsForNs, pluralKey, keySeparator ?? '.')
137
+ const isTranslated = !!value
138
+ if (isTranslated) translatedInNs++
139
+ keyDetails.push({ key: pluralKey, isTranslated })
140
+ }
141
+ } else {
142
+ // It's a simple key
143
+ totalInNs++
144
+ const value = getNestedValue(translationsForNs, baseKey, keySeparator ?? '.')
145
+ const isTranslated = !!value
146
+ if (isTranslated) translatedInNs++
147
+ keyDetails.push({ key: baseKey, isTranslated })
148
+ }
149
+ }
150
+
151
+ namespaces.set(ns, { totalKeys: totalInNs, translatedKeys: translatedInNs, keyDetails })
137
152
  totalTranslatedForLocale += translatedInNs
153
+ totalKeysForLocale += totalInNs
138
154
  }
139
- report.locales.set(locale, { totalTranslated: totalTranslatedForLocale, namespaces })
155
+ report.locales.set(locale, { totalKeys: totalKeysForLocale, totalTranslated: totalTranslatedForLocale, namespaces })
140
156
  }
141
-
142
157
  return report
143
158
  }
144
159
 
@@ -198,7 +213,7 @@ function displayDetailedLocaleReport (report: StatusReport, config: I18nextToolk
198
213
  console.log(chalk.bold(`\nKey Status for "${chalk.cyan(locale)}":`))
199
214
 
200
215
  const totalKeysForLocale = Array.from(report.keysByNs.values()).flat().length
201
- printProgressBar('Overall', localeData.totalTranslated, totalKeysForLocale)
216
+ printProgressBar('Overall', localeData.totalTranslated, localeData.totalKeys)
202
217
 
203
218
  const namespacesToDisplay = namespaceFilter ? [namespaceFilter] : Array.from(localeData.namespaces.keys()).sort()
204
219
 
@@ -273,16 +288,16 @@ function displayOverallSummaryReport (report: StatusReport, config: I18nextToolk
273
288
 
274
289
  console.log(chalk.cyan.bold('\ni18next Project Status'))
275
290
  console.log('------------------------')
276
- console.log(`🔑 Keys Found: ${chalk.bold(report.totalKeys)}`)
291
+ console.log(`🔑 Keys Found: ${chalk.bold(report.totalBaseKeys)}`)
277
292
  console.log(`📚 Namespaces Found: ${chalk.bold(report.keysByNs.size)}`)
278
293
  console.log(`🌍 Locales: ${chalk.bold(config.locales.join(', '))}`)
279
294
  console.log(`✅ Primary Language: ${chalk.bold(primaryLanguage)}`)
280
295
  console.log('\nTranslation Progress:')
281
296
 
282
297
  for (const [locale, localeData] of report.locales.entries()) {
283
- const percentage = report.totalKeys > 0 ? Math.round((localeData.totalTranslated / report.totalKeys) * 100) : 100
298
+ const percentage = localeData.totalKeys > 0 ? Math.round((localeData.totalTranslated / localeData.totalKeys) * 100) : 100
284
299
  const bar = generateProgressBarText(percentage)
285
- console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${report.totalKeys} keys)`)
300
+ console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${localeData.totalKeys} keys)`)
286
301
  }
287
302
 
288
303
  printLocizeFunnel()
@@ -312,7 +327,7 @@ function printProgressBar (label: string, current: number, total: number) {
312
327
  */
313
328
  function generateProgressBarText (percentage: number): string {
314
329
  const totalBars = 20
315
- const filledBars = Math.round((percentage / 100) * totalBars)
330
+ const filledBars = Math.floor((percentage / 100) * totalBars)
316
331
  const emptyBars = totalBars - filledBars
317
332
  return `[${chalk.green(''.padStart(filledBars, '■'))}${''.padStart(emptyBars, '□')}]`
318
333
  }
package/src/types.ts CHANGED
@@ -251,6 +251,9 @@ export interface ExtractedKey {
251
251
  /** Whether this key is used with pluralization (count parameter) */
252
252
  hasCount?: boolean;
253
253
 
254
+ /** Whether this key is used with ordinal pluralization */
255
+ isOrdinal?: boolean;
256
+
254
257
  /** hold the raw context expression from the AST */
255
258
  contextExpression?: Expression;
256
259
  }
@@ -1 +1 @@
1
- {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyF9B"}
1
+ {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6E9B"}
@@ -162,6 +162,7 @@ export declare class ASTVisitors {
162
162
  * @param key - Base key name for pluralization
163
163
  * @param ns - Namespace for the keys
164
164
  * @param options - object expression options
165
+ * @param isOrdinal - isOrdinal flag
165
166
  *
166
167
  * @private
167
168
  */
@@ -271,5 +272,35 @@ export declare class ASTVisitors {
271
272
  * @returns The resolved UseTranslationHookConfig when a match is found, otherwise undefined
272
273
  */
273
274
  private getUseTranslationConfig;
275
+ /**
276
+ * Serializes a callee node (Identifier or MemberExpression) into a string.
277
+ *
278
+ * Produces a dotted name for simple callees that can be used for scope lookups
279
+ * or configuration matching.
280
+ *
281
+ * Supported inputs:
282
+ * - Identifier: returns the identifier name (e.g., `t` -> "t")
283
+ * - MemberExpression with Identifier parts: returns a dotted path of identifiers
284
+ * (e.g., `i18n.t` -> "i18n.t", `i18n.getFixedT` -> "i18n.getFixedT")
285
+ *
286
+ * Behavior notes:
287
+ * - Computed properties are not supported and cause this function to return null
288
+ * (e.g., `i18n['t']` -> null).
289
+ * - The base of a MemberExpression must be a simple Identifier. More complex bases
290
+ * (other expressions, `this`, etc.) will result in null.
291
+ * - This function does not attempt to resolve or evaluate expressions — it only
292
+ * serializes static identifier/member chains.
293
+ *
294
+ * Examples:
295
+ * - Identifier callee: { type: 'Identifier', value: 't' } -> "t"
296
+ * - Member callee: { type: 'MemberExpression', object: { type: 'Identifier', value: 'i18n' }, property: { type: 'Identifier', value: 't' } } -> "i18n.t"
297
+ *
298
+ * @param callee - The CallExpression callee node to serialize
299
+ * @returns A dotted string name for supported callees, or null when the callee
300
+ * is a computed/unsupported expression.
301
+ *
302
+ * @private
303
+ */
304
+ private getFunctionName;
274
305
  }
275
306
  //# sourceMappingURL=ast-visitors.d.ts.map
@@ -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;AAoB9E;;;;;;;;;;;;;;;;;;;;;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;IAoDZ;;;;;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;IAyH5B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,gBAAgB;IAqDxB;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;IAuDxB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,kBAAkB;IA6B1B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,2BAA2B;IAmBnC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;CAmBhC"}
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;AAoB9E;;;;;;;;;;;;;;;;;;;;;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;IAoDZ;;;;;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;IAwI5B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,gBAAgB;IA4DxB;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;IAuDxB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,kBAAkB;IA6B1B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,2BAA2B;IAmBnC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,OAAO,CAAC,eAAe;CAwBxB"}
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,SAAS,CAAA;AAGjE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAYzF"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,SAAS,CAAA;AAGjE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4BD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAYzF"}
package/types/types.d.ts CHANGED
@@ -210,6 +210,8 @@ export interface ExtractedKey {
210
210
  ns?: string;
211
211
  /** Whether this key is used with pluralization (count parameter) */
212
212
  hasCount?: boolean;
213
+ /** Whether this key is used with ordinal pluralization */
214
+ isOrdinal?: boolean;
213
215
  /** hold the raw context expression from the AST */
214
216
  contextExpression?: Expression;
215
217
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}