i18next-cli 0.9.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 +46 -0
- package/LICENSE +21 -0
- package/README.md +489 -0
- package/dist/cjs/cli.js +2 -0
- package/dist/cjs/config.js +1 -0
- package/dist/cjs/extractor/core/extractor.js +1 -0
- package/dist/cjs/extractor/core/key-finder.js +1 -0
- package/dist/cjs/extractor/core/translation-manager.js +1 -0
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
- package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
- package/dist/cjs/extractor/plugin-manager.js +1 -0
- package/dist/cjs/heuristic-config.js +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/init.js +1 -0
- package/dist/cjs/linter.js +1 -0
- package/dist/cjs/locize.js +1 -0
- package/dist/cjs/migrator.js +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/status.js +1 -0
- package/dist/cjs/syncer.js +1 -0
- package/dist/cjs/types-generator.js +1 -0
- package/dist/cjs/utils/file-utils.js +1 -0
- package/dist/cjs/utils/logger.js +1 -0
- package/dist/cjs/utils/nested-object.js +1 -0
- package/dist/cjs/utils/validation.js +1 -0
- package/dist/esm/cli.js +2 -0
- package/dist/esm/config.js +1 -0
- package/dist/esm/extractor/core/extractor.js +1 -0
- package/dist/esm/extractor/core/key-finder.js +1 -0
- package/dist/esm/extractor/core/translation-manager.js +1 -0
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
- package/dist/esm/extractor/parsers/comment-parser.js +1 -0
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
- package/dist/esm/extractor/plugin-manager.js +1 -0
- package/dist/esm/heuristic-config.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/init.js +1 -0
- package/dist/esm/linter.js +1 -0
- package/dist/esm/locize.js +1 -0
- package/dist/esm/migrator.js +1 -0
- package/dist/esm/status.js +1 -0
- package/dist/esm/syncer.js +1 -0
- package/dist/esm/types-generator.js +1 -0
- package/dist/esm/utils/file-utils.js +1 -0
- package/dist/esm/utils/logger.js +1 -0
- package/dist/esm/utils/nested-object.js +1 -0
- package/dist/esm/utils/validation.js +1 -0
- package/package.json +81 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +158 -0
- package/src/extractor/core/extractor.ts +195 -0
- package/src/extractor/core/key-finder.ts +70 -0
- package/src/extractor/core/translation-manager.ts +115 -0
- package/src/extractor/index.ts +7 -0
- package/src/extractor/parsers/ast-visitors.ts +637 -0
- package/src/extractor/parsers/comment-parser.ts +125 -0
- package/src/extractor/parsers/jsx-parser.ts +166 -0
- package/src/extractor/plugin-manager.ts +54 -0
- package/src/extractor.ts +15 -0
- package/src/heuristic-config.ts +64 -0
- package/src/index.ts +12 -0
- package/src/init.ts +156 -0
- package/src/linter.ts +191 -0
- package/src/locize.ts +251 -0
- package/src/migrator.ts +139 -0
- package/src/status.ts +192 -0
- package/src/syncer.ts +114 -0
- package/src/types-generator.ts +116 -0
- package/src/types.ts +312 -0
- package/src/utils/file-utils.ts +81 -0
- package/src/utils/logger.ts +36 -0
- package/src/utils/nested-object.ts +113 -0
- package/src/utils/validation.ts +69 -0
- package/tryme.js +8 -0
- package/tsconfig.json +71 -0
- package/types/cli.d.ts +3 -0
- package/types/cli.d.ts.map +1 -0
- package/types/config.d.ts +50 -0
- package/types/config.d.ts.map +1 -0
- package/types/extractor/core/extractor.d.ts +66 -0
- package/types/extractor/core/extractor.d.ts.map +1 -0
- package/types/extractor/core/key-finder.d.ts +31 -0
- package/types/extractor/core/key-finder.d.ts.map +1 -0
- package/types/extractor/core/translation-manager.d.ts +31 -0
- package/types/extractor/core/translation-manager.d.ts.map +1 -0
- package/types/extractor/index.d.ts +8 -0
- package/types/extractor/index.d.ts.map +1 -0
- package/types/extractor/parsers/ast-visitors.d.ts +235 -0
- package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
- package/types/extractor/parsers/comment-parser.d.ts +24 -0
- package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
- package/types/extractor/parsers/jsx-parser.d.ts +35 -0
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
- package/types/extractor/plugin-manager.d.ts +37 -0
- package/types/extractor/plugin-manager.d.ts.map +1 -0
- package/types/extractor.d.ts +7 -0
- package/types/extractor.d.ts.map +1 -0
- package/types/heuristic-config.d.ts +10 -0
- package/types/heuristic-config.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/init.d.ts +29 -0
- package/types/init.d.ts.map +1 -0
- package/types/linter.d.ts +33 -0
- package/types/linter.d.ts.map +1 -0
- package/types/locize.d.ts +5 -0
- package/types/locize.d.ts.map +1 -0
- package/types/migrator.d.ts +37 -0
- package/types/migrator.d.ts.map +1 -0
- package/types/status.d.ts +20 -0
- package/types/status.d.ts.map +1 -0
- package/types/syncer.d.ts +33 -0
- package/types/syncer.d.ts.map +1 -0
- package/types/types-generator.d.ts +29 -0
- package/types/types-generator.d.ts.map +1 -0
- package/types/types.d.ts +268 -0
- package/types/types.d.ts.map +1 -0
- package/types/utils/file-utils.d.ts +61 -0
- package/types/utils/file-utils.d.ts.map +1 -0
- package/types/utils/logger.d.ts +34 -0
- package/types/utils/logger.d.ts.map +1 -0
- package/types/utils/nested-object.d.ts +71 -0
- package/types/utils/nested-object.d.ts.map +1 -0
- package/types/utils/validation.d.ts +47 -0
- package/types/utils/validation.d.ts.map +1 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("node:path"),o=require("node:fs/promises"),t=require("node:url");const n=e.resolve(process.cwd(),"i18next-parser.config.js"),a=e.resolve(process.cwd(),"i18next.config.ts"),r=["i18next.config.ts","i18next.config.js","i18next.config.mjs","i18next.config.cjs"];exports.runMigrator=async function(){console.log("Attempting to migrate legacy i18next-parser.config.js...");try{await o.access(n)}catch(e){return void console.log("No i18next-parser.config.js found. Nothing to migrate.")}try{await o.access(n)}catch(e){return void console.log("No i18next-parser.config.js found. Nothing to migrate.")}for(const t of r)try{const n=e.resolve(process.cwd(),t);return await o.access(n),void console.warn(`Warning: A new configuration file already exists at "${t}". Migration skipped to avoid overwriting.`)}catch(e){}const s=t.pathToFileURL(n).href,i=(await import(s)).default;if(!i)return void console.error("Could not read the legacy config file.");const c={locales:i.locales||["en"],extract:{input:i.input||"src/**/*.{js,jsx,ts,tsx}",output:(i.output||"locales/$LOCALE/$NAMESPACE.json").replace("$LOCALE","{{language}}").replace("$NAMESPACE","{{namespace}}"),defaultNS:i.defaultNamespace||"translation",keySeparator:i.keySeparator,nsSeparator:i.namespaceSeparator,contextSeparator:i.contextSeparator,functions:i.lexers?.js?.functions||["t"],transComponents:i.lexers?.js?.components||["Trans"]},typesafe:{input:"locales/{{language}}/{{namespace}}.json",output:"src/types/i18next.d.ts"},sync:{primaryLanguage:i.locales?.[0]||"en",secondaryLanguages:i.locales.filter(e=>e!==(i.locales?.[0]||"en"))}},l=`\nimport { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${JSON.stringify(c,null,2)});\n`;await o.writeFile(a,l.trim()),console.log("✅ Success! Migration complete."),console.log(`New configuration file created at: ${a}`),console.warn('\nPlease review the generated file and adjust paths for "typesafe.input" if necessary.'),i.keepRemoved&&console.warn('Warning: The "keepRemoved" option is deprecated. Consider using the "preservePatterns" feature for dynamic keys.')};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +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)}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("node:fs/promises"),t=require("path"),r=require("chalk"),o=require("ora"),a=require("./utils/nested-object.js"),n=require("./utils/file-utils.js");exports.runSyncer=async function(c){const l=o("Running i18next locale synchronizer...\n").start();c.extract.primaryLanguage||=c.locales[0]||"en";const{primaryLanguage:s}=c.extract,i=c.locales.filter(e=>e!==s),u=c.extract.keySeparator??".",d=[];let y=!1;const g=c.extract.defaultNS??"translation",f=n.getOutputPath(c.extract.output,s,g),p=t.resolve(process.cwd(),f);let h;try{const t=await e.readFile(p,"utf-8");h=JSON.parse(t)}catch(e){return void console.error(`Primary language file not found at ${f}. Cannot sync.`)}const x=a.getNestedKeys(h,u);for(const o of i){const l=n.getOutputPath(c.extract.output,o,g),s=t.resolve(process.cwd(),l);let i={},f="";try{f=await e.readFile(s,"utf-8"),i=JSON.parse(f)}catch(e){}const p={};for(const e of x){const t=a.getNestedValue(i,e,u)??(c.extract?.defaultValue||"");a.setNestedValue(p,e,t,u)}const h=c.extract.indentation??2,w=JSON.stringify(p,null,h);w!==f?(y=!0,await e.mkdir(t.dirname(s),{recursive:!0}),await e.writeFile(s,w),d.push(` ${r.green("✓")} Synchronized: ${l}`)):d.push(` ${r.gray("-")} Already in sync: ${l}`)}l.succeed(r.bold("Synchronization complete!")),d.forEach(e=>console.log(e)),y?(console.log(r.green.bold("\n✅ Sync complete.")),console.log(r.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${r.cyan("npx i18next-cli locize-migrate")}`)):console.log(r.green.bold("\n✅ All locales are already in sync."))};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("i18next-resources-for-ts"),t=require("glob"),s=require("ora"),r=require("chalk"),i=require("node:fs/promises"),n=require("node:path");exports.runTypesGenerator=async function(o){const a=s("Generating TypeScript types for translations...\n").start();try{if(o.types||(o.types={input:["locales/en/*.json"],output:"src/types/i18next.d.ts"}),void 0===o.types.input&&(o.types.input=["locales/en/*.json"]),o.types.output||(o.types.output="src/types/i18next.d.ts"),o.types.resourcesFile||(o.types.resourcesFile=n.join(n.dirname(o.types?.output),"resources.d.ts")),!o.types?.input||o.types?.input.length<0)return void console.log("No input defined!");const s=await t.glob(o.types?.input||[],{cwd:process.cwd()}),c=[];for(const e of s){const t=n.basename(e,n.extname(e)),s=await i.readFile(e,"utf-8"),r=JSON.parse(s);c.push({name:t,resources:r})}const p=[],u=o.types?.enableSelector||!1,l=e.mergeResourcesAsInterface(c,{optimize:!!u}),d=n.resolve(process.cwd(),o.types?.output||""),y=n.resolve(process.cwd(),o.types.resourcesFile);let f;await i.mkdir(n.dirname(y),{recursive:!0}),await i.writeFile(y,l),p.push(` ${r.green("✓")} Resources interface written to ${o.types.resourcesFile}`);try{await i.access(d),f=!0}catch(e){f=!1}if(!f){const e=`// This file is automatically generated by i18next-cli. Do not edit manually.\nimport Resources from './${n.relative(n.dirname(d),y).replace(/\\/g,"/").replace(/\.d\.ts$/,"")}';\n\ndeclare module 'i18next' {\n interface CustomTypeOptions {\n enableSelector: ${"string"==typeof u?`"${u}"`:u};\n defaultNS: '${o.extract.defaultNS||"translation"}';\n resources: Resources;\n }\n}`;await i.mkdir(n.dirname(d),{recursive:!0}),await i.writeFile(d,e),p.push(` ${r.green("✓")} TypeScript definitions written to ${o.types.output||""}`),a.succeed(r.bold("TypeScript definitions generated successfully.")),p.forEach(e=>console.log(e))}}catch(e){a.fail(r.red("Failed to generate TypeScript definitions.")),console.error(e)}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";require("node:fs/promises"),require("node:path"),exports.getOutputPath=function(e,r,a){return e.replace("{{language}}",r).replace("{{lng}}",r).replace("{{namespace}}",a).replace("{{ns}}",a)};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";exports.ConsoleLogger=class{info(o){console.log(o)}warn(o){console.warn(o)}error(o){console.error(o)}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";exports.getNestedKeys=function e(t,r,s=""){return!1===r?Object.keys(t):Object.entries(t).reduce((t,[u,n])=>{const c=s?`${s}${r}${u}`:u;return"object"!=typeof n||null===n||Array.isArray(n)?t.push(c):t.push(...e(n,r,c)),t},[])},exports.getNestedValue=function(e,t,r){return!1===r?e[t]:t.split(r).reduce((e,t)=>e&&e[t],e)},exports.setNestedValue=function(e,t,r,s){if(!1===s)return void(e[t]=r);const u=t.split(s);u.reduce((e,t,s)=>(s===u.length-1?e[t]=r:e[t]=e[t]||{},e[t]),e)};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";class t extends Error{file;cause;constructor(t,e,r){super(e?`${t} in file ${e}`:t),this.file=e,this.cause=r,this.name="ExtractorError"}}exports.ExtractorError=t,exports.validateExtractorConfig=function(e){if(!e.extract.input?.length)throw new t("extract.input must be specified and non-empty");if(!e.extract.output)throw new t("extract.output must be specified");if(!e.locales?.length)throw new t("locales must be specified and non-empty");if(!e.extract.output.includes("{{language}}")&&!e.extract.output.includes("{{lng}}"))throw new t("extract.output must contain {{language}} placeholder")};
|
package/dist/esm/cli.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
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.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.").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);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolve as o}from"node:path";import{pathToFileURL as r}from"node:url";import{access as t}from"node:fs/promises";import{createJiti as e}from"jiti";import n from"inquirer";import i from"chalk";import{runInit as a}from"./init.js";const c=["i18next.config.ts","i18next.config.js","i18next.config.mjs","i18next.config.cjs"];function l(o){return o}async function s(){const n=await async function(){for(const r of c){const e=o(process.cwd(),r);try{return await t(e),e}catch{}}return null}();if(!n)return null;try{let o;if(n.endsWith(".ts")){const r=e(import.meta.url),t=await r.import(n,{default:!0});o=t}else{const t=r(n).href,e=await import(`${t}?t=${Date.now()}`);o=e.default}return o?(o.extract||={},o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(r=>r!==o.extract.primaryLanguage),o):(console.error(`Error: No default export found in ${n}`),null)}catch(o){return console.error(`Error loading configuration from ${n}`),console.error(o),null}}async function f(){let o=await s();if(o)return o;const{shouldInit:r}=await n.prompt([{type:"confirm",name:"shouldInit",message:i.yellow("Configuration file not found. Would you like to create one now?"),default:!0}]);if(r){if(await a(),console.log(i.green("Configuration created. Resuming command...")),o=await s(),o)return o;console.error(i.red("Error: Failed to load configuration after creation. Please try running the command again.")),process.exit(1)}else console.log("Operation cancelled. Please create a configuration file to proceed."),process.exit(0)}export{l as defineConfig,f as ensureConfig,s as loadConfig};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"ora";import a from"chalk";import{parse as r}from"@swc/core";import{mkdir as o,writeFile as e,readFile as n}from"node:fs/promises";import{dirname as s}from"node:path";import{findKeys as i}from"./key-finder.js";import{getTranslations as c}from"./translation-manager.js";import{validateExtractorConfig as f,ExtractorError as p}from"../../utils/validation.js";import{createPluginContext as m}from"../plugin-manager.js";import{extractKeysFromComments as l}from"../parsers/comment-parser.js";import{ASTVisitors as u}from"../parsers/ast-visitors.js";import{ConsoleLogger as g}from"../../utils/logger.js";async function y(r,n=new g){r.extract.primaryLanguage||(r.extract.primaryLanguage=r.locales[0]||"en"),r.extract.secondaryLanguages||(r.extract.secondaryLanguages=r.locales.filter(t=>t!==r?.extract?.primaryLanguage)),f(r);const p=t("Running i18next key extractor...\n").start();try{const t=await i(r,n);p.text=`Found ${t.size} unique keys. Updating translation files...`;const f=await c(t,r);let m=!1;for(const t of f)t.updated&&(m=!0,await o(s(t.path),{recursive:!0}),await e(t.path,JSON.stringify(t.newTranslations,null,2)),n.info(a.green(`Updated: ${t.path}`)));return p.succeed(a.bold("Extraction complete!")),m}catch(t){throw p.fail(a.red("Extraction failed.")),t}}async function d(t,a,o,e){try{let s=await n(t,"utf-8");for(const r of a.plugins||[])s=await(r.onLoad?.(s,t))??s;const i=await r(s,{syntax:"typescript",tsx:!0,comments:!0}),c=m(e);l(s,a.extract.functions||["t"],c,a);new u(a,c,o).visit(i),(a.plugins||[]).length>0&&x(i,a.plugins||[],c)}catch(a){throw new p("Failed to process file",t,a)}}function x(t,a,r){if(t&&"object"==typeof t){for(const o of a)try{o.onVisitNode?.(t,r)}catch(t){console.warn(`Plugin ${o.name} onVisitNode failed:`,t)}for(const o of Object.keys(t)){const e=t[o];if(Array.isArray(e))for(const t of e)t&&"object"==typeof t&&x(t,a,r);else e&&"object"==typeof e&&x(e,a,r)}}}async function w(t){t.extract.primaryLanguage||(t.extract.primaryLanguage=t.locales[0]),t.extract.secondaryLanguages||(t.extract.secondaryLanguages=t.locales.filter(a=>a!==t?.extract?.primaryLanguage)),t.extract.functions||(t.extract.functions=["t"]),t.extract.transComponents||(t.extract.transComponents=["Trans"]);const a=await i(t);return c(a,t)}export{w as extract,d as processFile,y as runExtractor};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{glob as o}from"glob";import{processFile as t}from"./extractor.js";import{ConsoleLogger as n}from"../../utils/logger.js";import{initializePlugins as r}from"../plugin-manager.js";async function a(a,i=new n){const s=await async function(t){return await o(t.extract.input,{ignore:"node_modules/**",cwd:process.cwd()})}(a),e=new Map;await r(a.plugins||[]);for(const o of s)await t(o,a,i,e);for(const o of a.plugins||[])await(o.onEnd?.(e));return e}export{a as findKeys};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFile as t}from"node:fs/promises";import{resolve as e}from"node:path";import{getNestedKeys as a,getNestedValue as r,setNestedValue as o}from"../../utils/nested-object.js";import{getOutputPath as s}from"../../utils/file-utils.js";function n(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}async function c(c,i){const p=i.extract.defaultNS??"translation",l=i.extract.keySeparator??".",f=(i.extract.preservePatterns??[]).map(n);i.extract.primaryLanguage||(i.extract.primaryLanguage=i.locales[0]||"en"),i.extract.secondaryLanguages||(i.extract.secondaryLanguages=i.locales.filter(t=>t!==i.extract.primaryLanguage));const u=new Map;for(const t of c.values()){const e=t.ns||p;u.has(e)||u.set(e,[]),u.get(e).push(t)}const g=[];for(const n of i.locales)for(const[c,p]of u.entries()){const u=s(i.extract.output,n,c),m=e(process.cwd(),u);let x="",y={};try{x=await t(m,"utf-8"),y=JSON.parse(x)}catch(t){}const d={},h=a(y,l);for(const t of h)if(f.some(e=>e.test(t))){const e=r(y,t,l);o(d,t,e,l)}const L=!1===i.extract.sort?p:p.sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:e}of L){const a=r(y,t,l)??(n===i.extract?.primaryLanguage?e:"");o(d,t,a,l)}const w=i.extract.indentation??2,k=JSON.stringify(d,null,w);g.push({path:m,updated:k!==x,newTranslations:d,existingTranslations:y})}return g}export{c as getTranslations};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{extractFromTransComponent as e}from"./jsx-parser.js";class t{pluginContext;config;logger;scopeStack=[];constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;"ObjectExpression"===r?.type&&(s=this.getObjectPropValue(r,"keyPrefix")),this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=(this.config.extract.functions||[]).includes(t.value),i=this.getVarFromScope(t.value);if(!n&&!(void 0!==i))return;if(0===e.arguments.length)return;const r=e.arguments[0].expression;let s,a=null;if("StringLiteral"===r.type?a=r.value:"ArrowFunctionExpression"===r.type&&(a=this.extractKeyFromSelector(r)),!a)return;let o=a;const l=e.arguments.length>1?e.arguments[1].expression:void 0;"ObjectExpression"===l?.type&&(s=this.getObjectPropValue(l,"ns")),!s&&i?.defaultNs&&(s=i.defaultNs);const p=this.config.extract.nsSeparator??":",u=this.config.extract.contextSeparator??"_";if(!s&&p&&a.includes(p)){const e=a.split(p);s=e.shift(),a=e.join(p),o=a}if(s||(s=this.config.extract.defaultNS),i?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${i.keyPrefix}${e}${a}`}const c="StringLiteral"===r.type?this.getDefaultValue(e,a):a;if("ObjectExpression"===l?.type){const e=this.getObjectPropValue(l,"context");if(e)return void this.pluginContext.addKey({key:`${o}${u}${e}`,ns:s,defaultValue:c});if(void 0!==this.getObjectPropValue(l,"count"))return void this.handlePluralKeys(o,c,s)}this.pluginContext.addKey({key:o,ns:s,defaultValue:c})}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}getDefaultValue(e,t){if(e.arguments.length<=1)return t;const n=e.arguments[1].expression;return"StringLiteral"===n.type?n.value||t:"ObjectExpression"===n.type&&this.getObjectPropValue(n,"defaultValue")||t}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}n.ns||(n.ns=this.config.extract.defaultNS),n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}}export{t as ASTVisitors};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,s,c,o){const r=s.map(e=>e.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join("|"),l=new RegExp(`(?:${r})\\s*\\(\\s*(['"])([^'"]+)\\1`,"g"),u=function(e){const t=[],n=new Set,s=/\/\/(.*)|\/\*([\s\S]*?)\*\//g;let c;for(;null!==(c=s.exec(e));){const e=(c[1]??c[2]).trim();e&&!n.has(e)&&(n.add(e),t.push(e))}return t}(e);for(const e of u){let s;for(;null!==(s=l.exec(e));){let r,l=s[2];const u=e.slice(s.index+s[0].length),i=t(u);r=n(u);const f=o.extract.nsSeparator??":";if(!r&&f&&l.includes(f)){const e=l.split(f);r=e.shift(),l=e.join(f)}r||(r=o.extract.defaultNS),c.addKey({key:l,ns:r,defaultValue:i??l})}}}function t(e){const t=/^\s*,\s*(['"])(.*?)\1/.exec(e);if(t)return t[2];const n=/^\s*,\s*\{[^}]*defaultValue\s*:\s*(['"])(.*?)\1/.exec(e);return n?n[2]:void 0}function n(e){const t=/^\s*,\s*\{[^}]*ns\s*:\s*(['"])(.*?)\1/.exec(e);if(t)return t[2]}export{e as extractKeysFromComments};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,n){const i=e.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),r=e.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),a=e.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),l=!!a;let u;if(u="JSXAttribute"===i?.type&&"StringLiteral"===i.value?.type?i.value.value:t(e.children,n),!u)return null;const p=e.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value),s="JSXAttribute"===p?.type&&"StringLiteral"===p.value?.type?p.value.value:void 0;let o=n.extract.defaultValue||"";return o="JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type?r.value.value:t(e.children,n),{key:u,ns:s,defaultValue:o||u,hasCount:l}}function t(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);return function e(t){let i="";return t.forEach((t,r)=>{if("JSXText"===t.type)i+=t.value;else if("JSXExpressionContainer"===t.type){const e=t.expression;if("StringLiteral"===e.type)i+=e.value;else if("Identifier"===e.type)i+=`{{${e.value}}}`;else if("ObjectExpression"===e.type){const t=e.properties[0];t&&"Identifier"===t.type&&(i+=`{{${t.value}}}`)}}else if("JSXElement"===t.type){let a;"Identifier"===t.opening.name.type&&(a=t.opening.name.value);const l=e(t.children);a&&n.has(a)?i+=`<${a}>${l}</${a}>`:i+=`<${r}>${l}</${r}>`}else"JSXFragment"===t.type&&(i+=e(t.children))}),i}(e).trim().replace(/\s{2,}/g," ")}export{e as extractFromTransComponent};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async function t(t){for(const n of t)await(n.setup?.())}function n(t){return{addKey:n=>{const a=`${n.ns??"translation"}:${n.key}`;if(!t.has(a)){const e=n.defaultValue??n.key;t.set(a,{...n,defaultValue:e})}}}}export{n as createPluginContext,t as initializePlugins};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{glob as e}from"glob";import{readdir as s}from"node:fs/promises";import{dirname as n,join as o}from"node:path";const t=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json"];async function l(){for(const l of t){const t=await e(l,{ignore:"node_modules/**"});if(t.length>0){const e=t[0],l=n(n(e));try{let e=(await s(l)).filter(e=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(e));if(e.length>0)return e.sort(),e.includes("dev")&&(e=["dev",...e.filter(e=>"dev"!==e)]),e.includes("en")&&(e=["en",...e.filter(e=>"en"!==e)]),{locales:e,extract:{input:["src/**/*.{js,jsx,ts,tsx}"],output:o(l,"{{language}}","{{namespace}}.json"),primaryLanguage:e.includes("en")?"en":e[0]}}}catch{continue}}}return null}export{l as detectConfig};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{defineConfig}from"./config.js";export{extract}from"./extractor/core/extractor.js";export{findKeys}from"./extractor/core/key-finder.js";export{getTranslations}from"./extractor/core/translation-manager.js";
|
package/dist/esm/init.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"inquirer";import{writeFile as t,readFile as n}from"node:fs/promises";import{resolve as i}from"node:path";async function o(){console.log("Welcome to the i18next-cli setup wizard!");const o=await e.prompt([{type:"list",name:"fileType",message:"What kind of configuration file do you want?",choices:["TypeScript (i18next.config.ts)","JavaScript (i18next.config.js)"]},{type:"input",name:"locales",message:"What locales does your project support? (comma-separated)",default:"en,de,fr",filter:e=>e.split(",").map(e=>e.trim())},{type:"input",name:"input",message:"What is the glob pattern for your source files?",default:"src/**/*.{js,jsx,ts,tsx}"},{type:"input",name:"output",message:"What is the path for your output resource files?",default:"public/locales/{{language}}/{{namespace}}.json"}]),r=o.fileType.includes("TypeScript"),s=await async function(){try{const e=i(process.cwd(),"package.json"),t=await n(e,"utf-8");return"module"===JSON.parse(t).type}catch{return!0}}(),a=r?"i18next.config.ts":"i18next.config.js",p={locales:o.locales,extract:{input:o.input,output:o.output}};function f(e,t=2,n=0){const i=e=>" ".repeat(e*t),o=i(n),r=i(n+1);if(null===e||"number"==typeof e||"boolean"==typeof e)return JSON.stringify(e);if("string"==typeof e)return JSON.stringify(e);if(Array.isArray(e)){if(0===e.length)return"[]";return`[\n${e.map(e=>`${r}${f(e,t,n+1)}`).join(",\n")}\n${o}]`}if("object"==typeof e){const i=Object.keys(e);if(0===i.length)return"{}";return`{\n${i.map(i=>{const o=/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(i)?i:JSON.stringify(i);return`${r}${o}: ${f(e[i],t,n+1)}`}).join(",\n")}\n${o}}`}return JSON.stringify(e)}let c="";c=r?`import { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${f(p)});`:s?`import { defineConfig } from 'i18next-cli';\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nexport default defineConfig(${f(p)});`:`const { defineConfig } = require('i18next-cli');\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nmodule.exports = defineConfig(${f(p)});`;const u=i(process.cwd(),a);await t(u,c.trim()),console.log(`✅ Configuration file created at: ${u}`)}export{o as runInit};
|
|
@@ -0,0 +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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execa as e}from"execa";import o from"chalk";import t from"ora";import n from"inquirer";import{resolve as i}from"node:path";function s(e,o={},t={}){const n=[];if("sync"===e){(o.updateValues??t.updateValues)&&n.push("--update-values","true");(o.srcLngOnly??t.sourceLanguageOnly)&&n.push("--reference-language-only","true");(o.compareMtime??t.compareModificationTime)&&n.push("--compare-modification-time","true");(o.dryRun??t.dryRun)&&n.push("--dry","true")}return n}async function r(r,c,a={}){await async function(){try{await e("locize",["--version"])}catch(e){"ENOENT"===e.code&&(console.error(o.red("Error: `locize-cli` command not found.")),console.log(o.yellow("Please install it globally to use the locize integration:")),console.log(o.cyan("npm install -g locize-cli")),process.exit(1))}}();const l=t(`Running 'locize ${r}'...\n`).start(),u=c.locize||{},{projectId:p,apiKey:d,version:y}=u;let g=[r];p&&g.push("--project-id",p),d&&g.push("--api-key",d),y&&g.push("--ver",y),g.push(...s(r,a,u));const m=i(process.cwd(),c.extract.output.split("/{{language}}/")[0]);g.push("--path",m);try{console.log(o.cyan(`\nRunning 'locize ${g.join(" ")}'...`));const t=await e("locize",g,{stdio:"pipe"});l.succeed(o.green(`'locize ${r}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(t){const i=t.stderr||"";if(i.includes("missing required argument")){const t=await async function(e){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const t=await n.prompt([{type:"input",name:"projectId",message:"What is your locize Project ID? (Find this in your project settings on www.locize.app)",validate:e=>!!e||"Project ID cannot be empty."},{type:"password",name:"apiKey",message:'What is your locize API key? (Create or use one in your project settings > "API Keys")',validate:e=>!!e||"API Key cannot be empty."},{type:"input",name:"version",message:"What version do you want to sync with?",default:"latest"}]);if(!t.projectId)return void console.error(o.red("Project ID is required to continue."));e.locize={projectId:t.projectId,apiKey:t.apiKey,version:t.version};const{save:i}=await n.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(i){const e=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${t.apiKey}\n`,n=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${t.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${t.version}',\n },`;console.log(o.cyan("\nGreat! For the best security, we recommend using environment variables for your API key.")),console.log(o.bold("\nRecommended approach (.env file):")),console.log(o.green(e)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(n))}return e.locize}(c);if(t){g=[r],t.projectId&&g.push("--project-id",t.projectId),t.apiKey&&g.push("--api-key",t.apiKey),t.version&&g.push("--ver",t.version),g.push(...s(r,a,u)),g.push("--path",m);try{l.start("Retrying with new credentials...");const t=await e("locize",g,{stdio:"pipe"});l.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){l.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else l.fail("Operation cancelled."),process.exit(1)}else l.fail(o.red(`Error executing 'locize ${r}'.`)),console.error(i||t.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${r}' completed successfully.`))}const c=(e,o)=>r("sync",e,o),a=(e,o)=>r("download",e,o),l=(e,o)=>r("migrate",e,o);export{a as runLocizeDownload,l as runLocizeMigrate,c as runLocizeSync};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolve as e}from"node:path";import{access as o,writeFile as t}from"node:fs/promises";import{pathToFileURL as n}from"node:url";const a=e(process.cwd(),"i18next-parser.config.js"),r=e(process.cwd(),"i18next.config.ts"),s=["i18next.config.ts","i18next.config.js","i18next.config.mjs","i18next.config.cjs"];async function i(){console.log("Attempting to migrate legacy i18next-parser.config.js...");try{await o(a)}catch(e){return void console.log("No i18next-parser.config.js found. Nothing to migrate.")}try{await o(a)}catch(e){return void console.log("No i18next-parser.config.js found. Nothing to migrate.")}for(const t of s)try{const n=e(process.cwd(),t);return await o(n),void console.warn(`Warning: A new configuration file already exists at "${t}". Migration skipped to avoid overwriting.`)}catch(e){}const i=n(a).href,c=(await import(i)).default;if(!c)return void console.error("Could not read the legacy config file.");const l={locales:c.locales||["en"],extract:{input:c.input||"src/**/*.{js,jsx,ts,tsx}",output:(c.output||"locales/$LOCALE/$NAMESPACE.json").replace("$LOCALE","{{language}}").replace("$NAMESPACE","{{namespace}}"),defaultNS:c.defaultNamespace||"translation",keySeparator:c.keySeparator,nsSeparator:c.namespaceSeparator,contextSeparator:c.contextSeparator,functions:c.lexers?.js?.functions||["t"],transComponents:c.lexers?.js?.components||["Trans"]},typesafe:{input:"locales/{{language}}/{{namespace}}.json",output:"src/types/i18next.d.ts"},sync:{primaryLanguage:c.locales?.[0]||"en",secondaryLanguages:c.locales.filter(e=>e!==(c.locales?.[0]||"en"))}},p=`\nimport { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${JSON.stringify(l,null,2)});\n`;await t(r,p.trim()),console.log("✅ Success! Migration complete."),console.log(`New configuration file created at: ${r}`),console.warn('\nPlease review the generated file and adjust paths for "typesafe.input" if necessary.'),c.keepRemoved&&console.warn('Warning: The "keepRemoved" option is deprecated. Consider using the "preservePatterns" feature for dynamic keys.')}export{i as runMigrator};
|
|
@@ -0,0 +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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFile as t,mkdir as o,writeFile as e}from"node:fs/promises";import{resolve as r,dirname as n}from"path";import a from"chalk";import c from"ora";import{getNestedKeys as l,getNestedValue as s,setNestedValue as i}from"./utils/nested-object.js";import{getOutputPath as f}from"./utils/file-utils.js";async function u(u){const p=c("Running i18next locale synchronizer...\n").start();u.extract.primaryLanguage||=u.locales[0]||"en";const{primaryLanguage:y}=u.extract,d=u.locales.filter(t=>t!==y),m=u.extract.keySeparator??".",g=[];let h=!1;const x=u.extract.defaultNS??"translation",w=f(u.extract.output,y,x),S=r(process.cwd(),w);let $;try{const o=await t(S,"utf-8");$=JSON.parse(o)}catch(t){return void console.error(`Primary language file not found at ${w}. Cannot sync.`)}const b=l($,m);for(const c of d){const l=f(u.extract.output,c,x),p=r(process.cwd(),l);let y={},d="";try{d=await t(p,"utf-8"),y=JSON.parse(d)}catch(t){}const w={};for(const t of b){const o=s(y,t,m)??(u.extract?.defaultValue||"");i(w,t,o,m)}const S=u.extract.indentation??2,$=JSON.stringify(w,null,S);$!==d?(h=!0,await o(n(p),{recursive:!0}),await e(p,$),g.push(` ${a.green("✓")} Synchronized: ${l}`)):g.push(` ${a.gray("-")} Already in sync: ${l}`)}p.succeed(a.bold("Synchronization complete!")),g.forEach(t=>console.log(t)),h?(console.log(a.green.bold("\n✅ Sync complete.")),console.log(a.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${a.cyan("npx i18next-cli locize-migrate")}`)):console.log(a.green.bold("\n✅ All locales are already in sync."))}export{u as runSyncer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{mergeResourcesAsInterface as e}from"i18next-resources-for-ts";import{glob as t}from"glob";import s from"ora";import o from"chalk";import{readFile as r,mkdir as i,writeFile as n,access as p}from"node:fs/promises";import{join as c,dirname as a,basename as u,extname as l,resolve as y,relative as f}from"node:path";async function d(d){const m=s("Generating TypeScript types for translations...\n").start();try{if(d.types||(d.types={input:["locales/en/*.json"],output:"src/types/i18next.d.ts"}),void 0===d.types.input&&(d.types.input=["locales/en/*.json"]),d.types.output||(d.types.output="src/types/i18next.d.ts"),d.types.resourcesFile||(d.types.resourcesFile=c(a(d.types?.output),"resources.d.ts")),!d.types?.input||d.types?.input.length<0)return void console.log("No input defined!");const s=await t(d.types?.input||[],{cwd:process.cwd()}),w=[];for(const e of s){const t=u(e,l(e)),s=await r(e,"utf-8"),o=JSON.parse(s);w.push({name:t,resources:o})}const g=[],h=d.types?.enableSelector||!1,S=e(w,{optimize:!!h}),$=y(process.cwd(),d.types?.output||""),x=y(process.cwd(),d.types.resourcesFile);let T;await i(a(x),{recursive:!0}),await n(x,S),g.push(` ${o.green("✓")} Resources interface written to ${d.types.resourcesFile}`);try{await p($),T=!0}catch(e){T=!1}if(!T){const e=`// This file is automatically generated by i18next-cli. Do not edit manually.\nimport Resources from './${f(a($),x).replace(/\\/g,"/").replace(/\.d\.ts$/,"")}';\n\ndeclare module 'i18next' {\n interface CustomTypeOptions {\n enableSelector: ${"string"==typeof h?`"${h}"`:h};\n defaultNS: '${d.extract.defaultNS||"translation"}';\n resources: Resources;\n }\n}`;await i(a($),{recursive:!0}),await n($,e),g.push(` ${o.green("✓")} TypeScript definitions written to ${d.types.output||""}`),m.succeed(o.bold("TypeScript definitions generated successfully.")),g.forEach(e=>console.log(e))}}catch(e){m.fail(o.red("Failed to generate TypeScript definitions.")),console.error(e)}}export{d as runTypesGenerator};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"node:fs/promises";import"node:path";function e(e,p,r){return e.replace("{{language}}",p).replace("{{lng}}",p).replace("{{namespace}}",r).replace("{{ns}}",r)}export{e as getOutputPath};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class o{info(o){console.log(o)}warn(o){console.warn(o)}error(o){console.error(o)}}export{o as ConsoleLogger};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,t,r,n){if(!1===n)return void(e[t]=r);const u=t.split(n);u.reduce((e,t,n)=>(n===u.length-1?e[t]=r:e[t]=e[t]||{},e[t]),e)}function t(e,t,r){return!1===r?e[t]:t.split(r).reduce((e,t)=>e&&e[t],e)}function r(e,t,n=""){return!1===t?Object.keys(e):Object.entries(e).reduce((e,[u,c])=>{const i=n?`${n}${t}${u}`:u;return"object"!=typeof c||null===c||Array.isArray(c)?e.push(i):e.push(...r(c,t,i)),e},[])}export{r as getNestedKeys,t as getNestedValue,e as setNestedValue};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function t(t){if(!t.extract.input?.length)throw new e("extract.input must be specified and non-empty");if(!t.extract.output)throw new e("extract.output must be specified");if(!t.locales?.length)throw new e("locales must be specified and non-empty");if(!t.extract.output.includes("{{language}}")&&!t.extract.output.includes("{{lng}}"))throw new e("extract.output must contain {{language}} placeholder")}class e extends Error{file;cause;constructor(t,e,n){super(e?`${t} in file ${e}`:t),this.file=e,this.cause=n,this.name="ExtractorError"}}export{e as ExtractorError,t as validateExtractorConfig};
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "i18next-cli",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A unified, high-performance i18next CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"i18next-cli": "./dist/esm/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/cjs/index.js",
|
|
10
|
+
"module": "./dist/esm/index.js",
|
|
11
|
+
"types": "./types/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./types/index.d.ts",
|
|
16
|
+
"require": "./dist/cjs/index.js",
|
|
17
|
+
"default": "./dist/esm/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./cjs": {
|
|
20
|
+
"types": "./types/index.d.ts",
|
|
21
|
+
"default": "./dist/cjs/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./esm": {
|
|
24
|
+
"types": "./types/index.d.ts",
|
|
25
|
+
"default": "./dist/esm/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "npm run update-cli-meta && rm -rf dist && rollup -c && chmod +x dist/esm/cli.js && chmod +x dist/cjs/cli.js && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && rm -rf types && tsc -d --declarationDir types --declarationMap --emitDeclarationOnly",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"test": "npm run lint && vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"test:coverage": "vitest run --coverage",
|
|
34
|
+
"update-cli-meta": "node ./scripts/updateCliMeta.js",
|
|
35
|
+
"preversion": "npm run test && npm run build",
|
|
36
|
+
"postversion": "npm run build && git add . && git commit -m 'build' && git push && git push --tags"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"i18next",
|
|
40
|
+
"swc",
|
|
41
|
+
"cli"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/i18next/i18next-cli.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/i18next/i18next-cli",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/i18next/i18next-cli/issues"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@rollup/plugin-terser": "0.4.4",
|
|
54
|
+
"@swc/core": "1.13.5",
|
|
55
|
+
"@types/inquirer": "9.0.9",
|
|
56
|
+
"@types/node": "20.19.17",
|
|
57
|
+
"@types/react": "19.1.13",
|
|
58
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
59
|
+
"eslint": "9.36.0",
|
|
60
|
+
"eslint-plugin-import": "2.32.0",
|
|
61
|
+
"glob": "^10.3.12",
|
|
62
|
+
"i18next-resources-for-ts": "^1.6.0",
|
|
63
|
+
"memfs": "4.43.0",
|
|
64
|
+
"neostandard": "0.12.2",
|
|
65
|
+
"rollup-plugin-typescript2": "0.36.0",
|
|
66
|
+
"swc-walk": "^1.0.0-rc.3",
|
|
67
|
+
"ts-node": "^10.9.2",
|
|
68
|
+
"typescript": "^5.4.5",
|
|
69
|
+
"unplugin-swc": "1.5.7",
|
|
70
|
+
"vitest": "3.2.4"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"chalk": "5.6.2",
|
|
74
|
+
"chokidar": "4.0.3",
|
|
75
|
+
"commander": "14.0.1",
|
|
76
|
+
"execa": "9.6.0",
|
|
77
|
+
"inquirer": "12.9.6",
|
|
78
|
+
"jiti": "2.6.0",
|
|
79
|
+
"ora": "9.0.0"
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
import chokidar from 'chokidar'
|
|
5
|
+
import { glob } from 'glob'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import { loadConfig, ensureConfig } from './config'
|
|
8
|
+
import { detectConfig } from './heuristic-config'
|
|
9
|
+
import { runExtractor } from './extractor'
|
|
10
|
+
import { runTypesGenerator } from './types-generator'
|
|
11
|
+
import { runSyncer } from './syncer'
|
|
12
|
+
import { runMigrator } from './migrator'
|
|
13
|
+
import { runInit } from './init'
|
|
14
|
+
import { runLinter } from './linter'
|
|
15
|
+
import { runStatus } from './status'
|
|
16
|
+
import { runLocizeSync, runLocizeDownload, runLocizeMigrate } from './locize'
|
|
17
|
+
import type { I18nextToolkitConfig } from './types'
|
|
18
|
+
|
|
19
|
+
const program = new Command()
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('i18next-cli')
|
|
23
|
+
.description('A unified, high-performance i18next CLI.')
|
|
24
|
+
.version('0.9.0')
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('extract')
|
|
28
|
+
.description('Extract translation keys from source files and update resource files.')
|
|
29
|
+
.option('-w, --watch', 'Watch for file changes and re-run the extractor.')
|
|
30
|
+
.option('--ci', 'Exit with a non-zero status code if any files are updated.')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
const config = await ensureConfig()
|
|
33
|
+
|
|
34
|
+
const run = async () => {
|
|
35
|
+
const filesWereUpdated = await runExtractor(config)
|
|
36
|
+
if (options.ci && filesWereUpdated) {
|
|
37
|
+
console.error(chalk.red.bold('\n[CI Mode] Error: Translation files were updated. Please commit the changes.'))
|
|
38
|
+
console.log(chalk.yellow('💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started'))
|
|
39
|
+
console.log(` Learn more: ${chalk.cyan('npx i18next-cli locize-sync')}`)
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await run()
|
|
44
|
+
|
|
45
|
+
if (options.watch) {
|
|
46
|
+
console.log('\nWatching for changes...')
|
|
47
|
+
const watcher = chokidar.watch(await glob(config.extract.input), {
|
|
48
|
+
ignored: /node_modules/,
|
|
49
|
+
persistent: true,
|
|
50
|
+
})
|
|
51
|
+
watcher.on('change', path => {
|
|
52
|
+
console.log(`\nFile changed: ${path}`)
|
|
53
|
+
run()
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('status [locale]')
|
|
60
|
+
.description('Display translation status. Provide a locale for a detailed key-by-key view.')
|
|
61
|
+
.action(async (locale) => {
|
|
62
|
+
let config = await loadConfig()
|
|
63
|
+
if (!config) {
|
|
64
|
+
console.log(chalk.blue('No config file found. Attempting to detect project structure...'))
|
|
65
|
+
const detected = await detectConfig()
|
|
66
|
+
if (!detected) {
|
|
67
|
+
console.error(chalk.red('Could not automatically detect your project structure.'))
|
|
68
|
+
console.log(`Please create a config file first by running: ${chalk.cyan('npx i18next-cli init')}`)
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
console.log(chalk.green('Project structure detected successfully!'))
|
|
72
|
+
config = detected as I18nextToolkitConfig
|
|
73
|
+
}
|
|
74
|
+
await runStatus(config, { detail: locale })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command('types')
|
|
79
|
+
.description('Generate TypeScript definitions from translation resource files.')
|
|
80
|
+
.option('-w, --watch', 'Watch for file changes and re-run the type generator.')
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
const config = await ensureConfig()
|
|
83
|
+
|
|
84
|
+
const run = () => runTypesGenerator(config)
|
|
85
|
+
await run()
|
|
86
|
+
|
|
87
|
+
if (options.watch) {
|
|
88
|
+
console.log('\nWatching for changes...')
|
|
89
|
+
const watcher = chokidar.watch(await glob(config.types?.input || []), {
|
|
90
|
+
persistent: true,
|
|
91
|
+
})
|
|
92
|
+
watcher.on('change', path => {
|
|
93
|
+
console.log(`\nFile changed: ${path}`)
|
|
94
|
+
run()
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
program
|
|
100
|
+
.command('sync')
|
|
101
|
+
.description('Synchronize secondary language files with the primary language file.')
|
|
102
|
+
.action(async () => {
|
|
103
|
+
const config = await ensureConfig()
|
|
104
|
+
await runSyncer(config)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('migrate-config')
|
|
109
|
+
.description('Migrate a legacy i18next-parser.config.js to the new format.')
|
|
110
|
+
.action(async () => {
|
|
111
|
+
await runMigrator()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('init')
|
|
116
|
+
.description('Create a new i18next.config.ts/js file with an interactive setup wizard.')
|
|
117
|
+
.action(runInit)
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command('lint')
|
|
121
|
+
.description('Find potential issues like hardcoded strings in your codebase.')
|
|
122
|
+
.action(async () => {
|
|
123
|
+
let config = await loadConfig()
|
|
124
|
+
if (!config) {
|
|
125
|
+
console.log(chalk.blue('No config file found. Attempting to detect project structure...'))
|
|
126
|
+
const detected = await detectConfig()
|
|
127
|
+
if (!detected) {
|
|
128
|
+
console.error(chalk.red('Could not automatically detect your project structure.'))
|
|
129
|
+
console.log(`Please create a config file first by running: ${chalk.cyan('npx i1e-toolkit init')}`)
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
console.log(chalk.green('Project structure detected successfully!'))
|
|
133
|
+
config = detected as I18nextToolkitConfig
|
|
134
|
+
}
|
|
135
|
+
await runLinter(config)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command('locize-sync')
|
|
140
|
+
.description('Synchronize local translations with your locize project.')
|
|
141
|
+
.option('--update-values', 'Update values of existing translations on locize.')
|
|
142
|
+
.option('--src-lng-only', 'Check for changes in source language only.')
|
|
143
|
+
.option('--compare-mtime', 'Compare modification times when syncing.')
|
|
144
|
+
.option('--dry-run', 'Run the command without making any changes.')
|
|
145
|
+
.action(async (options) => {
|
|
146
|
+
const config = await ensureConfig()
|
|
147
|
+
await runLocizeSync(config, options)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
program
|
|
151
|
+
.command('locize-download')
|
|
152
|
+
.description('Download all translations from your locize project.')
|
|
153
|
+
.action(async (options) => {
|
|
154
|
+
const config = await ensureConfig()
|
|
155
|
+
await runLocizeDownload(config, options)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
program
|
|
159
|
+
.command('locize-migrate')
|
|
160
|
+
.description('Migrate local translation files to a new locize project.')
|
|
161
|
+
.action(async (options) => {
|
|
162
|
+
const config = await ensureConfig()
|
|
163
|
+
await runLocizeMigrate(config, options)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
program.parse(process.argv)
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { access } from 'node:fs/promises'
|
|
4
|
+
import { createJiti } from 'jiti'
|
|
5
|
+
import inquirer from 'inquirer'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import type { I18nextToolkitConfig } from './types'
|
|
8
|
+
import { runInit } from './init'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* List of supported configuration file names in order of precedence
|
|
12
|
+
*/
|
|
13
|
+
const CONFIG_FILES = [
|
|
14
|
+
'i18next.config.ts',
|
|
15
|
+
'i18next.config.js',
|
|
16
|
+
'i18next.config.mjs',
|
|
17
|
+
'i18next.config.cjs',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A helper function for defining the i18next-cli config with type-safety.
|
|
22
|
+
*
|
|
23
|
+
* @param config - The configuration object to define
|
|
24
|
+
* @returns The same configuration object with type safety
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* export default defineConfig({
|
|
29
|
+
* locales: ['en', 'de'],
|
|
30
|
+
* extract: {
|
|
31
|
+
* input: 'src',
|
|
32
|
+
* output: 'locales/{{language}}/{{namespace}}.json'
|
|
33
|
+
* }
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function defineConfig (config: I18nextToolkitConfig): I18nextToolkitConfig {
|
|
38
|
+
return config
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Helper function to find the first existing config file in the current working directory.
|
|
43
|
+
* Searches for files in the order defined by CONFIG_FILES.
|
|
44
|
+
*
|
|
45
|
+
* @returns Promise that resolves to the full path of the found config file, or null if none found
|
|
46
|
+
*/
|
|
47
|
+
async function findConfigFile (): Promise<string | null> {
|
|
48
|
+
for (const file of CONFIG_FILES) {
|
|
49
|
+
const fullPath = resolve(process.cwd(), file)
|
|
50
|
+
try {
|
|
51
|
+
await access(fullPath)
|
|
52
|
+
return fullPath
|
|
53
|
+
} catch {
|
|
54
|
+
// File doesn't exist, continue to the next one
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Loads and validates the i18next toolkit configuration from the project root.
|
|
62
|
+
*
|
|
63
|
+
* This function:
|
|
64
|
+
* 1. Searches for a config file using findConfigFile()
|
|
65
|
+
* 2. Dynamically imports the config file using ESM import()
|
|
66
|
+
* 3. Validates the configuration structure
|
|
67
|
+
* 4. Sets default values for sync options
|
|
68
|
+
* 5. Adds cache busting for watch mode
|
|
69
|
+
*
|
|
70
|
+
* @returns Promise that resolves to the loaded configuration object, or null if loading failed
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const config = await loadConfig()
|
|
75
|
+
* if (!config) {
|
|
76
|
+
* console.error('Failed to load configuration')
|
|
77
|
+
* process.exit(1)
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export async function loadConfig (): Promise<I18nextToolkitConfig | null> {
|
|
82
|
+
const configPath = await findConfigFile()
|
|
83
|
+
|
|
84
|
+
if (!configPath) {
|
|
85
|
+
// QUIETLY RETURN NULL: The caller will handle the "not found" case.
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
let config: any
|
|
91
|
+
|
|
92
|
+
// Use jiti for TypeScript files, native import for JavaScript
|
|
93
|
+
if (configPath.endsWith('.ts')) {
|
|
94
|
+
const jiti = createJiti(import.meta.url)
|
|
95
|
+
const configModule = await jiti.import(configPath, { default: true })
|
|
96
|
+
config = configModule
|
|
97
|
+
} else {
|
|
98
|
+
const configUrl = pathToFileURL(configPath).href
|
|
99
|
+
const configModule = await import(`${configUrl}?t=${Date.now()}`)
|
|
100
|
+
config = configModule.default
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!config) {
|
|
104
|
+
console.error(`Error: No default export found in ${configPath}`)
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Set default sync options
|
|
109
|
+
config.extract ||= {}
|
|
110
|
+
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
111
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config.extract.primaryLanguage)
|
|
112
|
+
|
|
113
|
+
return config
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`Error loading configuration from ${configPath}`)
|
|
116
|
+
console.error(error)
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* NEW: Ensures a configuration exists, prompting the user to create one if necessary.
|
|
123
|
+
* This function is a wrapper around loadConfig that provides an interactive fallback.
|
|
124
|
+
*
|
|
125
|
+
* @returns A promise that resolves to a valid configuration object.
|
|
126
|
+
* @throws Exits the process if the user declines to create a config or if loading fails after creation.
|
|
127
|
+
*/
|
|
128
|
+
export async function ensureConfig (): Promise<I18nextToolkitConfig> {
|
|
129
|
+
let config = await loadConfig()
|
|
130
|
+
|
|
131
|
+
if (config) {
|
|
132
|
+
return config
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// No config found, so we prompt the user.
|
|
136
|
+
const { shouldInit } = await inquirer.prompt([{
|
|
137
|
+
type: 'confirm',
|
|
138
|
+
name: 'shouldInit',
|
|
139
|
+
message: chalk.yellow('Configuration file not found. Would you like to create one now?'),
|
|
140
|
+
default: true,
|
|
141
|
+
}])
|
|
142
|
+
|
|
143
|
+
if (shouldInit) {
|
|
144
|
+
await runInit() // Run the interactive setup wizard
|
|
145
|
+
console.log(chalk.green('Configuration created. Resuming command...'))
|
|
146
|
+
config = await loadConfig() // Try loading the newly created config
|
|
147
|
+
|
|
148
|
+
if (config) {
|
|
149
|
+
return config
|
|
150
|
+
} else {
|
|
151
|
+
console.error(chalk.red('Error: Failed to load configuration after creation. Please try running the command again.'))
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
console.log('Operation cancelled. Please create a configuration file to proceed.')
|
|
156
|
+
process.exit(0)
|
|
157
|
+
}
|
|
158
|
+
}
|