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 +14 -1
- package/README.md +44 -6
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/linter.js +1 -1
- package/dist/cjs/status.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/linter.js +1 -1
- package/dist/esm/status.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +4 -3
- package/src/linter.ts +62 -55
- package/src/status.ts +221 -106
- package/src/types.ts +3 -0
- package/types/linter.d.ts.map +1 -1
- package/types/status.d.ts +8 -1
- package/types/status.d.ts.map +1 -1
- package/types/types.d.ts +2 -0
- package/types/types.d.ts.map +1 -1
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.
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
-
|
|
136
|
-
|
|
137
|
-
|
|
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"),
|
|
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);
|
package/dist/cjs/linter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("glob"),t=require("node:fs/promises"),r=require("@swc/core"),
|
|
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)}};
|
package/dist/cjs/status.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),
|
|
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
|
|
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);
|
package/dist/esm/linter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{glob as t}from"glob";import{readFile as e}from"node:fs/promises";import{parse as
|
|
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};
|
package/dist/esm/status.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{readFile as
|
|
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
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.
|
|
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
|
-
.
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
144
|
+
return allIgnoredTags.has(elementName)
|
|
162
145
|
})
|
|
163
146
|
|
|
164
|
-
if (isIgnored)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
|
|
98
|
+
const report: StatusReport = {
|
|
99
|
+
totalKeys: allExtractedKeys.size,
|
|
100
|
+
keysByNs,
|
|
101
|
+
locales: new Map(),
|
|
58
102
|
}
|
|
59
103
|
|
|
60
|
-
|
|
104
|
+
for (const locale of secondaryLanguages) {
|
|
105
|
+
let totalTranslatedForLocale = 0
|
|
106
|
+
const namespaces = new Map<string, any>()
|
|
61
107
|
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
console.log(chalk.bold(`\nKey Status for "${locale}":`))
|
|
183
|
+
const localeData = report.locales.get(locale)
|
|
92
184
|
|
|
93
|
-
|
|
94
|
-
|
|
185
|
+
if (!localeData) {
|
|
186
|
+
console.error(chalk.red(`Error: Locale "${locale}" is not a valid secondary language.`))
|
|
187
|
+
return
|
|
188
|
+
}
|
|
95
189
|
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(`\
|
|
212
|
+
console.log(chalk.yellow.bold(`\nSummary: Found ${missingCount} missing translations for "${locale}".`))
|
|
119
213
|
} else {
|
|
120
|
-
console.log(chalk.green.bold(`\
|
|
214
|
+
console.log(chalk.green.bold(`\nSummary: 🎉 All keys are translated for "${locale}".`))
|
|
121
215
|
}
|
|
122
216
|
}
|
|
123
217
|
|
|
124
218
|
/**
|
|
125
|
-
* Displays a
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
235
|
+
console.log(chalk.cyan.bold(`\nStatus for Namespace: "${namespace}"`))
|
|
236
|
+
console.log('------------------------')
|
|
137
237
|
|
|
138
|
-
const
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* @
|
|
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
|
|
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
|
|
package/types/linter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"
|
|
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 {};
|
package/types/status.d.ts.map
CHANGED
|
@@ -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;
|
|
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) */
|
package/types/types.d.ts.map
CHANGED
|
@@ -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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"}
|