i18next-cli 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +6 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/key-finder.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +1 -1
- package/dist/cjs/linter.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/key-finder.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +1 -1
- package/dist/esm/linter.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/extractor/core/key-finder.ts +9 -1
- package/src/extractor/core/translation-manager.ts +6 -1
- package/src/linter.ts +8 -1
- package/src/types.ts +6 -0
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/linter.d.ts.map +1 -1
- package/types/types.d.ts +4 -0
- package/types/types.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0](https://github.com/i18next/i18next-cli/compare/v1.0.2...v1.1.0) - 2025-10-02
|
|
9
|
+
|
|
10
|
+
- **Extractor:** Added a new `extract.removeUnusedKeys` option to control whether keys no longer found in the source code are removed from translation files. This defaults to `true` to maintain the existing pruning behavior. Set it to `false` to preserve all existing keys, which is useful for projects with dynamic keys. [#18](https://github.com/i18next/i18next-cli/issues/18)
|
|
11
|
+
|
|
12
|
+
## [1.0.2](https://github.com/i18next/i18next-cli/compare/v1.0.1...v1.0.2) - 2025-10-01
|
|
13
|
+
|
|
14
|
+
- **Extractor & Linter:** Added a new `extract.ignore` option to provide a simpler and more reliable way to exclude files from processing. This option accepts an array of glob patterns and is respected by both the `extract` and `lint` commands, avoiding the need for complex negative glob patterns.
|
|
15
|
+
|
|
8
16
|
## [1.0.1](https://github.com/i18next/i18next-cli/compare/v1.0.0...v1.0.1) - 2025-10-01
|
|
9
17
|
|
|
10
18
|
- **Extractor:** Fixed a bug where the comment parser was too aggressive, causing it to incorrectly extract keys from non-translation functions (like `test()` or `http.get()`) found inside comments. The parser is now more specific and safely targets only valid, commented-out `t()` calls. [#13](https://github.com/i18next/i18next-cli/issues/13)
|
package/README.md
CHANGED
|
@@ -287,6 +287,9 @@ export default defineConfig({
|
|
|
287
287
|
input: ['src/**/*.{ts,tsx}'],
|
|
288
288
|
output: 'locales/{{language}}/{{namespace}}.json',
|
|
289
289
|
|
|
290
|
+
/** Glob pattern(s) for files to ignore during extraction */
|
|
291
|
+
ignore: ['node_modules/**'],
|
|
292
|
+
|
|
290
293
|
// Use '.ts' files with `export default` instead of '.json'
|
|
291
294
|
outputFormat: 'ts',
|
|
292
295
|
|
|
@@ -344,6 +347,9 @@ export default defineConfig({
|
|
|
344
347
|
secondaryLanguages: ['de', 'fr'], // Defaults to all locales except primaryLanguage
|
|
345
348
|
|
|
346
349
|
defaultValue: '', // Default value for missing keys in secondary languages
|
|
350
|
+
|
|
351
|
+
/** If true, keys that are not found in the source code will be removed from translation files. (default: true) */
|
|
352
|
+
removeUnusedKeys: true,
|
|
347
353
|
},
|
|
348
354
|
|
|
349
355
|
// TypeScript type generation
|
package/dist/cjs/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.0
|
|
2
|
+
"use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.1.0"),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"),r=require("./extractor.js"),
|
|
1
|
+
"use strict";var e=require("glob"),r=require("./extractor.js"),t=require("../../utils/logger.js"),i=require("../plugin-manager.js"),s=require("../parsers/ast-visitors.js");exports.findKeys=async function(n,o=new t.ConsoleLogger){const a=await async function(r){const t=["node_modules/**"],i=Array.isArray(r.extract.ignore)?r.extract.ignore:r.extract.ignore?[r.extract.ignore]:[];return await e.glob(r.extract.input,{ignore:[...t,...i],cwd:process.cwd()})}(n),c=new Map,u=new s.ASTVisitors(n,i.createPluginContext(c),o);await i.initializePlugins(n.plugins||[]);for(const e of a)await r.processFile(e,n,c,u,o);for(const e of n.plugins||[])await(e.onEnd?.(c));return{allKeys:c,objectKeys:u.objectKeys}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("node:path"),e=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function a(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}exports.getTranslations=async function(r,n,o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(t=>t!==o?.extract?.primaryLanguage);const c=o.extract.primaryLanguage,l=o.extract.defaultNS??"translation",u=o.extract.keySeparator??".",i=[...o.extract.preservePatterns||[]],p=o.extract.mergeNamespaces??!1,
|
|
1
|
+
"use strict";var t=require("node:path"),e=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function a(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}exports.getTranslations=async function(r,n,o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(t=>t!==o?.extract?.primaryLanguage);const c=o.extract.primaryLanguage,l=o.extract.defaultNS??"translation",u=o.extract.keySeparator??".",i=[...o.extract.preservePatterns||[]],p=o.extract.mergeNamespaces??!1,g=o.extract.indentation??2,f=o.extract.removeUnusedKeys??!0;for(const t of n)i.push(`${t}.*`);const d=i.map(a),x=new Map;for(const t of r.values()){const e=t.ns||l;x.has(e)||x.set(e,[]),x.get(e).push(t)}const y=[];for(const a of o.locales){const r=p?await s.loadTranslationFile(t.resolve(process.cwd(),s.getOutputPath(o.extract.output,a)))||{}:null,n={};for(const[l,i]of x.entries()){const x=s.getOutputPath(o.extract.output,a,p?void 0:l),N=t.resolve(process.cwd(),x),h=p?r?.[l]||{}:await s.loadTranslationFile(N)||{},m=f?{}:JSON.parse(JSON.stringify(h));console.log(m);const w=e.getNestedKeys(h,u);for(const t of w)if(d.some(e=>e.test(t))){const s=e.getNestedValue(h,t,u);e.setNestedValue(m,t,s,u)}const O=!1===o.extract.sort?i:[...i].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:s}of O){const r=e.getNestedValue(h,t,u)??(a===c?s:o.extract.defaultValue??"");e.setNestedValue(m,t,r,u)}if(p)n[l]=m;else{const t=JSON.stringify(h,null,g),e=JSON.stringify(m,null,g);y.push({path:N,updated:e!==t,newTranslations:m,existingTranslations:h})}}if(p){const e=s.getOutputPath(o.extract.output,a),c=t.resolve(process.cwd(),e),l=JSON.stringify(r,null,g),u=JSON.stringify(n,null,g);y.push({path:c,updated:u!==l,newTranslations:n,existingTranslations:r||{}})}}return y};
|
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"),n=require("chalk"),s=require("ora");const o=e=>/^(https|http|\/\/|^\/)/.test(e);function i(e,t,r){const n=[],s=[],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("{{")&&s.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))&&s.push(e)}}for(const t of Object.keys(e)){if("span"===t)continue;const n=e[t];Array.isArray(n)?n.forEach(e=>p(e,r)):n&&"object"==typeof n&&p(n,r)}};p(e,[]);let g=0;for(const e of s){const r=e.raw??e.value,s=t.indexOf(r,g);s>-1&&(n.push({text:e.value.trim(),line:i(s)}),g=s+r.length)}return n}exports.runLinter=async function(o){const a=s("Analyzing source files...\n").start();try{const s=["node_modules/**"],c=Array.isArray(o.extract.ignore)?o.extract.ignore:o.extract.ignore?[o.extract.ignore]:[],l=await e.glob(o.extract.input,{ignore:[...s,...c]});let u=0;const f=new Map;for(const e of l){const n=await t.readFile(e,"utf-8"),s=i(await r.parse(n,{syntax:"typescript",tsx:!0,decorators:!0}),n,o);s.length>0&&(u+=s.length,f.set(e,s))}if(u>0){a.fail(n.red.bold(`Linter found ${u} potential issues.`));for(const[e,t]of f.entries())console.log(n.yellow(`\n${e}`)),t.forEach(({text:e,line:t})=>{console.log(` ${n.gray(`${t}:`)} ${n.red("Error:")} Found hardcoded string: "${e}"`)});process.exit(1)}else a.succeed(n.green.bold("No issues found."))}catch(e){a.fail(n.red("Linter failed to run.")),console.error(e),process.exit(1)}};
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as o}from"commander";import t from"chokidar";import{glob as e}from"glob";import i from"chalk";import{ensureConfig as n,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.0
|
|
2
|
+
import{Command as o}from"commander";import t from"chokidar";import{glob as e}from"glob";import i from"chalk";import{ensureConfig as n,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.1.0"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async o=>{const a=await n(),c=async()=>{const t=await r(a);o.ci&&t&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");t.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(o,t)=>{let e=await a();if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=o}await f(e,{detail:o,namespace:t.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");t.watch(await e(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(p),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=t}await d(o)}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async o=>{const t=await n();await g(t,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const t=await n();await u(t,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const t=await n();await y(t,o)}),w.parse(process.argv);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{glob as
|
|
1
|
+
import{glob as r}from"glob";import{processFile as t}from"./extractor.js";import{ConsoleLogger as o}from"../../utils/logger.js";import{initializePlugins as e,createPluginContext as n}from"../plugin-manager.js";import{ASTVisitors as a}from"../parsers/ast-visitors.js";async function i(i,s=new o){const c=await async function(t){const o=["node_modules/**"],e=Array.isArray(t.extract.ignore)?t.extract.ignore:t.extract.ignore?[t.extract.ignore]:[];return await r(t.extract.input,{ignore:[...o,...e],cwd:process.cwd()})}(i),p=new Map,g=new a(i,n(p),s);await e(i.plugins||[]);for(const r of c)await t(r,i,p,g,s);for(const r of i.plugins||[])await(r.onEnd?.(p));return{allKeys:p,objectKeys:g.objectKeys}}export{i as findKeys};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolve as t}from"node:path";import{getNestedKeys as e,getNestedValue as s,setNestedValue as a}from"../../utils/nested-object.js";import{loadTranslationFile as
|
|
1
|
+
import{resolve as t}from"node:path";import{getNestedKeys as e,getNestedValue as s,setNestedValue as a}from"../../utils/nested-object.js";import{loadTranslationFile as o,getOutputPath as n}from"../../utils/file-utils.js";function r(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}async function c(c,i,l){l.extract.primaryLanguage||=l.locales[0]||"en",l.extract.secondaryLanguages||=l.locales.filter(t=>t!==l?.extract?.primaryLanguage);const u=l.extract.primaryLanguage,p=l.extract.defaultNS??"translation",f=l.extract.keySeparator??".",g=[...l.extract.preservePatterns||[]],x=l.extract.mergeNamespaces??!1,d=l.extract.indentation??2,m=l.extract.removeUnusedKeys??!0;for(const t of i)g.push(`${t}.*`);const y=g.map(r),w=new Map;for(const t of c.values()){const e=t.ns||p;w.has(e)||w.set(e,[]),w.get(e).push(t)}const h=[];for(const r of l.locales){const c=x?await o(t(process.cwd(),n(l.extract.output,r)))||{}:null,i={};for(const[p,g]of w.entries()){const w=n(l.extract.output,r,x?void 0:p),N=t(process.cwd(),w),S=x?c?.[p]||{}:await o(N)||{},J=m?{}:JSON.parse(JSON.stringify(S));console.log(J);const O=e(S,f);for(const t of O)if(y.some(e=>e.test(t))){const e=s(S,t,f);a(J,t,e,f)}const $=!1===l.extract.sort?g:[...g].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:e}of $){const o=s(S,t,f)??(r===u?e:l.extract.defaultValue??"");a(J,t,o,f)}if(x)i[p]=J;else{const t=JSON.stringify(S,null,d),e=JSON.stringify(J,null,d);h.push({path:N,updated:e!==t,newTranslations:J,existingTranslations:S})}}if(x){const e=n(l.extract.output,r),s=t(process.cwd(),e),a=JSON.stringify(c,null,d),o=JSON.stringify(i,null,d);h.push({path:s,updated:o!==a,newTranslations:i,existingTranslations:c||{}})}}return h}export{c as getTranslations};
|
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 r}from"@swc/core";import o from"chalk";import n from"ora";async function s(s){const i=n("Analyzing source files...\n").start();try{const n=["node_modules/**"],c=Array.isArray(s.extract.ignore)?s.extract.ignore:s.extract.ignore?[s.extract.ignore]:[],l=await t(s.extract.input,{ignore:[...n,...c]});let f=0;const u=new Map;for(const t of l){const o=await e(t,"utf-8"),n=a(await r(o,{syntax:"typescript",tsx:!0,decorators:!0}),o,s);n.length>0&&(f+=n.length,u.set(t,n))}if(f>0){i.fail(o.red.bold(`Linter found ${f} potential issues.`));for(const[t,e]of u.entries())console.log(o.yellow(`\n${t}`)),e.forEach(({text:t,line:e})=>{console.log(` ${o.gray(`${e}:`)} ${o.red("Error:")} Found hardcoded string: "${t}"`)});process.exit(1)}else i.succeed(o.green.bold("No issues found."))}catch(t){i.fail(o.red("Linter failed to run.")),console.error(t),process.exit(1)}}const i=t=>/^(https|http|\/\/|^\/)/.test(t);function a(t,e,r){const o=[],n=[],s=t=>e.substring(0,t).split("\n").length,a=r.extract.transComponents||["Trans"],c=r.extract.ignoredTags||[],l=new Set([...a,"script","style","code",...c]),f=r.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 r=[...e,t];if("JSXText"===t.type){if(!r.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=r[r.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 o=t[e];Array.isArray(o)?o.forEach(t=>p(t,r)):o&&"object"==typeof o&&p(o,r)}};p(t,[]);let g=0;for(const t of n){const r=t.raw??t.value,n=e.indexOf(r,g);n>-1&&(o.push({text:t.value.trim(),line:s(n)}),g=n+r.length)}return o}export{s as runLinter};
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -67,8 +67,16 @@ export async function findKeys (
|
|
|
67
67
|
* @internal
|
|
68
68
|
*/
|
|
69
69
|
async function processSourceFiles (config: I18nextToolkitConfig): Promise<string[]> {
|
|
70
|
+
const defaultIgnore = ['node_modules/**']
|
|
71
|
+
|
|
72
|
+
// Normalize the user's ignore option into an array
|
|
73
|
+
const userIgnore = Array.isArray(config.extract.ignore)
|
|
74
|
+
? config.extract.ignore
|
|
75
|
+
: config.extract.ignore ? [config.extract.ignore] : []
|
|
76
|
+
|
|
70
77
|
return await glob(config.extract.input, {
|
|
71
|
-
ignore
|
|
78
|
+
// Combine default ignore patterns with user-configured ones
|
|
79
|
+
ignore: [...defaultIgnore, ...userIgnore],
|
|
72
80
|
cwd: process.cwd(),
|
|
73
81
|
})
|
|
74
82
|
}
|
|
@@ -55,6 +55,7 @@ export async function getTranslations (
|
|
|
55
55
|
const patternsToPreserve = [...(config.extract.preservePatterns || [])]
|
|
56
56
|
const mergeNamespaces = config.extract.mergeNamespaces ?? false
|
|
57
57
|
const indentation = config.extract.indentation ?? 2
|
|
58
|
+
const removeUnusedKeys = config.extract.removeUnusedKeys ?? true
|
|
58
59
|
|
|
59
60
|
for (const key of objectKeys) {
|
|
60
61
|
// Convert the object key to a glob pattern to preserve all its children
|
|
@@ -87,7 +88,11 @@ export async function getTranslations (
|
|
|
87
88
|
? existingMergedFile?.[ns] || {}
|
|
88
89
|
: await loadTranslationFile(fullPath) || {}
|
|
89
90
|
|
|
90
|
-
const newTranslations: Record<string, any> =
|
|
91
|
+
const newTranslations: Record<string, any> = removeUnusedKeys
|
|
92
|
+
? {}
|
|
93
|
+
: JSON.parse(JSON.stringify(existingTranslations))
|
|
94
|
+
|
|
95
|
+
console.log(newTranslations)
|
|
91
96
|
|
|
92
97
|
const existingKeys = getNestedKeys(existingTranslations, keySeparator)
|
|
93
98
|
for (const existingKey of existingKeys) {
|
package/src/linter.ts
CHANGED
|
@@ -39,7 +39,14 @@ export async function runLinter (config: I18nextToolkitConfig) {
|
|
|
39
39
|
const spinner = ora('Analyzing source files...\n').start()
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
42
|
+
const defaultIgnore = ['node_modules/**']
|
|
43
|
+
const userIgnore = Array.isArray(config.extract.ignore)
|
|
44
|
+
? config.extract.ignore
|
|
45
|
+
: config.extract.ignore ? [config.extract.ignore] : []
|
|
46
|
+
|
|
47
|
+
const sourceFiles = await glob(config.extract.input, {
|
|
48
|
+
ignore: [...defaultIgnore, ...userIgnore]
|
|
49
|
+
})
|
|
43
50
|
let totalIssues = 0
|
|
44
51
|
const issuesByFile = new Map<string, HardcodedString[]>()
|
|
45
52
|
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,9 @@ export interface I18nextToolkitConfig {
|
|
|
30
30
|
/** Glob pattern(s) for source files to scan for translation keys */
|
|
31
31
|
input: string | string[];
|
|
32
32
|
|
|
33
|
+
/** Glob pattern(s) for files to ignore during extraction */
|
|
34
|
+
ignore?: string | string[];
|
|
35
|
+
|
|
33
36
|
/** Output path template with placeholders: {{language}} for locale, {{namespace}} for namespace */
|
|
34
37
|
output: string;
|
|
35
38
|
|
|
@@ -110,6 +113,9 @@ export interface I18nextToolkitConfig {
|
|
|
110
113
|
* (default: false)
|
|
111
114
|
*/
|
|
112
115
|
mergeNamespaces?: boolean;
|
|
116
|
+
|
|
117
|
+
/** If true, keys that are not found in the source code will be removed from translation files. (default: true) */
|
|
118
|
+
removeUnusedKeys?: boolean;
|
|
113
119
|
};
|
|
114
120
|
|
|
115
121
|
/** Configuration options for TypeScript type generation */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAkF9B"}
|
package/types/linter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,iBAiD5D"}
|
package/types/types.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface I18nextToolkitConfig {
|
|
|
27
27
|
extract: {
|
|
28
28
|
/** Glob pattern(s) for source files to scan for translation keys */
|
|
29
29
|
input: string | string[];
|
|
30
|
+
/** Glob pattern(s) for files to ignore during extraction */
|
|
31
|
+
ignore?: string | string[];
|
|
30
32
|
/** Output path template with placeholders: {{language}} for locale, {{namespace}} for namespace */
|
|
31
33
|
output: string;
|
|
32
34
|
/** Default namespace when none is specified (default: 'translation') */
|
|
@@ -88,6 +90,8 @@ export interface I18nextToolkitConfig {
|
|
|
88
90
|
* (default: false)
|
|
89
91
|
*/
|
|
90
92
|
mergeNamespaces?: boolean;
|
|
93
|
+
/** If true, keys that are not found in the source code will be removed from translation files. (default: true) */
|
|
94
|
+
removeUnusedKeys?: boolean;
|
|
91
95
|
};
|
|
92
96
|
/** Configuration options for TypeScript type generation */
|
|
93
97
|
types?: {
|
package/types/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAEnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB,2DAA2D;IAC3D,OAAO,EAAE;QACP,oEAAoE;QACpE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAEzB,mGAAmG;QACnG,MAAM,EAAE,MAAM,CAAC;QAEf,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QAEnB,uEAAuE;QACvE,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;QAErC,8EAA8E;QAC9E,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;QAEpC,oDAAoD;QACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAE1B,mDAAmD;QACnD,eAAe,CAAC,EAAE,MAAM,CAAC;QAEzB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAErB,4EAA4E;QAC5E,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAE3B;;;;;WAKG;QACH,mBAAmB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;YACnC,IAAI,EAAE,MAAM,CAAC;YACb,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,YAAY,CAAC,EAAE,MAAM,CAAC;SACvB,CAAC,CAAC;QAEH,kFAAkF;QAClF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE7B,kGAAkG;QAClG,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAEvB,8FAA8F;QAC9F,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;QAEtC,wFAAwF;QACxF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE5B,0EAA0E;QAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;QAEf,yDAAyD;QACzD,WAAW,CAAC,EAAE,MAAM,CAAC;QAErB,2EAA2E;QAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;QAEtB,4EAA4E;QAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;QAEzB,0DAA0D;QAC1D,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE9B;;;;;;;WAOG;QACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;QAErE;;;;;WAKG;QACH,eAAe,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|