i18next-cli 1.5.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.3](https://github.com/i18next/i18next-cli/compare/v1.5.2...v1.5.3) - 2025-10-02
9
+
10
+ - **Extractor:** Fixed a regression where key sorting became case-sensitive (e.g., a key like `'Zebra'` would incorrectly appear before `'apple'`). Sorting has been restored to be case-insensitive, ensuring a natural alphabetical order at all levels of the translation files.
11
+
12
+ ## [1.5.2](https://github.com/i18next/i18next-cli/compare/v1.5.1...v1.5.2) - 2025-10-02
13
+
14
+ - **Extractor:** Fixed a regression where keys within nested objects were no longer being sorted alphabetically (e.g., `buttons.scroll-to-top` would appear before `buttons.cancel`). The sorting logic now recursively sorts keys at all levels of the translation object to ensure a consistent and predictable order.
15
+
8
16
  ## [1.5.1](https://github.com/i18next/i18next-cli/compare/v1.5.0...v1.5.1) - 2025-10-02
9
17
 
10
18
  - **Extractor:** Improved the default key extraction by updating the default `functions` option to `['t', '*.t']`. This allows the extractor to automatically find keys in common patterns like `i18n.t(...)` and `this.t(...)` without any configuration.
package/dist/cjs/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var e=require("commander"),t=require("chokidar"),n=require("glob"),o=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.5.1"),f.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a,{isWatchMode:e.watch,isDryRun:e.dryRun});e.ci&&t&&(console.error(o.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(o.yellow("šŸ’” Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${o.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),f.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(e,t)=>{let n=await i.loadConfig();if(!n){console.log(o.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),n=e}await g.runStatus(n,{detail:e,namespace:t.namespace})}),f.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const o=await i.ensureConfig(),a=()=>c.runTypesGenerator(o);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(o.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),f.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),f.command("migrate-config").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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async e=>{const r=async()=>{let e=await i.loadConfig();if(!e){console.log(o.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),e=t}await d.runLinter(e)};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await i.loadConfig();if(e?.extract?.input){t.watch(await n.glob(e.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
2
+ "use strict";var e=require("commander"),t=require("chokidar"),n=require("glob"),o=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.5.3"),f.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a,{isWatchMode:e.watch,isDryRun:e.dryRun});e.ci&&t&&(console.error(o.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(o.yellow("šŸ’” Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${o.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),f.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(e,t)=>{let n=await i.loadConfig();if(!n){console.log(o.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),n=e}await g.runStatus(n,{detail:e,namespace:t.namespace})}),f.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const o=await i.ensureConfig(),a=()=>c.runTypesGenerator(o);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await n.glob(o.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),f.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),f.command("migrate-config").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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async e=>{const r=async()=>{let e=await i.loadConfig();if(!e){console.log(o.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(o.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${o.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(o.green("Project structure detected successfully!")),e=t}await d.runLinter(e)};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await i.loadConfig();if(e?.extract?.input){t.watch(await n.glob(e.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
@@ -1 +1 @@
1
- "use strict";var e=require("ora"),t=require("chalk"),a=require("@swc/core"),r=require("node:fs/promises"),o=require("node:path"),n=require("./key-finder.js"),s=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),l=require("../parsers/comment-parser.js"),u=require("../../utils/logger.js"),g=require("../../utils/file-utils.js");let f=!1;function p(e,t,a,r=new u.ConsoleLogger){if(e&&"object"==typeof e){for(const o of t)try{o.onVisitNode?.(e,a)}catch(e){r.warn(`Plugin ${o.name} onVisitNode failed:`,e)}for(const o of Object.keys(e)){const n=e[o];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&p(e,t,a,r);else n&&"object"==typeof n&&p(n,t,a,r)}}}exports.extract=async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(t=>t!==e?.extract?.primaryLanguage),e.extract.functions||=["t","*.t"],e.extract.transComponents||=["Trans"];const{allKeys:t,objectKeys:a}=await n.findKeys(e);return s.getTranslations(t,a,e)},exports.processFile=async function(e,t,o,n,s=new u.ConsoleLogger){try{let i=await r.readFile(e,"utf-8");for(const a of t.plugins||[])i=await(a.onLoad?.(i,e))??i;const u=await a.parse(i,{syntax:"typescript",tsx:!0,decorators:!0,comments:!0}),g=c.createPluginContext(o);l.extractKeysFromComments(i,g,t),n.visit(u),(t.plugins||[]).length>0&&p(u,t.plugins||[],g,s)}catch(t){throw new i.ExtractorError("Failed to process file",e,t)}},exports.runExtractor=async function(a,{isWatchMode:c=!1,isDryRun:l=!1}={},p=new u.ConsoleLogger){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(e=>e!==a?.extract?.primaryLanguage),a.extract.functions||=["t","*.t"],a.extract.transComponents||=["Trans"],i.validateExtractorConfig(a);const y=e("Running i18next key extractor...\n").start();try{const{allKeys:e,objectKeys:i}=await n.findKeys(a,p);y.text=`Found ${e.size} unique keys. Updating translation files...`;const u=await s.getTranslations(e,i,a);let d=!1;for(const e of u)if(e.updated&&(d=!0,!l)){const n=g.serializeTranslationFile(e.newTranslations,a.extract.outputFormat,a.extract.indentation);await r.mkdir(o.dirname(e.path),{recursive:!0}),await r.writeFile(e.path,n),p.info(t.green(`Updated: ${e.path}`))}if((a.plugins||[]).length>0){y.text="Running post-extraction plugins...";for(const e of a.plugins||[])await(e.afterSync?.(u,a))}return y.succeed(t.bold("Extraction complete!")),d&&function(e=!1){if(e&&f)return;console.log(t.yellow.bold("\nšŸ’” Tip: Tired of running the extractor manually?")),console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,'),console.log(" where keys are created and translated automatically as you code."),console.log(` Learn more: ${t.cyan("https://www.locize.com/blog/i18next-savemissing-ai-automation")}`),console.log(` Watch the video: ${t.cyan("https://youtu.be/joPsZghT3wM")}`),e&&(f=!0)}(c),d}catch(e){throw y.fail(t.red("Extraction failed.")),e}};
1
+ "use strict";var e=require("ora"),t=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),o=require("./key-finder.js"),s=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),l=require("../parsers/comment-parser.js"),u=require("../../utils/logger.js"),g=require("../../utils/file-utils.js"),f=require("../../utils/funnel-msg-tracker.js");function p(e,t,r,a=new u.ConsoleLogger){if(e&&"object"==typeof e){for(const n of t)try{n.onVisitNode?.(e,r)}catch(e){a.warn(`Plugin ${n.name} onVisitNode failed:`,e)}for(const n of Object.keys(e)){const o=e[n];if(Array.isArray(o))for(const e of o)e&&"object"==typeof e&&p(e,t,r,a);else o&&"object"==typeof o&&p(o,t,r,a)}}}exports.extract=async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(t=>t!==e?.extract?.primaryLanguage),e.extract.functions||=["t","*.t"],e.extract.transComponents||=["Trans"];const{allKeys:t,objectKeys:r}=await o.findKeys(e);return s.getTranslations(t,r,e)},exports.processFile=async function(e,t,n,o,s=new u.ConsoleLogger){try{let i=await a.readFile(e,"utf-8");for(const r of t.plugins||[])i=await(r.onLoad?.(i,e))??i;const u=await r.parse(i,{syntax:"typescript",tsx:!0,decorators:!0,comments:!0}),g=c.createPluginContext(n);l.extractKeysFromComments(i,g,t),o.visit(u),(t.plugins||[]).length>0&&p(u,t.plugins||[],g,s)}catch(t){throw new i.ExtractorError("Failed to process file",e,t)}},exports.runExtractor=async function(r,{isWatchMode:c=!1,isDryRun:l=!1}={},p=new u.ConsoleLogger){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(e=>e!==r?.extract?.primaryLanguage),r.extract.functions||=["t","*.t"],r.extract.transComponents||=["Trans"],i.validateExtractorConfig(r);const y=e("Running i18next key extractor...\n").start();try{const{allKeys:e,objectKeys:i}=await o.findKeys(r,p);y.text=`Found ${e.size} unique keys. Updating translation files...`;const c=await s.getTranslations(e,i,r);let u=!1;for(const e of c)if(e.updated&&(u=!0,!l)){const o=g.serializeTranslationFile(e.newTranslations,r.extract.outputFormat,r.extract.indentation);await a.mkdir(n.dirname(e.path),{recursive:!0}),await a.writeFile(e.path,o),p.info(t.green(`Updated: ${e.path}`))}if((r.plugins||[]).length>0){y.text="Running post-extraction plugins...";for(const e of r.plugins||[])await(e.afterSync?.(c,r))}return y.succeed(t.bold("Extraction complete!")),u&&await async function(){if(!await f.shouldShowFunnel("extract"))return;return console.log(t.yellow.bold("\nšŸ’” Tip: Tired of running the extractor manually?")),console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,'),console.log(" where keys are created and translated automatically as you code."),console.log(` Learn more: ${t.cyan("https://www.locize.com/blog/i18next-savemissing-ai-automation")}`),console.log(` Watch the video: ${t.cyan("https://youtu.be/joPsZghT3wM")}`),f.recordFunnelShown("extract")}(),u}catch(e){throw y.fail(t.red("Extraction failed.")),e}};
@@ -1 +1 @@
1
- "use strict";var t=require("node:path"),e=require("glob"),s=require("../../utils/nested-object.js"),a=require("../../utils/file-utils.js");function n(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function o(t,e,a,n,o,r){const{keySeparator:c=".",sort:i=!0,removeUnusedKeys:u=!0,primaryLanguage:l,defaultValue:f=""}=a.extract;let g=u?{}:JSON.parse(JSON.stringify(e));const p=s.getNestedKeys(e,c??".");for(const t of p)if(o.some(e=>e.test(t))){const a=s.getNestedValue(e,t,c??".");s.setNestedValue(g,t,a,c??".")}for(const{key:a,defaultValue:o}of t){const i=s.getNestedValue(e,a,c??"."),u=!t.some(t=>t.key.startsWith(`${a}${c}`)&&t.key!==a),p="object"==typeof i&&null!==i&&u&&!r.has(a),y=void 0===i||p?n===l?o:f:i;s.setNestedValue(g,a,y,c??".")}if(!1!==i){const e={},s=Object.keys(g),a=new Map;for(const e of t){const t=!1===c?e.key:e.key.split(c)[0];a.has(t)||a.set(t,e)}s.sort((t,e)=>{if("function"==typeof i){const s=a.get(t),n=a.get(e);if(s&&n)return i(s,n)}return t.localeCompare(e)});for(const t of s)e[t]=g[t];g=e}return g}exports.getTranslations=async function(s,r,c){c.extract.primaryLanguage||=c.locales[0]||"en",c.extract.secondaryLanguages||=c.locales.filter(t=>t!==c?.extract?.primaryLanguage);const i=c.extract.defaultNS??"translation",u=[...c.extract.preservePatterns||[]],l=c.extract.indentation??2;for(const t of r)u.push(`${t}.*`);const f=u.map(n),g=new Map;for(const t of s.values()){const e=t.ns||i;g.has(e)||g.set(e,[]),g.get(e).push(t)}const p=[],y=Array.isArray(c.extract.ignore)?c.extract.ignore:c.extract.ignore?[c.extract.ignore]:[];for(const s of c.locales){if(c.extract.mergeNamespaces||!c.extract.output.includes("{{namespace}}")){const e={},n=a.getOutputPath(c.extract.output,s),i=t.resolve(process.cwd(),n),u=await a.loadTranslationFile(i)||{},y=new Set([...g.keys(),...Object.keys(u)]);for(const t of y){const a=g.get(t)||[],n=u[t]||{};e[t]=o(a,n,c,s,f,r)}const d=JSON.stringify(u,null,l),x=JSON.stringify(e,null,l);p.push({path:i,updated:x!==d,newTranslations:e,existingTranslations:u})}else{const n=new Set(g.keys()),i=a.getOutputPath(c.extract.output,s,"*"),u=await e.glob(i,{ignore:y});for(const e of u)n.add(t.basename(e,t.extname(e)));for(const e of n){const n=g.get(e)||[],i=a.getOutputPath(c.extract.output,s,e),u=t.resolve(process.cwd(),i),y=await a.loadTranslationFile(u)||{},d=o(n,y,c,s,f,r),x=JSON.stringify(y,null,l),h=JSON.stringify(d,null,l);p.push({path:u,updated:h!==x,newTranslations:d,existingTranslations:y})}}}return p};
1
+ "use strict";var t=require("node:path"),e=require("glob"),s=require("../../utils/nested-object.js"),n=require("../../utils/file-utils.js");function o(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function r(t){if("object"!=typeof t||null===t||Array.isArray(t))return t;const e={},s=Object.keys(t).sort((t,e)=>t.localeCompare(e,void 0,{sensitivity:"base"}));for(const n of s)e[n]=r(t[n]);return e}function a(t,e,n,o,a,c){const{keySeparator:i=".",sort:u=!0,removeUnusedKeys:l=!0,primaryLanguage:f,defaultValue:p=""}=n.extract;let g=l?{}:JSON.parse(JSON.stringify(e));const y=s.getNestedKeys(e,i??".");for(const t of y)if(a.some(e=>e.test(t))){const n=s.getNestedValue(e,t,i??".");s.setNestedValue(g,t,n,i??".")}for(const{key:n,defaultValue:r}of t){const a=s.getNestedValue(e,n,i??"."),u=!t.some(t=>t.key.startsWith(`${n}${i}`)&&t.key!==n),l="object"==typeof a&&null!==a&&u&&!c.has(n),y=void 0===a||l?o===f?r:p:a;s.setNestedValue(g,n,y,i??".")}if(!0===u)return r(g);if("function"==typeof u){const e={},s=Object.keys(g),n=new Map;for(const e of t){const t=!1===i?e.key:e.key.split(i)[0];n.has(t)||n.set(t,e)}s.sort((t,e)=>{if("function"==typeof u){const s=n.get(t),o=n.get(e);if(s&&o)return u(s,o)}return t.localeCompare(e,void 0,{sensitivity:"base"})});for(const t of s)e[t]=g[t];g=e}return g}exports.getTranslations=async function(s,r,c){c.extract.primaryLanguage||=c.locales[0]||"en",c.extract.secondaryLanguages||=c.locales.filter(t=>t!==c?.extract?.primaryLanguage);const i=c.extract.defaultNS??"translation",u=[...c.extract.preservePatterns||[]],l=c.extract.indentation??2;for(const t of r)u.push(`${t}.*`);const f=u.map(o),p=new Map;for(const t of s.values()){const e=t.ns||i;p.has(e)||p.set(e,[]),p.get(e).push(t)}const g=[],y=Array.isArray(c.extract.ignore)?c.extract.ignore:c.extract.ignore?[c.extract.ignore]:[];for(const s of c.locales){if(c.extract.mergeNamespaces||!c.extract.output.includes("{{namespace}}")){const e={},o=n.getOutputPath(c.extract.output,s),i=t.resolve(process.cwd(),o),u=await n.loadTranslationFile(i)||{},y=new Set([...p.keys(),...Object.keys(u)]);for(const t of y){const n=p.get(t)||[],o=u[t]||{};e[t]=a(n,o,c,s,f,r)}const d=JSON.stringify(u,null,l),x=JSON.stringify(e,null,l);g.push({path:i,updated:x!==d,newTranslations:e,existingTranslations:u})}else{const o=new Set(p.keys()),i=n.getOutputPath(c.extract.output,s,"*"),u=await e.glob(i,{ignore:y});for(const e of u)o.add(t.basename(e,t.extname(e)));for(const e of o){const o=p.get(e)||[],i=n.getOutputPath(c.extract.output,s,e),u=t.resolve(process.cwd(),i),y=await n.loadTranslationFile(u)||{},d=a(o,y,c,s,f,r),x=JSON.stringify(y,null,l),h=JSON.stringify(d,null,l);g.push({path:u,updated:h!==x,newTranslations:d,existingTranslations:y})}}}return g};
@@ -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.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)}};
1
+ "use strict";var e=require("chalk"),o=require("ora"),a=require("node:path"),t=require("./extractor/core/key-finder.js"),s=require("./utils/nested-object.js"),n=require("./utils/file-utils.js"),l=require("./utils/funnel-msg-tracker.js");function r(o,a,t){const s=t>0?Math.round(a/t*100):100,n=c(s);console.log(`${e.bold(o)}: ${n} ${s}% (${a}/${t})`)}function c(o){const a=Math.floor(o/100*20),t=20-a;return`[${e.green("".padStart(a,"ā– "))}${"".padStart(t,"ā–”")}]`}async function i(){if(await l.shouldShowFunnel("status"))return 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.`),l.recordFunnelShown("status")}exports.runStatus=async function(l,u={}){l.extract.primaryLanguage||=l.locales[0]||"en",l.extract.secondaryLanguages||=l.locales.filter(e=>e!==l?.extract?.primaryLanguage);const y=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 t.findKeys(e),{secondaryLanguages:l,keySeparator:r=".",defaultNS:c="translation",mergeNamespaces:i=!1,pluralSeparator:u="_"}=e.extract,y=new Map;for(const e of o.values()){const o=e.ns||c;y.has(o)||y.set(o,[]),y.get(o).push(e)}const d={totalBaseKeys:o.size,keysByNs:y,locales:new Map};for(const o of l){let t=0,l=0;const c=new Map,g=i?await n.loadTranslationFile(a.resolve(process.cwd(),n.getOutputPath(e.extract.output,o)))||{}:null;for(const[d,f]of y.entries()){const y=i?g?.[d]||{}:await n.loadTranslationFile(a.resolve(process.cwd(),n.getOutputPath(e.extract.output,o,d)))||{};let p=0,$=0;const m=[];for(const{key:e,hasCount:a,isOrdinal:t}of f)if(a){const a=t?"ordinal":"cardinal",n=new Intl.PluralRules(o,{type:a}).resolvedOptions().pluralCategories;for(const o of n){$++;const a=t?`${e}${u}ordinal${u}${o}`:`${e}${u}${o}`,n=!!s.getNestedValue(y,a,r??".");n&&p++,m.push({key:a,isTranslated:n})}}else{$++;const o=!!s.getNestedValue(y,e,r??".");o&&p++,m.push({key:e,isTranslated:o})}c.set(d,{totalKeys:$,translatedKeys:p,keyDetails:m}),t+=p,l+=$}d.locales.set(o,{totalKeys:l,totalTranslated:t,namespaces:c})}return d}(l);y.succeed("Analysis complete."),await async function(o,a,t){t.detail?await async function(o,a,t,s){if(t===a.extract.primaryLanguage)return void console.log(e.yellow(`Locale "${t}" is the primary language. All keys are considered present.`));if(!a.locales.includes(t))return void console.error(e.red(`Error: Locale "${t}" is not defined in your configuration.`));const n=o.locales.get(t);if(!n)return void console.error(e.red(`Error: Locale "${t}" is not a valid secondary language.`));console.log(e.bold(`\nKey Status for "${e.cyan(t)}":`));const l=Array.from(o.keysByNs.values()).flat().length;r("Overall",n.totalTranslated,n.totalKeys);const c=s?[s]:Array.from(n.namespaces.keys()).sort();for(const o of c){const a=n.namespaces.get(o);a&&(console.log(e.cyan.bold(`\nNamespace: ${o}`)),r("Namespace Progress",a.translatedKeys,a.totalKeys),a.keyDetails.forEach(({key:o,isTranslated:a})=>{const t=a?e.green("āœ“"):e.red("āœ—");console.log(` ${t} ${o}`)}))}const u=l-n.totalTranslated;u>0?console.log(e.yellow.bold(`\nSummary: Found ${u} missing translations for "${t}".`)):console.log(e.green.bold(`\nSummary: šŸŽ‰ All keys are translated for "${t}".`));await i()}(o,a,t.detail,t.namespace):t.namespace?await async function(o,a,t){const s=o.keysByNs.get(t);if(!s)return void console.error(e.red(`Error: Namespace "${t}" was not found in your source code.`));console.log(e.cyan.bold(`\nStatus for Namespace: "${t}"`)),console.log("------------------------");for(const[e,a]of o.locales.entries()){const o=a.namespaces.get(t);if(o){const a=o.totalKeys>0?Math.round(o.translatedKeys/o.totalKeys*100):100,t=c(a);console.log(`- ${e}: ${t} ${a}% (${o.translatedKeys}/${o.totalKeys} keys)`)}}await i()}(o,0,t.namespace):await async function(o,a){const{primaryLanguage:t}=a.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(a.locales.join(", "))}`),console.log(`āœ… Primary Language: ${e.bold(t)}`),console.log("\nTranslation Progress:");for(const[e,a]of o.locales.entries()){const o=a.totalKeys>0?Math.round(a.totalTranslated/a.totalKeys*100):100,t=c(o);console.log(`- ${e}: ${t} ${o}% (${a.totalTranslated}/${a.totalKeys} keys)`)}await i()}(o,a)}(o,l,u)}catch(e){y.fail("Failed to generate status report."),console.error(e)}};
@@ -1 +1 @@
1
- "use strict";var e=require("node:fs/promises"),o=require("path"),t=require("chalk"),n=require("ora"),r=require("glob"),a=require("./utils/nested-object.js"),i=require("./utils/file-utils.js");exports.runSyncer=async function(l){const s=n("Running i18next locale synchronizer...\n").start();try{const n=l.extract.primaryLanguage||l.locales[0]||"en",c=l.locales.filter(e=>e!==n),{output:u,keySeparator:d=".",outputFormat:f="json",indentation:y=2,defaultValue:g=""}=l.extract,h=[];let p=!1;const w=i.getOutputPath(u,n,"*"),m=await r.glob(w);if(0===m.length)return void s.warn(`No translation files found for primary language "${n}". Nothing to sync.`);for(const n of m){const r=o.basename(n).split(".")[0],l=await i.loadTranslationFile(n);if(!l){h.push(` ${t.yellow("-")} Could not read primary file: ${n}`);continue}const s=a.getNestedKeys(l,d??".");for(const n of c){const l=i.getOutputPath(u,n,r),c=o.resolve(process.cwd(),l),w=await i.loadTranslationFile(c)||{},m={};for(const e of s){const o=a.getNestedValue(w,e,d??".")??g;a.setNestedValue(m,e,o,d??".")}const S=JSON.stringify(w);if(JSON.stringify(m)!==S){p=!0;const n=i.serializeTranslationFile(m,f,y);await e.mkdir(o.dirname(c),{recursive:!0}),await e.writeFile(c,n),h.push(` ${t.green("āœ“")} Synchronized: ${l}`)}else h.push(` ${t.gray("-")} Already in sync: ${l}`)}}s.succeed(t.bold("Synchronization complete!")),h.forEach(e=>console.log(e)),p?(console.log(t.green.bold("\nāœ… Sync complete.")),console.log(t.yellow("šŸš€ Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${t.cyan("npx i18next-cli locize-migrate")}`)):console.log(t.green.bold("\nāœ… All locales are already in sync."))}catch(e){s.fail(t.red("Synchronization failed.")),console.error(e)}};
1
+ "use strict";var e=require("node:fs/promises"),t=require("path"),o=require("chalk"),n=require("ora"),r=require("glob"),a=require("./utils/nested-object.js"),i=require("./utils/file-utils.js"),l=require("./utils/funnel-msg-tracker.js");exports.runSyncer=async function(s){const c=n("Running i18next locale synchronizer...\n").start();try{const n=s.extract.primaryLanguage||s.locales[0]||"en",u=s.locales.filter(e=>e!==n),{output:d,keySeparator:f=".",outputFormat:y="json",indentation:g=2,defaultValue:h=""}=s.extract,p=[];let w=!1;const m=i.getOutputPath(d,n,"*"),S=await r.glob(m);if(0===S.length)return void c.warn(`No translation files found for primary language "${n}". Nothing to sync.`);for(const n of S){const r=t.basename(n).split(".")[0],l=await i.loadTranslationFile(n);if(!l){p.push(` ${o.yellow("-")} Could not read primary file: ${n}`);continue}const s=a.getNestedKeys(l,f??".");for(const n of u){const l=i.getOutputPath(d,n,r),c=t.resolve(process.cwd(),l),u=await i.loadTranslationFile(c)||{},m={};for(const e of s){const t=a.getNestedValue(u,e,f??".")??h;a.setNestedValue(m,e,t,f??".")}const S=JSON.stringify(u);if(JSON.stringify(m)!==S){w=!0;const n=i.serializeTranslationFile(m,y,g);await e.mkdir(t.dirname(c),{recursive:!0}),await e.writeFile(c,n),p.push(` ${o.green("āœ“")} Synchronized: ${l}`)}else p.push(` ${o.gray("-")} Already in sync: ${l}`)}}c.succeed(o.bold("Synchronization complete!")),p.forEach(e=>console.log(e)),w?await async function(){if(!await l.shouldShowFunnel("syncer"))return;return console.log(o.green.bold("\nāœ… Sync complete.")),console.log(o.yellow("šŸš€ Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${o.cyan("npx i18next-cli locize-migrate")}`),l.recordFunnelShown("syncer")}():console.log(o.green.bold("\nāœ… All locales are already in sync."))}catch(e){c.fail(o.red("Synchronization failed.")),console.error(e)}};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("node:path"),t=require("node:os"),n=require("node:fs/promises");const r={},i=e.join(t.tmpdir(),"i18next-cli-last-funnel-message-shown.json");exports.recordFunnelShown=async function(e){try{r[e]=!0;let t={};try{const e=await n.readFile(i,"utf-8");t=JSON.parse(e)}catch(e){}t[e]=Date.now(),await n.writeFile(i,JSON.stringify(t))}catch(e){}},exports.shouldShowFunnel=async function(e){if(r[e])return!1;try{const t=await n.readFile(i,"utf-8"),r=JSON.parse(t);if(Date.now()-(r[e]||0)<864e5)return!1}catch(e){}return!0};
package/dist/esm/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import n from"chalk";import{ensureConfig as i,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as g}from"./status.js";import{runLocizeSync as f,runLocizeDownload as u,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.5.1"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").action(async t=>{const a=await i(),c=async()=>{const o=await r(a,{isWatchMode:t.watch,isDryRun:t.dryRun});t.ci&&o&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("šŸ’” Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),t.watch){console.log("\nWatching for changes...");o.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(t,o)=>{let e=await a();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await g(e,{detail:t,namespace:o.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async t=>{const n=await i(),a=()=>s(n);if(await a(),t.watch){console.log("\nWatching for changes...");o.watch(await e(n.types?.input||[]),{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=await i();await l(t)}),w.command("migrate-config").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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async t=>{const i=async()=>{let t=await a();if(!t){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),t=o}await d(t)};if(await i(),t.watch){console.log("\nWatching for changes...");const t=await a();if(t?.extract?.input){o.watch(await e(t.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),i()})}}}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async t=>{const o=await i();await f(o,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=await i();await u(o,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=await i();await h(o,t)}),w.parse(process.argv);
2
+ import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import n from"chalk";import{ensureConfig as i,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as g}from"./status.js";import{runLocizeSync as f,runLocizeDownload as u,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.5.3"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").action(async t=>{const a=await i(),c=async()=>{const o=await r(a,{isWatchMode:t.watch,isDryRun:t.dryRun});t.ci&&o&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("šŸ’” Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),t.watch){console.log("\nWatching for changes...");o.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(t,o)=>{let e=await a();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await g(e,{detail:t,namespace:o.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async t=>{const n=await i(),a=()=>s(n);if(await a(),t.watch){console.log("\nWatching for changes...");o.watch(await e(n.types?.input||[]),{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=await i();await l(t)}),w.command("migrate-config").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.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async t=>{const i=async()=>{let t=await a();if(!t){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),t=o}await d(t)};if(await i(),t.watch){console.log("\nWatching for changes...");const t=await a();if(t?.extract?.input){o.watch(await e(t.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),i()})}}}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async t=>{const o=await i();await f(o,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=await i();await u(o,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=await i();await h(o,t)}),w.parse(process.argv);
@@ -1 +1 @@
1
- import t from"ora";import o from"chalk";import{parse as e}from"@swc/core";import{mkdir as a,writeFile as n,readFile as r}from"node:fs/promises";import{dirname as i}from"node:path";import{findKeys as s}from"./key-finder.js";import{getTranslations as c}from"./translation-manager.js";import{validateExtractorConfig as l,ExtractorError as f}from"../../utils/validation.js";import{createPluginContext as p}from"../plugin-manager.js";import{extractKeysFromComments as u}from"../parsers/comment-parser.js";import{ConsoleLogger as m}from"../../utils/logger.js";import{serializeTranslationFile as g}from"../../utils/file-utils.js";let y=!1;async function d(e,{isWatchMode:r=!1,isDryRun:f=!1}={},p=new m){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(t=>t!==e?.extract?.primaryLanguage),e.extract.functions||=["t","*.t"],e.extract.transComponents||=["Trans"],l(e);const u=t("Running i18next key extractor...\n").start();try{const{allKeys:t,objectKeys:l}=await s(e,p);u.text=`Found ${t.size} unique keys. Updating translation files...`;const m=await c(t,l,e);let d=!1;for(const t of m)if(t.updated&&(d=!0,!f)){const r=g(t.newTranslations,e.extract.outputFormat,e.extract.indentation);await a(i(t.path),{recursive:!0}),await n(t.path,r),p.info(o.green(`Updated: ${t.path}`))}if((e.plugins||[]).length>0){u.text="Running post-extraction plugins...";for(const t of e.plugins||[])await(t.afterSync?.(m,e))}return u.succeed(o.bold("Extraction complete!")),d&&function(t=!1){if(t&&y)return;console.log(o.yellow.bold("\nšŸ’” Tip: Tired of running the extractor manually?")),console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,'),console.log(" where keys are created and translated automatically as you code."),console.log(` Learn more: ${o.cyan("https://www.locize.com/blog/i18next-savemissing-ai-automation")}`),console.log(` Watch the video: ${o.cyan("https://youtu.be/joPsZghT3wM")}`),t&&(y=!0)}(r),d}catch(t){throw u.fail(o.red("Extraction failed.")),t}}async function w(t,o,a,n,i=new m){try{let s=await r(t,"utf-8");for(const e of o.plugins||[])s=await(e.onLoad?.(s,t))??s;const c=await e(s,{syntax:"typescript",tsx:!0,decorators:!0,comments:!0}),l=p(a);u(s,l,o),n.visit(c),(o.plugins||[]).length>0&&x(c,o.plugins||[],l,i)}catch(o){throw new f("Failed to process file",t,o)}}function x(t,o,e,a=new m){if(t&&"object"==typeof t){for(const n of o)try{n.onVisitNode?.(t,e)}catch(t){a.warn(`Plugin ${n.name} onVisitNode failed:`,t)}for(const n of Object.keys(t)){const r=t[n];if(Array.isArray(r))for(const t of r)t&&"object"==typeof t&&x(t,o,e,a);else r&&"object"==typeof r&&x(r,o,e,a)}}}async function h(t){t.extract.primaryLanguage||=t.locales[0]||"en",t.extract.secondaryLanguages||=t.locales.filter(o=>o!==t?.extract?.primaryLanguage),t.extract.functions||=["t","*.t"],t.extract.transComponents||=["Trans"];const{allKeys:o,objectKeys:e}=await s(t);return c(o,e,t)}export{h as extract,w as processFile,d as runExtractor};
1
+ import t from"ora";import o from"chalk";import{parse as e}from"@swc/core";import{mkdir as a,writeFile as n,readFile as r}from"node:fs/promises";import{dirname as i}from"node:path";import{findKeys as s}from"./key-finder.js";import{getTranslations as c}from"./translation-manager.js";import{validateExtractorConfig as l,ExtractorError as f}from"../../utils/validation.js";import{createPluginContext as u}from"../plugin-manager.js";import{extractKeysFromComments as m}from"../parsers/comment-parser.js";import{ConsoleLogger as p}from"../../utils/logger.js";import{serializeTranslationFile as g}from"../../utils/file-utils.js";import{shouldShowFunnel as y,recordFunnelShown as d}from"../../utils/funnel-msg-tracker.js";async function w(e,{isWatchMode:r=!1,isDryRun:f=!1}={},u=new p){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(t=>t!==e?.extract?.primaryLanguage),e.extract.functions||=["t","*.t"],e.extract.transComponents||=["Trans"],l(e);const m=t("Running i18next key extractor...\n").start();try{const{allKeys:t,objectKeys:r}=await s(e,u);m.text=`Found ${t.size} unique keys. Updating translation files...`;const l=await c(t,r,e);let p=!1;for(const t of l)if(t.updated&&(p=!0,!f)){const r=g(t.newTranslations,e.extract.outputFormat,e.extract.indentation);await a(i(t.path),{recursive:!0}),await n(t.path,r),u.info(o.green(`Updated: ${t.path}`))}if((e.plugins||[]).length>0){m.text="Running post-extraction plugins...";for(const t of e.plugins||[])await(t.afterSync?.(l,e))}return m.succeed(o.bold("Extraction complete!")),p&&await async function(){if(!await y("extract"))return;return console.log(o.yellow.bold("\nšŸ’” Tip: Tired of running the extractor manually?")),console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,'),console.log(" where keys are created and translated automatically as you code."),console.log(` Learn more: ${o.cyan("https://www.locize.com/blog/i18next-savemissing-ai-automation")}`),console.log(` Watch the video: ${o.cyan("https://youtu.be/joPsZghT3wM")}`),d("extract")}(),p}catch(t){throw m.fail(o.red("Extraction failed.")),t}}async function x(t,o,a,n,i=new p){try{let s=await r(t,"utf-8");for(const e of o.plugins||[])s=await(e.onLoad?.(s,t))??s;const c=await e(s,{syntax:"typescript",tsx:!0,decorators:!0,comments:!0}),l=u(a);m(s,l,o),n.visit(c),(o.plugins||[]).length>0&&h(c,o.plugins||[],l,i)}catch(o){throw new f("Failed to process file",t,o)}}function h(t,o,e,a=new p){if(t&&"object"==typeof t){for(const n of o)try{n.onVisitNode?.(t,e)}catch(t){a.warn(`Plugin ${n.name} onVisitNode failed:`,t)}for(const n of Object.keys(t)){const r=t[n];if(Array.isArray(r))for(const t of r)t&&"object"==typeof t&&h(t,o,e,a);else r&&"object"==typeof r&&h(r,o,e,a)}}}async function j(t){t.extract.primaryLanguage||=t.locales[0]||"en",t.extract.secondaryLanguages||=t.locales.filter(o=>o!==t?.extract?.primaryLanguage),t.extract.functions||=["t","*.t"],t.extract.transComponents||=["Trans"];const{allKeys:o,objectKeys:e}=await s(t);return c(o,e,t)}export{j as extract,x as processFile,w as runExtractor};
@@ -1 +1 @@
1
- import{resolve as t,basename as e,extname as o}from"node:path";import{glob as s}from"glob";import{getNestedKeys as n,getNestedValue as r,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as c,loadTranslationFile as i}from"../../utils/file-utils.js";function f(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function u(t,e,o,s,c,i){const{keySeparator:f=".",sort:u=!0,removeUnusedKeys:l=!0,primaryLanguage:p,defaultValue:g=""}=o.extract;let y=l?{}:JSON.parse(JSON.stringify(e));const x=n(e,f??".");for(const t of x)if(c.some(e=>e.test(t))){const o=r(e,t,f??".");a(y,t,o,f??".")}for(const{key:o,defaultValue:n}of t){const c=r(e,o,f??"."),u=!t.some(t=>t.key.startsWith(`${o}${f}`)&&t.key!==o),l="object"==typeof c&&null!==c&&u&&!i.has(o);a(y,o,void 0===c||l?s===p?n:g:c,f??".")}if(!1!==u){const e={},o=Object.keys(y),s=new Map;for(const e of t){const t=!1===f?e.key:e.key.split(f)[0];s.has(t)||s.set(t,e)}o.sort((t,e)=>{if("function"==typeof u){const o=s.get(t),n=s.get(e);if(o&&n)return u(o,n)}return t.localeCompare(e)});for(const t of o)e[t]=y[t];y=e}return y}async function l(n,r,a){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage);const l=a.extract.defaultNS??"translation",p=[...a.extract.preservePatterns||[]],g=a.extract.indentation??2;for(const t of r)p.push(`${t}.*`);const y=p.map(f),x=new Map;for(const t of n.values()){const e=t.ns||l;x.has(e)||x.set(e,[]),x.get(e).push(t)}const m=[],d=Array.isArray(a.extract.ignore)?a.extract.ignore:a.extract.ignore?[a.extract.ignore]:[];for(const n of a.locales){if(a.extract.mergeNamespaces||!a.extract.output.includes("{{namespace}}")){const e={},o=c(a.extract.output,n),s=t(process.cwd(),o),f=await i(s)||{},l=new Set([...x.keys(),...Object.keys(f)]);for(const t of l){const o=x.get(t)||[],s=f[t]||{};e[t]=u(o,s,a,n,y,r)}const p=JSON.stringify(f,null,g),d=JSON.stringify(e,null,g);m.push({path:s,updated:d!==p,newTranslations:e,existingTranslations:f})}else{const f=new Set(x.keys()),l=c(a.extract.output,n,"*"),p=await s(l,{ignore:d});for(const t of p)f.add(e(t,o(t)));for(const e of f){const o=x.get(e)||[],s=c(a.extract.output,n,e),f=t(process.cwd(),s),l=await i(f)||{},p=u(o,l,a,n,y,r),d=JSON.stringify(l,null,g),w=JSON.stringify(p,null,g);m.push({path:f,updated:w!==d,newTranslations:p,existingTranslations:l})}}}return m}export{l as getTranslations};
1
+ import{resolve as t,basename as e,extname as o}from"node:path";import{glob as s}from"glob";import{getNestedKeys as n,getNestedValue as r,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as c,loadTranslationFile as i}from"../../utils/file-utils.js";function f(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function u(t){if("object"!=typeof t||null===t||Array.isArray(t))return t;const e={},o=Object.keys(t).sort((t,e)=>t.localeCompare(e,void 0,{sensitivity:"base"}));for(const s of o)e[s]=u(t[s]);return e}function l(t,e,o,s,c,i){const{keySeparator:f=".",sort:l=!0,removeUnusedKeys:p=!0,primaryLanguage:y,defaultValue:g=""}=o.extract;let d=p?{}:JSON.parse(JSON.stringify(e));const m=n(e,f??".");for(const t of m)if(c.some(e=>e.test(t))){const o=r(e,t,f??".");a(d,t,o,f??".")}for(const{key:o,defaultValue:n}of t){const c=r(e,o,f??"."),u=!t.some(t=>t.key.startsWith(`${o}${f}`)&&t.key!==o),l="object"==typeof c&&null!==c&&u&&!i.has(o);a(d,o,void 0===c||l?s===y?n:g:c,f??".")}if(!0===l)return u(d);if("function"==typeof l){const e={},o=Object.keys(d),s=new Map;for(const e of t){const t=!1===f?e.key:e.key.split(f)[0];s.has(t)||s.set(t,e)}o.sort((t,e)=>{if("function"==typeof l){const o=s.get(t),n=s.get(e);if(o&&n)return l(o,n)}return t.localeCompare(e,void 0,{sensitivity:"base"})});for(const t of o)e[t]=d[t];d=e}return d}async function p(n,r,a){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage);const u=a.extract.defaultNS??"translation",p=[...a.extract.preservePatterns||[]],y=a.extract.indentation??2;for(const t of r)p.push(`${t}.*`);const g=p.map(f),d=new Map;for(const t of n.values()){const e=t.ns||u;d.has(e)||d.set(e,[]),d.get(e).push(t)}const m=[],x=Array.isArray(a.extract.ignore)?a.extract.ignore:a.extract.ignore?[a.extract.ignore]:[];for(const n of a.locales){if(a.extract.mergeNamespaces||!a.extract.output.includes("{{namespace}}")){const e={},o=c(a.extract.output,n),s=t(process.cwd(),o),f=await i(s)||{},u=new Set([...d.keys(),...Object.keys(f)]);for(const t of u){const o=d.get(t)||[],s=f[t]||{};e[t]=l(o,s,a,n,g,r)}const p=JSON.stringify(f,null,y),x=JSON.stringify(e,null,y);m.push({path:s,updated:x!==p,newTranslations:e,existingTranslations:f})}else{const f=new Set(d.keys()),u=c(a.extract.output,n,"*"),p=await s(u,{ignore:x});for(const t of p)f.add(e(t,o(t)));for(const e of f){const o=d.get(e)||[],s=c(a.extract.output,n,e),f=t(process.cwd(),s),u=await i(f)||{},p=l(o,u,a,n,g,r),x=JSON.stringify(u,null,y),w=JSON.stringify(p,null,y);m.push({path:f,updated:w!==x,newTranslations:p,existingTranslations:u})}}}return m}export{p as getTranslations};
@@ -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 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};
1
+ import o from"chalk";import e from"ora";import{resolve as a}from"node:path";import{findKeys as t}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";import{shouldShowFunnel as r,recordFunnelShown as c}from"./utils/funnel-msg-tracker.js";async function i(r,c={}){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(o=>o!==r?.extract?.primaryLanguage);const i=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 t(o),{secondaryLanguages:r,keySeparator:c=".",defaultNS:i="translation",mergeNamespaces:y=!1,pluralSeparator:u="_"}=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={totalBaseKeys:e.size,keysByNs:d,locales:new Map};for(const e of r){let t=0,r=0;const i=new Map,f=y?await n(a(process.cwd(),l(o.extract.output,e)))||{}:null;for(const[g,p]of d.entries()){const d=y?f?.[g]||{}:await n(a(process.cwd(),l(o.extract.output,e,g)))||{};let m=0,$=0;const w=[];for(const{key:o,hasCount:a,isOrdinal:t}of p)if(a){const a=t?"ordinal":"cardinal",n=new Intl.PluralRules(e,{type:a}).resolvedOptions().pluralCategories;for(const e of n){$++;const a=t?`${o}${u}ordinal${u}${e}`:`${o}${u}${e}`,n=!!s(d,a,c??".");n&&m++,w.push({key:a,isTranslated:n})}}else{$++;const e=!!s(d,o,c??".");e&&m++,w.push({key:o,isTranslated:e})}i.set(g,{totalKeys:$,translatedKeys:m,keyDetails:w}),t+=m,r+=$}g.locales.set(e,{totalKeys:r,totalTranslated:t,namespaces:i})}return g}(r);i.succeed("Analysis complete."),await async function(e,a,t){t.detail?await async function(e,a,t,s){if(t===a.extract.primaryLanguage)return void console.log(o.yellow(`Locale "${t}" is the primary language. All keys are considered present.`));if(!a.locales.includes(t))return void console.error(o.red(`Error: Locale "${t}" is not defined in your configuration.`));const n=e.locales.get(t);if(!n)return void console.error(o.red(`Error: Locale "${t}" is not a valid secondary language.`));console.log(o.bold(`\nKey Status for "${o.cyan(t)}":`));const l=Array.from(e.keysByNs.values()).flat().length;y("Overall",n.totalTranslated,n.totalKeys);const r=s?[s]:Array.from(n.namespaces.keys()).sort();for(const e of r){const a=n.namespaces.get(e);a&&(console.log(o.cyan.bold(`\nNamespace: ${e}`)),y("Namespace Progress",a.translatedKeys,a.totalKeys),a.keyDetails.forEach(({key:e,isTranslated:a})=>{const t=a?o.green("āœ“"):o.red("āœ—");console.log(` ${t} ${e}`)}))}const c=l-n.totalTranslated;c>0?console.log(o.yellow.bold(`\nSummary: Found ${c} missing translations for "${t}".`)):console.log(o.green.bold(`\nSummary: šŸŽ‰ All keys are translated for "${t}".`));await d()}(e,a,t.detail,t.namespace):t.namespace?await async function(e,a,t){const s=e.keysByNs.get(t);if(!s)return void console.error(o.red(`Error: Namespace "${t}" was not found in your source code.`));console.log(o.cyan.bold(`\nStatus for Namespace: "${t}"`)),console.log("------------------------");for(const[o,a]of e.locales.entries()){const e=a.namespaces.get(t);if(e){const a=e.totalKeys>0?Math.round(e.translatedKeys/e.totalKeys*100):100,t=u(a);console.log(`- ${o}: ${t} ${a}% (${e.translatedKeys}/${e.totalKeys} keys)`)}}await d()}(e,0,t.namespace):await async function(e,a){const{primaryLanguage:t}=a.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(a.locales.join(", "))}`),console.log(`āœ… Primary Language: ${o.bold(t)}`),console.log("\nTranslation Progress:");for(const[o,a]of e.locales.entries()){const e=a.totalKeys>0?Math.round(a.totalTranslated/a.totalKeys*100):100,t=u(e);console.log(`- ${o}: ${t} ${e}% (${a.totalTranslated}/${a.totalKeys} keys)`)}await d()}(e,a)}(e,r,c)}catch(o){i.fail("Failed to generate status report."),console.error(o)}}function y(e,a,t){const s=t>0?Math.round(a/t*100):100,n=u(s);console.log(`${o.bold(e)}: ${n} ${s}% (${a}/${t})`)}function u(e){const a=Math.floor(e/100*20),t=20-a;return`[${o.green("".padStart(a,"ā– "))}${"".padStart(t,"ā–”")}]`}async function d(){if(await r("status"))return 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.`),c("status")}export{i as runStatus};
@@ -1 +1 @@
1
- import{mkdir as o,writeFile as t}from"node:fs/promises";import{basename as e,resolve as n,dirname as r}from"path";import i from"chalk";import l from"ora";import{glob as a}from"glob";import{getNestedKeys as s,getNestedValue as c,setNestedValue as f}from"./utils/nested-object.js";import{getOutputPath as p,loadTranslationFile as u,serializeTranslationFile as y}from"./utils/file-utils.js";async function m(m){const d=l("Running i18next locale synchronizer...\n").start();try{const l=m.extract.primaryLanguage||m.locales[0]||"en",g=m.locales.filter(o=>o!==l),{output:h,keySeparator:w=".",outputFormat:S="json",indentation:$=2,defaultValue:x=""}=m.extract,b=[];let z=!1;const j=p(h,l,"*"),N=await a(j);if(0===N.length)return void d.warn(`No translation files found for primary language "${l}". Nothing to sync.`);for(const l of N){const a=e(l).split(".")[0],m=await u(l);if(!m){b.push(` ${i.yellow("-")} Could not read primary file: ${l}`);continue}const d=s(m,w??".");for(const e of g){const l=p(h,e,a),s=n(process.cwd(),l),m=await u(s)||{},g={};for(const o of d){const t=c(m,o,w??".");f(g,o,t??x,w??".")}const j=JSON.stringify(m);if(JSON.stringify(g)!==j){z=!0;const e=y(g,S,$);await o(r(s),{recursive:!0}),await t(s,e),b.push(` ${i.green("āœ“")} Synchronized: ${l}`)}else b.push(` ${i.gray("-")} Already in sync: ${l}`)}}d.succeed(i.bold("Synchronization complete!")),b.forEach(o=>console.log(o)),z?(console.log(i.green.bold("\nāœ… Sync complete.")),console.log(i.yellow("šŸš€ Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${i.cyan("npx i18next-cli locize-migrate")}`)):console.log(i.green.bold("\nāœ… All locales are already in sync."))}catch(o){d.fail(i.red("Synchronization failed.")),console.error(o)}}export{m as runSyncer};
1
+ import{mkdir as o,writeFile as t}from"node:fs/promises";import{basename as n,resolve as e,dirname as r}from"path";import i from"chalk";import a from"ora";import{glob as l}from"glob";import{getNestedKeys as s,getNestedValue as c,setNestedValue as f}from"./utils/nested-object.js";import{getOutputPath as u,loadTranslationFile as y,serializeTranslationFile as m}from"./utils/file-utils.js";import{shouldShowFunnel as p,recordFunnelShown as d}from"./utils/funnel-msg-tracker.js";async function g(g){const h=a("Running i18next locale synchronizer...\n").start();try{const a=g.extract.primaryLanguage||g.locales[0]||"en",w=g.locales.filter(o=>o!==a),{output:S,keySeparator:$=".",outputFormat:x="json",indentation:b=2,defaultValue:j=""}=g.extract,z=[];let N=!1;const k=u(S,a,"*"),v=await l(k);if(0===v.length)return void h.warn(`No translation files found for primary language "${a}". Nothing to sync.`);for(const a of v){const l=n(a).split(".")[0],p=await y(a);if(!p){z.push(` ${i.yellow("-")} Could not read primary file: ${a}`);continue}const d=s(p,$??".");for(const n of w){const a=u(S,n,l),s=e(process.cwd(),a),p=await y(s)||{},g={};for(const o of d){const t=c(p,o,$??".");f(g,o,t??j,$??".")}const h=JSON.stringify(p);if(JSON.stringify(g)!==h){N=!0;const n=m(g,x,b);await o(r(s),{recursive:!0}),await t(s,n),z.push(` ${i.green("āœ“")} Synchronized: ${a}`)}else z.push(` ${i.gray("-")} Already in sync: ${a}`)}}h.succeed(i.bold("Synchronization complete!")),z.forEach(o=>console.log(o)),N?await async function(){if(!await p("syncer"))return;return console.log(i.green.bold("\nāœ… Sync complete.")),console.log(i.yellow("šŸš€ Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${i.cyan("npx i18next-cli locize-migrate")}`),d("syncer")}():console.log(i.green.bold("\nāœ… All locales are already in sync."))}catch(o){h.fail(i.red("Synchronization failed.")),console.error(o)}}export{g as runSyncer};
@@ -0,0 +1 @@
1
+ import{join as t}from"node:path";import{tmpdir as n}from"node:os";import{readFile as o,writeFile as r}from"node:fs/promises";const a={},e=t(n(),"i18next-cli-last-funnel-message-shown.json");async function s(t){if(a[t])return!1;try{const n=await o(e,"utf-8"),r=JSON.parse(n);if(Date.now()-(r[t]||0)<864e5)return!1}catch(t){}return!0}async function i(t){try{a[t]=!0;let n={};try{const t=await o(e,"utf-8");n=JSON.parse(t)}catch(t){}n[t]=Date.now(),await r(e,JSON.stringify(n))}catch(t){}}export{i as recordFunnelShown,s as shouldShowFunnel};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -21,7 +21,7 @@ const program = new Command()
21
21
  program
22
22
  .name('i18next-cli')
23
23
  .description('A unified, high-performance i18next CLI.')
24
- .version('1.5.1')
24
+ .version('1.5.3')
25
25
 
26
26
  program
27
27
  .command('extract')
@@ -12,8 +12,7 @@ import { extractKeysFromComments } from '../parsers/comment-parser'
12
12
  import { ASTVisitors } from '../parsers/ast-visitors'
13
13
  import { ConsoleLogger } from '../../utils/logger'
14
14
  import { serializeTranslationFile } from '../../utils/file-utils'
15
-
16
- let hasLocizeFunnelBeenPrintedInWatchMode = false
15
+ import { shouldShowFunnel, recordFunnelShown } from '../../utils/funnel-msg-tracker'
17
16
 
18
17
  /**
19
18
  * Main extractor function that runs the complete key extraction and file generation process.
@@ -98,9 +97,7 @@ export async function runExtractor (
98
97
  spinner.succeed(chalk.bold('Extraction complete!'))
99
98
 
100
99
  // Show the funnel message only if files were actually changed.
101
- if (anyFileUpdated) {
102
- printLocizeFunnel(isWatchMode)
103
- }
100
+ if (anyFileUpdated) await printLocizeFunnel()
104
101
 
105
102
  return anyFileUpdated
106
103
  } catch (error) {
@@ -229,18 +226,14 @@ export async function extract (config: I18nextToolkitConfig) {
229
226
  * Prints a promotional message for the locize saveMissing workflow.
230
227
  * This message is shown after a successful extraction that resulted in changes.
231
228
  */
232
- function printLocizeFunnel (isWatchMode: boolean = false) {
233
- // Only print if not in watch mode, or if in watch mode and not yet printed.
234
- if (isWatchMode && hasLocizeFunnelBeenPrintedInWatchMode) {
235
- return
236
- }
229
+ async function printLocizeFunnel () {
230
+ if (!(await shouldShowFunnel('extract'))) return
231
+
237
232
  console.log(chalk.yellow.bold('\nšŸ’” Tip: Tired of running the extractor manually?'))
238
233
  console.log(' Discover a real-time "push" workflow with `saveMissing` and Locize AI,')
239
234
  console.log(' where keys are created and translated automatically as you code.')
240
235
  console.log(` Learn more: ${chalk.cyan('https://www.locize.com/blog/i18next-savemissing-ai-automation')}`)
241
236
  console.log(` Watch the video: ${chalk.cyan('https://youtu.be/joPsZghT3wM')}`)
242
237
 
243
- if (isWatchMode) {
244
- hasLocizeFunnelBeenPrintedInWatchMode = true // Mark as printed for watch mode
245
- }
238
+ return recordFunnelShown('extract')
246
239
  }
@@ -15,6 +15,24 @@ function globToRegex (glob: string): RegExp {
15
15
  return new RegExp(regexString)
16
16
  }
17
17
 
18
+ /**
19
+ * Recursively sorts the keys of an object.
20
+ */
21
+ function sortObject (obj: any): any {
22
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
23
+ return obj
24
+ }
25
+
26
+ const sortedObj: Record<string, any> = {}
27
+ const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
28
+
29
+ for (const key of keys) {
30
+ sortedObj[key] = sortObject(obj[key])
31
+ }
32
+
33
+ return sortedObj
34
+ }
35
+
18
36
  /**
19
37
  * A helper function to build a new translation object for a single namespace.
20
38
  * This centralizes the core logic of merging keys.
@@ -62,8 +80,14 @@ function buildNewTranslationsForNs (
62
80
  setNestedValue(newTranslations, key, valueToSet, keySeparator ?? '.')
63
81
  }
64
82
 
65
- // 2. If sorting is enabled, create a new sorted object from the one we just built.
66
- if (sort !== false) {
83
+ // 2. If sorting is enabled, recursively sort the entire object.
84
+ // This correctly handles both top-level and nested keys.
85
+ if (sort === true) {
86
+ return sortObject(newTranslations)
87
+ }
88
+ // Custom sort function logic remains as a future enhancement if needed,
89
+ // but for now, this robustly handles the most common `sort: true` case.
90
+ if (typeof sort === 'function') {
67
91
  const sortedObject: Record<string, any> = {}
68
92
  const topLevelKeys = Object.keys(newTranslations)
69
93
 
@@ -86,8 +110,8 @@ function buildNewTranslationsForNs (
86
110
  return sort(keyA, keyB)
87
111
  }
88
112
  }
89
- // Fallback to default alphabetical sort for `sort: true` or if keys can't be mapped.
90
- return a.localeCompare(b)
113
+ // Fallback to a case-insensitive alphabetical sort.
114
+ return a.localeCompare(b, undefined, { sensitivity: 'base' })
91
115
  })
92
116
 
93
117
  // 3. Rebuild the object in the final sorted order.
package/src/status.ts CHANGED
@@ -5,6 +5,7 @@ import { findKeys } from './extractor/core/key-finder'
5
5
  import { getNestedValue } from './utils/nested-object'
6
6
  import type { I18nextToolkitConfig, ExtractedKey } from './types'
7
7
  import { getOutputPath, loadTranslationFile } from './utils/file-utils'
8
+ import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker'
8
9
 
9
10
  /**
10
11
  * Options for configuring the status report display.
@@ -63,7 +64,7 @@ export async function runStatus (config: I18nextToolkitConfig, options: StatusOp
63
64
  try {
64
65
  const report = await generateStatusReport(config)
65
66
  spinner.succeed('Analysis complete.')
66
- displayStatusReport(report, config, options)
67
+ await displayStatusReport(report, config, options)
67
68
  } catch (error) {
68
69
  spinner.fail('Failed to generate status report.')
69
70
  console.error(error)
@@ -169,13 +170,13 @@ async function generateStatusReport (config: I18nextToolkitConfig): Promise<Stat
169
170
  * @param config - The i18next toolkit configuration object
170
171
  * @param options - Display options determining which report type to show
171
172
  */
172
- function displayStatusReport (report: StatusReport, config: I18nextToolkitConfig, options: StatusOptions) {
173
+ async function displayStatusReport (report: StatusReport, config: I18nextToolkitConfig, options: StatusOptions) {
173
174
  if (options.detail) {
174
- displayDetailedLocaleReport(report, config, options.detail, options.namespace)
175
+ await displayDetailedLocaleReport(report, config, options.detail, options.namespace)
175
176
  } else if (options.namespace) {
176
- displayNamespaceSummaryReport(report, config, options.namespace)
177
+ await displayNamespaceSummaryReport(report, config, options.namespace)
177
178
  } else {
178
- displayOverallSummaryReport(report, config)
179
+ await displayOverallSummaryReport(report, config)
179
180
  }
180
181
  }
181
182
 
@@ -193,7 +194,7 @@ function displayStatusReport (report: StatusReport, config: I18nextToolkitConfig
193
194
  * @param locale - The locale code to display details for
194
195
  * @param namespaceFilter - Optional namespace to filter the display
195
196
  */
196
- function displayDetailedLocaleReport (report: StatusReport, config: I18nextToolkitConfig, locale: string, namespaceFilter?: string) {
197
+ async function displayDetailedLocaleReport (report: StatusReport, config: I18nextToolkitConfig, locale: string, namespaceFilter?: string) {
197
198
  if (locale === config.extract.primaryLanguage) {
198
199
  console.log(chalk.yellow(`Locale "${locale}" is the primary language. All keys are considered present.`))
199
200
  return
@@ -237,7 +238,7 @@ function displayDetailedLocaleReport (report: StatusReport, config: I18nextToolk
237
238
  console.log(chalk.green.bold(`\nSummary: šŸŽ‰ All keys are translated for "${locale}".`))
238
239
  }
239
240
 
240
- printLocizeFunnel()
241
+ await printLocizeFunnel()
241
242
  }
242
243
 
243
244
  /**
@@ -250,7 +251,7 @@ function displayDetailedLocaleReport (report: StatusReport, config: I18nextToolk
250
251
  * @param config - The i18next toolkit configuration object
251
252
  * @param namespace - The namespace to display summary for
252
253
  */
253
- function displayNamespaceSummaryReport (report: StatusReport, config: I18nextToolkitConfig, namespace: string) {
254
+ async function displayNamespaceSummaryReport (report: StatusReport, config: I18nextToolkitConfig, namespace: string) {
254
255
  const nsData = report.keysByNs.get(namespace)
255
256
  if (!nsData) {
256
257
  console.error(chalk.red(`Error: Namespace "${namespace}" was not found in your source code.`))
@@ -269,7 +270,7 @@ function displayNamespaceSummaryReport (report: StatusReport, config: I18nextToo
269
270
  }
270
271
  }
271
272
 
272
- printLocizeFunnel()
273
+ await printLocizeFunnel()
273
274
  }
274
275
 
275
276
  /**
@@ -283,7 +284,7 @@ function displayNamespaceSummaryReport (report: StatusReport, config: I18nextToo
283
284
  * @param report - The generated status report data
284
285
  * @param config - The i18next toolkit configuration object
285
286
  */
286
- function displayOverallSummaryReport (report: StatusReport, config: I18nextToolkitConfig) {
287
+ async function displayOverallSummaryReport (report: StatusReport, config: I18nextToolkitConfig) {
287
288
  const { primaryLanguage } = config.extract
288
289
 
289
290
  console.log(chalk.cyan.bold('\ni18next Project Status'))
@@ -300,7 +301,7 @@ function displayOverallSummaryReport (report: StatusReport, config: I18nextToolk
300
301
  console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${localeData.totalKeys} keys)`)
301
302
  }
302
303
 
303
- printLocizeFunnel()
304
+ await printLocizeFunnel()
304
305
  }
305
306
 
306
307
  /**
@@ -332,8 +333,12 @@ function generateProgressBarText (percentage: number): string {
332
333
  return `[${chalk.green(''.padStart(filledBars, 'ā– '))}${''.padStart(emptyBars, 'ā–”')}]`
333
334
  }
334
335
 
335
- function printLocizeFunnel () {
336
+ async function printLocizeFunnel () {
337
+ if (!(await shouldShowFunnel('status'))) return
338
+
336
339
  console.log(chalk.yellow.bold('\n✨ Take your localization to the next level!'))
337
340
  console.log('Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started')
338
341
  console.log(`Run ${chalk.cyan('npx i18next-cli locize-migrate')} to get started.`)
342
+
343
+ return recordFunnelShown('status')
339
344
  }
package/src/syncer.ts CHANGED
@@ -6,6 +6,7 @@ import { glob } from 'glob'
6
6
  import type { I18nextToolkitConfig } from './types'
7
7
  import { getNestedKeys, getNestedValue, setNestedValue } from './utils/nested-object'
8
8
  import { getOutputPath, loadTranslationFile, serializeTranslationFile } from './utils/file-utils'
9
+ import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker'
9
10
 
10
11
  /**
11
12
  * Synchronizes translation files across different locales by ensuring all secondary
@@ -107,7 +108,7 @@ export async function runSyncer (config: I18nextToolkitConfig) {
107
108
  logMessages.forEach(msg => console.log(msg))
108
109
 
109
110
  if (wasAnythingSynced) {
110
- printLocizeFunnel()
111
+ await printLocizeFunnel()
111
112
  } else {
112
113
  console.log(chalk.green.bold('\nāœ… All locales are already in sync.'))
113
114
  }
@@ -117,8 +118,12 @@ export async function runSyncer (config: I18nextToolkitConfig) {
117
118
  }
118
119
  }
119
120
 
120
- function printLocizeFunnel () {
121
+ async function printLocizeFunnel () {
122
+ if (!(await shouldShowFunnel('syncer'))) return
123
+
121
124
  console.log(chalk.green.bold('\nāœ… Sync complete.'))
122
125
  console.log(chalk.yellow('šŸš€ Ready to collaborate with translators? Move your files to the cloud.'))
123
126
  console.log(` Get started with the official TMS for i18next: ${chalk.cyan('npx i18next-cli locize-migrate')}`)
127
+
128
+ return recordFunnelShown('syncer')
124
129
  }
@@ -0,0 +1,84 @@
1
+ import { join } from 'node:path'
2
+ import { tmpdir } from 'node:os'
3
+ import { readFile, writeFile } from 'node:fs/promises'
4
+
5
+ /**
6
+ * In-memory cache to track which funnel messages have been shown in the current session.
7
+ */
8
+ const hasLocizeFunnelBeenShown: Record<string, boolean> = {}
9
+
10
+ /**
11
+ * Path to the persistent file that stores the last time each funnel message was shown.
12
+ * Stored in the OS temporary directory to persist across CLI sessions.
13
+ */
14
+ const LAST_FUNNEL_FILE = join(tmpdir(), 'i18next-cli-last-funnel-message-shown.json') // Store in OS temp dir
15
+
16
+ /**
17
+ * Cooldown period in milliseconds before a funnel message can be shown again.
18
+ * Currently set to 24 hours.
19
+ */
20
+ const TIP_COOLDOWN_MS = 24 * 60 * 60 * 1000 // 24 hours
21
+
22
+ /**
23
+ * Determines whether a funnel message should be shown to the user.
24
+ *
25
+ * A funnel message will not be shown if:
26
+ * - It has already been shown in the current session (in-memory cache)
27
+ * - It was shown within the last 24 hours (persistent file cache)
28
+ *
29
+ * @param funnelMessage - The unique identifier for the funnel message
30
+ * @returns Promise that resolves to true if the message should be shown, false otherwise
31
+ */
32
+ export async function shouldShowFunnel (funnelMessage: string): Promise<boolean> {
33
+ if (hasLocizeFunnelBeenShown[funnelMessage]) return false
34
+
35
+ try {
36
+ const content = await readFile(LAST_FUNNEL_FILE, 'utf-8')
37
+ const cnt: Record<string, number> = JSON.parse(content)
38
+ if (Date.now() - (cnt[funnelMessage] || 0) < TIP_COOLDOWN_MS) {
39
+ return false // Less than 24 hours since last shown
40
+ }
41
+ } catch (e) {
42
+ // File doesn't exist or is invalid, assume it's okay to show the tip
43
+ }
44
+ return true
45
+ }
46
+
47
+ /**
48
+ * Records that a funnel message has been shown to the user.
49
+ *
50
+ * Updates both the in-memory cache and the persistent file cache with the current timestamp.
51
+ * This prevents the message from being shown again within the cooldown period.
52
+ *
53
+ * @param funnelMessage - The unique identifier for the funnel message that was shown
54
+ * @returns Promise that resolves when the record has been updated
55
+ */
56
+ export async function recordFunnelShown (funnelMessage: string): Promise<void> {
57
+ try {
58
+ hasLocizeFunnelBeenShown[funnelMessage] = true
59
+ let data: Record<string, number> = {}
60
+ try {
61
+ const existing = await readFile(LAST_FUNNEL_FILE, 'utf-8')
62
+ data = JSON.parse(existing) as Record<string, number>
63
+ } catch (err) {
64
+ // ignore, we'll create a new file
65
+ }
66
+ data[funnelMessage] = Date.now()
67
+ await writeFile(LAST_FUNNEL_FILE, JSON.stringify(data))
68
+ } catch (e) {
69
+ // Ignore errors here, it's just a best-effort cache
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Resets the in-memory cache for a specific funnel message.
75
+ *
76
+ * This function is intended for testing purposes only. It clears the session cache
77
+ * but does not affect the persistent file cache.
78
+ *
79
+ * @param funnelMessage - The unique identifier for the funnel message to reset
80
+ */
81
+ // just for the tests
82
+ export function reset (funnelMessage: string) {
83
+ delete hasLocizeFunnelBeenShown[funnelMessage]
84
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAM5F,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAMrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,EACE,WAAmB,EACnB,QAAgB,EACjB,GAAE;IACD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACf,EACN,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,OAAO,CAAC,CAyDlB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,WAAW,EACxB,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAmCD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,sDAO1D"}
1
+ {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAM5F,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAKrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,EACE,WAAmB,EACnB,QAAgB,EACjB,GAAE;IACD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACf,EACN,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,OAAO,CAAC,CAuDlB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,WAAW,EACxB,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAmCD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,sDAO1D"}
@@ -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;AAsGnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;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,CA8E9B"}
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;AA8HnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;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,CA8E9B"}
@@ -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;AA4BD;;;;;;;;;;;;;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;AAIjE;;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"}
@@ -1 +1 @@
1
- {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAInD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,iBA8E5D"}
1
+ {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAKnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,iBA8E5D"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Determines whether a funnel message should be shown to the user.
3
+ *
4
+ * A funnel message will not be shown if:
5
+ * - It has already been shown in the current session (in-memory cache)
6
+ * - It was shown within the last 24 hours (persistent file cache)
7
+ *
8
+ * @param funnelMessage - The unique identifier for the funnel message
9
+ * @returns Promise that resolves to true if the message should be shown, false otherwise
10
+ */
11
+ export declare function shouldShowFunnel(funnelMessage: string): Promise<boolean>;
12
+ /**
13
+ * Records that a funnel message has been shown to the user.
14
+ *
15
+ * Updates both the in-memory cache and the persistent file cache with the current timestamp.
16
+ * This prevents the message from being shown again within the cooldown period.
17
+ *
18
+ * @param funnelMessage - The unique identifier for the funnel message that was shown
19
+ * @returns Promise that resolves when the record has been updated
20
+ */
21
+ export declare function recordFunnelShown(funnelMessage: string): Promise<void>;
22
+ /**
23
+ * Resets the in-memory cache for a specific funnel message.
24
+ *
25
+ * This function is intended for testing purposes only. It clears the session cache
26
+ * but does not affect the persistent file cache.
27
+ *
28
+ * @param funnelMessage - The unique identifier for the funnel message to reset
29
+ */
30
+ export declare function reset(funnelMessage: string): void;
31
+ //# sourceMappingURL=funnel-msg-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"funnel-msg-tracker.d.ts","sourceRoot":"","sources":["../../src/utils/funnel-msg-tracker.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa/E;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe7E;AAED;;;;;;;GAOG;AAEH,wBAAgB,KAAK,CAAE,aAAa,EAAE,MAAM,QAE3C"}