i18next-cli 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,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.17.1](https://github.com/i18next/i18next-cli/compare/v1.17.0...v1.17.q) - 2025-10-29
9
+
10
+ - fix: regression for windows introduced in v1.17.0
11
+
12
+ ## [1.17.0](https://github.com/i18next/i18next-cli/compare/v1.16.0...v1.17.0) - 2025-10-29
13
+
14
+ - feat: support functional `extract.output` for per-package paths [#80](https://github.com/i18next/i18next-cli/issues/80)
15
+
8
16
  ## [1.16.0](https://github.com/i18next/i18next-cli/compare/v1.15.0...v1.16.0) - 2025-10-28
9
17
 
10
18
  - feat(extractor): better support for custom translation hooks returning t/getFixedT [#78](https://github.com/i18next/i18next-cli/issues/78)
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("minimatch"),i=require("chalk"),a=require("./config.js"),r=require("./heuristic-config.js"),c=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var s=require("./types-generator.js"),l=require("./syncer.js"),u=require("./migrator.js"),g=require("./init.js"),d=require("./linter.js"),p=require("./status.js"),f=require("./locize.js");const m=new e.Command;m.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.16.0"),m.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),m.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").option("--sync-primary","Sync primary language values with default values from code.").action(async e=>{try{const o=m.opts().config,i=await a.ensureConfig(o),r=async()=>{const t=await c.runExtractor(i,{isWatchMode:!!e.watch,isDryRun:!!e.dryRun,syncPrimaryWithDefaults:!!e.syncPrimary});return e.ci&&!t?(console.log("✅ No files were updated."),process.exit(0)):e.ci&&t&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),t};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.extract.input),o=y(i.extract.ignore),a=h(i.extract.output),c=[...o,...a].filter(Boolean),s=e.filter(e=>!c.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}catch(e){console.error("Error running extractor:",e),process.exit(1)}}),m.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)=>{const o=m.opts().config;let n=await a.loadConfig(o);if(!n){console.log(i.blue("No config file found. Attempting to detect project structure..."));const e=await r.detectConfig();e||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),n=e}await p.runStatus(n,{detail:e,namespace:t.namespace})}),m.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const o=m.opts().config,i=await a.ensureConfig(o),r=()=>s.runTypesGenerator(i);if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.types?.input||[]),o=[...y(i.extract?.ignore)].filter(Boolean),a=e.filter(e=>!o.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}),m.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=m.opts().config,t=await a.ensureConfig(e);await l.runSyncer(t)}),m.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async e=>{await u.runMigrator(e)}),m.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(g.runInit),m.command("lint").description("Find potential issues like hardcoded strings in your codebase.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async e=>{const o=m.opts().config,c=async()=>{let e=await a.loadConfig(o);if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await r.detectConfig();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 i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=t}await d.runLinter(e)};if(await c(),e.watch){console.log("\nWatching for changes...");const e=await a.loadConfig(o);if(e?.extract?.input){const o=await w(e.extract.input),i=[...y(e.extract.ignore),...h(e.extract.output)].filter(Boolean),a=o.filter(e=>!i.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}}),m.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=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeSync(o,e)}),m.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeDownload(o,e)}),m.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeMigrate(o,e)}),m.parse(process.argv);const y=e=>Array.isArray(e)?e:e?[e]:[],h=e=>e&&"string"==typeof e?[e.replace(/\{\{[^}]+\}\}/g,"*")]:[],w=async(e=[])=>{const t=y(e),n=await Promise.all(t.map(e=>o.glob(e||"",{nodir:!0})));return Array.from(new Set(n.flat()))};
2
+ "use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("minimatch"),i=require("chalk"),a=require("./config.js"),r=require("./heuristic-config.js"),c=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var s=require("./types-generator.js"),l=require("./syncer.js"),u=require("./migrator.js"),g=require("./init.js"),d=require("./linter.js"),p=require("./status.js"),f=require("./locize.js");const m=new e.Command;m.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.17.1"),m.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),m.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").option("--sync-primary","Sync primary language values with default values from code.").action(async e=>{try{const o=m.opts().config,i=await a.ensureConfig(o),r=async()=>{const t=await c.runExtractor(i,{isWatchMode:!!e.watch,isDryRun:!!e.dryRun,syncPrimaryWithDefaults:!!e.syncPrimary});return e.ci&&!t?(console.log("✅ No files were updated."),process.exit(0)):e.ci&&t&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),t};if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.extract.input),o=y(i.extract.ignore),a=h(i.extract.output),c=[...o,...a].filter(Boolean),s=e.filter(e=>!c.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}catch(e){console.error("Error running extractor:",e),process.exit(1)}}),m.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)=>{const o=m.opts().config;let n=await a.loadConfig(o);if(!n){console.log(i.blue("No config file found. Attempting to detect project structure..."));const e=await r.detectConfig();e||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),n=e}await p.runStatus(n,{detail:e,namespace:t.namespace})}),m.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const o=m.opts().config,i=await a.ensureConfig(o),r=()=>s.runTypesGenerator(i);if(await r(),e.watch){console.log("\nWatching for changes...");const e=await w(i.types?.input||[]),o=[...y(i.extract?.ignore)].filter(Boolean),a=e.filter(e=>!o.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),r()})}}),m.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=m.opts().config,t=await a.ensureConfig(e);await l.runSyncer(t)}),m.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async e=>{await u.runMigrator(e)}),m.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(g.runInit),m.command("lint").description("Find potential issues like hardcoded strings in your codebase.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async e=>{const o=m.opts().config,c=async()=>{let e=await a.loadConfig(o);if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await r.detectConfig();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 i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=t}await d.runLinter(e)};if(await c(),e.watch){console.log("\nWatching for changes...");const e=await a.loadConfig(o);if(e?.extract?.input){const o=await w(e.extract.input),i=[...y(e.extract.ignore),...h(e.extract.output)].filter(Boolean),a=o.filter(e=>!i.some(t=>n.minimatch(e,t,{dot:!0})));t.watch(a,{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}}),m.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=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeSync(o,e)}),m.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeDownload(o,e)}),m.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=m.opts().config,o=await a.ensureConfig(t);await f.runLocizeMigrate(o,e)}),m.parse(process.argv);const y=e=>Array.isArray(e)?e:e?[e]:[],h=e=>e&&"string"==typeof e?[e.replace(/\{\{[^}]+\}\}/g,"*")]:[],w=async(e=[])=>{const t=y(e),n=await Promise.all(t.map(e=>o.glob(e||"",{nodir:!0})));return Array.from(new Set(n.flat()))};
@@ -1 +1 @@
1
- "use strict";var e=require("node:path"),t=require("glob"),s=require("../../utils/nested-object.js"),r=require("../../utils/file-utils.js"),n=require("../../utils/default-value.js");function a(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}function o(e,t){if("object"!=typeof e||null===e||Array.isArray(e))return e;const s={},r=t?.extract?.pluralSeparator??"_",n=["zero","one","two","few","many","other"],a=n.map(e=>`ordinal${r}${e}`),l=Object.keys(e).sort((e,t)=>{const s=e=>{for(const t of a)if(e.endsWith(`${r}${t}`)){return{base:e.slice(0,-(r.length+t.length)),form:t,isOrdinal:!0,isPlural:!0,fullKey:e}}for(const t of n)if(e.endsWith(`${r}${t}`)){return{base:e.slice(0,-(r.length+t.length)),form:t,isOrdinal:!1,isPlural:!0,fullKey:e}}return{base:e,form:"",isOrdinal:!1,isPlural:!1,fullKey:e}},o=s(e),l=s(t);if(o.isPlural&&l.isPlural){const e=o.base.localeCompare(l.base,void 0,{sensitivity:"base"});if(0!==e)return e;if(o.isOrdinal!==l.isOrdinal)return o.isOrdinal?1:-1;const t=o.isOrdinal?a:n,s=t.indexOf(o.form),r=t.indexOf(l.form);return-1!==s&&-1!==r?s-r:o.form.localeCompare(l.form)}const i=e.localeCompare(t,void 0,{sensitivity:"base"});return 0===i?e.localeCompare(t,void 0,{sensitivity:"case"}):i});for(const r of l)s[r]=o(e[r],t);return s}function l(e,t,r,a,l,i=[],c=new Set,u=!1){const{keySeparator:f=".",sort:d=!0,removeUnusedKeys:p=!0,primaryLanguage:g,defaultValue:y="",pluralSeparator:h="_",contextSeparator:m="_"}=r.extract,x=new Set;try{const e=new Intl.PluralRules(a,{type:"cardinal"}),t=new Intl.PluralRules(a,{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(e=>x.add(e)),t.resolvedOptions().pluralCategories.forEach(e=>x.add(`ordinal_${e}`))}catch(e){const t=new Intl.PluralRules(g||"en",{type:"cardinal"}),s=new Intl.PluralRules(g||"en",{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(e=>x.add(e)),s.resolvedOptions().pluralCategories.forEach(e=>x.add(`ordinal_${e}`))}const O=e.filter(({key:e,hasCount:t,isOrdinal:s})=>{if(i.some(t=>t.test(e)))return!1;if(!t)return!0;const r=e.split(h);if(s&&r.includes("ordinal")){const e=r[r.length-1];return x.has(`ordinal_${e}`)}if(t){const e=r[r.length-1];return x.has(e)}return!0});let v=p?{}:JSON.parse(JSON.stringify(t));const w=s.getNestedKeys(t,f??".");for(const e of w)if(i.some(t=>t.test(e))){const r=s.getNestedValue(t,e,f??".");s.setNestedValue(v,e,r,f??".")}if(p){const e=s.getNestedKeys(t,f??".");for(const r of e){const e=r.split(h);if("zero"===e[e.length-1]){const n=e.slice(0,-1).join(h);if(O.some(({key:e})=>e.split(h).slice(0,-1).join(h)===n)){const e=s.getNestedValue(t,r,f??".");s.setNestedValue(v,r,e,f??".")}}}}for(const{key:e,defaultValue:o,explicitDefault:i}of O){const d=s.getNestedValue(t,e,f??"."),p=!O.some(t=>t.key.startsWith(`${e}${f}`)&&t.key!==e),x="object"==typeof d&&null!==d&&(c.has(e)||!o||o===e),w="object"==typeof d&&null!==d&&p&&!c.has(e)&&!x;if(x){s.setNestedValue(v,e,d,f??".");continue}let N;if(void 0===d||w)if(a===g)if(u){const t=o&&(o===e||e!==o&&(e.startsWith(o+h)||e.startsWith(o+m)));N=o&&!t?o:n.resolveDefaultValue(y,e,l||r?.extract?.defaultNS||"translation",a,o)}else N=o||e;else N=n.resolveDefaultValue(y,e,l||r?.extract?.defaultNS||"translation",a,o);else if(a===g&&u){const t=o&&(o===e||e!==o&&(e.startsWith(o+h)||e.startsWith(o+m)));N=(e.includes(h)||e.includes(m))&&!i?d:o&&!t?o:d}else N=d;s.setNestedValue(v,e,N,f??".")}if(!0===d)return o(v,r);if("function"==typeof d){const e={},t=Object.keys(v),s=new Map;for(const e of O){const t=!1===f?e.key:e.key.split(f)[0];s.has(t)||s.set(t,e)}t.sort((e,t)=>{if("function"==typeof d){const r=s.get(e),n=s.get(t);if(r&&n)return d(r,n)}return e.localeCompare(t,void 0,{sensitivity:"base"})});for(const s of t)e[s]=o(v[s],r);v=e}return v}exports.getTranslations=async function(s,n,o,{syncPrimaryWithDefaults:i=!1}={}){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(e=>e!==o?.extract?.primaryLanguage);const c=[...o.extract.preservePatterns||[]],u=o.extract.indentation??2;for(const e of n)c.push(`${e}.*`);const f=c.map(a),d="__no_namespace__",p=new Map;for(const e of s.values()){const t=e.nsIsImplicit&&!1===o.extract.defaultNS?d:String(e.ns??o.extract.defaultNS??"translation");p.has(t)||p.set(t,[]),p.get(t).push(e)}const g=[],y=Array.isArray(o.extract.ignore)?o.extract.ignore:o.extract.ignore?[o.extract.ignore]:[];for(const s of o.locales){if(o.extract.mergeNamespaces||!o.extract.output.includes("{{namespace}}")){const t={},a=r.getOutputPath(o.extract.output,s),c=e.resolve(process.cwd(),a),y=await r.loadTranslationFile(c)||{},h=new Set([...p.keys(),...Object.keys(y)]);for(const e of h){const r=p.get(e)||[];if(e===d){const e=l(r,y,o,s,void 0,f,n,i);Object.assign(t,e)}else{const a=y[e]||{};t[e]=l(r,a,o,s,e,f,n,i)}}const m=JSON.stringify(y,null,u),x=JSON.stringify(t,null,u);g.push({path:c,updated:x!==m,newTranslations:t,existingTranslations:y})}else{const a=new Set(p.keys()),c=r.getOutputPath(o.extract.output,s,"*"),d=await t.glob(c,{ignore:y});for(const t of d)a.add(e.basename(t,e.extname(t)));for(const t of a){const a=p.get(t)||[],c=r.getOutputPath(o.extract.output,s,t),d=e.resolve(process.cwd(),c),y=await r.loadTranslationFile(d)||{},h=l(a,y,o,s,t,f,n,i),m=JSON.stringify(y,null,u),x=JSON.stringify(h,null,u);g.push({path:d,updated:x!==m,newTranslations:h,existingTranslations:y})}}}return g};
1
+ "use strict";var e=require("node:path"),t=require("glob"),s=require("../../utils/nested-object.js"),r=require("../../utils/file-utils.js"),n=require("../../utils/default-value.js");function a(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}function o(e,t){if("object"!=typeof e||null===e||Array.isArray(e))return e;const s={},r=t?.extract?.pluralSeparator??"_",n=["zero","one","two","few","many","other"],a=n.map(e=>`ordinal${r}${e}`),l=Object.keys(e).sort((e,t)=>{const s=e=>{for(const t of a)if(e.endsWith(`${r}${t}`)){return{base:e.slice(0,-(r.length+t.length)),form:t,isOrdinal:!0,isPlural:!0,fullKey:e}}for(const t of n)if(e.endsWith(`${r}${t}`)){return{base:e.slice(0,-(r.length+t.length)),form:t,isOrdinal:!1,isPlural:!0,fullKey:e}}return{base:e,form:"",isOrdinal:!1,isPlural:!1,fullKey:e}},o=s(e),l=s(t);if(o.isPlural&&l.isPlural){const e=o.base.localeCompare(l.base,void 0,{sensitivity:"base"});if(0!==e)return e;if(o.isOrdinal!==l.isOrdinal)return o.isOrdinal?1:-1;const t=o.isOrdinal?a:n,s=t.indexOf(o.form),r=t.indexOf(l.form);return-1!==s&&-1!==r?s-r:o.form.localeCompare(l.form)}const i=e.localeCompare(t,void 0,{sensitivity:"base"});return 0===i?e.localeCompare(t,void 0,{sensitivity:"case"}):i});for(const r of l)s[r]=o(e[r],t);return s}function l(e,t,r,a,l,i=[],c=new Set,u=!1){const{keySeparator:f=".",sort:d=!0,removeUnusedKeys:p=!0,primaryLanguage:g,defaultValue:y="",pluralSeparator:h="_",contextSeparator:m="_"}=r.extract,x=new Set;try{const e=new Intl.PluralRules(a,{type:"cardinal"}),t=new Intl.PluralRules(a,{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(e=>x.add(e)),t.resolvedOptions().pluralCategories.forEach(e=>x.add(`ordinal_${e}`))}catch(e){const t=new Intl.PluralRules(g||"en",{type:"cardinal"}),s=new Intl.PluralRules(g||"en",{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(e=>x.add(e)),s.resolvedOptions().pluralCategories.forEach(e=>x.add(`ordinal_${e}`))}const O=e.filter(({key:e,hasCount:t,isOrdinal:s})=>{if(i.some(t=>t.test(e)))return!1;if(!t)return!0;const r=e.split(h);if(s&&r.includes("ordinal")){const e=r[r.length-1];return x.has(`ordinal_${e}`)}if(t){const e=r[r.length-1];return x.has(e)}return!0});let v=p?{}:JSON.parse(JSON.stringify(t));const w=s.getNestedKeys(t,f??".");for(const e of w)if(i.some(t=>t.test(e))){const r=s.getNestedValue(t,e,f??".");s.setNestedValue(v,e,r,f??".")}if(p){const e=s.getNestedKeys(t,f??".");for(const r of e){const e=r.split(h);if("zero"===e[e.length-1]){const n=e.slice(0,-1).join(h);if(O.some(({key:e})=>e.split(h).slice(0,-1).join(h)===n)){const e=s.getNestedValue(t,r,f??".");s.setNestedValue(v,r,e,f??".")}}}}for(const{key:e,defaultValue:o,explicitDefault:i}of O){const d=s.getNestedValue(t,e,f??"."),p=!O.some(t=>t.key.startsWith(`${e}${f}`)&&t.key!==e),x="object"==typeof d&&null!==d&&(c.has(e)||!o||o===e),w="object"==typeof d&&null!==d&&p&&!c.has(e)&&!x;if(x){s.setNestedValue(v,e,d,f??".");continue}let N;if(void 0===d||w)if(a===g)if(u){const t=o&&(o===e||e!==o&&(e.startsWith(o+h)||e.startsWith(o+m)));N=o&&!t?o:n.resolveDefaultValue(y,e,l||r?.extract?.defaultNS||"translation",a,o)}else N=o||e;else N=n.resolveDefaultValue(y,e,l||r?.extract?.defaultNS||"translation",a,o);else if(a===g&&u){const t=o&&(o===e||e!==o&&(e.startsWith(o+h)||e.startsWith(o+m)));N=(e.includes(h)||e.includes(m))&&!i?d:o&&!t?o:d}else N=d;s.setNestedValue(v,e,N,f??".")}if(!0===d)return o(v,r);if("function"==typeof d){const e={},t=Object.keys(v),s=new Map;for(const e of O){const t=!1===f?e.key:e.key.split(f)[0];s.has(t)||s.set(t,e)}t.sort((e,t)=>{if("function"==typeof d){const r=s.get(e),n=s.get(t);if(r&&n)return d(r,n)}return e.localeCompare(t,void 0,{sensitivity:"base"})});for(const s of t)e[s]=o(v[s],r);v=e}return v}exports.getTranslations=async function(s,n,o,{syncPrimaryWithDefaults:i=!1}={}){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(e=>e!==o?.extract?.primaryLanguage);const c=[...o.extract.preservePatterns||[]],u=o.extract.indentation??2;for(const e of n)c.push(`${e}.*`);const f=c.map(a),d="__no_namespace__",p=new Map;for(const e of s.values()){const t=e.nsIsImplicit&&!1===o.extract.defaultNS?d:String(e.ns??o.extract.defaultNS??"translation");p.has(t)||p.set(t,[]),p.get(t).push(e)}const g=[],y=Array.isArray(o.extract.ignore)?o.extract.ignore:o.extract.ignore?[o.extract.ignore]:[];for(const s of o.locales){if(o.extract.mergeNamespaces||"string"==typeof o.extract.output&&!o.extract.output.includes("{{namespace}}")){const t={},a=r.getOutputPath(o.extract.output,s),c=e.resolve(process.cwd(),a),y=await r.loadTranslationFile(c)||{},h=new Set([...p.keys(),...Object.keys(y)]);for(const e of h){const r=p.get(e)||[];if(e===d){const e=l(r,y,o,s,void 0,f,n,i);Object.assign(t,e)}else{const a=y[e]||{};t[e]=l(r,a,o,s,e,f,n,i)}}const m=JSON.stringify(y,null,u),x=JSON.stringify(t,null,u);g.push({path:c,updated:x!==m,newTranslations:t,existingTranslations:y})}else{const a=new Set(p.keys()),c=r.getOutputPath(o.extract.output,s,"*").replace(/\\/g,"/"),d=await t.glob(c,{ignore:y});for(const t of d)a.add(e.basename(t,e.extname(t)));for(const t of a){const a=p.get(t)||[],c=r.getOutputPath(o.extract.output,s,t),d=e.resolve(process.cwd(),c),y=await r.loadTranslationFile(d)||{},h=l(a,y,o,s,t,f,n,i),m=JSON.stringify(y,null,u),x=JSON.stringify(h,null,u);g.push({path:d,updated:x!==m,newTranslations:h,existingTranslations:y})}}}return g};
package/dist/cjs/init.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("inquirer"),t=require("node:fs/promises"),n=require("node:path"),i=require("./heuristic-config.js");exports.runInit=async function(){console.log("Welcome to the i18next-cli setup wizard!"),console.log("Scanning your project for a recommended configuration...");const o=await i.detectConfig();o?console.log("✅ Found a potential project structure. Using it for suggestions."):console.log("Could not detect a project structure. Using standard defaults."),"string"==typeof o?.extract?.input&&(o.extract.input=[o?.extract?.input]);const r=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:o?.locales?.join(",")||"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:o?.extract?.input?(o.extract.input||[])[0]:"src/**/*.{js,jsx,ts,tsx}"},{type:"input",name:"output",message:"What is the path for your output resource files?",default:o?.extract?.output||"public/locales/{{language}}/{{namespace}}.json"}]),s=r.fileType.includes("TypeScript"),a=await async function(){try{const e=n.resolve(process.cwd(),"package.json"),i=await t.readFile(e,"utf-8");return"module"===JSON.parse(i).type}catch{return!0}}(),c=s?"i18next.config.ts":"i18next.config.js",u={locales:r.locales,extract:{input:r.input,output:r.output}};function p(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}${p(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}: ${p(e[i],t,n+1)}`}).join(",\n")}\n${o}}`}return JSON.stringify(e)}let f="";f=s?`import { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${p(u)});`:a?`import { defineConfig } from 'i18next-cli';\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nexport default defineConfig(${p(u)});`:`const { defineConfig } = require('i18next-cli');\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nmodule.exports = defineConfig(${p(u)});`;const l=n.resolve(process.cwd(),c);await t.writeFile(l,f.trim()),console.log(`✅ Configuration file created at: ${l}`)};
1
+ "use strict";var t=require("inquirer"),e=require("node:fs/promises"),n=require("node:path"),i=require("./heuristic-config.js");exports.runInit=async function(){console.log("Welcome to the i18next-cli setup wizard!"),console.log("Scanning your project for a recommended configuration...");const o=await i.detectConfig();o?console.log("✅ Found a potential project structure. Using it for suggestions."):console.log("Could not detect a project structure. Using standard defaults."),"string"==typeof o?.extract?.input&&(o.extract.input=[o?.extract?.input]),o&&"function"==typeof o.extract?.output&&delete o.extract.output;const r=await t.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:o?.locales?.join(",")||"en,de,fr",filter:t=>t.split(",").map(t=>t.trim())},{type:"input",name:"input",message:"What is the glob pattern for your source files?",default:o?.extract?.input?(o.extract.input||[])[0]:"src/**/*.{js,jsx,ts,tsx}"},{type:"input",name:"output",message:"What is the path for your output resource files?",default:"string"==typeof o?.extract?.output?o.extract.output:"public/locales/{{language}}/{{namespace}}.json"}]),s=r.fileType.includes("TypeScript"),a=await async function(){try{const t=n.resolve(process.cwd(),"package.json"),i=await e.readFile(t,"utf-8");return"module"===JSON.parse(i).type}catch{return!0}}(),c=s?"i18next.config.ts":"i18next.config.js",u={locales:r.locales,extract:{input:r.input,output:r.output}};function p(t,e=2,n=0){const i=t=>" ".repeat(t*e),o=i(n),r=i(n+1);if(null===t||"number"==typeof t||"boolean"==typeof t)return JSON.stringify(t);if("string"==typeof t)return JSON.stringify(t);if(Array.isArray(t)){if(0===t.length)return"[]";return`[\n${t.map(t=>`${r}${p(t,e,n+1)}`).join(",\n")}\n${o}]`}if("object"==typeof t){const i=Object.keys(t);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}: ${p(t[i],e,n+1)}`}).join(",\n")}\n${o}}`}return JSON.stringify(t)}let f="";f=s?`import { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${p(u)});`:a?`import { defineConfig } from 'i18next-cli';\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nexport default defineConfig(${p(u)});`:`const { defineConfig } = require('i18next-cli');\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nmodule.exports = defineConfig(${p(u)});`;const l=n.resolve(process.cwd(),c);await e.writeFile(l,f.trim()),console.log(`✅ Configuration file created at: ${l}`)};
@@ -1 +1 @@
1
- "use strict";var e=require("execa"),o=require("chalk"),n=require("ora"),t=require("inquirer"),i=require("node:path");function r(e,o,n){const{locize:t={},extract:r}=o,{projectId:s,apiKey:c,version:a}=t,l=[e];if(s&&l.push("--project-id",s),c&&l.push("--api-key",c),a&&l.push("--ver",a),"sync"===e){(n.updateValues??t.updateValues)&&l.push("--update-values","true");(n.srcLngOnly??t.sourceLanguageOnly)&&l.push("--reference-language-only","true");(n.compareMtime??t.compareModificationTime)&&l.push("--compare-modification-time","true");(n.dryRun??t.dryRun)&&l.push("--dry","true")}const u=String(r.output||"").replace(/\\/g,"/"),p=(u.includes("/{{language}}/")?u.split("/{{language}}/")[0]:u.replace("{{language}}","")).split("/").join(i.sep),d=i.resolve(process.cwd(),p);return l.push("--path",d),l}async function s(i,s,c={}){await async function(){try{await e.execa("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 a=n(`Running 'locize ${i}'...\n`).start();let l=s;try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green(`'locize ${i}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(n){const s=n.stderr||"";if(s.includes("missing required argument")){const n=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await t.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(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:n}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(n){const n=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.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(n)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(n){l={...l,locize:n},a.start("Retrying with new credentials...");try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${i}'.`)),console.error(s||n.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${i}' completed successfully.`))}exports.runLocizeDownload=(e,o)=>s("download",e,o),exports.runLocizeMigrate=(e,o)=>s("migrate",e,o),exports.runLocizeSync=(e,o)=>s("sync",e,o);
1
+ "use strict";var e=require("execa"),o=require("chalk"),n=require("ora"),t=require("inquirer"),r=require("node:path");function s(e,o,n){const{locize:t={},extract:s}=o,{projectId:i,apiKey:c,version:a}=t,l=[e];if(i&&l.push("--project-id",i),c&&l.push("--api-key",c),a&&l.push("--ver",a),"sync"===e){(n.updateValues??t.updateValues)&&l.push("--update-values","true");(n.srcLngOnly??t.sourceLanguageOnly)&&l.push("--reference-language-only","true");(n.compareMtime??t.compareModificationTime)&&l.push("--compare-modification-time","true");(n.dryRun??t.dryRun)&&l.push("--dry","true")}let u;try{if("string"==typeof s.output){const e=s.output.replace(/\\/g,"/"),o=(e.includes("/{{language}}/")?e.split("/{{language}}/")[0]:e.replace("{{language}}","")).split("/").join(r.sep);u=r.resolve(process.cwd(),o)}else if("function"==typeof s.output)try{const e=s.output(o.extract.primaryLanguage||"en"),n=String(e).replace(/\\/g,"/"),t=n.includes("/"+(o.extract.primaryLanguage||"en")+"/")?n.split("/"+(o.extract.primaryLanguage||"en")+"/")[0]:n.replace(o.extract.primaryLanguage||"en","");u=r.resolve(process.cwd(),t.split("/").join(r.sep))}catch{u=r.resolve(process.cwd(),".")}else u=r.resolve(process.cwd(),".")}catch{u=r.resolve(process.cwd(),".")}return l.push("--path",u),l}async function i(r,i,c={}){await async function(){try{await e.execa("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 a=n(`Running 'locize ${r}'...\n`).start();let l=i;try{const n=s(r,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green(`'locize ${r}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(n){const i=n.stderr||"";if(i.includes("missing required argument")){const n=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await t.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(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:n}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(n){const n=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.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(n)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(n){l={...l,locize:n},a.start("Retrying with new credentials...");try{const n=s(r,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e.execa("locize",n,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${r}'.`)),console.error(i||n.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${r}' completed successfully.`))}exports.runLocizeDownload=(e,o)=>i("download",e,o),exports.runLocizeMigrate=(e,o)=>i("migrate",e,o),exports.runLocizeSync=(e,o)=>i("sync",e,o);
@@ -1 +1 @@
1
- "use strict";var e=require("node:fs/promises"),t=require("node:path"),r=require("jiti"),s=require("../config.js");exports.getOutputPath=function(e,t,r=""){return e.replace("{{language}}",t).replace("{{lng}}",t).replace("{{namespace}}",r).replace("{{ns}}",r)},exports.loadTranslationFile=async function(n){const a=t.resolve(process.cwd(),n);try{await e.access(a)}catch{return null}try{const n=t.extname(a).toLowerCase();if(".json"===n){const t=await e.readFile(a,"utf-8");return JSON.parse(t)}if(".ts"===n||".js"===n){const e=await s.getTsConfigAliases(),t=r.createJiti(process.cwd(),{alias:e,interopDefault:!0});return await t.import(a,{default:!0})}return null}catch(e){return console.warn(`Could not parse translation file ${n}:`,e),null}},exports.serializeTranslationFile=function(e,t="json",r=2){const s=JSON.stringify(e,null,r);switch(t){case"js":case"js-esm":return`export default ${s};\n`;case"js-cjs":return`module.exports = ${s};\n`;case"ts":return`export default ${s} as const;\n`;default:return`${s}\n`}};
1
+ "use strict";var e=require("node:fs/promises"),t=require("node:path"),r=require("jiti"),n=require("../config.js");exports.getOutputPath=function(e,r,n){if(!e)return t.normalize(`locales/${r}/${n??"translation"}.json`);if("function"==typeof e)try{const a=String(e(r,n));return t.normalize(a.replace(/\/\/+/g,"/"))}catch{return t.normalize(`locales/${r}/${n??"translation"}.json`)}let a=String(e);return a=a.replace(/\{\{language\}\}|\{\{lng\}\}/g,r),a=null!=n?a.replace(/\{\{namespace\}\}/g,n):a.replace(/\/?\{\{namespace\}\}/g,""),a=a.replace(/\/\/+/g,"/"),t.normalize(a)},exports.loadTranslationFile=async function(a){const s=t.resolve(process.cwd(),a);try{await e.access(s)}catch{return null}try{const a=t.extname(s).toLowerCase();if(".json"===a){const t=await e.readFile(s,"utf-8");return JSON.parse(t)}if(".ts"===a||".js"===a){const e=await n.getTsConfigAliases(),t=r.createJiti(process.cwd(),{alias:e,interopDefault:!0});return await t.import(s,{default:!0})}return null}catch(e){return console.warn(`Could not parse translation file ${a}:`,e),null}},exports.serializeTranslationFile=function(e,t="json",r=2){const n=JSON.stringify(e,null,r);switch(t){case"js":case"js-esm":return`export default ${n};\n`;case"js-cjs":return`module.exports = ${n};\n`;case"ts":return`export default ${n} as const;\n`;default:return`${n}\n`}};
@@ -1 +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")};
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("string"==typeof e.extract.output&&!e.extract.output.includes("{{language}}")&&!e.extract.output.includes("{{lng}}"))throw new t("extract.output must contain {{language}} placeholder")};
package/dist/esm/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import{minimatch as i}from"minimatch";import n from"chalk";import{ensureConfig as a,loadConfig as r}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as s}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as l}from"./types-generator.js";import{runSyncer as p}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as f}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as g}from"./status.js";import{runLocizeSync as u,runLocizeDownload as y,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.16.0"),w.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").option("--sync-primary","Sync primary language values with default values from code.").action(async t=>{try{const e=w.opts().config,n=await a(e),r=async()=>{const o=await s(n,{isWatchMode:!!t.watch,isDryRun:!!t.dryRun,syncPrimaryWithDefaults:!!t.syncPrimary});return t.ci&&!o?(console.log("✅ No files were updated."),process.exit(0)):t.ci&&o&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),o};if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.extract.input),e=x(n.extract.ignore),a=j(n.extract.output),c=[...e,...a].filter(Boolean),s=t.filter(t=>!c.some(o=>i(t,o,{dot:!0})));o.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}catch(t){console.error("Error running extractor:",t),process.exit(1)}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(t,o)=>{const e=w.opts().config;let i=await r(e);if(!i){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),i=t}await g(i,{detail:t,namespace:o.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async t=>{const e=w.opts().config,n=await a(e),r=()=>l(n);if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.types?.input||[]),e=[...x(n.extract?.ignore)].filter(Boolean),a=t.filter(t=>!e.some(o=>i(t,o,{dot:!0})));o.watch(a,{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=w.opts().config,o=await a(t);await p(o)}),w.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async t=>{await m(t)}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(f),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async t=>{const e=w.opts().config,a=async()=>{let t=await r(e);if(!t){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),t=o}await d(t)};if(await a(),t.watch){console.log("\nWatching for changes...");const t=await r(e);if(t?.extract?.input){const e=await z(t.extract.input),n=[...x(t.extract.ignore),...j(t.extract.output)].filter(Boolean),r=e.filter(t=>!n.some(o=>i(t,o,{dot:!0})));o.watch(r,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async t=>{const o=w.opts().config,e=await a(o);await u(e,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await y(e,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await h(e,t)}),w.parse(process.argv);const x=t=>Array.isArray(t)?t:t?[t]:[],j=t=>t&&"string"==typeof t?[t.replace(/\{\{[^}]+\}\}/g,"*")]:[],z=async(t=[])=>{const o=x(t),i=await Promise.all(o.map(t=>e(t||"",{nodir:!0})));return Array.from(new Set(i.flat()))};
2
+ import{Command as t}from"commander";import o from"chokidar";import{glob as e}from"glob";import{minimatch as i}from"minimatch";import n from"chalk";import{ensureConfig as a,loadConfig as r}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as s}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as l}from"./types-generator.js";import{runSyncer as p}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as f}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as g}from"./status.js";import{runLocizeSync as u,runLocizeDownload as y,runLocizeMigrate as h}from"./locize.js";const w=new t;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("1.17.1"),w.option("-c, --config <path>","Path to i18next-cli config file (overrides detection)"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").option("--dry-run","Run the extractor without writing any files to disk.").option("--sync-primary","Sync primary language values with default values from code.").action(async t=>{try{const e=w.opts().config,n=await a(e),r=async()=>{const o=await s(n,{isWatchMode:!!t.watch,isDryRun:!!t.dryRun,syncPrimaryWithDefaults:!!t.syncPrimary});return t.ci&&!o?(console.log("✅ No files were updated."),process.exit(0)):t.ci&&o&&(console.error("❌ Some files were updated. This should not happen in CI mode."),process.exit(1)),o};if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.extract.input),e=x(n.extract.ignore),a=j(n.extract.output),c=[...e,...a].filter(Boolean),s=t.filter(t=>!c.some(o=>i(t,o,{dot:!0})));o.watch(s,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}catch(t){console.error("Error running extractor:",t),process.exit(1)}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(t,o)=>{const e=w.opts().config;let i=await r(e);if(!i){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),i=t}await g(i,{detail:t,namespace:o.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async t=>{const e=w.opts().config,n=await a(e),r=()=>l(n);if(await r(),t.watch){console.log("\nWatching for changes...");const t=await z(n.types?.input||[]),e=[...x(n.extract?.ignore)].filter(Boolean),a=t.filter(t=>!e.some(o=>i(t,o,{dot:!0})));o.watch(a,{persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),r()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const t=w.opts().config,o=await a(t);await p(o)}),w.command("migrate-config [configPath]").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async t=>{await m(t)}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(f),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").option("-w, --watch","Watch for file changes and re-run the linter.").action(async t=>{const e=w.opts().config,a=async()=>{let t=await r(e);if(!t){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),t=o}await d(t)};if(await a(),t.watch){console.log("\nWatching for changes...");const t=await r(e);if(t?.extract?.input){const e=await z(t.extract.input),n=[...x(t.extract.ignore),...j(t.extract.output)].filter(Boolean),r=e.filter(t=>!n.some(o=>i(t,o,{dot:!0})));o.watch(r,{ignored:/node_modules/,persistent:!0}).on("change",t=>{console.log(`\nFile changed: ${t}`),a()})}}}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async t=>{const o=w.opts().config,e=await a(o);await u(e,t)}),w.command("locize-download").description("Download all translations from your locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await y(e,t)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async t=>{const o=w.opts().config,e=await a(o);await h(e,t)}),w.parse(process.argv);const x=t=>Array.isArray(t)?t:t?[t]:[],j=t=>t&&"string"==typeof t?[t.replace(/\{\{[^}]+\}\}/g,"*")]:[],z=async(t=[])=>{const o=x(t),i=await Promise.all(o.map(t=>e(t||"",{nodir:!0})));return Array.from(new Set(i.flat()))};
@@ -1 +1 @@
1
- import{resolve as t,basename as e,extname as r}from"node:path";import{glob as s}from"glob";import{getNestedKeys as n,getNestedValue as o,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as i,loadTranslationFile as l}from"../../utils/file-utils.js";import{resolveDefaultValue as c}from"../../utils/default-value.js";function f(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function u(t,e){if("object"!=typeof t||null===t||Array.isArray(t))return t;const r={},s=e?.extract?.pluralSeparator??"_",n=["zero","one","two","few","many","other"],o=n.map(t=>`ordinal${s}${t}`),a=Object.keys(t).sort((t,e)=>{const r=t=>{for(const e of o)if(t.endsWith(`${s}${e}`)){return{base:t.slice(0,-(s.length+e.length)),form:e,isOrdinal:!0,isPlural:!0,fullKey:t}}for(const e of n)if(t.endsWith(`${s}${e}`)){return{base:t.slice(0,-(s.length+e.length)),form:e,isOrdinal:!1,isPlural:!0,fullKey:t}}return{base:t,form:"",isOrdinal:!1,isPlural:!1,fullKey:t}},a=r(t),i=r(e);if(a.isPlural&&i.isPlural){const t=a.base.localeCompare(i.base,void 0,{sensitivity:"base"});if(0!==t)return t;if(a.isOrdinal!==i.isOrdinal)return a.isOrdinal?1:-1;const e=a.isOrdinal?o:n,r=e.indexOf(a.form),s=e.indexOf(i.form);return-1!==r&&-1!==s?r-s:a.form.localeCompare(i.form)}const l=t.localeCompare(e,void 0,{sensitivity:"base"});return 0===l?t.localeCompare(e,void 0,{sensitivity:"case"}):l});for(const s of a)r[s]=u(t[s],e);return r}function p(t,e,r,s,i,l=[],f=new Set,p=!1){const{keySeparator:d=".",sort:y=!0,removeUnusedKeys:g=!0,primaryLanguage:m,defaultValue:h="",pluralSeparator:x="_",contextSeparator:O="_"}=r.extract,w=new Set;try{const t=new Intl.PluralRules(s,{type:"cardinal"}),e=new Intl.PluralRules(s,{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(t=>w.add(t)),e.resolvedOptions().pluralCategories.forEach(t=>w.add(`ordinal_${t}`))}catch(t){const e=new Intl.PluralRules(m||"en",{type:"cardinal"}),r=new Intl.PluralRules(m||"en",{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(t=>w.add(t)),r.resolvedOptions().pluralCategories.forEach(t=>w.add(`ordinal_${t}`))}const S=t.filter(({key:t,hasCount:e,isOrdinal:r})=>{if(l.some(e=>e.test(t)))return!1;if(!e)return!0;const s=t.split(x);if(r&&s.includes("ordinal")){const t=s[s.length-1];return w.has(`ordinal_${t}`)}if(e){const t=s[s.length-1];return w.has(t)}return!0});let v=g?{}:JSON.parse(JSON.stringify(e));const b=n(e,d??".");for(const t of b)if(l.some(e=>e.test(t))){const r=o(e,t,d??".");a(v,t,r,d??".")}if(g){const t=n(e,d??".");for(const r of t){const t=r.split(x);if("zero"===t[t.length-1]){const s=t.slice(0,-1).join(x);if(S.some(({key:t})=>t.split(x).slice(0,-1).join(x)===s)){const t=o(e,r,d??".");a(v,r,t,d??".")}}}}for(const{key:t,defaultValue:n,explicitDefault:l}of S){const u=o(e,t,d??"."),y=!S.some(e=>e.key.startsWith(`${t}${d}`)&&e.key!==t),g="object"==typeof u&&null!==u&&(f.has(t)||!n||n===t),w="object"==typeof u&&null!==u&&y&&!f.has(t)&&!g;if(g){a(v,t,u,d??".");continue}let b;if(void 0===u||w)if(s===m)if(p){const e=n&&(n===t||t!==n&&(t.startsWith(n+x)||t.startsWith(n+O)));b=n&&!e?n:c(h,t,i||r?.extract?.defaultNS||"translation",s,n)}else b=n||t;else b=c(h,t,i||r?.extract?.defaultNS||"translation",s,n);else if(s===m&&p){const e=n&&(n===t||t!==n&&(t.startsWith(n+x)||t.startsWith(n+O)));b=(t.includes(x)||t.includes(O))&&!l?u:n&&!e?n:u}else b=u;a(v,t,b,d??".")}if(!0===y)return u(v,r);if("function"==typeof y){const t={},e=Object.keys(v),s=new Map;for(const t of S){const e=!1===d?t.key:t.key.split(d)[0];s.has(e)||s.set(e,t)}e.sort((t,e)=>{if("function"==typeof y){const r=s.get(t),n=s.get(e);if(r&&n)return y(r,n)}return t.localeCompare(e,void 0,{sensitivity:"base"})});for(const s of e)t[s]=u(v[s],r);v=t}return v}async function d(n,o,a,{syncPrimaryWithDefaults:c=!1}={}){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage);const u=[...a.extract.preservePatterns||[]],d=a.extract.indentation??2;for(const t of o)u.push(`${t}.*`);const y=u.map(f),g="__no_namespace__",m=new Map;for(const t of n.values()){const e=t.nsIsImplicit&&!1===a.extract.defaultNS?g:String(t.ns??a.extract.defaultNS??"translation");m.has(e)||m.set(e,[]),m.get(e).push(t)}const h=[],x=Array.isArray(a.extract.ignore)?a.extract.ignore:a.extract.ignore?[a.extract.ignore]:[];for(const n of a.locales){if(a.extract.mergeNamespaces||!a.extract.output.includes("{{namespace}}")){const e={},r=i(a.extract.output,n),s=t(process.cwd(),r),f=await l(s)||{},u=new Set([...m.keys(),...Object.keys(f)]);for(const t of u){const r=m.get(t)||[];if(t===g){const t=p(r,f,a,n,void 0,y,o,c);Object.assign(e,t)}else{const s=f[t]||{};e[t]=p(r,s,a,n,t,y,o,c)}}const x=JSON.stringify(f,null,d),O=JSON.stringify(e,null,d);h.push({path:s,updated:O!==x,newTranslations:e,existingTranslations:f})}else{const f=new Set(m.keys()),u=i(a.extract.output,n,"*"),g=await s(u,{ignore:x});for(const t of g)f.add(e(t,r(t)));for(const e of f){const r=m.get(e)||[],s=i(a.extract.output,n,e),f=t(process.cwd(),s),u=await l(f)||{},g=p(r,u,a,n,e,y,o,c),x=JSON.stringify(u,null,d),O=JSON.stringify(g,null,d);h.push({path:f,updated:O!==x,newTranslations:g,existingTranslations:u})}}}return h}export{d as getTranslations};
1
+ import{resolve as t,basename as e,extname as r}from"node:path";import{glob as s}from"glob";import{getNestedKeys as n,getNestedValue as o,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as i,loadTranslationFile as l}from"../../utils/file-utils.js";import{resolveDefaultValue as c}from"../../utils/default-value.js";function f(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}function u(t,e){if("object"!=typeof t||null===t||Array.isArray(t))return t;const r={},s=e?.extract?.pluralSeparator??"_",n=["zero","one","two","few","many","other"],o=n.map(t=>`ordinal${s}${t}`),a=Object.keys(t).sort((t,e)=>{const r=t=>{for(const e of o)if(t.endsWith(`${s}${e}`)){return{base:t.slice(0,-(s.length+e.length)),form:e,isOrdinal:!0,isPlural:!0,fullKey:t}}for(const e of n)if(t.endsWith(`${s}${e}`)){return{base:t.slice(0,-(s.length+e.length)),form:e,isOrdinal:!1,isPlural:!0,fullKey:t}}return{base:t,form:"",isOrdinal:!1,isPlural:!1,fullKey:t}},a=r(t),i=r(e);if(a.isPlural&&i.isPlural){const t=a.base.localeCompare(i.base,void 0,{sensitivity:"base"});if(0!==t)return t;if(a.isOrdinal!==i.isOrdinal)return a.isOrdinal?1:-1;const e=a.isOrdinal?o:n,r=e.indexOf(a.form),s=e.indexOf(i.form);return-1!==r&&-1!==s?r-s:a.form.localeCompare(i.form)}const l=t.localeCompare(e,void 0,{sensitivity:"base"});return 0===l?t.localeCompare(e,void 0,{sensitivity:"case"}):l});for(const s of a)r[s]=u(t[s],e);return r}function p(t,e,r,s,i,l=[],f=new Set,p=!1){const{keySeparator:d=".",sort:y=!0,removeUnusedKeys:g=!0,primaryLanguage:m,defaultValue:h="",pluralSeparator:x="_",contextSeparator:O="_"}=r.extract,w=new Set;try{const t=new Intl.PluralRules(s,{type:"cardinal"}),e=new Intl.PluralRules(s,{type:"ordinal"});t.resolvedOptions().pluralCategories.forEach(t=>w.add(t)),e.resolvedOptions().pluralCategories.forEach(t=>w.add(`ordinal_${t}`))}catch(t){const e=new Intl.PluralRules(m||"en",{type:"cardinal"}),r=new Intl.PluralRules(m||"en",{type:"ordinal"});e.resolvedOptions().pluralCategories.forEach(t=>w.add(t)),r.resolvedOptions().pluralCategories.forEach(t=>w.add(`ordinal_${t}`))}const S=t.filter(({key:t,hasCount:e,isOrdinal:r})=>{if(l.some(e=>e.test(t)))return!1;if(!e)return!0;const s=t.split(x);if(r&&s.includes("ordinal")){const t=s[s.length-1];return w.has(`ordinal_${t}`)}if(e){const t=s[s.length-1];return w.has(t)}return!0});let v=g?{}:JSON.parse(JSON.stringify(e));const b=n(e,d??".");for(const t of b)if(l.some(e=>e.test(t))){const r=o(e,t,d??".");a(v,t,r,d??".")}if(g){const t=n(e,d??".");for(const r of t){const t=r.split(x);if("zero"===t[t.length-1]){const s=t.slice(0,-1).join(x);if(S.some(({key:t})=>t.split(x).slice(0,-1).join(x)===s)){const t=o(e,r,d??".");a(v,r,t,d??".")}}}}for(const{key:t,defaultValue:n,explicitDefault:l}of S){const u=o(e,t,d??"."),y=!S.some(e=>e.key.startsWith(`${t}${d}`)&&e.key!==t),g="object"==typeof u&&null!==u&&(f.has(t)||!n||n===t),w="object"==typeof u&&null!==u&&y&&!f.has(t)&&!g;if(g){a(v,t,u,d??".");continue}let b;if(void 0===u||w)if(s===m)if(p){const e=n&&(n===t||t!==n&&(t.startsWith(n+x)||t.startsWith(n+O)));b=n&&!e?n:c(h,t,i||r?.extract?.defaultNS||"translation",s,n)}else b=n||t;else b=c(h,t,i||r?.extract?.defaultNS||"translation",s,n);else if(s===m&&p){const e=n&&(n===t||t!==n&&(t.startsWith(n+x)||t.startsWith(n+O)));b=(t.includes(x)||t.includes(O))&&!l?u:n&&!e?n:u}else b=u;a(v,t,b,d??".")}if(!0===y)return u(v,r);if("function"==typeof y){const t={},e=Object.keys(v),s=new Map;for(const t of S){const e=!1===d?t.key:t.key.split(d)[0];s.has(e)||s.set(e,t)}e.sort((t,e)=>{if("function"==typeof y){const r=s.get(t),n=s.get(e);if(r&&n)return y(r,n)}return t.localeCompare(e,void 0,{sensitivity:"base"})});for(const s of e)t[s]=u(v[s],r);v=t}return v}async function d(n,o,a,{syncPrimaryWithDefaults:c=!1}={}){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage);const u=[...a.extract.preservePatterns||[]],d=a.extract.indentation??2;for(const t of o)u.push(`${t}.*`);const y=u.map(f),g="__no_namespace__",m=new Map;for(const t of n.values()){const e=t.nsIsImplicit&&!1===a.extract.defaultNS?g:String(t.ns??a.extract.defaultNS??"translation");m.has(e)||m.set(e,[]),m.get(e).push(t)}const h=[],x=Array.isArray(a.extract.ignore)?a.extract.ignore:a.extract.ignore?[a.extract.ignore]:[];for(const n of a.locales){if(a.extract.mergeNamespaces||"string"==typeof a.extract.output&&!a.extract.output.includes("{{namespace}}")){const e={},r=i(a.extract.output,n),s=t(process.cwd(),r),f=await l(s)||{},u=new Set([...m.keys(),...Object.keys(f)]);for(const t of u){const r=m.get(t)||[];if(t===g){const t=p(r,f,a,n,void 0,y,o,c);Object.assign(e,t)}else{const s=f[t]||{};e[t]=p(r,s,a,n,t,y,o,c)}}const x=JSON.stringify(f,null,d),O=JSON.stringify(e,null,d);h.push({path:s,updated:O!==x,newTranslations:e,existingTranslations:f})}else{const f=new Set(m.keys()),u=i(a.extract.output,n,"*").replace(/\\/g,"/"),g=await s(u,{ignore:x});for(const t of g)f.add(e(t,r(t)));for(const e of f){const r=m.get(e)||[],s=i(a.extract.output,n,e),f=t(process.cwd(),s),u=await l(f)||{},g=p(r,u,a,n,e,y,o,c),x=JSON.stringify(u,null,d),O=JSON.stringify(g,null,d);h.push({path:f,updated:O!==x,newTranslations:g,existingTranslations:u})}}}return h}export{d as getTranslations};
package/dist/esm/init.js CHANGED
@@ -1 +1 @@
1
- import t from"inquirer";import{writeFile as e,readFile as n}from"node:fs/promises";import{resolve as o}from"node:path";import{detectConfig as i}from"./heuristic-config.js";async function r(){console.log("Welcome to the i18next-cli setup wizard!"),console.log("Scanning your project for a recommended configuration...");const r=await i();r?console.log("✅ Found a potential project structure. Using it for suggestions."):console.log("Could not detect a project structure. Using standard defaults."),"string"==typeof r?.extract?.input&&(r.extract.input=[r?.extract?.input]);const s=await t.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:r?.locales?.join(",")||"en,de,fr",filter:t=>t.split(",").map(t=>t.trim())},{type:"input",name:"input",message:"What is the glob pattern for your source files?",default:r?.extract?.input?(r.extract.input||[])[0]:"src/**/*.{js,jsx,ts,tsx}"},{type:"input",name:"output",message:"What is the path for your output resource files?",default:r?.extract?.output||"public/locales/{{language}}/{{namespace}}.json"}]),a=s.fileType.includes("TypeScript"),c=await async function(){try{const t=o(process.cwd(),"package.json"),e=await n(t,"utf-8");return"module"===JSON.parse(e).type}catch{return!0}}(),p=a?"i18next.config.ts":"i18next.config.js",u={locales:s.locales,extract:{input:s.input,output:s.output}};function f(t,e=2,n=0){const o=t=>" ".repeat(t*e),i=o(n),r=o(n+1);if(null===t||"number"==typeof t||"boolean"==typeof t)return JSON.stringify(t);if("string"==typeof t)return JSON.stringify(t);if(Array.isArray(t)){if(0===t.length)return"[]";return`[\n${t.map(t=>`${r}${f(t,e,n+1)}`).join(",\n")}\n${i}]`}if("object"==typeof t){const o=Object.keys(t);if(0===o.length)return"{}";return`{\n${o.map(o=>{const i=/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(o)?o:JSON.stringify(o);return`${r}${i}: ${f(t[o],e,n+1)}`}).join(",\n")}\n${i}}`}return JSON.stringify(t)}let l="";l=a?`import { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${f(u)});`:c?`import { defineConfig } from 'i18next-cli';\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nexport default defineConfig(${f(u)});`:`const { defineConfig } = require('i18next-cli');\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nmodule.exports = defineConfig(${f(u)});`;const g=o(process.cwd(),p);await e(g,l.trim()),console.log(`✅ Configuration file created at: ${g}`)}export{r as runInit};
1
+ import t from"inquirer";import{writeFile as e,readFile as n}from"node:fs/promises";import{resolve as o}from"node:path";import{detectConfig as i}from"./heuristic-config.js";async function r(){console.log("Welcome to the i18next-cli setup wizard!"),console.log("Scanning your project for a recommended configuration...");const r=await i();r?console.log("✅ Found a potential project structure. Using it for suggestions."):console.log("Could not detect a project structure. Using standard defaults."),"string"==typeof r?.extract?.input&&(r.extract.input=[r?.extract?.input]),r&&"function"==typeof r.extract?.output&&delete r.extract.output;const a=await t.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:r?.locales?.join(",")||"en,de,fr",filter:t=>t.split(",").map(t=>t.trim())},{type:"input",name:"input",message:"What is the glob pattern for your source files?",default:r?.extract?.input?(r.extract.input||[])[0]:"src/**/*.{js,jsx,ts,tsx}"},{type:"input",name:"output",message:"What is the path for your output resource files?",default:"string"==typeof r?.extract?.output?r.extract.output:"public/locales/{{language}}/{{namespace}}.json"}]),s=a.fileType.includes("TypeScript"),c=await async function(){try{const t=o(process.cwd(),"package.json"),e=await n(t,"utf-8");return"module"===JSON.parse(e).type}catch{return!0}}(),p=s?"i18next.config.ts":"i18next.config.js",u={locales:a.locales,extract:{input:a.input,output:a.output}};function f(t,e=2,n=0){const o=t=>" ".repeat(t*e),i=o(n),r=o(n+1);if(null===t||"number"==typeof t||"boolean"==typeof t)return JSON.stringify(t);if("string"==typeof t)return JSON.stringify(t);if(Array.isArray(t)){if(0===t.length)return"[]";return`[\n${t.map(t=>`${r}${f(t,e,n+1)}`).join(",\n")}\n${i}]`}if("object"==typeof t){const o=Object.keys(t);if(0===o.length)return"{}";return`{\n${o.map(o=>{const i=/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(o)?o:JSON.stringify(o);return`${r}${i}: ${f(t[o],e,n+1)}`}).join(",\n")}\n${i}}`}return JSON.stringify(t)}let l="";l=s?`import { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${f(u)});`:c?`import { defineConfig } from 'i18next-cli';\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nexport default defineConfig(${f(u)});`:`const { defineConfig } = require('i18next-cli');\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nmodule.exports = defineConfig(${f(u)});`;const g=o(process.cwd(),p);await e(g,l.trim()),console.log(`✅ Configuration file created at: ${g}`)}export{r as runInit};
@@ -1 +1 @@
1
- import{execa as e}from"execa";import o from"chalk";import n from"ora";import t from"inquirer";import{sep as i,resolve as s}from"node:path";function r(e,o,n){const{locize:t={},extract:r}=o,{projectId:c,apiKey:a,version:l}=t,u=[e];if(c&&u.push("--project-id",c),a&&u.push("--api-key",a),l&&u.push("--ver",l),"sync"===e){(n.updateValues??t.updateValues)&&u.push("--update-values","true");(n.srcLngOnly??t.sourceLanguageOnly)&&u.push("--reference-language-only","true");(n.compareMtime??t.compareModificationTime)&&u.push("--compare-modification-time","true");(n.dryRun??t.dryRun)&&u.push("--dry","true")}const p=String(r.output||"").replace(/\\/g,"/"),d=(p.includes("/{{language}}/")?p.split("/{{language}}/")[0]:p.replace("{{language}}","")).split("/").join(i),g=s(process.cwd(),d);return u.push("--path",g),u}async function c(i,s,c={}){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 a=n(`Running 'locize ${i}'...\n`).start();let l=s;try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e("locize",n,{stdio:"pipe"});a.succeed(o.green(`'locize ${i}' completed successfully.`)),t?.stdout&&console.log(t.stdout)}catch(n){const s=n.stderr||"";if(s.includes("missing required argument")){const n=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await t.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(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:n}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(n){const n=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.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(n)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(n){l={...l,locize:n},a.start("Retrying with new credentials...");try{const n=r(i,l,c);console.log(o.cyan(`\nRunning 'locize ${n.join(" ")}'...`));const t=await e("locize",n,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),t?.stdout&&console.log(t.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${i}'.`)),console.error(s||n.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${i}' completed successfully.`))}const a=(e,o)=>c("sync",e,o),l=(e,o)=>c("download",e,o),u=(e,o)=>c("migrate",e,o);export{l as runLocizeDownload,u as runLocizeMigrate,a as runLocizeSync};
1
+ import{execa as e}from"execa";import o from"chalk";import t from"ora";import n from"inquirer";import{sep as r,resolve as i}from"node:path";function s(e,o,t){const{locize:n={},extract:s}=o,{projectId:c,apiKey:a,version:l}=n,u=[e];if(c&&u.push("--project-id",c),a&&u.push("--api-key",a),l&&u.push("--ver",l),"sync"===e){(t.updateValues??n.updateValues)&&u.push("--update-values","true");(t.srcLngOnly??n.sourceLanguageOnly)&&u.push("--reference-language-only","true");(t.compareMtime??n.compareModificationTime)&&u.push("--compare-modification-time","true");(t.dryRun??n.dryRun)&&u.push("--dry","true")}let p;try{if("string"==typeof s.output){const e=s.output.replace(/\\/g,"/"),o=(e.includes("/{{language}}/")?e.split("/{{language}}/")[0]:e.replace("{{language}}","")).split("/").join(r);p=i(process.cwd(),o)}else if("function"==typeof s.output)try{const e=s.output(o.extract.primaryLanguage||"en"),t=String(e).replace(/\\/g,"/"),n=t.includes("/"+(o.extract.primaryLanguage||"en")+"/")?t.split("/"+(o.extract.primaryLanguage||"en")+"/")[0]:t.replace(o.extract.primaryLanguage||"en","");p=i(process.cwd(),n.split("/").join(r))}catch{p=i(process.cwd(),".")}else p=i(process.cwd(),".")}catch{p=i(process.cwd(),".")}return u.push("--path",p),u}async function c(r,i,c={}){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 a=t(`Running 'locize ${r}'...\n`).start();let l=i;try{const t=s(r,l,c);console.log(o.cyan(`\nRunning 'locize ${t.join(" ")}'...`));const n=await e("locize",t,{stdio:"pipe"});a.succeed(o.green(`'locize ${r}' completed successfully.`)),n?.stdout&&console.log(n.stdout)}catch(t){const i=t.stderr||"";if(i.includes("missing required argument")){const t=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=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(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:t}=await n.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(t){const t=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,n=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.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(t)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(n))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(t){l={...l,locize:t},a.start("Retrying with new credentials...");try{const t=s(r,l,c);console.log(o.cyan(`\nRunning 'locize ${t.join(" ")}'...`));const n=await e("locize",t,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),n?.stdout&&console.log(n.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.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 a=(e,o)=>c("sync",e,o),l=(e,o)=>c("download",e,o),u=(e,o)=>c("migrate",e,o);export{l as runLocizeDownload,u as runLocizeMigrate,a as runLocizeSync};
@@ -1 +1 @@
1
- import{access as t,readFile as r}from"node:fs/promises";import{resolve as e,extname as n}from"node:path";import{createJiti as s}from"jiti";import{getTsConfigAliases as o}from"../config.js";function a(t,r,e=""){return t.replace("{{language}}",r).replace("{{lng}}",r).replace("{{namespace}}",e).replace("{{ns}}",e)}async function c(a){const c=e(process.cwd(),a);try{await t(c)}catch{return null}try{const t=n(c).toLowerCase();if(".json"===t){const t=await r(c,"utf-8");return JSON.parse(t)}if(".ts"===t||".js"===t){const t=await o(),r=s(process.cwd(),{alias:t,interopDefault:!0});return await r.import(c,{default:!0})}return null}catch(t){return console.warn(`Could not parse translation file ${a}:`,t),null}}function u(t,r="json",e=2){const n=JSON.stringify(t,null,e);switch(r){case"js":case"js-esm":return`export default ${n};\n`;case"js-cjs":return`module.exports = ${n};\n`;case"ts":return`export default ${n} as const;\n`;default:return`${n}\n`}}export{a as getOutputPath,c as loadTranslationFile,u as serializeTranslationFile};
1
+ import{access as t,readFile as n}from"node:fs/promises";import{normalize as r,resolve as e,extname as a}from"node:path";import{createJiti as o}from"jiti";import{getTsConfigAliases as s}from"../config.js";function c(t,n,e){if(!t)return r(`locales/${n}/${e??"translation"}.json`);if("function"==typeof t)try{const a=String(t(n,e));return r(a.replace(/\/\/+/g,"/"))}catch{return r(`locales/${n}/${e??"translation"}.json`)}let a=String(t);return a=a.replace(/\{\{language\}\}|\{\{lng\}\}/g,n),a=null!=e?a.replace(/\{\{namespace\}\}/g,e):a.replace(/\/?\{\{namespace\}\}/g,""),a=a.replace(/\/\/+/g,"/"),r(a)}async function l(r){const c=e(process.cwd(),r);try{await t(c)}catch{return null}try{const t=a(c).toLowerCase();if(".json"===t){const t=await n(c,"utf-8");return JSON.parse(t)}if(".ts"===t||".js"===t){const t=await s(),n=o(process.cwd(),{alias:t,interopDefault:!0});return await n.import(c,{default:!0})}return null}catch(t){return console.warn(`Could not parse translation file ${r}:`,t),null}}function i(t,n="json",r=2){const e=JSON.stringify(t,null,r);switch(n){case"js":case"js-esm":return`export default ${e};\n`;case"js-cjs":return`module.exports = ${e};\n`;case"ts":return`export default ${e} as const;\n`;default:return`${e}\n`}}export{c as getOutputPath,l as loadTranslationFile,i as serializeTranslationFile};
@@ -1 +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};
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("string"==typeof t.extract.output&&!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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,31 +55,31 @@
55
55
  "devDependencies": {
56
56
  "@rollup/plugin-terser": "0.4.4",
57
57
  "@types/inquirer": "9.0.9",
58
- "@types/node": "24.6.1",
59
- "@types/react": "19.1.16",
60
- "@vitest/coverage-v8": "3.2.4",
61
- "eslint": "9.36.0",
58
+ "@types/node": "24.9.2",
59
+ "@types/react": "19.2.2",
60
+ "@vitest/coverage-v8": "4.0.4",
61
+ "eslint": "9.38.0",
62
62
  "eslint-plugin-import": "2.32.0",
63
- "memfs": "4.47.0",
63
+ "memfs": "4.50.0",
64
64
  "neostandard": "0.12.2",
65
65
  "rollup-plugin-typescript2": "0.36.0",
66
66
  "ts-node": "10.9.2",
67
67
  "typescript": "5.9.3",
68
- "unplugin-swc": "1.5.7",
69
- "vitest": "3.2.4"
68
+ "unplugin-swc": "1.5.8",
69
+ "vitest": "4.0.4"
70
70
  },
71
71
  "dependencies": {
72
72
  "@swc/core": "1.13.19",
73
73
  "chalk": "5.6.2",
74
74
  "chokidar": "4.0.3",
75
- "commander": "14.0.1",
75
+ "commander": "14.0.2",
76
76
  "execa": "9.6.0",
77
77
  "glob": "11.0.3",
78
78
  "i18next-resources-for-ts": "1.7.4",
79
- "inquirer": "12.9.6",
79
+ "inquirer": "12.10.0",
80
80
  "jiti": "2.6.1",
81
81
  "jsonc-parser": "3.3.1",
82
- "minimatch": "10.0.3",
82
+ "minimatch": "10.1.1",
83
83
  "ora": "9.0.0",
84
84
  "swc-walk": "1.0.0"
85
85
  }
package/src/cli.ts CHANGED
@@ -22,7 +22,7 @@ const program = new Command()
22
22
  program
23
23
  .name('i18next-cli')
24
24
  .description('A unified, high-performance i18next CLI.')
25
- .version('1.16.0')
25
+ .version('1.17.1')
26
26
 
27
27
  // new: global config override option
28
28
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)')
@@ -238,7 +238,7 @@ program
238
238
  program.parse(process.argv)
239
239
 
240
240
  const toArray = (v: any) => Array.isArray(v) ? v : (v ? [v] : [])
241
- const deriveOutputIgnore = (output?: string) => {
241
+ const deriveOutputIgnore = (output?: string | ((language: string, namespace?: string) => string)) => {
242
242
  if (!output || typeof output !== 'string') return []
243
243
  return [output.replace(/\{\{[^}]+\}\}/g, '*')]
244
244
  }
@@ -396,7 +396,10 @@ export async function getTranslations (
396
396
 
397
397
  // Process each locale one by one
398
398
  for (const locale of config.locales) {
399
- const shouldMerge = config.extract.mergeNamespaces || !config.extract.output.includes('{{namespace}}')
399
+ // If output is a string we can detect the presence of the namespace placeholder.
400
+ // If it's a function we cannot reliably detect that here — default to not merged
401
+ // unless mergeNamespaces is explicitly true.
402
+ const shouldMerge = config.extract.mergeNamespaces || (typeof config.extract.output === 'string' ? !config.extract.output.includes('{{namespace}}') : false)
400
403
 
401
404
  // LOGIC PATH 1: Merged Namespaces
402
405
  if (shouldMerge) {
@@ -429,7 +432,9 @@ export async function getTranslations (
429
432
  // Find all namespaces that exist on disk for this locale
430
433
  const namespacesToProcess = new Set(keysByNS.keys())
431
434
  const existingNsPattern = getOutputPath(config.extract.output, locale, '*')
432
- const existingNsFiles = await glob(existingNsPattern, { ignore: userIgnore })
435
+ // Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
436
+ const existingNsGlobPattern = existingNsPattern.replace(/\\/g, '/')
437
+ const existingNsFiles = await glob(existingNsGlobPattern, { ignore: userIgnore })
433
438
  for (const file of existingNsFiles) {
434
439
  namespacesToProcess.add(basename(file, extname(file)))
435
440
  }
package/src/init.ts CHANGED
@@ -69,6 +69,12 @@ export async function runInit () {
69
69
  }
70
70
  if (typeof detectedConfig?.extract?.input === 'string') detectedConfig.extract.input = [detectedConfig?.extract?.input]
71
71
 
72
+ // If heuristic detection returned a function for extract.output, don't use it as a prompt default.
73
+ // Prompt defaults must be strings; leave undefined so the prompt falls back to a sensible default.
74
+ if (detectedConfig && typeof detectedConfig.extract?.output === 'function') {
75
+ delete (detectedConfig.extract as any).output
76
+ }
77
+
72
78
  const answers = await inquirer.prompt([
73
79
  {
74
80
  type: 'list',
@@ -93,7 +99,10 @@ export async function runInit () {
93
99
  type: 'input',
94
100
  name: 'output',
95
101
  message: 'What is the path for your output resource files?',
96
- default: detectedConfig?.extract?.output || 'public/locales/{{language}}/{{namespace}}.json',
102
+ // ensure the default is a string (detectedConfig.extract.output may be a function)
103
+ default: typeof detectedConfig?.extract?.output === 'string'
104
+ ? detectedConfig!.extract!.output!
105
+ : 'public/locales/{{language}}/{{namespace}}.json',
97
106
  },
98
107
  ])
99
108
 
package/src/locize.ts CHANGED
@@ -143,14 +143,37 @@ function buildArgs (command: string, config: I18nextToolkitConfig, cliOptions: a
143
143
  if (dryRun) commandArgs.push('--dry', 'true')
144
144
  }
145
145
 
146
- // Normalize path separators in the configured output so splitting works on both POSIX and Windows
147
- const outputNormalized = String(extract.output || '').replace(/\\/g, '/')
148
- const baseCandidate = outputNormalized.includes('/{{language}}/')
149
- ? outputNormalized.split('/{{language}}/')[0]
150
- : outputNormalized.replace('{{language}}', '')
151
- // Convert to OS-specific separators before resolving so the resulting path uses path.sep
152
- const baseCandidateWithSep = baseCandidate.split('/').join(sep)
153
- const basePath = resolve(process.cwd(), baseCandidateWithSep)
146
+ // Derive a sensible base path for locize from the configured output.
147
+ // If output is a string template we can strip the language placeholder.
148
+ // If output is a function we cannot reliably infer the base; fall back to cwd.
149
+ let basePath: string
150
+ try {
151
+ if (typeof extract.output === 'string') {
152
+ const outputNormalized = extract.output.replace(/\\/g, '/')
153
+ const baseCandidate = outputNormalized.includes('/{{language}}/')
154
+ ? outputNormalized.split('/{{language}}/')[0]
155
+ : outputNormalized.replace('{{language}}', '')
156
+ const baseCandidateWithSep = baseCandidate.split('/').join(sep)
157
+ basePath = resolve(process.cwd(), baseCandidateWithSep)
158
+ } else if (typeof extract.output === 'function') {
159
+ // Try calling the function with the primary language to get an example path,
160
+ // then strip the language folder if present. If that fails, fallback to cwd.
161
+ try {
162
+ const sample = extract.output(config.extract.primaryLanguage || 'en')
163
+ const sampleNormalized = String(sample).replace(/\\/g, '/')
164
+ const baseCandidate = sampleNormalized.includes('/' + (config.extract.primaryLanguage || 'en') + '/')
165
+ ? sampleNormalized.split('/' + (config.extract.primaryLanguage || 'en') + '/')[0]
166
+ : sampleNormalized.replace(config.extract.primaryLanguage || 'en', '')
167
+ basePath = resolve(process.cwd(), baseCandidate.split('/').join(sep))
168
+ } catch {
169
+ basePath = resolve(process.cwd(), '.')
170
+ }
171
+ } else {
172
+ basePath = resolve(process.cwd(), '.')
173
+ }
174
+ } catch {
175
+ basePath = resolve(process.cwd(), '.')
176
+ }
154
177
 
155
178
  commandArgs.push('--path', basePath)
156
179
 
package/src/types.ts CHANGED
@@ -34,7 +34,7 @@ export interface I18nextToolkitConfig {
34
34
  ignore?: string | string[];
35
35
 
36
36
  /** Output path template with placeholders: {{language}} for locale, {{namespace}} for namespace */
37
- output: string;
37
+ output: string | ((language: string, namespace?: string) => string);
38
38
 
39
39
  /**
40
40
  * Default namespace when none is specified (default: 'translation').
@@ -1,5 +1,5 @@
1
1
  import { mkdir, readFile, writeFile, access } from 'node:fs/promises'
2
- import { dirname, extname, resolve } from 'node:path'
2
+ import { dirname, extname, resolve, normalize } from 'node:path'
3
3
  import { createJiti } from 'jiti'
4
4
  import type { I18nextToolkitConfig } from '../types'
5
5
  import { getTsConfigAliases } from '../config'
@@ -54,33 +54,48 @@ export async function writeFileAsync (filePath: string, data: string): Promise<v
54
54
  }
55
55
 
56
56
  /**
57
- * Generates a file path by replacing template placeholders with actual values.
58
- * Supports both legacy and modern placeholder formats for language and namespace.
57
+ * Resolve an output template (string or function) into an actual path string.
59
58
  *
60
- * @param template - The template string containing placeholders
61
- * @param locale - The locale/language code to substitute
62
- * @param namespace - The namespace to substitute
63
- * @returns The resolved file path with placeholders replaced
64
- *
65
- * @example
66
- * ```typescript
67
- * // Modern format
68
- * getOutputPath('locales/{{language}}/{{namespace}}.json', 'de', 'validation')
69
- * // Returns: 'locales/de/validation.json'
70
- *
71
- * // Legacy format (also supported)
72
- * getOutputPath('locales/{{lng}}/{{ns}}.json', 'en', 'common')
73
- * // Returns: 'locales/en/common.json'
74
- * ```
59
+ * - If `outputTemplate` is a function, call it with (language, namespace)
60
+ * - If it's a string, replace placeholders:
61
+ * - {{language}} or {{lng}} -> language
62
+ * - {{namespace}} -> namespace (or removed if namespace is undefined)
63
+ * - Normalizes duplicate slashes and returns a platform-correct path.
75
64
  */
76
65
  export function getOutputPath (
77
- template: string,
78
- locale: string,
79
- namespace: string = ''
66
+ outputTemplate: string | ((language: string, namespace?: string) => string) | undefined,
67
+ language: string,
68
+ namespace?: string
80
69
  ): string {
81
- return template
82
- .replace('{{language}}', locale).replace('{{lng}}', locale)
83
- .replace('{{namespace}}', namespace).replace('{{ns}}', namespace)
70
+ if (!outputTemplate) {
71
+ // Fallback to a sensible default
72
+ return normalize(`locales/${language}/${namespace ?? 'translation'}.json`)
73
+ }
74
+
75
+ if (typeof outputTemplate === 'function') {
76
+ try {
77
+ const result = String(outputTemplate(language, namespace))
78
+ return normalize(result.replace(/\/\/+/g, '/'))
79
+ } catch {
80
+ // If user function throws, fallback to default path
81
+ return normalize(`locales/${language}/${namespace ?? 'translation'}.json`)
82
+ }
83
+ }
84
+
85
+ // It's a string template
86
+ let out = String(outputTemplate)
87
+ out = out.replace(/\{\{language\}\}|\{\{lng\}\}/g, language)
88
+
89
+ if (namespace !== undefined && namespace !== null) {
90
+ out = out.replace(/\{\{namespace\}\}/g, namespace)
91
+ } else {
92
+ // remove any occurrences of /{{namespace}} or {{namespace}} (keeping surrounding slashes tidy)
93
+ out = out.replace(/\/?\{\{namespace\}\}/g, '')
94
+ }
95
+
96
+ // collapse duplicate slashes and normalize to platform-specific separators
97
+ out = out.replace(/\/\/+/g, '/')
98
+ return normalize(out)
84
99
  }
85
100
 
86
101
  /**
@@ -36,8 +36,11 @@ export function validateExtractorConfig (config: I18nextToolkitConfig): void {
36
36
  throw new ExtractorError('locales must be specified and non-empty')
37
37
  }
38
38
 
39
- if (!config.extract.output.includes('{{language}}') && !config.extract.output.includes('{{lng}}')) {
40
- throw new ExtractorError('extract.output must contain {{language}} placeholder')
39
+ // If output is a function, we accept it (user is responsible for producing a valid path).
40
+ if (typeof config.extract.output === 'string') {
41
+ if (!config.extract.output.includes('{{language}}') && !config.extract.output.includes('{{lng}}')) {
42
+ throw new ExtractorError('extract.output must contain {{language}} placeholder')
43
+ }
41
44
  }
42
45
  }
43
46
 
@@ -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;AA2UnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9E,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAwF9B"}
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;AA2UnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9E,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6F9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,OAAO,kBA0G5B"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,OAAO,kBAmH5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"locize.d.ts","sourceRoot":"","sources":["../src/locize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAwOnD,eAAO,MAAM,aAAa,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAiD,CAAA;AAC7H,eAAO,MAAM,iBAAiB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAqD,CAAA;AACrI,eAAO,MAAM,gBAAgB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAoD,CAAA"}
1
+ {"version":3,"file":"locize.d.ts","sourceRoot":"","sources":["../src/locize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AA+PnD,eAAO,MAAM,aAAa,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAiD,CAAA;AAC7H,eAAO,MAAM,iBAAiB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAqD,CAAA;AACrI,eAAO,MAAM,gBAAgB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAoD,CAAA"}
package/types/types.d.ts CHANGED
@@ -30,7 +30,7 @@ export interface I18nextToolkitConfig {
30
30
  /** Glob pattern(s) for files to ignore during extraction */
31
31
  ignore?: string | string[];
32
32
  /** Output path template with placeholders: {{language}} for locale, {{namespace}} for namespace */
33
- output: string;
33
+ output: string | ((language: string, namespace?: string) => string);
34
34
  /**
35
35
  * Default namespace when none is specified (default: 'translation').
36
36
  * Set to false will not generate any namespace, useful if i.e. the output is a single language json with 1 namespace (and no nesting).
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -39,26 +39,15 @@ export declare function readFileAsync(filePath: string): Promise<string>;
39
39
  */
40
40
  export declare function writeFileAsync(filePath: string, data: string): Promise<void>;
41
41
  /**
42
- * Generates a file path by replacing template placeholders with actual values.
43
- * Supports both legacy and modern placeholder formats for language and namespace.
42
+ * Resolve an output template (string or function) into an actual path string.
44
43
  *
45
- * @param template - The template string containing placeholders
46
- * @param locale - The locale/language code to substitute
47
- * @param namespace - The namespace to substitute
48
- * @returns The resolved file path with placeholders replaced
49
- *
50
- * @example
51
- * ```typescript
52
- * // Modern format
53
- * getOutputPath('locales/{{language}}/{{namespace}}.json', 'de', 'validation')
54
- * // Returns: 'locales/de/validation.json'
55
- *
56
- * // Legacy format (also supported)
57
- * getOutputPath('locales/{{lng}}/{{ns}}.json', 'en', 'common')
58
- * // Returns: 'locales/en/common.json'
59
- * ```
44
+ * - If `outputTemplate` is a function, call it with (language, namespace)
45
+ * - If it's a string, replace placeholders:
46
+ * - {{language}} or {{lng}} -> language
47
+ * - {{namespace}} -> namespace (or removed if namespace is undefined)
48
+ * - Normalizes duplicate slashes and returns a platform-correct path.
60
49
  */
61
- export declare function getOutputPath(template: string, locale: string, namespace?: string): string;
50
+ export declare function getOutputPath(outputTemplate: string | ((language: string, namespace?: string) => string) | undefined, language: string, namespace?: string): string;
62
51
  /**
63
52
  * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
64
53
  * @param filePath - The path to the translation file.
@@ -1 +1 @@
1
- {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAGpD;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,MAAW,GACrB,MAAM,CAIR;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAgChG;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,EAChE,WAAW,GAAE,MAAM,GAAG,MAAU,GAC/B,MAAM,CAgBR"}
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAGpD;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,cAAc,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,EACvF,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CA8BR;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAgChG;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,EAChE,WAAW,GAAE,MAAM,GAAG,MAAU,GAC/B,MAAM,CAgBR"}
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAE,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAgB3E;AAED;;;;;;;;GAQG;AACH,qBAAa,cAAe,SAAQ,KAAK;aAUrB,IAAI,CAAC,EAAE,MAAM;aACb,KAAK,CAAC,EAAE,KAAK;IAV/B;;;;;;OAMG;gBAED,OAAO,EAAE,MAAM,EACC,IAAI,CAAC,EAAE,MAAM,YAAA,EACb,KAAK,CAAC,EAAE,KAAK,YAAA;CAKhC"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAE,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAmB3E;AAED;;;;;;;;GAQG;AACH,qBAAa,cAAe,SAAQ,KAAK;aAUrB,IAAI,CAAC,EAAE,MAAM;aACb,KAAK,CAAC,EAAE,KAAK;IAV/B;;;;;;OAMG;gBAED,OAAO,EAAE,MAAM,EACC,IAAI,CAAC,EAAE,MAAM,YAAA,EACb,KAAK,CAAC,EAAE,KAAK,YAAA;CAKhC"}