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 +8 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/extractor.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +1 -1
- package/dist/cjs/status.js +1 -1
- package/dist/cjs/syncer.js +1 -1
- package/dist/cjs/utils/funnel-msg-tracker.js +1 -0
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/extractor.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +1 -1
- package/dist/esm/status.js +1 -1
- package/dist/esm/syncer.js +1 -1
- package/dist/esm/utils/funnel-msg-tracker.js +1 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/extractor/core/extractor.ts +6 -13
- package/src/extractor/core/translation-manager.ts +28 -4
- package/src/status.ts +17 -12
- package/src/syncer.ts +7 -2
- package/src/utils/funnel-msg-tracker.ts +84 -0
- package/types/extractor/core/extractor.d.ts.map +1 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/status.d.ts.map +1 -1
- package/types/syncer.d.ts.map +1 -1
- package/types/utils/funnel-msg-tracker.d.ts +31 -0
- package/types/utils/funnel-msg-tracker.d.ts.map +1 -0
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.
|
|
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"),
|
|
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"),
|
|
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};
|
package/dist/cjs/status.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("chalk"),o=require("ora"),
|
|
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)}};
|
package/dist/cjs/syncer.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("node:fs/promises"),
|
|
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.
|
|
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
|
|
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:
|
|
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};
|
package/dist/esm/status.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import o from"chalk";import e from"ora";import{resolve as
|
|
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};
|
package/dist/esm/syncer.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mkdir as o,writeFile as t}from"node:fs/promises";import{basename as
|
|
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
package/src/cli.ts
CHANGED
|
@@ -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 (
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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,
|
|
66
|
-
|
|
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
|
|
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;
|
|
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;
|
|
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"}
|
package/types/status.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/types/syncer.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|