i18next-cli 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,10 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.0.0](https://github.com/i18next/i18next-cli/compare/v0.9.3...v1.0.0) - 2025-xx-yy
8
+ ## [1.0.0](https://github.com/i18next/i18next-cli/compare/v0.9.5...v1.0.0) - 2025-xx-yy
9
9
 
10
10
  - not yet released
11
11
 
12
+ ## [0.9.5] - 2025-09-25
13
+
14
+ - introduced ignoredTags option
15
+
16
+ ## [0.9.4] - 2025-09-25
17
+
18
+ ### Added
19
+ - **Status Command:** Added a `--namespace` option to filter the status report by a single namespace.
20
+
21
+ ### Fixed
22
+ - **Linter:** Corrected a persistent bug causing inaccurate line number reporting for found issues.
23
+ - **Linter:** Significantly improved accuracy by adding heuristics to ignore URLs, paths, symbols, and common non-translatable JSX attributes (like `className`, `type`, etc.).
24
+
12
25
  ## [0.9.3] - 2025-09-25
13
26
 
14
27
  - improved heuristic-config
package/README.md CHANGED
@@ -128,13 +128,48 @@ npx i18next-cli extract --ci
128
128
 
129
129
  Displays a health check of your project's translation status. Can run without a config file.
130
130
 
131
- - Run `npx i18next-cli status` for a high-level summary.
132
- - Run `npx i18next-cli status <locale>` for a detailed, key-by-key report grouped by namespace.
131
+ **Options:**
132
+ - `--namespace <ns>, -n <ns>`: Filter the report by a specific namespace.
133
+
134
+ **Usage Examples:**
135
+
136
+ ```bash
137
+ # Get a high-level summary for all locales and namespaces
138
+ npx i18next-cli status
139
+
140
+ # Get a detailed, key-by-key report for the 'de' locale
141
+ npx i18next-cli status de
142
+
143
+ # Get a summary for only the 'common' namespace across all locales
144
+ npx i18next-cli status --namespace common
133
145
 
134
- This command provides:
135
- - Total number of translation keys found in your source code
136
- - Translation progress for each secondary language with visual progress bars
137
- - Percentage and key counts for easy tracking
146
+ # Get a detailed report for the 'de' locale, showing only the 'common' namespace
147
+ npx i18next-cli status de --namespace common
148
+ ```
149
+
150
+ The detailed view provides a rich, at-a-glance summary for each namespace, followed by a list of every key and its translation status.
151
+
152
+ **Example Output (`npx i18next-cli status de`):**
153
+
154
+ ```bash
155
+ Key Status for "de":
156
+
157
+ Overall: [■■■■■■■■■■■■■■■■■■■■] 100% (12/12)
158
+
159
+ Namespace: common
160
+ Namespace Progress: [■■■■■■■■■■■■■■■■■■■■] 100% (4/4)
161
+ ✓ button.save
162
+ ✓ button.cancel
163
+ ✓ greeting
164
+ ✓ farewell
165
+
166
+ Namespace: translation
167
+ Namespace Progress: [■■■■■■■■■■■■■■■■□□□□] 80% (8/10)
168
+ ✓ app.title
169
+ ✓ app.welcome
170
+ ✗ app.description
171
+ ...
172
+ ```
138
173
 
139
174
  ### `types`
140
175
  Generates TypeScript definitions from your translation files for full type-safety and autocompletion.
@@ -248,6 +283,9 @@ export default defineConfig({
248
283
 
249
284
  // Add custom JSX attributes to ignore during linting
250
285
  ignoredAttributes: ['data-testid', 'aria-label'],
286
+
287
+ // JSX tag names whose content should be ignored when linting
288
+ ignoredTags: ['pre'],
251
289
 
252
290
  // Namespace and key configuration
253
291
  defaultNS: 'translation',
package/dist/cjs/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var e=require("commander"),o=require("chokidar"),t=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:fs/promises"),require("node:path");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"),f=require("./locize.js");const p=new e.Command;p.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.3"),p.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const o=await r.runExtractor(a);e.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(),e.watch){console.log("\nWatching for changes...");o.watch(await t.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),p.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").action(async e=>{let o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{detail:e})}),p.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");o.watch(await t.glob(n.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),p.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),p.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),p.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),p.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await a.detectConfig();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 i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=o}await d.runLinter(e)}),p.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 o=await i.ensureConfig();await f.runLocizeSync(o,e)}),p.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeDownload(o,e)}),p.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeMigrate(o,e)}),p.parse(process.argv);
2
+ "use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:fs/promises"),require("node:path");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.5"),f.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a);e.ci&&t&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),f.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(e,t)=>{let o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{detail:e,namespace:t.namespace})}),f.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(n.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),f.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),f.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),f.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),f.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await d.runLinter(e)}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
@@ -1 +1 @@
1
- "use strict";var e=require("glob"),t=require("node:fs/promises"),r=require("@swc/core"),n=require("swc-walk"),s=require("chalk"),o=require("ora");function i(e,t,r){const s=[],o=[0];for(let e=0;e<t.length;e++)"\n"===t[e]&&o.push(e+1);const i=e=>{let t=1;for(let r=0;r<o.length&&!(o[r]>e);r++)t=r+1;return t},a=r.extract.transComponents||["Trans"],l=r.extract.ignoredAttributes||[],c=new Set(["className","key","id","style","href","i18nKey","defaults","type",...l]);return n.ancestor(e,{JSXText(e,t){const r=t[t.length-2],n=r?.opening?.name?.value;if(n&&(a.includes(n)||"script"===n||"style"===n))return;if(t.some(e=>{if("JSXElement"!==e.type)return!1;const t=e.opening?.name?.value;return a.includes(t)||["script","style","code"].includes(t)}))return;const o=e.value.trim();o&&isNaN(Number(o))&&!o.startsWith("{{")&&s.push({text:o,line:i(e.span.start)})},StringLiteral(e,t){const r=t[t.length-2];if("JSXAttribute"===r?.type&&!c.has(r.name.value)){const t=e.value.trim();t&&isNaN(Number(t))&&s.push({text:t,line:i(e.span.start)})}}}),s}exports.runLinter=async function(n){const a=o("Analyzing source files...\n").start();try{const o=await e.glob(n.extract.input);let l=0;const c=new Map;for(const e of o){const s=await t.readFile(e,"utf-8"),o=i(await r.parse(s,{syntax:"typescript",tsx:!0}),s,n);o.length>0&&(l+=o.length,c.set(e,o))}if(l>0){a.fail(s.red.bold(`Linter found ${l} potential issues.`));for(const[e,t]of c.entries())console.log(s.yellow(`\n${e}`)),t.forEach(({text:e,line:t})=>{console.log(` ${s.gray(`${t}:`)} ${s.red("Error:")} Found hardcoded string: "${e}"`)});process.exit(1)}else a.succeed(s.green.bold("No issues found."))}catch(e){a.fail(s.red("Linter failed to run.")),console.error(e),process.exit(1)}};
1
+ "use strict";var e=require("glob"),t=require("node:fs/promises"),r=require("@swc/core"),s=require("chalk"),n=require("ora");const o=e=>/^(https|http|\/\/|^\/)/.test(e);function i(e,t,r){const s=[],n=[],i=e=>t.substring(0,e).split("\n").length,a=r.extract.transComponents||["Trans"],c=r.extract.ignoredTags||[],l=new Set([...a,"script","style","code",...c]),u=r.extract.ignoredAttributes||[],f=new Set(["className","key","id","style","href","i18nKey","defaults","type","target",...u]),p=(e,t)=>{if(!e||"object"!=typeof e)return;const r=[...t,e];if("JSXText"===e.type){if(!r.some(e=>{if("JSXElement"!==e.type)return!1;const t=e.opening?.name?.value;return l.has(t)})){const t=e.value.trim();t&&t.length>1&&!o(t)&&isNaN(Number(t))&&!t.startsWith("{{")&&n.push(e)}}if("StringLiteral"===e.type){const t=r[r.length-2];if("JSXAttribute"===t?.type&&!f.has(t.name.value)){const t=e.value.trim();t&&!o(t)&&isNaN(Number(t))&&n.push(e)}}for(const t of Object.keys(e)){if("span"===t)continue;const s=e[t];Array.isArray(s)?s.forEach(e=>p(e,r)):s&&"object"==typeof s&&p(s,r)}};p(e,[]);let d=0;for(const e of n){const r=e.raw??e.value,n=t.indexOf(r,d);n>-1&&(s.push({text:e.value.trim(),line:i(n)}),d=n+r.length)}return s}exports.runLinter=async function(o){const a=n("Analyzing source files...\n").start();try{const n=await e.glob(o.extract.input);let c=0;const l=new Map;for(const e of n){const s=await t.readFile(e,"utf-8"),n=i(await r.parse(s,{syntax:"typescript",tsx:!0}),s,o);n.length>0&&(c+=n.length,l.set(e,n))}if(c>0){a.fail(s.red.bold(`Linter found ${c} potential issues.`));for(const[e,t]of l.entries())console.log(s.yellow(`\n${e}`)),t.forEach(({text:e,line:t})=>{console.log(` ${s.gray(`${t}:`)} ${s.red("Error:")} Found hardcoded string: "${e}"`)});process.exit(1)}else a.succeed(s.green.bold("No issues found."))}catch(e){a.fail(s.red("Linter failed to run.")),console.error(e),process.exit(1)}};
@@ -1 +1 @@
1
- "use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),s=require("node:fs/promises"),n=require("./extractor/core/key-finder.js"),a=require("./utils/nested-object.js"),l=require("./utils/file-utils.js");function r(o){const t=Math.round(o/100*20),s=20-t;return`[${e.green("".padStart(t,"■"))}${"".padStart(s,"□")}]`}exports.runStatus=async function(c,i={}){const u=o("Analyzing project localization status...\n").start();try{i.detail?await async function(o,r,c){const{primaryLanguage:i,keySeparator:u=".",defaultNS:d="translation"}=o.extract;if(!o.locales.includes(r))return void console.error(e.red(`Error: Locale "${r}" is not defined in your configuration.`));if(r===i)return void console.log(e.yellow(`Locale "${r}" is the primary language, so all keys are considered present.`));console.log(`Analyzing detailed status for locale: ${e.bold.cyan(r)}...`);const g=await n.findKeys(o);if(c.succeed("Analysis complete."),0===g.size)return void console.log(e.green("No keys found in source code."));const y=new Map;for(const e of g.values()){const o=e.ns||d;y.has(o)||y.set(o,[]),y.get(o).push(e)}const f=new Map;for(const e of y.keys()){const n=l.getOutputPath(o.extract.output,r,e);try{const o=await s.readFile(t.resolve(process.cwd(),n),"utf-8");f.set(e,JSON.parse(o))}catch{f.set(e,{})}}let p=0;console.log(e.bold(`\nKey Status for "${r}":`));const $=Array.from(y.keys()).sort();for(const o of $){console.log(e.cyan.bold(`\nNamespace: ${o}`));const t=(y.get(o)||[]).sort((e,o)=>e.key.localeCompare(o.key)),s=f.get(o)||{};for(const{key:o}of t){a.getNestedValue(s,o,u??".")?console.log(` ${e.green("")} ${o}`):(p++,console.log(` ${e.red("✗")} ${o}`))}}p>0?console.log(e.yellow.bold(`\n\nSummary: Found ${p} missing translations for "${r}".`)):console.log(e.green.bold(`\n\nSummary: 🎉 All ${g.size} keys are translated for "${r}".`))}(c,i.detail,u):await async function(o,c){console.log("Analyzing project localization status...");const i=await n.findKeys(o),u=i.size,{primaryLanguage:d,keySeparator:g=".",defaultNS:y="translation"}=o.extract,f=o.locales.filter(e=>e!==d),p=new Set(Array.from(i.values()).map(e=>e.ns||y));c.succeed("Analysis complete."),console.log(e.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${e.bold(u)}`),console.log(`🌍 Locales: ${e.bold(o.locales.join(", "))}`),console.log(`✅ Primary Language: ${e.bold(d)}`),console.log("\nTranslation Progress:");for(const e of f){let n=0;for(const r of p){const c=l.getOutputPath(o.extract.output,e,r);try{const e=await s.readFile(t.resolve(process.cwd(),c),"utf-8"),o=JSON.parse(e),l=a.getNestedKeys(o,g??".");n+=l.filter(e=>!!a.getNestedValue(o,e,g??".")&&i.has(`${r}:${e}`)).length}catch{}}const c=u>0?Math.round(n/u*100):100,d=r(c);console.log(`- ${e}: ${d} ${c}% (${n}/${u} keys)`)}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.`)}(c,u)}catch(e){u.fail("Failed to generate status report."),console.error(e)}};
1
+ "use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),a=require("node:fs/promises"),s=require("./extractor/core/key-finder.js"),n=require("./utils/nested-object.js"),r=require("./utils/file-utils.js");function l(o,t,a){const s=a>0?Math.round(t/a*100):100,n=c(s);console.log(`${e.bold(o)}: ${n} ${s}% (${t}/${a})`)}function c(o){const t=Math.round(o/100*20),a=20-t;return`[${e.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}exports.runStatus=async function(i,u={}){i.extract.primaryLanguage||(i.extract.primaryLanguage=i.locales[0]||"en"),i.extract.secondaryLanguages||(i.extract.secondaryLanguages=i.locales.filter(e=>e!==i?.extract?.primaryLanguage));const y=o("Analyzing project localization status...\n").start();try{const o=await async function(e){const o=await s.findKeys(e),{primaryLanguage:l,keySeparator:c=".",defaultNS:i="translation"}=e.extract,u=e.locales.filter(e=>e!==l),y=new Map;for(const e of o.values()){const o=e.ns||i;y.has(o)||y.set(o,[]),y.get(o).push(e)}const d={totalKeys:o.size,keysByNs:y,locales:new Map};for(const o of u){let s=0;const l=new Map;for(const[i,u]of y.entries()){const y=r.getOutputPath(e.extract.output,o,i);let d={};try{const e=await a.readFile(t.resolve(process.cwd(),y),"utf-8");d=JSON.parse(e)}catch{}let g=0;const f=u.map(({key:e})=>{const o=!!n.getNestedValue(d,e,c??".");return o&&g++,{key:e,isTranslated:o}});l.set(i,{totalKeys:u.length,translatedKeys:g,keyDetails:f}),s+=g}d.locales.set(o,{totalTranslated:s,namespaces:l})}return d}(i);y.succeed("Analysis complete."),function(o,t,a){a.detail?function(o,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(e.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(e.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=o.locales.get(a);if(!n)return void console.error(e.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(e.bold(`\nKey Status for "${e.cyan(a)}":`));const r=Array.from(o.keysByNs.values()).flat().length;l("Overall",n.totalTranslated,r);const c=s?[s]:Array.from(n.namespaces.keys()).sort();for(const o of c){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 i=r-n.totalTranslated;i>0?console.log(e.yellow.bold(`\nSummary: Found ${i} missing translations for "${a}".`)):console.log(e.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`))}(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=c(t);console.log(`- ${e}: ${a} ${t}% (${o.translatedKeys}/${o.totalKeys} keys)`)}}}(o,0,a.namespace):function(o,t){const{primaryLanguage:a}=t.extract;console.log(e.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${e.bold(o.totalKeys)}`),console.log(`🌍 Locales: ${e.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${e.bold(a)}`),console.log("\nTranslation Progress:");for(const[e,t]of o.locales.entries()){const a=o.totalKeys>0?Math.round(t.totalTranslated/o.totalKeys*100):100,s=c(a);console.log(`- ${e}: ${s} ${a}% (${t.totalTranslated}/${o.totalKeys} keys)`)}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.`)}(o,t)}(o,i,u)}catch(e){y.fail("Failed to generate status report."),console.error(e)}};
package/dist/esm/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as o}from"commander";import t from"chokidar";import{glob as e}from"glob";import i from"chalk";import{ensureConfig as n,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:fs/promises";import"node:path";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 d}from"./init.js";import{runLinter as p}from"./linter.js";import{runStatus as f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.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.").action(async o=>{const a=await n(),c=async()=>{const t=await r(a);o.ci&&t&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");t.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").action(async o=>{let t=await a();if(!t){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),t=o}await f(t,{detail:o})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");t.watch(await e(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(d),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=t}await p(o)}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async o=>{const t=await n();await g(t,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const t=await n();await u(t,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const t=await n();await y(t,o)}),w.parse(process.argv);
2
+ import{Command as o}from"commander";import e from"chokidar";import{glob as t}from"glob";import i from"chalk";import{ensureConfig as n,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:fs/promises";import"node:path";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.5"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async o=>{const a=await n(),c=async()=>{const e=await r(a);o.ci&&e&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");e.watch(await t(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(o,e)=>{let t=await a();if(!t){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),t=o}await f(t,{detail:o,namespace:e.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");e.watch(await t(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(p),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const e=await c();e||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=e}await d(o)}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async o=>{const e=await n();await g(e,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const e=await n();await u(e,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const e=await n();await y(e,o)}),w.parse(process.argv);
@@ -1 +1 @@
1
- import{glob as t}from"glob";import{readFile as e}from"node:fs/promises";import{parse as n}from"@swc/core";import{ancestor as r}from"swc-walk";import s from"chalk";import o from"ora";async function i(r){const i=o("Analyzing source files...\n").start();try{const o=await t(r.extract.input);let l=0;const c=new Map;for(const t of o){const s=await e(t,"utf-8"),o=a(await n(s,{syntax:"typescript",tsx:!0}),s,r);o.length>0&&(l+=o.length,c.set(t,o))}if(l>0){i.fail(s.red.bold(`Linter found ${l} potential issues.`));for(const[t,e]of c.entries())console.log(s.yellow(`\n${t}`)),e.forEach(({text:t,line:e})=>{console.log(` ${s.gray(`${e}:`)} ${s.red("Error:")} Found hardcoded string: "${t}"`)});process.exit(1)}else i.succeed(s.green.bold("No issues found."))}catch(t){i.fail(s.red("Linter failed to run.")),console.error(t),process.exit(1)}}function a(t,e,n){const s=[],o=[0];for(let t=0;t<e.length;t++)"\n"===e[t]&&o.push(t+1);const i=t=>{let e=1;for(let n=0;n<o.length&&!(o[n]>t);n++)e=n+1;return e},a=n.extract.transComponents||["Trans"],l=n.extract.ignoredAttributes||[],c=new Set(["className","key","id","style","href","i18nKey","defaults","type",...l]);return r(t,{JSXText(t,e){const n=e[e.length-2],r=n?.opening?.name?.value;if(r&&(a.includes(r)||"script"===r||"style"===r))return;if(e.some(t=>{if("JSXElement"!==t.type)return!1;const e=t.opening?.name?.value;return a.includes(e)||["script","style","code"].includes(e)}))return;const o=t.value.trim();o&&isNaN(Number(o))&&!o.startsWith("{{")&&s.push({text:o,line:i(t.span.start)})},StringLiteral(t,e){const n=e[e.length-2];if("JSXAttribute"===n?.type&&!c.has(n.name.value)){const e=t.value.trim();e&&isNaN(Number(e))&&s.push({text:e,line:i(t.span.start)})}}}),s}export{i as runLinter};
1
+ import{glob as t}from"glob";import{readFile as e}from"node:fs/promises";import{parse as o}from"@swc/core";import r from"chalk";import n from"ora";async function s(s){const i=n("Analyzing source files...\n").start();try{const n=await t(s.extract.input);let c=0;const l=new Map;for(const t of n){const r=await e(t,"utf-8"),n=a(await o(r,{syntax:"typescript",tsx:!0}),r,s);n.length>0&&(c+=n.length,l.set(t,n))}if(c>0){i.fail(r.red.bold(`Linter found ${c} potential issues.`));for(const[t,e]of l.entries())console.log(r.yellow(`\n${t}`)),e.forEach(({text:t,line:e})=>{console.log(` ${r.gray(`${e}:`)} ${r.red("Error:")} Found hardcoded string: "${t}"`)});process.exit(1)}else i.succeed(r.green.bold("No issues found."))}catch(t){i.fail(r.red("Linter failed to run.")),console.error(t),process.exit(1)}}const i=t=>/^(https|http|\/\/|^\/)/.test(t);function a(t,e,o){const r=[],n=[],s=t=>e.substring(0,t).split("\n").length,a=o.extract.transComponents||["Trans"],c=o.extract.ignoredTags||[],l=new Set([...a,"script","style","code",...c]),f=o.extract.ignoredAttributes||[],u=new Set(["className","key","id","style","href","i18nKey","defaults","type","target",...f]),p=(t,e)=>{if(!t||"object"!=typeof t)return;const o=[...e,t];if("JSXText"===t.type){if(!o.some(t=>{if("JSXElement"!==t.type)return!1;const e=t.opening?.name?.value;return l.has(e)})){const e=t.value.trim();e&&e.length>1&&!i(e)&&isNaN(Number(e))&&!e.startsWith("{{")&&n.push(t)}}if("StringLiteral"===t.type){const e=o[o.length-2];if("JSXAttribute"===e?.type&&!u.has(e.name.value)){const e=t.value.trim();e&&!i(e)&&isNaN(Number(e))&&n.push(t)}}for(const e of Object.keys(t)){if("span"===e)continue;const r=t[e];Array.isArray(r)?r.forEach(t=>p(t,o)):r&&"object"==typeof r&&p(r,o)}};p(t,[]);let m=0;for(const t of n){const o=t.raw??t.value,n=e.indexOf(o,m);n>-1&&(r.push({text:t.value.trim(),line:s(n)}),m=n+o.length)}return r}export{s as runLinter};
@@ -1 +1 @@
1
- import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{readFile as n}from"node:fs/promises";import{findKeys as s}from"./extractor/core/key-finder.js";import{getNestedValue as a,getNestedKeys as l}from"./utils/nested-object.js";import{getOutputPath as r}from"./utils/file-utils.js";async function c(c,u={}){const d=e("Analyzing project localization status...\n").start();try{u.detail?await async function(e,l,c){const{primaryLanguage:i,keySeparator:u=".",defaultNS:d="translation"}=e.extract;if(!e.locales.includes(l))return void console.error(o.red(`Error: Locale "${l}" is not defined in your configuration.`));if(l===i)return void console.log(o.yellow(`Locale "${l}" is the primary language, so all keys are considered present.`));console.log(`Analyzing detailed status for locale: ${o.bold.cyan(l)}...`);const g=await s(e);if(c.succeed("Analysis complete."),0===g.size)return void console.log(o.green("No keys found in source code."));const f=new Map;for(const o of g.values()){const e=o.ns||d;f.has(e)||f.set(e,[]),f.get(e).push(o)}const y=new Map;for(const o of f.keys()){const s=r(e.extract.output,l,o);try{const e=await n(t(process.cwd(),s),"utf-8");y.set(o,JSON.parse(e))}catch{y.set(o,{})}}let p=0;console.log(o.bold(`\nKey Status for "${l}":`));const m=Array.from(f.keys()).sort();for(const e of m){console.log(o.cyan.bold(`\nNamespace: ${e}`));const t=(f.get(e)||[]).sort((o,e)=>o.key.localeCompare(e.key)),n=y.get(e)||{};for(const{key:e}of t){a(n,e,u??".")?console.log(` ${o.green("✓")} ${e}`):(p++,console.log(` ${o.red("✗")} ${e}`))}}p>0?console.log(o.yellow.bold(`\n\nSummary: Found ${p} missing translations for "${l}".`)):console.log(o.green.bold(`\n\nSummary: 🎉 All ${g.size} keys are translated for "${l}".`))}(c,u.detail,d):await async function(e,c){console.log("Analyzing project localization status...");const u=await s(e),d=u.size,{primaryLanguage:g,keySeparator:f=".",defaultNS:y="translation"}=e.extract,p=e.locales.filter(o=>o!==g),m=new Set(Array.from(u.values()).map(o=>o.ns||y));c.succeed("Analysis complete."),console.log(o.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${o.bold(d)}`),console.log(`🌍 Locales: ${o.bold(e.locales.join(", "))}`),console.log(`✅ Primary Language: ${o.bold(g)}`),console.log("\nTranslation Progress:");for(const o of p){let s=0;for(const c of m){const i=r(e.extract.output,o,c);try{const o=await n(t(process.cwd(),i),"utf-8"),e=JSON.parse(o),r=l(e,f??".");s+=r.filter(o=>!!a(e,o,f??".")&&u.has(`${c}:${o}`)).length}catch{}}const c=d>0?Math.round(s/d*100):100,g=i(c);console.log(`- ${o}: ${g} ${c}% (${s}/${d} keys)`)}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,d)}catch(o){d.fail("Failed to generate status report."),console.error(o)}}function i(e){const t=Math.round(e/100*20),n=20-t;return`[${o.green("".padStart(t,"■"))}${"".padStart(n,"□")}]`}export{c as runStatus};
1
+ import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{readFile as a}from"node:fs/promises";import{findKeys as s}from"./extractor/core/key-finder.js";import{getNestedValue as n}from"./utils/nested-object.js";import{getOutputPath as r}from"./utils/file-utils.js";async function l(l,y={}){l.extract.primaryLanguage||(l.extract.primaryLanguage=l.locales[0]||"en"),l.extract.secondaryLanguages||(l.extract.secondaryLanguages=l.locales.filter(o=>o!==l?.extract?.primaryLanguage));const d=e("Analyzing project localization status...\n").start();try{const e=await async function(o){const e=await s(o),{primaryLanguage:l,keySeparator:c=".",defaultNS:i="translation"}=o.extract,y=o.locales.filter(o=>o!==l),d=new Map;for(const o of e.values()){const e=o.ns||i;d.has(e)||d.set(e,[]),d.get(e).push(o)}const g={totalKeys:e.size,keysByNs:d,locales:new Map};for(const e of y){let s=0;const l=new Map;for(const[i,y]of d.entries()){const d=r(o.extract.output,e,i);let g={};try{const o=await a(t(process.cwd(),d),"utf-8");g=JSON.parse(o)}catch{}let u=0;const f=y.map(({key:o})=>{const e=!!n(g,o,c??".");return e&&u++,{key:o,isTranslated:e}});l.set(i,{totalKeys:y.length,translatedKeys:u,keyDetails:f}),s+=u}g.locales.set(e,{totalTranslated:s,namespaces:l})}return g}(l);d.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 r=Array.from(e.keysByNs.values()).flat().length;c("Overall",n.totalTranslated,r);const l=s?[s]:Array.from(n.namespaces.keys()).sort();for(const e of l){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=r-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}".`))}(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)`)}}}(e,0,a.namespace):function(e,t){const{primaryLanguage:a}=t.extract;console.log(o.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${o.bold(e.totalKeys)}`),console.log(`🌍 Locales: ${o.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${o.bold(a)}`),console.log("\nTranslation Progress:");for(const[o,t]of e.locales.entries()){const a=e.totalKeys>0?Math.round(t.totalTranslated/e.totalKeys*100):100,s=i(a);console.log(`- ${o}: ${s} ${a}% (${t.totalTranslated}/${e.totalKeys} keys)`)}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.`)}(e,t)}(e,l,y)}catch(o){d.fail("Failed to generate status report."),console.error(o)}}function c(e,t,a){const s=a>0?Math.round(t/a*100):100,n=i(s);console.log(`${o.bold(e)}: ${n} ${s}% (${t}/${a})`)}function i(e){const t=Math.round(e/100*20),a=20-t;return`[${o.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}export{l as runStatus};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -21,7 +21,7 @@ const program = new Command()
21
21
  program
22
22
  .name('i18next-cli')
23
23
  .description('A unified, high-performance i18next CLI.')
24
- .version('0.9.3')
24
+ .version('0.9.5')
25
25
 
26
26
  program
27
27
  .command('extract')
@@ -58,7 +58,8 @@ program
58
58
  program
59
59
  .command('status [locale]')
60
60
  .description('Display translation status. Provide a locale for a detailed key-by-key view.')
61
- .action(async (locale) => {
61
+ .option('-n, --namespace <ns>', 'Filter the status report by a specific namespace')
62
+ .action(async (locale, options) => {
62
63
  let config = await loadConfig()
63
64
  if (!config) {
64
65
  console.log(chalk.blue('No config file found. Attempting to detect project structure...'))
@@ -71,7 +72,7 @@ program
71
72
  console.log(chalk.green('Project structure detected successfully!'))
72
73
  config = detected as I18nextToolkitConfig
73
74
  }
74
- await runStatus(config, { detail: locale })
75
+ await runStatus(config, { detail: locale, namespace: options.namespace })
75
76
  })
76
77
 
77
78
  program
package/src/linter.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { glob } from 'glob'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { parse } from '@swc/core'
4
- import { ancestor } from 'swc-walk'
5
4
  import chalk from 'chalk'
6
5
  import ora from 'ora'
7
6
  import type { I18nextToolkitConfig } from './types'
@@ -86,6 +85,8 @@ interface HardcodedString {
86
85
  line: number;
87
86
  }
88
87
 
88
+ const isUrlOrPath = (text: string) => /^(https|http|\/\/|^\/)/.test(text)
89
+
89
90
  /**
90
91
  * Analyzes an AST to find potentially hardcoded strings that should be translated.
91
92
  *
@@ -115,77 +116,83 @@ interface HardcodedString {
115
116
  */
116
117
  function findHardcodedStrings (ast: any, code: string, config: I18nextToolkitConfig): HardcodedString[] {
117
118
  const issues: HardcodedString[] = []
118
- const lineStarts: number[] = [0]
119
- for (let i = 0; i < code.length; i++) {
120
- if (code[i] === '\n') lineStarts.push(i + 1)
121
- }
119
+ // A list of AST nodes that have been identified as potential issues.
120
+ const nodesToLint: any[] = []
122
121
 
123
- /**
124
- * Converts a character position to a line number.
125
- *
126
- * @param pos - Character position in the source code
127
- * @returns Line number (1-based)
128
- */
129
122
  const getLineNumber = (pos: number): number => {
130
- let line = 1
131
- for (let i = 0; i < lineStarts.length; i++) {
132
- if (lineStarts[i] > pos) break
133
- line = i + 1
134
- }
135
- return line
123
+ return code.substring(0, pos).split('\n').length
136
124
  }
137
125
 
138
126
  const transComponents = config.extract.transComponents || ['Trans']
139
- const defaultIgnoredAttributes = ['className', 'key', 'id', 'style', 'href', 'i18nKey', 'defaults', 'type']
127
+ const defaultIgnoredAttributes = ['className', 'key', 'id', 'style', 'href', 'i18nKey', 'defaults', 'type', 'target']
128
+ const defaultIgnoredTags = ['script', 'style', 'code']
129
+ const customIgnoredTags = config.extract.ignoredTags || []
130
+ const allIgnoredTags = new Set([...transComponents, ...defaultIgnoredTags, ...customIgnoredTags])
140
131
  const customIgnoredAttributes = config.extract.ignoredAttributes || []
141
132
  const ignoredAttributes = new Set([...defaultIgnoredAttributes, ...customIgnoredAttributes])
142
133
 
143
- ancestor(ast, {
144
- /**
145
- * Processes JSX text nodes to identify hardcoded content.
146
- *
147
- * @param node - JSX text node
148
- * @param ancestors - Array of ancestor nodes for context
149
- */
150
- JSXText (node: any, ancestors: any[]) {
151
- const parent = ancestors[ancestors.length - 2]
152
- const parentName = parent?.opening?.name?.value
153
-
154
- if (parentName && (transComponents.includes(parentName) || parentName === 'script' || parentName === 'style')) {
155
- return
156
- }
134
+ // --- PHASE 1: Collect all potentially problematic nodes ---
135
+ const walk = (node: any, ancestors: any[]) => {
136
+ if (!node || typeof node !== 'object') return
137
+
138
+ const currentAncestors = [...ancestors, node]
157
139
 
158
- const isIgnored = ancestors.some(ancestorNode => {
140
+ if (node.type === 'JSXText') {
141
+ const isIgnored = currentAncestors.some(ancestorNode => {
159
142
  if (ancestorNode.type !== 'JSXElement') return false
160
143
  const elementName = ancestorNode.opening?.name?.value
161
- return transComponents.includes(elementName) || ['script', 'style', 'code'].includes(elementName)
144
+ return allIgnoredTags.has(elementName)
162
145
  })
163
146
 
164
- if (isIgnored) return
165
-
166
- const text = node.value.trim()
167
- if (text && isNaN(Number(text)) && !text.startsWith('{{')) {
168
- issues.push({ text, line: getLineNumber(node.span.start) })
147
+ if (!isIgnored) {
148
+ const text = node.value.trim()
149
+ if (text && text.length > 1 && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
150
+ nodesToLint.push(node) // Collect the node
151
+ }
169
152
  }
170
- },
171
-
172
- /**
173
- * Processes string literals in JSX attributes.
174
- *
175
- * @param node - String literal node
176
- * @param ancestors - Array of ancestor nodes for context
177
- */
178
- StringLiteral (node: any, ancestors: any[]) {
179
- const parent = ancestors[ancestors.length - 2]
180
-
181
- // This check now uses the new combined Set
153
+ }
154
+
155
+ if (node.type === 'StringLiteral') {
156
+ const parent = currentAncestors[currentAncestors.length - 2]
182
157
  if (parent?.type === 'JSXAttribute' && !ignoredAttributes.has(parent.name.value)) {
183
158
  const text = node.value.trim()
184
- if (text && isNaN(Number(text))) {
185
- issues.push({ text, line: getLineNumber(node.span.start) })
159
+ if (text && !isUrlOrPath(text) && isNaN(Number(text))) {
160
+ nodesToLint.push(node) // Collect the node
186
161
  }
187
162
  }
188
- },
189
- })
163
+ }
164
+
165
+ // Recurse into children
166
+ for (const key of Object.keys(node)) {
167
+ if (key === 'span') continue
168
+ const child = node[key]
169
+ if (Array.isArray(child)) {
170
+ child.forEach(item => walk(item, currentAncestors))
171
+ } else if (child && typeof child === 'object') {
172
+ walk(child, currentAncestors)
173
+ }
174
+ }
175
+ }
176
+
177
+ walk(ast, []) // Run the walk to collect nodes
178
+
179
+ // --- PHASE 2: Find line numbers using a tracked search on the raw source code ---
180
+ let lastSearchIndex = 0
181
+ for (const node of nodesToLint) {
182
+ // For StringLiterals, the `raw` property includes the quotes ("..."), which is
183
+ // much more unique for searching than the plain `value`.
184
+ const searchText = node.raw ?? node.value
185
+
186
+ const position = code.indexOf(searchText, lastSearchIndex)
187
+
188
+ if (position > -1) {
189
+ issues.push({
190
+ text: node.value.trim(),
191
+ line: getLineNumber(position),
192
+ })
193
+ lastSearchIndex = position + searchText.length
194
+ }
195
+ }
196
+
190
197
  return issues
191
198
  }
package/src/status.ts CHANGED
@@ -1,14 +1,44 @@
1
1
  import chalk from 'chalk'
2
- import ora, { Ora } from 'ora'
2
+ import ora from 'ora'
3
3
  import { resolve } from 'node:path'
4
4
  import { readFile } from 'node:fs/promises'
5
5
  import { findKeys } from './extractor/core/key-finder'
6
- import { getNestedKeys, getNestedValue } from './utils/nested-object'
6
+ import { getNestedValue } from './utils/nested-object'
7
7
  import type { I18nextToolkitConfig, ExtractedKey } from './types'
8
8
  import { getOutputPath } from './utils/file-utils'
9
9
 
10
+ /**
11
+ * Options for configuring the status report display.
12
+ */
10
13
  interface StatusOptions {
14
+ /** Locale code to display detailed information for a specific language */
11
15
  detail?: string;
16
+ /** Namespace to filter the report by */
17
+ namespace?: string;
18
+ }
19
+
20
+ /**
21
+ * Structured report containing all translation status data.
22
+ */
23
+ interface StatusReport {
24
+ /** Total number of extracted keys across all namespaces */
25
+ totalKeys: number;
26
+ /** Map of namespace names to their extracted keys */
27
+ keysByNs: Map<string, ExtractedKey[]>;
28
+ /** Map of locale codes to their translation status data */
29
+ locales: Map<string, {
30
+ /** Total number of translated keys for this locale */
31
+ totalTranslated: number;
32
+ /** Map of namespace names to their translation details for this locale */
33
+ namespaces: Map<string, {
34
+ /** Total number of keys in this namespace */
35
+ totalKeys: number;
36
+ /** Number of translated keys in this namespace */
37
+ translatedKeys: number;
38
+ /** Detailed status for each key in this namespace */
39
+ keyDetails: Array<{ key: string; isTranslated: boolean }>;
40
+ }>;
41
+ }>;
12
42
  }
13
43
 
14
44
  /**
@@ -22,16 +52,17 @@ interface StatusOptions {
22
52
  * 5. Serving as a value-driven funnel to introduce the locize commercial service.
23
53
  *
24
54
  * @param config - The i18next toolkit configuration object.
25
- * @param options Options object, may contain a `detail` property with a locale string.
55
+ * @param options - Options object, may contain a `detail` property with a locale string.
56
+ * @throws {Error} When unable to extract keys or read translation files
26
57
  */
27
58
  export async function runStatus (config: I18nextToolkitConfig, options: StatusOptions = {}) {
59
+ if (!config.extract.primaryLanguage) config.extract.primaryLanguage = config.locales[0] || 'en'
60
+ if (!config.extract.secondaryLanguages) config.extract.secondaryLanguages = config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
28
61
  const spinner = ora('Analyzing project localization status...\n').start()
29
62
  try {
30
- if (options.detail) {
31
- await displayDetailedStatus(config, options.detail, spinner)
32
- } else {
33
- await displaySummaryStatus(config, spinner)
34
- }
63
+ const report = await generateStatusReport(config)
64
+ spinner.succeed('Analysis complete.')
65
+ displayStatusReport(report, config, options)
35
66
  } catch (error) {
36
67
  spinner.fail('Failed to generate status report.')
37
68
  console.error(error)
@@ -39,139 +70,206 @@ export async function runStatus (config: I18nextToolkitConfig, options: StatusOp
39
70
  }
40
71
 
41
72
  /**
42
- * Displays a detailed, key-by-key translation status for a specific locale,
43
- * grouped by namespace.
44
- * @param config The toolkit configuration.
45
- * @param locale The locale to display the detailed status for.
46
- * @internal
73
+ * Gathers all translation data and compiles it into a structured report.
74
+ *
75
+ * This function:
76
+ * - Extracts all keys from source code using the configured extractor
77
+ * - Groups keys by namespace
78
+ * - Reads translation files for each secondary language
79
+ * - Compares extracted keys against existing translations
80
+ * - Compiles translation statistics for each locale and namespace
81
+ *
82
+ * @param config - The i18next toolkit configuration object
83
+ * @returns Promise that resolves to a complete status report
84
+ * @throws {Error} When key extraction fails or configuration is invalid
47
85
  */
48
- async function displayDetailedStatus (config: I18nextToolkitConfig, locale: string, spinner: Ora) {
86
+ async function generateStatusReport (config: I18nextToolkitConfig): Promise<StatusReport> {
87
+ const allExtractedKeys = await findKeys(config)
49
88
  const { primaryLanguage, keySeparator = '.', defaultNS = 'translation' } = config.extract
89
+ const secondaryLanguages = config.locales.filter(l => l !== primaryLanguage)
50
90
 
51
- if (!config.locales.includes(locale)) {
52
- console.error(chalk.red(`Error: Locale "${locale}" is not defined in your configuration.`))
53
- return
91
+ const keysByNs = new Map<string, ExtractedKey[]>()
92
+ for (const key of allExtractedKeys.values()) {
93
+ const ns = key.ns || defaultNS
94
+ if (!keysByNs.has(ns)) keysByNs.set(ns, [])
95
+ keysByNs.get(ns)!.push(key)
54
96
  }
55
- if (locale === primaryLanguage) {
56
- console.log(chalk.yellow(`Locale "${locale}" is the primary language, so all keys are considered present.`))
57
- return
97
+
98
+ const report: StatusReport = {
99
+ totalKeys: allExtractedKeys.size,
100
+ keysByNs,
101
+ locales: new Map(),
58
102
  }
59
103
 
60
- console.log(`Analyzing detailed status for locale: ${chalk.bold.cyan(locale)}...`)
104
+ for (const locale of secondaryLanguages) {
105
+ let totalTranslatedForLocale = 0
106
+ const namespaces = new Map<string, any>()
61
107
 
62
- const allExtractedKeys = await findKeys(config)
108
+ for (const [ns, keysInNs] of keysByNs.entries()) {
109
+ const langFilePath = getOutputPath(config.extract.output, locale, ns)
110
+ let translations: Record<string, any> = {}
111
+ try {
112
+ const content = await readFile(resolve(process.cwd(), langFilePath), 'utf-8')
113
+ translations = JSON.parse(content)
114
+ } catch {}
63
115
 
64
- spinner.succeed('Analysis complete.')
116
+ let translatedInNs = 0
117
+ const keyDetails = keysInNs.map(({ key }) => {
118
+ const value = getNestedValue(translations, key, keySeparator ?? '.')
119
+ const isTranslated = !!value
120
+ if (isTranslated) translatedInNs++
121
+ return { key, isTranslated }
122
+ })
65
123
 
66
- if (allExtractedKeys.size === 0) {
67
- console.log(chalk.green('No keys found in source code.'))
68
- return
124
+ namespaces.set(ns, {
125
+ totalKeys: keysInNs.length,
126
+ translatedKeys: translatedInNs,
127
+ keyDetails,
128
+ })
129
+ totalTranslatedForLocale += translatedInNs
130
+ }
131
+ report.locales.set(locale, { totalTranslated: totalTranslatedForLocale, namespaces })
69
132
  }
70
133
 
71
- // Group keys by namespace to read the correct files
72
- const keysByNs = new Map<string, ExtractedKey[]>()
73
- for (const key of allExtractedKeys.values()) {
74
- const ns = key.ns || defaultNS
75
- if (!keysByNs.has(ns)) keysByNs.set(ns, [])
76
- keysByNs.get(ns)!.push(key)
134
+ return report
135
+ }
136
+
137
+ /**
138
+ * Main display router that calls the appropriate display function based on options.
139
+ *
140
+ * Routes to one of three display modes:
141
+ * - Detailed locale report: Shows per-key status for a specific locale
142
+ * - Namespace summary: Shows translation progress for all locales in a specific namespace
143
+ * - Overall summary: Shows high-level statistics across all locales and namespaces
144
+ *
145
+ * @param report - The generated status report data
146
+ * @param config - The i18next toolkit configuration object
147
+ * @param options - Display options determining which report type to show
148
+ */
149
+ function displayStatusReport (report: StatusReport, config: I18nextToolkitConfig, options: StatusOptions) {
150
+ if (options.detail) {
151
+ displayDetailedLocaleReport(report, config, options.detail, options.namespace)
152
+ } else if (options.namespace) {
153
+ displayNamespaceSummaryReport(report, config, options.namespace)
154
+ } else {
155
+ displayOverallSummaryReport(report, config)
77
156
  }
157
+ }
78
158
 
79
- const translationsByNs = new Map<string, Record<string, any>>()
80
- for (const ns of keysByNs.keys()) {
81
- const langFilePath = getOutputPath(config.extract.output, locale, ns)
82
- try {
83
- const content = await readFile(resolve(process.cwd(), langFilePath), 'utf-8')
84
- translationsByNs.set(ns, JSON.parse(content))
85
- } catch {
86
- translationsByNs.set(ns, {}) // File not found, treat as empty
87
- }
159
+ /**
160
+ * Displays the detailed, grouped report for a single locale.
161
+ *
162
+ * Shows:
163
+ * - Overall progress for the locale
164
+ * - Progress for each namespace (or filtered namespace)
165
+ * - Individual key status (translated/missing) with visual indicators
166
+ * - Summary message with total missing translations
167
+ *
168
+ * @param report - The generated status report data
169
+ * @param config - The i18next toolkit configuration object
170
+ * @param locale - The locale code to display details for
171
+ * @param namespaceFilter - Optional namespace to filter the display
172
+ */
173
+ function displayDetailedLocaleReport (report: StatusReport, config: I18nextToolkitConfig, locale: string, namespaceFilter?: string) {
174
+ if (locale === config.extract.primaryLanguage) {
175
+ console.log(chalk.yellow(`Locale "${locale}" is the primary language. All keys are considered present.`))
176
+ return
177
+ }
178
+ if (!config.locales.includes(locale)) {
179
+ console.error(chalk.red(`Error: Locale "${locale}" is not defined in your configuration.`))
180
+ return
88
181
  }
89
182
 
90
- let missingCount = 0
91
- console.log(chalk.bold(`\nKey Status for "${locale}":`))
183
+ const localeData = report.locales.get(locale)
92
184
 
93
- // 1. Get and sort the namespace names alphabetically
94
- const sortedNamespaces = Array.from(keysByNs.keys()).sort()
185
+ if (!localeData) {
186
+ console.error(chalk.red(`Error: Locale "${locale}" is not a valid secondary language.`))
187
+ return
188
+ }
95
189
 
96
- // 2. Loop through each namespace
97
- for (const ns of sortedNamespaces) {
98
- console.log(chalk.cyan.bold(`\nNamespace: ${ns}`))
190
+ console.log(chalk.bold(`\nKey Status for "${chalk.cyan(locale)}":`))
99
191
 
100
- const keysForNs = keysByNs.get(ns) || []
101
- const sortedKeysForNs = keysForNs.sort((a, b) => a.key.localeCompare(b.key))
102
- const translations = translationsByNs.get(ns) || {}
192
+ const totalKeysForLocale = Array.from(report.keysByNs.values()).flat().length
193
+ printProgressBar('Overall', localeData.totalTranslated, totalKeysForLocale)
103
194
 
104
- // 3. Loop through the keys within the current namespace
105
- for (const { key } of sortedKeysForNs) {
106
- const value = getNestedValue(translations, key, keySeparator ?? '.')
195
+ const namespacesToDisplay = namespaceFilter ? [namespaceFilter] : Array.from(localeData.namespaces.keys()).sort()
107
196
 
108
- if (value) {
109
- console.log(` ${chalk.green('✓')} ${key}`)
110
- } else {
111
- missingCount++
112
- console.log(` ${chalk.red('✗')} ${key}`)
113
- }
114
- }
197
+ for (const ns of namespacesToDisplay) {
198
+ const nsData = localeData.namespaces.get(ns)
199
+ if (!nsData) continue
200
+
201
+ console.log(chalk.cyan.bold(`\nNamespace: ${ns}`))
202
+ printProgressBar('Namespace Progress', nsData.translatedKeys, nsData.totalKeys)
203
+
204
+ nsData.keyDetails.forEach(({ key, isTranslated }) => {
205
+ const icon = isTranslated ? chalk.green('✓') : chalk.red('✗')
206
+ console.log(` ${icon} ${key}`)
207
+ })
115
208
  }
116
209
 
210
+ const missingCount = totalKeysForLocale - localeData.totalTranslated
117
211
  if (missingCount > 0) {
118
- console.log(chalk.yellow.bold(`\n\nSummary: Found ${missingCount} missing translations for "${locale}".`))
212
+ console.log(chalk.yellow.bold(`\nSummary: Found ${missingCount} missing translations for "${locale}".`))
119
213
  } else {
120
- console.log(chalk.green.bold(`\n\nSummary: 🎉 All ${allExtractedKeys.size} keys are translated for "${locale}".`))
214
+ console.log(chalk.green.bold(`\nSummary: 🎉 All keys are translated for "${locale}".`))
121
215
  }
122
216
  }
123
217
 
124
218
  /**
125
- * Displays a high-level summary report of translation progress for all locales.
126
- * @param config The toolkit configuration.
127
- * @internal
219
+ * Displays a summary report filtered by a single namespace.
220
+ *
221
+ * Shows translation progress for the specified namespace across all secondary locales,
222
+ * including percentage completion and translated/total key counts.
223
+ *
224
+ * @param report - The generated status report data
225
+ * @param config - The i18next toolkit configuration object
226
+ * @param namespace - The namespace to display summary for
128
227
  */
129
- async function displaySummaryStatus (config: I18nextToolkitConfig, spinner: Ora) {
130
- console.log('Analyzing project localization status...')
131
-
132
- const allExtractedKeys = await findKeys(config)
133
- const totalKeys = allExtractedKeys.size
228
+ function displayNamespaceSummaryReport (report: StatusReport, config: I18nextToolkitConfig, namespace: string) {
229
+ const nsData = report.keysByNs.get(namespace)
230
+ if (!nsData) {
231
+ console.error(chalk.red(`Error: Namespace "${namespace}" was not found in your source code.`))
232
+ return
233
+ }
134
234
 
135
- const { primaryLanguage, keySeparator = '.', defaultNS = 'translation' } = config.extract
136
- const secondaryLanguages = config.locales.filter(l => l !== primaryLanguage)
235
+ console.log(chalk.cyan.bold(`\nStatus for Namespace: "${namespace}"`))
236
+ console.log('------------------------')
137
237
 
138
- const allNamespaces = new Set<string>(
139
- Array.from(allExtractedKeys.values()).map(k => k.ns || defaultNS)
140
- )
238
+ for (const [locale, localeData] of report.locales.entries()) {
239
+ const nsLocaleData = localeData.namespaces.get(namespace)
240
+ if (nsLocaleData) {
241
+ const percentage = nsLocaleData.totalKeys > 0 ? Math.round((nsLocaleData.translatedKeys / nsLocaleData.totalKeys) * 100) : 100
242
+ const bar = generateProgressBarText(percentage)
243
+ console.log(`- ${locale}: ${bar} ${percentage}% (${nsLocaleData.translatedKeys}/${nsLocaleData.totalKeys} keys)`)
244
+ }
245
+ }
246
+ }
141
247
 
142
- spinner.succeed('Analysis complete.')
248
+ /**
249
+ * Displays the default, high-level summary report for all locales.
250
+ *
251
+ * Shows:
252
+ * - Project overview (total keys, locales, primary language)
253
+ * - Translation progress for each secondary locale with progress bars
254
+ * - Promotional message for locize service
255
+ *
256
+ * @param report - The generated status report data
257
+ * @param config - The i18next toolkit configuration object
258
+ */
259
+ function displayOverallSummaryReport (report: StatusReport, config: I18nextToolkitConfig) {
260
+ const { primaryLanguage } = config.extract
143
261
 
144
262
  console.log(chalk.cyan.bold('\ni18next Project Status'))
145
263
  console.log('------------------------')
146
- console.log(`🔑 Keys Found: ${chalk.bold(totalKeys)}`)
264
+ console.log(`🔑 Keys Found: ${chalk.bold(report.totalKeys)}`)
147
265
  console.log(`🌍 Locales: ${chalk.bold(config.locales.join(', '))}`)
148
266
  console.log(`✅ Primary Language: ${chalk.bold(primaryLanguage)}`)
149
267
  console.log('\nTranslation Progress:')
150
268
 
151
- for (const lang of secondaryLanguages) {
152
- let translatedKeysCount = 0
153
-
154
- for (const ns of allNamespaces) {
155
- const langFilePath = getOutputPath(config.extract.output, lang, ns)
156
- try {
157
- const content = await readFile(resolve(process.cwd(), langFilePath), 'utf-8')
158
- const translations = JSON.parse(content)
159
- const translatedKeysInFile = getNestedKeys(translations, keySeparator ?? '.')
160
-
161
- const countForNs = translatedKeysInFile.filter(k => {
162
- const value = getNestedValue(translations, k, keySeparator ?? '.')
163
- // A key is counted if it has a non-empty value AND it was extracted from the source for this namespace
164
- return !!value && allExtractedKeys.has(`${ns}:${k}`)
165
- }).length
166
- translatedKeysCount += countForNs
167
- } catch {
168
- // File not found for this namespace, so its contribution to the count is 0
169
- }
170
- }
171
-
172
- const percentage = totalKeys > 0 ? Math.round((translatedKeysCount / totalKeys) * 100) : 100
173
- const progressBar = generateProgressBar(percentage)
174
- console.log(`- ${lang}: ${progressBar} ${percentage}% (${translatedKeysCount}/${totalKeys} keys)`)
269
+ for (const [locale, localeData] of report.locales.entries()) {
270
+ const percentage = report.totalKeys > 0 ? Math.round((localeData.totalTranslated / report.totalKeys) * 100) : 100
271
+ const bar = generateProgressBarText(percentage)
272
+ console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${report.totalKeys} keys)`)
175
273
  }
176
274
 
177
275
  console.log(chalk.yellow.bold('\n✨ Take your localization to the next level!'))
@@ -180,11 +278,28 @@ async function displaySummaryStatus (config: I18nextToolkitConfig, spinner: Ora)
180
278
  }
181
279
 
182
280
  /**
183
- * Generates a simple text-based progress bar.
184
- * @param percentage - The percentage to display (0-100).
185
- * @internal
281
+ * Prints a formatted progress bar with label, percentage, and counts.
282
+ *
283
+ * @param label - The label to display before the progress bar
284
+ * @param current - The current count (translated keys)
285
+ * @param total - The total count (all keys)
286
+ */
287
+ function printProgressBar (label: string, current: number, total: number) {
288
+ const percentage = total > 0 ? Math.round((current / total) * 100) : 100
289
+ const bar = generateProgressBarText(percentage)
290
+ console.log(`${chalk.bold(label)}: ${bar} ${percentage}% (${current}/${total})`)
291
+ }
292
+
293
+ /**
294
+ * Generates a visual progress bar string based on percentage completion.
295
+ *
296
+ * Creates a 20-character progress bar using filled (■) and empty (□) squares,
297
+ * with the filled portion colored green.
298
+ *
299
+ * @param percentage - The completion percentage (0-100)
300
+ * @returns A formatted progress bar string with colors
186
301
  */
187
- function generateProgressBar (percentage: number): string {
302
+ function generateProgressBarText (percentage: number): string {
188
303
  const totalBars = 20
189
304
  const filledBars = Math.round((percentage / 100) * totalBars)
190
305
  const emptyBars = totalBars - filledBars
package/src/types.ts CHANGED
@@ -60,6 +60,9 @@ export interface I18nextToolkitConfig {
60
60
  /** A list of JSX attribute names to ignore when linting for hardcoded strings. */
61
61
  ignoredAttributes?: string[];
62
62
 
63
+ /** A list of JSX tag names whose content should be ignored when linting (e.g., 'code', 'pre'). */
64
+ ignoredTags?: string[];
65
+
63
66
  /** HTML tags to preserve in Trans component serialization (default: ['br', 'strong', 'i']) */
64
67
  transKeepBasicHtmlNodesFor?: string[];
65
68
 
@@ -1 +1 @@
1
- {"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,iBAsC5D"}
1
+ {"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,iBAsC5D"}
package/types/status.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { I18nextToolkitConfig } from './types';
2
+ /**
3
+ * Options for configuring the status report display.
4
+ */
2
5
  interface StatusOptions {
6
+ /** Locale code to display detailed information for a specific language */
3
7
  detail?: string;
8
+ /** Namespace to filter the report by */
9
+ namespace?: string;
4
10
  }
5
11
  /**
6
12
  * Runs a health check on the project's i18next translations and displays a status report.
@@ -13,7 +19,8 @@ interface StatusOptions {
13
19
  * 5. Serving as a value-driven funnel to introduce the locize commercial service.
14
20
  *
15
21
  * @param config - The i18next toolkit configuration object.
16
- * @param options Options object, may contain a `detail` property with a locale string.
22
+ * @param options - Options object, may contain a `detail` property with a locale string.
23
+ * @throws {Error} When unable to extract keys or read translation files
17
24
  */
18
25
  export declare function runStatus(config: I18nextToolkitConfig, options?: StatusOptions): Promise<void>;
19
26
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,SAAS,CAAA;AAGjE,UAAU,aAAa;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;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":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,SAAS,CAAA;AAGjE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAYzF"}
package/types/types.d.ts CHANGED
@@ -47,6 +47,8 @@ export interface I18nextToolkitConfig {
47
47
  useTranslationNames?: string[];
48
48
  /** A list of JSX attribute names to ignore when linting for hardcoded strings. */
49
49
  ignoredAttributes?: string[];
50
+ /** A list of JSX tag names whose content should be ignored when linting (e.g., 'code', 'pre'). */
51
+ ignoredTags?: string[];
50
52
  /** HTML tags to preserve in Trans component serialization (default: ['br', 'strong', 'i']) */
51
53
  transKeepBasicHtmlNodesFor?: string[];
52
54
  /** Glob patterns for keys to preserve even if not found in source (for dynamic keys) */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}