i18next-cli 0.9.9 → 0.9.10
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 +6 -0
- package/README.md +60 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/extractor.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +1 -1
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -1
- package/dist/cjs/heuristic-config.js +1 -1
- package/dist/cjs/status.js +1 -1
- package/dist/cjs/syncer.js +1 -1
- package/dist/cjs/utils/file-utils.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/extractor.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +1 -1
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -1
- package/dist/esm/heuristic-config.js +1 -1
- package/dist/esm/status.js +1 -1
- package/dist/esm/syncer.js +1 -1
- package/dist/esm/utils/file-utils.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/extractor/core/extractor.ts +13 -7
- package/src/extractor/core/translation-manager.ts +38 -21
- package/src/extractor/parsers/ast-visitors.ts +1 -1
- package/src/heuristic-config.ts +14 -3
- package/src/status.ts +21 -13
- package/src/syncer.ts +77 -67
- package/src/types.ts +18 -0
- package/src/utils/file-utils.ts +54 -1
- package/types/extractor/core/extractor.d.ts.map +1 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/heuristic-config.d.ts.map +1 -1
- package/types/status.d.ts.map +1 -1
- package/types/syncer.d.ts.map +1 -1
- package/types/types.d.ts +16 -0
- package/types/types.d.ts.map +1 -1
- package/types/utils/file-utils.d.ts +16 -1
- package/types/utils/file-utils.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
- not yet released
|
|
11
11
|
|
|
12
|
+
## [0.9.10] - 2025-09-25
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **JavaScript/TypeScript Translation Files:** Added the `outputFormat` option to support generating translation files as `.json` (default), `.js` (ESM or CJS), or `.ts` modules.
|
|
16
|
+
- **Merged Namespace Files:** Added the `mergeNamespaces` option to combine all namespaces into a single file per language, streamlining imports and file structures.
|
|
17
|
+
|
|
12
18
|
## [0.9.9] - 2025-09-25
|
|
13
19
|
|
|
14
20
|
- **Extractor:** Now supports static and dynamic (ternary) `context` options in both `t()` and `<Trans>`.
|
package/README.md
CHANGED
|
@@ -271,6 +271,13 @@ export default defineConfig({
|
|
|
271
271
|
extract: {
|
|
272
272
|
input: ['src/**/*.{ts,tsx}'],
|
|
273
273
|
output: 'locales/{{language}}/{{namespace}}.json',
|
|
274
|
+
|
|
275
|
+
// Use '.ts' files with `export default` instead of '.json'
|
|
276
|
+
outputFormat: 'ts',
|
|
277
|
+
|
|
278
|
+
// Combine all namespaces into a single file per language (e.g., locales/en.ts)
|
|
279
|
+
// Note: `output` path must not contain `{{namespace}}` when this is true.
|
|
280
|
+
mergeNamespaces: false,
|
|
274
281
|
|
|
275
282
|
// Translation functions to detect
|
|
276
283
|
functions: ['t', 'i18n.t', 'i18next.t'],
|
|
@@ -416,6 +423,59 @@ Extract keys from comments for documentation or edge cases:
|
|
|
416
423
|
// t('user.greeting', { defaultValue: 'Hello!', ns: 'common' })
|
|
417
424
|
```
|
|
418
425
|
|
|
426
|
+
### JavaScript & TypeScript Translation Files
|
|
427
|
+
|
|
428
|
+
For projects that prefer to keep everything in a single module type, you can configure the CLI to output JavaScript or TypeScript files instead of JSON.
|
|
429
|
+
|
|
430
|
+
Configuration (`i18next.config.ts`):
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
export default defineConfig({
|
|
434
|
+
extract: {
|
|
435
|
+
output: 'src/locales/{{language}}/{{namespace}}.ts', // Note the .ts extension
|
|
436
|
+
outputFormat: 'ts', // Use TypeScript with ES Modules
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
This will generate files like `src/locales/en/translation.ts` with the following content:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
export default {
|
|
445
|
+
"myKey": "My value"
|
|
446
|
+
} as const;
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Merging Namespaces
|
|
450
|
+
|
|
451
|
+
You can also combine all namespaces into a single file per language. This is useful for reducing the number of network requests in some application setups.
|
|
452
|
+
|
|
453
|
+
Configuration (`i18next.config.ts`):
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
export default defineConfig({
|
|
457
|
+
extract: {
|
|
458
|
+
// Note: The `output` path no longer contains the {{namespace}} placeholder
|
|
459
|
+
output: 'src/locales/{{language}}.ts',
|
|
460
|
+
outputFormat: 'ts',
|
|
461
|
+
mergeNamespaces: true,
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
This will generate a single file per language, like `src/locales/en.ts`, with namespaces as top-level keys:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
export default {
|
|
470
|
+
"translation": {
|
|
471
|
+
"key1": "Value 1"
|
|
472
|
+
},
|
|
473
|
+
"common": {
|
|
474
|
+
"keyA": "Value A"
|
|
475
|
+
}
|
|
476
|
+
} as const;
|
|
477
|
+
```
|
|
478
|
+
|
|
419
479
|
## Migration from i18next-parser
|
|
420
480
|
|
|
421
481
|
Automatically migrate from legacy `i18next-parser.config.js`:
|
package/dist/cjs/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:fs/promises"),require("
|
|
2
|
+
"use strict";var e=require("commander"),t=require("chokidar"),o=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:path"),require("node:fs/promises"),require("jiti");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),p=require("./locize.js");const f=new e.Command;f.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.10"),f.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const t=await r.runExtractor(a);e.ci&&t&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),f.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(e,t)=>{let o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{detail:e,namespace:t.namespace})}),f.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");t.watch(await o.glob(n.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),f.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),f.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),f.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),f.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const t=await a.detectConfig();t||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=t}await d.runLinter(e)}),f.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeSync(t,e)}),f.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeDownload(t,e)}),f.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const t=await i.ensureConfig();await p.runLocizeMigrate(t,e)}),f.parse(process.argv);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("ora"),t=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),o=require("./key-finder.js"),s=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),l=require("../parsers/comment-parser.js"),u=require("../../utils/logger.js");function g(e,t,r,a=new u.ConsoleLogger){if(e&&"object"==typeof e){for(const n of t)try{n.onVisitNode?.(e,r)}catch(e){a.warn(`Plugin ${n.name} onVisitNode failed:`,e)}for(const n of Object.keys(e)){const o=e[n];if(Array.isArray(o))for(const e of o)e&&"object"==typeof e&&g(e,t,r,a);else o&&"object"==typeof o&&g(o,t,r,a)}}}exports.extract=async function(e){e.extract.primaryLanguage
|
|
1
|
+
"use strict";var e=require("ora"),t=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),o=require("./key-finder.js"),s=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),l=require("../parsers/comment-parser.js"),u=require("../../utils/logger.js"),f=require("../../utils/file-utils.js");function g(e,t,r,a=new u.ConsoleLogger){if(e&&"object"==typeof e){for(const n of t)try{n.onVisitNode?.(e,r)}catch(e){a.warn(`Plugin ${n.name} onVisitNode failed:`,e)}for(const n of Object.keys(e)){const o=e[n];if(Array.isArray(o))for(const e of o)e&&"object"==typeof e&&g(e,t,r,a);else o&&"object"==typeof o&&g(o,t,r,a)}}}exports.extract=async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(t=>t!==e?.extract?.primaryLanguage),e.extract.functions||=["t"],e.extract.transComponents||=["Trans"];const{allKeys:t,objectKeys:r}=await o.findKeys(e);return s.getTranslations(t,r,e)},exports.processFile=async function(e,t,n,o,s=new u.ConsoleLogger){try{let i=await a.readFile(e,"utf-8");for(const r of t.plugins||[])i=await(r.onLoad?.(i,e))??i;const u=await r.parse(i,{syntax:"typescript",tsx:!0,comments:!0}),f=c.createPluginContext(n);l.extractKeysFromComments(i,t.extract.functions||["t"],f,t),o.visit(u),(t.plugins||[]).length>0&&g(u,t.plugins||[],f,s)}catch(t){throw new i.ExtractorError("Failed to process file",e,t)}},exports.runExtractor=async function(r,c=new u.ConsoleLogger){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(e=>e!==r?.extract?.primaryLanguage),i.validateExtractorConfig(r);const l=e("Running i18next key extractor...\n").start();try{const{allKeys:e,objectKeys:i}=await o.findKeys(r,c);l.text=`Found ${e.size} unique keys. Updating translation files...`;const u=await s.getTranslations(e,i,r);let g=!1;for(const e of u)if(e.updated){g=!0;const o=f.serializeTranslationFile(e.newTranslations,r.extract.outputFormat,r.extract.indentation);await a.mkdir(n.dirname(e.path),{recursive:!0}),await a.writeFile(e.path,o),c.info(t.green(`Updated: ${e.path}`))}return l.succeed(t.bold("Extraction complete!")),g}catch(e){throw l.fail(t.red("Extraction failed.")),e}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var t=require("node:path"),e=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function a(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}exports.getTranslations=async function(n,r,o){const c=o.extract.defaultNS??"translation",i=o.extract.keySeparator??".",l=[...o.extract.preservePatterns||[]],u=o.extract.mergeNamespaces??!1;for(const t of r)l.push(`${t}.*`);const p=l.map(a);o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(t=>t!==o?.extract?.primaryLanguage);const g=o.extract.primaryLanguage,f=new Map;for(const t of n.values()){const e=t.ns||c;f.has(e)||f.set(e,[]),f.get(e).push(t)}const d=[];for(const a of o.locales){const n={},r={};for(const[c,l]of f.entries()){const f=s.getOutputPath(o.extract.output,a,u?void 0:c),x=t.resolve(process.cwd(),f),y=await s.loadTranslationFile(x)||{},h={},N=e.getNestedKeys(y,i);for(const t of N)if(p.some(e=>e.test(t))){const s=e.getNestedValue(y,t,i);e.setNestedValue(h,t,s,i)}const m=!1===o.extract.sort?l:[...l].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:s}of m){const n=e.getNestedValue(y,t,i)??(a===g?s:o.extract.defaultValue??"");e.setNestedValue(h,t,n,i)}if(u)n[c]=h,Object.keys(y).length>0&&(r[c]=y);else{const t=y?JSON.stringify(y,null,o.extract.indentation??2):"",e=JSON.stringify(h,null,o.extract.indentation??2);d.push({path:x,updated:e!==t,newTranslations:h,existingTranslations:y})}}if(u){const e=s.getOutputPath(o.extract.output,a),c=t.resolve(process.cwd(),e),i=Object.keys(r).length>0?JSON.stringify(r,null,o.extract.indentation??2):"",l=JSON.stringify(n,null,o.extract.indentation??2);d.push({path:c,updated:l!==i,newTranslations:n,existingTranslations:r})}}return d};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("./jsx-parser.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||[]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}};
|
|
1
|
+
"use strict";var e=require("./jsx-parser.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||["t"]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("glob"),s=require("node:fs/promises"),n=require("node:path");const
|
|
1
|
+
"use strict";var e=require("glob"),s=require("node:fs/promises"),n=require("node:path");const t=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","app/i18n/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json","app/i18n/locales/en/*.json"];exports.detectConfig=async function(){for(const o of t){const t=await e.glob(o,{ignore:"node_modules/**"});if(t.length>0){const e=t[0],o=n.dirname(n.dirname(e)),l=n.extname(e);let a="json";".ts"===l?a="ts":".js"===l&&(a="js");try{let e=(await s.readdir(o)).filter(e=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(e));if(e.length>0)return e.sort(),e.includes("dev")&&(e=["dev",...e.filter(e=>"dev"!==e)]),e.includes("en")&&(e=["en",...e.filter(e=>"en"!==e)]),{locales:e,extract:{input:["src/**/*.{js,jsx,ts,tsx}","app/**/*.{js,jsx,ts,tsx}","pages/**/*.{js,jsx,ts,tsx}","components/**/*.{js,jsx,ts,tsx}"],output:n.join(o,"{{language}}",`{{namespace}}${l}`),outputFormat:a,primaryLanguage:e.includes("en")?"en":e[0]}}}catch{continue}}}return null};
|
package/dist/cjs/status.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),a=require("
|
|
1
|
+
"use strict";var e=require("chalk"),o=require("ora"),t=require("node:path"),a=require("./extractor/core/key-finder.js"),s=require("./utils/nested-object.js"),n=require("./utils/file-utils.js");function l(o,t,a){const s=a>0?Math.round(t/a*100):100,n=r(s);console.log(`${e.bold(o)}: ${n} ${s}% (${t}/${a})`)}function r(o){const t=Math.round(o/100*20),a=20-t;return`[${e.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function c(){console.log(e.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${e.cyan("npx i18next-cli locize-migrate")} to get started.`)}exports.runStatus=async function(i,u={}){i.extract.primaryLanguage||=i.locales[0]||"en",i.extract.secondaryLanguages||=i.locales.filter(e=>e!==i?.extract?.primaryLanguage);const d=o("Analyzing project localization status...\n").start();try{const o=await async function(e){e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(o=>o!==e?.extract?.primaryLanguage);const{allKeys:o}=await a.findKeys(e),{secondaryLanguages:l,keySeparator:r=".",defaultNS:c="translation",mergeNamespaces:i=!1}=e.extract,u=new Map;for(const e of o.values()){const o=e.ns||c;u.has(o)||u.set(o,[]),u.get(o).push(e)}const d={totalKeys:o.size,keysByNs:u,locales:new Map};for(const o of l){let a=0;const l=new Map,c=i?await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o)))||{}:null;for(const[d,y]of u.entries()){const u=i?c?.[d]||{}:await n.loadTranslationFile(t.resolve(process.cwd(),n.getOutputPath(e.extract.output,o,d)))||{};let g=0;const f=y.map(({key:e})=>{const o=!!s.getNestedValue(u,e,r??".");return o&&g++,{key:e,isTranslated:o}});l.set(d,{totalKeys:y.length,translatedKeys:g,keyDetails:f}),a+=g}d.locales.set(o,{totalTranslated:a,namespaces:l})}return d}(i);d.succeed("Analysis complete."),function(o,t,a){a.detail?function(o,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(e.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(e.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=o.locales.get(a);if(!n)return void console.error(e.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(e.bold(`\nKey Status for "${e.cyan(a)}":`));const r=Array.from(o.keysByNs.values()).flat().length;l("Overall",n.totalTranslated,r);const i=s?[s]:Array.from(n.namespaces.keys()).sort();for(const o of i){const t=n.namespaces.get(o);t&&(console.log(e.cyan.bold(`\nNamespace: ${o}`)),l("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:o,isTranslated:t})=>{const a=t?e.green("✓"):e.red("✗");console.log(` ${a} ${o}`)}))}const u=r-n.totalTranslated;u>0?console.log(e.yellow.bold(`\nSummary: Found ${u} missing translations for "${a}".`)):console.log(e.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));c()}(o,t,a.detail,a.namespace):a.namespace?function(o,t,a){const s=o.keysByNs.get(a);if(!s)return void console.error(e.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(e.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[e,t]of o.locales.entries()){const o=t.namespaces.get(a);if(o){const t=o.totalKeys>0?Math.round(o.translatedKeys/o.totalKeys*100):100,a=r(t);console.log(`- ${e}: ${a} ${t}% (${o.translatedKeys}/${o.totalKeys} keys)`)}}c()}(o,0,a.namespace):function(o,t){const{primaryLanguage:a}=t.extract;console.log(e.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${e.bold(o.totalKeys)}`),console.log(`📚 Namespaces Found: ${e.bold(o.keysByNs.size)}`),console.log(`🌍 Locales: ${e.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${e.bold(a)}`),console.log("\nTranslation Progress:");for(const[e,t]of o.locales.entries()){const a=o.totalKeys>0?Math.round(t.totalTranslated/o.totalKeys*100):100,s=r(a);console.log(`- ${e}: ${s} ${a}% (${t.totalTranslated}/${o.totalKeys} keys)`)}c()}(o,t)}(o,i,u)}catch(e){d.fail("Failed to generate status report."),console.error(e)}};
|
package/dist/cjs/syncer.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("node:fs/promises"),
|
|
1
|
+
"use strict";var e=require("node:fs/promises"),o=require("path"),t=require("chalk"),n=require("ora"),r=require("glob"),a=require("./utils/nested-object.js"),i=require("./utils/file-utils.js");exports.runSyncer=async function(l){const s=n("Running i18next locale synchronizer...\n").start();try{const n=l.extract.primaryLanguage||l.locales[0]||"en",c=l.locales.filter(e=>e!==n),{output:u,keySeparator:d=".",outputFormat:f="json",indentation:y=2,defaultValue:g=""}=l.extract,h=[];let p=!1;const w=i.getOutputPath(u,n,"*"),m=await r.glob(w);if(0===m.length)return void s.warn(`No translation files found for primary language "${n}". Nothing to sync.`);for(const n of m){const r=o.basename(n).split(".")[0],l=await i.loadTranslationFile(n);if(!l){h.push(` ${t.yellow("-")} Could not read primary file: ${n}`);continue}const s=a.getNestedKeys(l,d??".");for(const n of c){const l=i.getOutputPath(u,n,r),c=o.resolve(process.cwd(),l),w=await i.loadTranslationFile(c)||{},m={};for(const e of s){const o=a.getNestedValue(w,e,d??".")??g;a.setNestedValue(m,e,o,d??".")}const S=JSON.stringify(w);if(JSON.stringify(m)!==S){p=!0;const n=i.serializeTranslationFile(m,f,y);await e.mkdir(o.dirname(c),{recursive:!0}),await e.writeFile(c,n),h.push(` ${t.green("✓")} Synchronized: ${l}`)}else h.push(` ${t.gray("-")} Already in sync: ${l}`)}}s.succeed(t.bold("Synchronization complete!")),h.forEach(e=>console.log(e)),p?(console.log(t.green.bold("\n✅ Sync complete.")),console.log(t.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${t.cyan("npx i18next-cli locize-migrate")}`)):console.log(t.green.bold("\n✅ All locales are already in sync."))}catch(e){s.fail(t.red("Synchronization failed.")),console.error(e)}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";require("node:fs/promises")
|
|
1
|
+
"use strict";var e=require("node:fs/promises");require("node:path");var t=require("jiti"),r="undefined"!=typeof document?document.currentScript:null;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){try{if(n.endsWith(".json")){const t=await e.readFile(n,"utf-8");return JSON.parse(t)}if(n.endsWith(".ts")||n.endsWith(".js")){const e=t.createJiti("undefined"==typeof document?require("url").pathToFileURL(__filename).href:r&&"SCRIPT"===r.tagName.toUpperCase()&&r.src||new URL("utils/file-utils.js",document.baseURI).href);return await e.import(n,{default:!0})}}catch(e){return null}return 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}};
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as o}from"commander";import
|
|
2
|
+
import{Command as o}from"commander";import t from"chokidar";import{glob as e}from"glob";import i from"chalk";import{ensureConfig as n,loadConfig as a}from"./config.js";import{detectConfig as c}from"./heuristic-config.js";import{runExtractor as r}from"./extractor/core/extractor.js";import"node:path";import"node:fs/promises";import"jiti";import{runTypesGenerator as s}from"./types-generator.js";import{runSyncer as l}from"./syncer.js";import{runMigrator as m}from"./migrator.js";import{runInit as p}from"./init.js";import{runLinter as d}from"./linter.js";import{runStatus as f}from"./status.js";import{runLocizeSync as g,runLocizeDownload as u,runLocizeMigrate as y}from"./locize.js";const w=new o;w.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.10"),w.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async o=>{const a=await n(),c=async()=>{const t=await r(a);o.ci&&t&&(console.error(i.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(i.yellow("💡 Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${i.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),o.watch){console.log("\nWatching for changes...");t.watch(await e(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),c()})}}),w.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").option("-n, --namespace <ns>","Filter the status report by a specific namespace").action(async(o,t)=>{let e=await a();if(!e){console.log(i.blue("No config file found. Attempting to detect project structure..."));const o=await c();o||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),e=o}await f(e,{detail:o,namespace:t.namespace})}),w.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async o=>{const i=await n(),a=()=>s(i);if(await a(),o.watch){console.log("\nWatching for changes...");t.watch(await e(i.types?.input||[]),{persistent:!0}).on("change",o=>{console.log(`\nFile changed: ${o}`),a()})}}),w.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const o=await n();await l(o)}),w.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await m()}),w.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(p),w.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let o=await a();if(!o){console.log(i.blue("No config file found. Attempting to detect project structure..."));const t=await c();t||(console.error(i.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${i.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(i.green("Project structure detected successfully!")),o=t}await d(o)}),w.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async o=>{const t=await n();await g(t,o)}),w.command("locize-download").description("Download all translations from your locize project.").action(async o=>{const t=await n();await u(t,o)}),w.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async o=>{const t=await n();await y(t,o)}),w.parse(process.argv);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"ora";import
|
|
1
|
+
import t from"ora";import o from"chalk";import{parse as a}from"@swc/core";import{mkdir as e,writeFile as r,readFile as n}from"node:fs/promises";import{dirname as i}from"node:path";import{findKeys as s}from"./key-finder.js";import{getTranslations as c}from"./translation-manager.js";import{validateExtractorConfig as f,ExtractorError as l}from"../../utils/validation.js";import{createPluginContext as p}from"../plugin-manager.js";import{extractKeysFromComments as m}from"../parsers/comment-parser.js";import{ConsoleLogger as u}from"../../utils/logger.js";import{serializeTranslationFile as y}from"../../utils/file-utils.js";async function g(a,n=new u){a.extract.primaryLanguage||=a.locales[0]||"en",a.extract.secondaryLanguages||=a.locales.filter(t=>t!==a?.extract?.primaryLanguage),f(a);const l=t("Running i18next key extractor...\n").start();try{const{allKeys:t,objectKeys:f}=await s(a,n);l.text=`Found ${t.size} unique keys. Updating translation files...`;const p=await c(t,f,a);let m=!1;for(const t of p)if(t.updated){m=!0;const s=y(t.newTranslations,a.extract.outputFormat,a.extract.indentation);await e(i(t.path),{recursive:!0}),await r(t.path,s),n.info(o.green(`Updated: ${t.path}`))}return l.succeed(o.bold("Extraction complete!")),m}catch(t){throw l.fail(o.red("Extraction failed.")),t}}async function d(t,o,e,r,i=new u){try{let s=await n(t,"utf-8");for(const a of o.plugins||[])s=await(a.onLoad?.(s,t))??s;const c=await a(s,{syntax:"typescript",tsx:!0,comments:!0}),f=p(e);m(s,o.extract.functions||["t"],f,o),r.visit(c),(o.plugins||[]).length>0&&x(c,o.plugins||[],f,i)}catch(o){throw new l("Failed to process file",t,o)}}function x(t,o,a,e=new u){if(t&&"object"==typeof t){for(const r of o)try{r.onVisitNode?.(t,a)}catch(t){e.warn(`Plugin ${r.name} onVisitNode failed:`,t)}for(const r of Object.keys(t)){const n=t[r];if(Array.isArray(n))for(const t of n)t&&"object"==typeof t&&x(t,o,a,e);else n&&"object"==typeof n&&x(n,o,a,e)}}}async function w(t){t.extract.primaryLanguage||=t.locales[0]||"en",t.extract.secondaryLanguages||=t.locales.filter(o=>o!==t?.extract?.primaryLanguage),t.extract.functions||=["t"],t.extract.transComponents||=["Trans"];const{allKeys:o,objectKeys:a}=await s(t);return c(o,a,t)}export{w as extract,d as processFile,g as runExtractor};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{resolve as t}from"node:path";import{getNestedKeys as e,getNestedValue as n,setNestedValue as a}from"../../utils/nested-object.js";import{getOutputPath as s,loadTranslationFile as o}from"../../utils/file-utils.js";function r(t){const e=`^${t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(e)}async function c(c,i,l){const u=l.extract.defaultNS??"translation",f=l.extract.keySeparator??".",p=[...l.extract.preservePatterns||[]],g=l.extract.mergeNamespaces??!1;for(const t of i)p.push(`${t}.*`);const x=p.map(r);l.extract.primaryLanguage||=l.locales[0]||"en",l.extract.secondaryLanguages||=l.locales.filter(t=>t!==l?.extract?.primaryLanguage);const d=l.extract.primaryLanguage,y=new Map;for(const t of c.values()){const e=t.ns||u;y.has(e)||y.set(e,[]),y.get(e).push(t)}const m=[];for(const r of l.locales){const c={},i={};for(const[u,p]of y.entries()){const y=s(l.extract.output,r,g?void 0:u),h=t(process.cwd(),y),w=await o(h)||{},k={},N=e(w,f);for(const t of N)if(x.some(e=>e.test(t))){const e=n(w,t,f);a(k,t,e,f)}const O=!1===l.extract.sort?p:[...p].sort((t,e)=>t.key.localeCompare(e.key));for(const{key:t,defaultValue:e}of O){const s=n(w,t,f)??(r===d?e:l.extract.defaultValue??"");a(k,t,s,f)}if(g)c[u]=k,Object.keys(w).length>0&&(i[u]=w);else{const t=w?JSON.stringify(w,null,l.extract.indentation??2):"",e=JSON.stringify(k,null,l.extract.indentation??2);m.push({path:h,updated:e!==t,newTranslations:k,existingTranslations:w})}}if(g){const e=s(l.extract.output,r),n=t(process.cwd(),e),a=Object.keys(i).length>0?JSON.stringify(i,null,l.extract.indentation??2):"",o=JSON.stringify(c,null,l.extract.indentation??2);m.push({path:n,updated:o!==a,newTranslations:c,existingTranslations:i})}}return m}export{c as getTranslations};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{extractFromTransComponent as e}from"./jsx-parser.js";class t{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||[]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}}export{t as ASTVisitors};
|
|
1
|
+
import{extractFromTransComponent as e}from"./jsx-parser.js";class t{pluginContext;config;logger;scopeStack=[];objectKeys=new Set;constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation","getT","useT"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;if("ObjectExpression"===r?.type){const e=this.getObjectPropValue(r,"keyPrefix");s="string"==typeof e?e:void 0}this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=this.getVarFromScope(t.value);if(!((this.config.extract.functions||["t"]).includes(t.value)||void 0!==n)||0===e.arguments.length)return;const i=e.arguments[0].expression,r=[];if("StringLiteral"===i.type)r.push(i.value);else if("ArrowFunctionExpression"===i.type){const e=this.extractKeyFromSelector(i);e&&r.push(e)}else if("ArrayExpression"===i.type)for(const e of i.elements)"StringLiteral"===e?.expression.type&&r.push(e.expression.value);if(0===r.length)return;let s,a;if(e.arguments.length>1){const t=e.arguments[1].expression;"ObjectExpression"===t.type?a=t:"StringLiteral"===t.type&&(s=t.value)}if(e.arguments.length>2){const t=e.arguments[2].expression;"ObjectExpression"===t.type&&(a=t)}const o=a?this.getObjectPropValue(a,"defaultValue"):void 0,l="string"==typeof o?o:s;for(let e=0;e<r.length;e++){let t,i=r[e];if("ObjectExpression"===a?.type){const e=this.getObjectPropValue(a,"ns");"string"==typeof e&&(t=e)}!t&&n?.defaultNs&&(t=n.defaultNs);const s=this.config.extract.nsSeparator??":";if(!t&&s&&i.includes(s)){const e=i.split(s);t=e.shift(),i=e.join(s)}t||(t=this.config.extract.defaultNS);let o=i;if(n?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${n.keyPrefix}${e}${i}`}const p=e===r.length-1&&l||i;if("ObjectExpression"===a?.type){const e=this.getObjectProperty(a,"context");if("ConditionalExpression"===e?.value?.type){const n=this.resolvePossibleStringValues(e.value),i=this.config.extract.contextSeparator??"_";if(n.length>0){n.forEach(e=>{this.pluginContext.addKey({key:`${o}${i}${e}`,ns:t,defaultValue:p})}),this.pluginContext.addKey({key:o,ns:t,defaultValue:p});continue}}const n=this.getObjectPropValue(a,"context");if("string"==typeof n&&n){const e=this.config.extract.contextSeparator??"_";this.pluginContext.addKey({key:`${o}${e}${n}`,ns:t,defaultValue:p});continue}if(void 0!==this.getObjectPropValue(a,"count")){this.handlePluralKeys(o,p,t);continue}!0===this.getObjectPropValue(a,"returnObjects")&&this.objectKeys.add(o)}this.pluginContext.addKey({key:o,ns:t,defaultValue:p})}}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}if(n.ns||(n.ns=this.config.extract.defaultNS),n.contextExpression){const e=this.resolvePossibleStringValues(n.contextExpression),t=this.config.extract.contextSeparator??"_";if(e.length>0){for(const i of e)this.pluginContext.addKey({key:`${n.key}${t}${i}`,ns:n.ns,defaultValue:n.defaultValue});this.pluginContext.addKey(n)}}else n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type||("BooleanLiteral"===e.type||"NumericLiteral"===e.type)?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}resolvePossibleStringValues(e){if("StringLiteral"===e.type)return[e.value];if("ConditionalExpression"===e.type){return[...this.resolvePossibleStringValues(e.consequent),...this.resolvePossibleStringValues(e.alternate)]}return"Identifier"===e.type&&e.value,[]}getObjectProperty(e,t){return e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t))}}export{t as ASTVisitors};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{glob as s}from"glob";import{readdir as e}from"node:fs/promises";import{dirname as n,join as
|
|
1
|
+
import{glob as s}from"glob";import{readdir as e}from"node:fs/promises";import{dirname as n,extname as o,join as t}from"node:path";const l=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","app/i18n/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json","app/i18n/locales/en/*.json"];async function a(){for(const a of l){const l=await s(a,{ignore:"node_modules/**"});if(l.length>0){const s=l[0],a=n(n(s)),c=o(s);let r="json";".ts"===c?r="ts":".js"===c&&(r="js");try{let s=(await e(a)).filter(s=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(s));if(s.length>0)return s.sort(),s.includes("dev")&&(s=["dev",...s.filter(s=>"dev"!==s)]),s.includes("en")&&(s=["en",...s.filter(s=>"en"!==s)]),{locales:s,extract:{input:["src/**/*.{js,jsx,ts,tsx}","app/**/*.{js,jsx,ts,tsx}","pages/**/*.{js,jsx,ts,tsx}","components/**/*.{js,jsx,ts,tsx}"],output:t(a,"{{language}}",`{{namespace}}${c}`),outputFormat:r,primaryLanguage:s.includes("en")?"en":s[0]}}}catch{continue}}}return null}export{a as detectConfig};
|
package/dist/esm/status.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{
|
|
1
|
+
import o from"chalk";import e from"ora";import{resolve as t}from"node:path";import{findKeys as a}from"./extractor/core/key-finder.js";import{getNestedValue as s}from"./utils/nested-object.js";import{loadTranslationFile as n,getOutputPath as l}from"./utils/file-utils.js";async function r(r,d={}){r.extract.primaryLanguage||=r.locales[0]||"en",r.extract.secondaryLanguages||=r.locales.filter(o=>o!==r?.extract?.primaryLanguage);const g=e("Analyzing project localization status...\n").start();try{const e=await async function(o){o.extract.primaryLanguage||=o.locales[0]||"en",o.extract.secondaryLanguages||=o.locales.filter(e=>e!==o?.extract?.primaryLanguage);const{allKeys:e}=await a(o),{secondaryLanguages:r,keySeparator:c=".",defaultNS:i="translation",mergeNamespaces:y=!1}=o.extract,d=new Map;for(const o of e.values()){const e=o.ns||i;d.has(e)||d.set(e,[]),d.get(e).push(o)}const g={totalKeys:e.size,keysByNs:d,locales:new Map};for(const e of r){let a=0;const r=new Map,i=y?await n(t(process.cwd(),l(o.extract.output,e)))||{}:null;for(const[g,u]of d.entries()){const d=y?i?.[g]||{}:await n(t(process.cwd(),l(o.extract.output,e,g)))||{};let f=0;const m=u.map(({key:o})=>{const e=!!s(d,o,c??".");return e&&f++,{key:o,isTranslated:e}});r.set(g,{totalKeys:u.length,translatedKeys:f,keyDetails:m}),a+=f}g.locales.set(e,{totalTranslated:a,namespaces:r})}return g}(r);g.succeed("Analysis complete."),function(e,t,a){a.detail?function(e,t,a,s){if(a===t.extract.primaryLanguage)return void console.log(o.yellow(`Locale "${a}" is the primary language. All keys are considered present.`));if(!t.locales.includes(a))return void console.error(o.red(`Error: Locale "${a}" is not defined in your configuration.`));const n=e.locales.get(a);if(!n)return void console.error(o.red(`Error: Locale "${a}" is not a valid secondary language.`));console.log(o.bold(`\nKey Status for "${o.cyan(a)}":`));const l=Array.from(e.keysByNs.values()).flat().length;c("Overall",n.totalTranslated,l);const r=s?[s]:Array.from(n.namespaces.keys()).sort();for(const e of r){const t=n.namespaces.get(e);t&&(console.log(o.cyan.bold(`\nNamespace: ${e}`)),c("Namespace Progress",t.translatedKeys,t.totalKeys),t.keyDetails.forEach(({key:e,isTranslated:t})=>{const a=t?o.green("✓"):o.red("✗");console.log(` ${a} ${e}`)}))}const i=l-n.totalTranslated;i>0?console.log(o.yellow.bold(`\nSummary: Found ${i} missing translations for "${a}".`)):console.log(o.green.bold(`\nSummary: 🎉 All keys are translated for "${a}".`));y()}(e,t,a.detail,a.namespace):a.namespace?function(e,t,a){const s=e.keysByNs.get(a);if(!s)return void console.error(o.red(`Error: Namespace "${a}" was not found in your source code.`));console.log(o.cyan.bold(`\nStatus for Namespace: "${a}"`)),console.log("------------------------");for(const[o,t]of e.locales.entries()){const e=t.namespaces.get(a);if(e){const t=e.totalKeys>0?Math.round(e.translatedKeys/e.totalKeys*100):100,a=i(t);console.log(`- ${o}: ${a} ${t}% (${e.translatedKeys}/${e.totalKeys} keys)`)}}y()}(e,0,a.namespace):function(e,t){const{primaryLanguage:a}=t.extract;console.log(o.cyan.bold("\ni18next Project Status")),console.log("------------------------"),console.log(`🔑 Keys Found: ${o.bold(e.totalKeys)}`),console.log(`📚 Namespaces Found: ${o.bold(e.keysByNs.size)}`),console.log(`🌍 Locales: ${o.bold(t.locales.join(", "))}`),console.log(`✅ Primary Language: ${o.bold(a)}`),console.log("\nTranslation Progress:");for(const[o,t]of e.locales.entries()){const a=e.totalKeys>0?Math.round(t.totalTranslated/e.totalKeys*100):100,s=i(a);console.log(`- ${o}: ${s} ${a}% (${t.totalTranslated}/${e.totalKeys} keys)`)}y()}(e,t)}(e,r,d)}catch(o){g.fail("Failed to generate status report."),console.error(o)}}function c(e,t,a){const s=a>0?Math.round(t/a*100):100,n=i(s);console.log(`${o.bold(e)}: ${n} ${s}% (${t}/${a})`)}function i(e){const t=Math.round(e/100*20),a=20-t;return`[${o.green("".padStart(t,"■"))}${"".padStart(a,"□")}]`}function y(){console.log(o.yellow.bold("\n✨ Take your localization to the next level!")),console.log("Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started"),console.log(`Run ${o.cyan("npx i18next-cli locize-migrate")} to get started.`)}export{r as runStatus};
|
package/dist/esm/syncer.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{mkdir as o,writeFile as t}from"node:fs/promises";import{basename as e,resolve as n,dirname as r}from"path";import i from"chalk";import l from"ora";import{glob as a}from"glob";import{getNestedKeys as s,getNestedValue as c,setNestedValue as f}from"./utils/nested-object.js";import{getOutputPath as p,loadTranslationFile as u,serializeTranslationFile as y}from"./utils/file-utils.js";async function m(m){const d=l("Running i18next locale synchronizer...\n").start();try{const l=m.extract.primaryLanguage||m.locales[0]||"en",g=m.locales.filter(o=>o!==l),{output:h,keySeparator:w=".",outputFormat:S="json",indentation:$=2,defaultValue:x=""}=m.extract,b=[];let z=!1;const j=p(h,l,"*"),N=await a(j);if(0===N.length)return void d.warn(`No translation files found for primary language "${l}". Nothing to sync.`);for(const l of N){const a=e(l).split(".")[0],m=await u(l);if(!m){b.push(` ${i.yellow("-")} Could not read primary file: ${l}`);continue}const d=s(m,w??".");for(const e of g){const l=p(h,e,a),s=n(process.cwd(),l),m=await u(s)||{},g={};for(const o of d){const t=c(m,o,w??".");f(g,o,t??x,w??".")}const j=JSON.stringify(m);if(JSON.stringify(g)!==j){z=!0;const e=y(g,S,$);await o(r(s),{recursive:!0}),await t(s,e),b.push(` ${i.green("✓")} Synchronized: ${l}`)}else b.push(` ${i.gray("-")} Already in sync: ${l}`)}}d.succeed(i.bold("Synchronization complete!")),b.forEach(o=>console.log(o)),z?(console.log(i.green.bold("\n✅ Sync complete.")),console.log(i.yellow("🚀 Ready to collaborate with translators? Move your files to the cloud.")),console.log(` Get started with the official TMS for i18next: ${i.cyan("npx i18next-cli locize-migrate")}`)):console.log(i.green.bold("\n✅ All locales are already in sync."))}catch(o){d.fail(i.red("Synchronization failed.")),console.error(o)}}export{m as runSyncer};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"node:fs/promises";import"node:path";function
|
|
1
|
+
import{readFile as t}from"node:fs/promises";import"node:path";import{createJiti as e}from"jiti";function n(t,e,n=""){return t.replace("{{language}}",e).replace("{{lng}}",e).replace("{{namespace}}",n).replace("{{ns}}",n)}async function r(n){try{if(n.endsWith(".json")){const e=await t(n,"utf-8");return JSON.parse(e)}if(n.endsWith(".ts")||n.endsWith(".js")){const t=e(import.meta.url);return await t.import(n,{default:!0})}}catch(t){return null}return null}function s(t,e="json",n=2){const r=JSON.stringify(t,null,n);switch(e){case"js":case"js-esm":return`export default ${r};\n`;case"js-cjs":return`module.exports = ${r};\n`;case"ts":return`export default ${r} as const;\n`;default:return r}}export{n as getOutputPath,r as loadTranslationFile,s as serializeTranslationFile};
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { createPluginContext } from '../plugin-manager'
|
|
|
11
11
|
import { extractKeysFromComments } from '../parsers/comment-parser'
|
|
12
12
|
import { ASTVisitors } from '../parsers/ast-visitors'
|
|
13
13
|
import { ConsoleLogger } from '../../utils/logger'
|
|
14
|
+
import { serializeTranslationFile } from '../../utils/file-utils'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Main extractor function that runs the complete key extraction and file generation process.
|
|
@@ -42,8 +43,8 @@ export async function runExtractor (
|
|
|
42
43
|
config: I18nextToolkitConfig,
|
|
43
44
|
logger: Logger = new ConsoleLogger()
|
|
44
45
|
): Promise<boolean> {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
47
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
47
48
|
|
|
48
49
|
validateExtractorConfig(config)
|
|
49
50
|
|
|
@@ -59,8 +60,13 @@ export async function runExtractor (
|
|
|
59
60
|
for (const result of results) {
|
|
60
61
|
if (result.updated) {
|
|
61
62
|
anyFileUpdated = true
|
|
63
|
+
const fileContent = serializeTranslationFile(
|
|
64
|
+
result.newTranslations,
|
|
65
|
+
config.extract.outputFormat,
|
|
66
|
+
config.extract.indentation
|
|
67
|
+
)
|
|
62
68
|
await mkdir(dirname(result.path), { recursive: true })
|
|
63
|
-
await writeFile(result.path,
|
|
69
|
+
await writeFile(result.path, fileContent)
|
|
64
70
|
logger.info(chalk.green(`Updated: ${result.path}`))
|
|
65
71
|
}
|
|
66
72
|
}
|
|
@@ -180,10 +186,10 @@ function traverseEveryNode (node: any, plugins: any[], pluginContext: PluginCont
|
|
|
180
186
|
* ```
|
|
181
187
|
*/
|
|
182
188
|
export async function extract (config: I18nextToolkitConfig) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
190
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
191
|
+
config.extract.functions ||= ['t']
|
|
192
|
+
config.extract.transComponents ||= ['Trans']
|
|
187
193
|
const { allKeys, objectKeys } = await findKeys(config)
|
|
188
194
|
return getTranslations(allKeys, objectKeys, config)
|
|
189
195
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { TranslationResult, ExtractedKey, I18nextToolkitConfig } from '../../types'
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
2
|
import { resolve } from 'node:path'
|
|
4
3
|
import { getNestedValue, setNestedValue, getNestedKeys } from '../../utils/nested-object'
|
|
5
|
-
import { getOutputPath } from '../../utils/file-utils'
|
|
4
|
+
import { getOutputPath, loadTranslationFile } from '../../utils/file-utils'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Converts a glob pattern to a regular expression for matching keys
|
|
@@ -51,13 +50,15 @@ export async function getTranslations (
|
|
|
51
50
|
const defaultNS = config.extract.defaultNS ?? 'translation'
|
|
52
51
|
const keySeparator = config.extract.keySeparator ?? '.'
|
|
53
52
|
const patternsToPreserve = [...(config.extract.preservePatterns || [])]
|
|
53
|
+
const mergeNamespaces = config.extract.mergeNamespaces ?? false
|
|
54
54
|
for (const key of objectKeys) {
|
|
55
55
|
// Convert the object key to a glob pattern to preserve all its children
|
|
56
56
|
patternsToPreserve.push(`${key}.*`)
|
|
57
57
|
}
|
|
58
58
|
const preservePatterns = patternsToPreserve.map(globToRegex)
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
60
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
61
|
+
const primaryLanguage = config.extract.primaryLanguage
|
|
61
62
|
|
|
62
63
|
// Group keys by namespace
|
|
63
64
|
const keysByNS = new Map<string, ExtractedKey[]>()
|
|
@@ -72,21 +73,16 @@ export async function getTranslations (
|
|
|
72
73
|
const results: TranslationResult[] = []
|
|
73
74
|
|
|
74
75
|
for (const locale of config.locales) {
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const mergedTranslations: Record<string, any> = {}
|
|
77
|
+
const mergedExisting: Record<string, any> = {}
|
|
77
78
|
|
|
79
|
+
for (const [ns, nsKeys] of keysByNS.entries()) {
|
|
80
|
+
const outputPath = getOutputPath(config.extract.output, locale, mergeNamespaces ? undefined : ns)
|
|
78
81
|
const fullPath = resolve(process.cwd(), outputPath)
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
let existingTranslations: Record<string, any> = {}
|
|
82
|
-
try {
|
|
83
|
-
oldContent = await readFile(fullPath, 'utf-8')
|
|
84
|
-
existingTranslations = JSON.parse(oldContent)
|
|
85
|
-
} catch (e) { /* File doesn't exist, which is fine */ }
|
|
86
|
-
|
|
83
|
+
const existingTranslations = await loadTranslationFile(fullPath) || {}
|
|
87
84
|
const newTranslations: Record<string, any> = {}
|
|
88
85
|
|
|
89
|
-
// 1. Preserve keys from existing translations that match patterns
|
|
90
86
|
const existingKeys = getNestedKeys(existingTranslations, keySeparator)
|
|
91
87
|
for (const existingKey of existingKeys) {
|
|
92
88
|
if (preservePatterns.some(re => re.test(existingKey))) {
|
|
@@ -95,24 +91,45 @@ export async function getTranslations (
|
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
// 2. Merge in newly found keys for this namespace
|
|
99
94
|
const sortedKeys = (config.extract.sort === false)
|
|
100
95
|
? nsKeys
|
|
101
|
-
: nsKeys.sort((a, b) => a.key.localeCompare(b.key))
|
|
96
|
+
: [...nsKeys].sort((a, b) => a.key.localeCompare(b.key))
|
|
97
|
+
|
|
102
98
|
for (const { key, defaultValue } of sortedKeys) {
|
|
103
99
|
const existingValue = getNestedValue(existingTranslations, key, keySeparator)
|
|
104
|
-
const valueToSet = existingValue ?? (locale ===
|
|
100
|
+
const valueToSet = existingValue ?? (locale === primaryLanguage ? defaultValue : (config.extract.defaultValue ?? ''))
|
|
105
101
|
setNestedValue(newTranslations, key, valueToSet, keySeparator)
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
if (mergeNamespaces) {
|
|
105
|
+
mergedTranslations[ns] = newTranslations
|
|
106
|
+
if (Object.keys(existingTranslations).length > 0) {
|
|
107
|
+
mergedExisting[ns] = existingTranslations
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const oldContent = existingTranslations ? JSON.stringify(existingTranslations, null, config.extract.indentation ?? 2) : ''
|
|
111
|
+
const newContent = JSON.stringify(newTranslations, null, config.extract.indentation ?? 2)
|
|
112
|
+
|
|
113
|
+
results.push({
|
|
114
|
+
path: fullPath,
|
|
115
|
+
updated: newContent !== oldContent,
|
|
116
|
+
newTranslations,
|
|
117
|
+
existingTranslations,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (mergeNamespaces) {
|
|
123
|
+
const outputPath = getOutputPath(config.extract.output, locale)
|
|
124
|
+
const fullPath = resolve(process.cwd(), outputPath)
|
|
125
|
+
const oldContent = Object.keys(mergedExisting).length > 0 ? JSON.stringify(mergedExisting, null, config.extract.indentation ?? 2) : ''
|
|
126
|
+
const newContent = JSON.stringify(mergedTranslations, null, config.extract.indentation ?? 2)
|
|
110
127
|
|
|
111
128
|
results.push({
|
|
112
129
|
path: fullPath,
|
|
113
130
|
updated: newContent !== oldContent,
|
|
114
|
-
newTranslations,
|
|
115
|
-
existingTranslations,
|
|
131
|
+
newTranslations: mergedTranslations,
|
|
132
|
+
existingTranslations: mergedExisting,
|
|
116
133
|
})
|
|
117
134
|
}
|
|
118
135
|
}
|
|
@@ -337,7 +337,7 @@ export class ASTVisitors {
|
|
|
337
337
|
if (callee.type !== 'Identifier') return
|
|
338
338
|
|
|
339
339
|
const scopeInfo = this.getVarFromScope(callee.value)
|
|
340
|
-
const isFunctionToParse = (this.config.extract.functions || []).includes(callee.value) || scopeInfo !== undefined
|
|
340
|
+
const isFunctionToParse = (this.config.extract.functions || ['t']).includes(callee.value) || scopeInfo !== undefined
|
|
341
341
|
if (!isFunctionToParse || node.arguments.length === 0) return
|
|
342
342
|
|
|
343
343
|
const firstArg = node.arguments[0].expression
|
package/src/heuristic-config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { glob } from 'glob'
|
|
2
2
|
import { readdir } from 'node:fs/promises'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
3
|
+
import { dirname, join, extname } from 'node:path'
|
|
4
4
|
import type { I18nextToolkitConfig } from './types'
|
|
5
5
|
|
|
6
6
|
// A list of common glob patterns for the primary language ('en') or ('dev') translation files.
|
|
@@ -32,10 +32,20 @@ export async function detectConfig (): Promise<Partial<I18nextToolkitConfig> | n
|
|
|
32
32
|
if (files.length > 0) {
|
|
33
33
|
const firstFile = files[0]
|
|
34
34
|
const basePath = dirname(dirname(firstFile))
|
|
35
|
+
const extension = extname(firstFile)
|
|
36
|
+
|
|
37
|
+
// Infer outputFormat from the file extension
|
|
38
|
+
let outputFormat: I18nextToolkitConfig['extract']['outputFormat'] = 'json'
|
|
39
|
+
if (extension === '.ts') {
|
|
40
|
+
outputFormat = 'ts'
|
|
41
|
+
} else if (extension === '.js') {
|
|
42
|
+
// We can't know if it's ESM or CJS, so we default to a safe choice.
|
|
43
|
+
// The tool's file loaders can handle both.
|
|
44
|
+
outputFormat = 'js'
|
|
45
|
+
}
|
|
35
46
|
|
|
36
47
|
try {
|
|
37
48
|
const allDirs = await readdir(basePath)
|
|
38
|
-
// CORRECTED REGEX: Now accepts 'dev' in addition to standard locale codes.
|
|
39
49
|
let locales = allDirs.filter(dir => /^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(dir))
|
|
40
50
|
|
|
41
51
|
if (locales.length > 0) {
|
|
@@ -57,7 +67,8 @@ export async function detectConfig (): Promise<Partial<I18nextToolkitConfig> | n
|
|
|
57
67
|
'pages/**/*.{js,jsx,ts,tsx}',
|
|
58
68
|
'components/**/*.{js,jsx,ts,tsx}'
|
|
59
69
|
],
|
|
60
|
-
output: join(basePath, '{{language}}',
|
|
70
|
+
output: join(basePath, '{{language}}', `{{namespace}}${extension}`),
|
|
71
|
+
outputFormat,
|
|
61
72
|
primaryLanguage: locales.includes('en') ? 'en' : locales[0],
|
|
62
73
|
},
|
|
63
74
|
}
|
package/src/status.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
|
-
import { readFile } from 'node:fs/promises'
|
|
5
4
|
import { findKeys } from './extractor/core/key-finder'
|
|
6
5
|
import { getNestedValue } from './utils/nested-object'
|
|
7
6
|
import type { I18nextToolkitConfig, ExtractedKey } from './types'
|
|
8
|
-
import { getOutputPath } from './utils/file-utils'
|
|
7
|
+
import { getOutputPath, loadTranslationFile } from './utils/file-utils'
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Options for configuring the status report display.
|
|
@@ -56,8 +55,8 @@ interface StatusReport {
|
|
|
56
55
|
* @throws {Error} When unable to extract keys or read translation files
|
|
57
56
|
*/
|
|
58
57
|
export async function runStatus (config: I18nextToolkitConfig, options: StatusOptions = {}) {
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
59
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
61
60
|
const spinner = ora('Analyzing project localization status...\n').start()
|
|
62
61
|
try {
|
|
63
62
|
const report = await generateStatusReport(config)
|
|
@@ -84,9 +83,16 @@ export async function runStatus (config: I18nextToolkitConfig, options: StatusOp
|
|
|
84
83
|
* @throws {Error} When key extraction fails or configuration is invalid
|
|
85
84
|
*/
|
|
86
85
|
async function generateStatusReport (config: I18nextToolkitConfig): Promise<StatusReport> {
|
|
86
|
+
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
87
|
+
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
88
|
+
|
|
87
89
|
const { allKeys: allExtractedKeys } = await findKeys(config)
|
|
88
|
-
const {
|
|
89
|
-
|
|
90
|
+
const {
|
|
91
|
+
secondaryLanguages,
|
|
92
|
+
keySeparator = '.',
|
|
93
|
+
defaultNS = 'translation',
|
|
94
|
+
mergeNamespaces = false,
|
|
95
|
+
} = config.extract
|
|
90
96
|
|
|
91
97
|
const keysByNs = new Map<string, ExtractedKey[]>()
|
|
92
98
|
for (const key of allExtractedKeys.values()) {
|
|
@@ -105,17 +111,19 @@ async function generateStatusReport (config: I18nextToolkitConfig): Promise<Stat
|
|
|
105
111
|
let totalTranslatedForLocale = 0
|
|
106
112
|
const namespaces = new Map<string, any>()
|
|
107
113
|
|
|
114
|
+
const mergedTranslations = mergeNamespaces
|
|
115
|
+
? await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale))) || {}
|
|
116
|
+
: null
|
|
117
|
+
|
|
108
118
|
for (const [ns, keysInNs] of keysByNs.entries()) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const content = await readFile(resolve(process.cwd(), langFilePath), 'utf-8')
|
|
113
|
-
translations = JSON.parse(content)
|
|
114
|
-
} catch {}
|
|
119
|
+
const translationsForNs = mergeNamespaces
|
|
120
|
+
? mergedTranslations?.[ns] || {}
|
|
121
|
+
: await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale, ns))) || {}
|
|
115
122
|
|
|
116
123
|
let translatedInNs = 0
|
|
117
124
|
const keyDetails = keysInNs.map(({ key }) => {
|
|
118
|
-
|
|
125
|
+
// Search for the key within the correct namespace object
|
|
126
|
+
const value = getNestedValue(translationsForNs, key, keySeparator ?? '.')
|
|
119
127
|
const isTranslated = !!value
|
|
120
128
|
if (isTranslated) translatedInNs++
|
|
121
129
|
return { key, isTranslated }
|
package/src/syncer.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve, dirname } from 'path'
|
|
1
|
+
import { writeFile, mkdir } from 'node:fs/promises'
|
|
2
|
+
import { resolve, dirname, basename } from 'path'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import ora from 'ora'
|
|
5
|
+
import { glob } from 'glob'
|
|
5
6
|
import type { I18nextToolkitConfig } from './types'
|
|
6
7
|
import { getNestedKeys, getNestedValue, setNestedValue } from './utils/nested-object'
|
|
7
|
-
import { getOutputPath } from './utils/file-utils'
|
|
8
|
+
import { getOutputPath, loadTranslationFile, serializeTranslationFile } from './utils/file-utils'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Synchronizes translation files across different locales by ensuring all secondary
|
|
@@ -38,77 +39,86 @@ import { getOutputPath } from './utils/file-utils'
|
|
|
38
39
|
*/
|
|
39
40
|
export async function runSyncer (config: I18nextToolkitConfig) {
|
|
40
41
|
const spinner = ora('Running i18next locale synchronizer...\n').start()
|
|
41
|
-
|
|
42
|
-
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
43
|
-
const { primaryLanguage } = config.extract
|
|
44
|
-
const secondaryLanguages = config.locales.filter(l => l !== primaryLanguage)
|
|
45
|
-
const keySeparator = config.extract.keySeparator ?? '.'
|
|
46
|
-
|
|
47
|
-
const logMessages: string[] = []
|
|
48
|
-
let wasAnythingSynced = false
|
|
49
|
-
|
|
50
|
-
// Assume sync operates on the default namespace for simplicity
|
|
51
|
-
const defaultNS = config.extract.defaultNS ?? 'translation'
|
|
52
|
-
|
|
53
|
-
// 1. Get all keys from the primary language file
|
|
54
|
-
const primaryPath = getOutputPath(config.extract.output, primaryLanguage, defaultNS)
|
|
55
|
-
|
|
56
|
-
const fullPrimaryPath = resolve(process.cwd(), primaryPath)
|
|
57
|
-
|
|
58
|
-
let primaryTranslations: Record<string, any>
|
|
59
42
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const newSecondaryTranslations: Record<string, any> = {}
|
|
43
|
+
const primaryLanguage = config.extract.primaryLanguage || config.locales[0] || 'en'
|
|
44
|
+
const secondaryLanguages = config.locales.filter((l) => l !== primaryLanguage)
|
|
45
|
+
const {
|
|
46
|
+
output,
|
|
47
|
+
keySeparator = '.',
|
|
48
|
+
outputFormat = 'json',
|
|
49
|
+
indentation = 2,
|
|
50
|
+
defaultValue = '',
|
|
51
|
+
} = config.extract
|
|
52
|
+
|
|
53
|
+
const logMessages: string[] = []
|
|
54
|
+
let wasAnythingSynced = false
|
|
55
|
+
|
|
56
|
+
// 1. Find all namespace files for the primary language
|
|
57
|
+
const primaryNsPattern = getOutputPath(output, primaryLanguage, '*')
|
|
58
|
+
const primaryNsFiles = await glob(primaryNsPattern)
|
|
59
|
+
|
|
60
|
+
if (primaryNsFiles.length === 0) {
|
|
61
|
+
spinner.warn(`No translation files found for primary language "${primaryLanguage}". Nothing to sync.`)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
82
64
|
|
|
83
|
-
//
|
|
84
|
-
for (const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
// 2. Loop through each primary namespace file
|
|
66
|
+
for (const primaryPath of primaryNsFiles) {
|
|
67
|
+
const ns = basename(primaryPath).split('.')[0]
|
|
68
|
+
const primaryTranslations = await loadTranslationFile(primaryPath)
|
|
69
|
+
|
|
70
|
+
if (!primaryTranslations) {
|
|
71
|
+
logMessages.push(` ${chalk.yellow('-')} Could not read primary file: ${primaryPath}`)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const primaryKeys = getNestedKeys(primaryTranslations, keySeparator ?? '.')
|
|
76
|
+
|
|
77
|
+
// 3. For each secondary language, sync the current namespace
|
|
78
|
+
for (const lang of secondaryLanguages) {
|
|
79
|
+
const secondaryPath = getOutputPath(output, lang, ns)
|
|
80
|
+
const fullSecondaryPath = resolve(process.cwd(), secondaryPath)
|
|
81
|
+
const existingSecondaryTranslations = await loadTranslationFile(fullSecondaryPath) || {}
|
|
82
|
+
const newSecondaryTranslations: Record<string, any> = {}
|
|
83
|
+
|
|
84
|
+
for (const key of primaryKeys) {
|
|
85
|
+
const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.')
|
|
86
|
+
const valueToSet = existingValue ?? defaultValue
|
|
87
|
+
setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Use JSON.stringify for a reliable object comparison, regardless of format
|
|
91
|
+
const oldContent = JSON.stringify(existingSecondaryTranslations)
|
|
92
|
+
const newContent = JSON.stringify(newSecondaryTranslations)
|
|
93
|
+
|
|
94
|
+
if (newContent !== oldContent) {
|
|
95
|
+
wasAnythingSynced = true
|
|
96
|
+
const serializedContent = serializeTranslationFile(newSecondaryTranslations, outputFormat, indentation)
|
|
97
|
+
await mkdir(dirname(fullSecondaryPath), { recursive: true })
|
|
98
|
+
await writeFile(fullSecondaryPath, serializedContent)
|
|
99
|
+
logMessages.push(` ${chalk.green('✓')} Synchronized: ${secondaryPath}`)
|
|
100
|
+
} else {
|
|
101
|
+
logMessages.push(` ${chalk.gray('-')} Already in sync: ${secondaryPath}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
spinner.succeed(chalk.bold('Synchronization complete!'))
|
|
107
|
+
logMessages.forEach(msg => console.log(msg))
|
|
93
108
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
await mkdir(dirname(fullSecondaryPath), { recursive: true })
|
|
97
|
-
await writeFile(fullSecondaryPath, newContent)
|
|
98
|
-
logMessages.push(` ${chalk.green('✓')} Synchronized: ${secondaryPath}`)
|
|
109
|
+
if (wasAnythingSynced) {
|
|
110
|
+
printLocizeFunnel()
|
|
99
111
|
} else {
|
|
100
|
-
|
|
112
|
+
console.log(chalk.green.bold('\n✅ All locales are already in sync.'))
|
|
101
113
|
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
spinner.fail(chalk.red('Synchronization failed.'))
|
|
116
|
+
console.error(error)
|
|
102
117
|
}
|
|
118
|
+
}
|
|
103
119
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
console.log(chalk.green.bold('\n✅ Sync complete.'))
|
|
109
|
-
console.log(chalk.yellow('🚀 Ready to collaborate with translators? Move your files to the cloud.'))
|
|
110
|
-
console.log(` Get started with the official TMS for i18next: ${chalk.cyan('npx i18next-cli locize-migrate')}`)
|
|
111
|
-
} else {
|
|
112
|
-
console.log(chalk.green.bold('\n✅ All locales are already in sync.'))
|
|
113
|
-
}
|
|
120
|
+
function printLocizeFunnel () {
|
|
121
|
+
console.log(chalk.green.bold('\n✅ Sync complete.'))
|
|
122
|
+
console.log(chalk.yellow('🚀 Ready to collaborate with translators? Move your files to the cloud.'))
|
|
123
|
+
console.log(` Get started with the official TMS for i18next: ${chalk.cyan('npx i18next-cli locize-migrate')}`)
|
|
114
124
|
}
|
package/src/types.ts
CHANGED
|
@@ -83,6 +83,24 @@ export interface I18nextToolkitConfig {
|
|
|
83
83
|
|
|
84
84
|
/** Secondary languages that get empty values initially */
|
|
85
85
|
secondaryLanguages?: string[];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The format of the output translation files.
|
|
89
|
+
* 'json': Standard JSON file (default)
|
|
90
|
+
* 'js': JavaScript file with ES Module syntax (export default)
|
|
91
|
+
* 'js-esm': JavaScript file with ES Module syntax (export default)
|
|
92
|
+
* 'js-cjs': JavaScript file with CommonJS syntax (module.exports)
|
|
93
|
+
* 'ts': TypeScript file with ES Module syntax and `as const` for type safety
|
|
94
|
+
*/
|
|
95
|
+
outputFormat?: 'json' | 'js' | 'js-esm' | 'js-esm' | 'js-cjs' | 'ts';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* If true, all namespaces will be merged into a single file per language.
|
|
99
|
+
* The `output` path should not contain the `{{namespace}}` placeholder.
|
|
100
|
+
* Example output: `locales/en.js`
|
|
101
|
+
* (default: false)
|
|
102
|
+
*/
|
|
103
|
+
mergeNamespaces?: boolean;
|
|
86
104
|
};
|
|
87
105
|
|
|
88
106
|
/** Configuration options for TypeScript type generation */
|
package/src/utils/file-utils.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
|
+
import { createJiti } from 'jiti'
|
|
4
|
+
import type { I18nextToolkitConfig } from '../types'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Ensures that the directory for a given file path exists.
|
|
@@ -73,9 +75,60 @@ export async function writeFileAsync (filePath: string, data: string): Promise<v
|
|
|
73
75
|
export function getOutputPath (
|
|
74
76
|
template: string,
|
|
75
77
|
locale: string,
|
|
76
|
-
namespace: string
|
|
78
|
+
namespace: string = ''
|
|
77
79
|
): string {
|
|
78
80
|
return template
|
|
79
81
|
.replace('{{language}}', locale).replace('{{lng}}', locale)
|
|
80
82
|
.replace('{{namespace}}', namespace).replace('{{ns}}', namespace)
|
|
81
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Dynamically loads a translation file, supporting .json, .js, and .ts formats.
|
|
87
|
+
* @param filePath - The path to the translation file.
|
|
88
|
+
* @returns The parsed content of the file, or null if not found or failed to parse.
|
|
89
|
+
*/
|
|
90
|
+
export async function loadTranslationFile (filePath: string): Promise<Record<string, any> | null> {
|
|
91
|
+
try {
|
|
92
|
+
if (filePath.endsWith('.json')) {
|
|
93
|
+
const content = await readFile(filePath, 'utf-8')
|
|
94
|
+
return JSON.parse(content)
|
|
95
|
+
} else if (filePath.endsWith('.ts') || filePath.endsWith('.js')) {
|
|
96
|
+
const jiti = createJiti(import.meta.url)
|
|
97
|
+
const module = await jiti.import(filePath, { default: true })
|
|
98
|
+
return module as Record<string, any> | null
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// File not found or parse error
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Serializes a translation object into a string based on the desired format.
|
|
109
|
+
* @param data - The translation data object.
|
|
110
|
+
* @param format - The desired output format ('json', 'js-esm', etc.).
|
|
111
|
+
* @param indentation - The number of spaces for indentation.
|
|
112
|
+
* @returns The serialized file content as a string.
|
|
113
|
+
*/
|
|
114
|
+
export function serializeTranslationFile (
|
|
115
|
+
data: Record<string, any>,
|
|
116
|
+
format: I18nextToolkitConfig['extract']['outputFormat'] = 'json',
|
|
117
|
+
indentation: number = 2
|
|
118
|
+
): string {
|
|
119
|
+
const jsonString = JSON.stringify(data, null, indentation)
|
|
120
|
+
|
|
121
|
+
switch (format) {
|
|
122
|
+
case 'js':
|
|
123
|
+
case 'js-esm':
|
|
124
|
+
return `export default ${jsonString};\n`
|
|
125
|
+
case 'js-cjs':
|
|
126
|
+
return `module.exports = ${jsonString};\n`
|
|
127
|
+
case 'ts':
|
|
128
|
+
// Using `as const` provides better type inference for TypeScript users
|
|
129
|
+
return `export default ${jsonString} as const;\n`
|
|
130
|
+
case 'json':
|
|
131
|
+
default:
|
|
132
|
+
return jsonString
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAM5F,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/extractor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAM5F,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,OAAO,CAAC,CAoClB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,WAAW,EACxB,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAmCD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAE,MAAM,EAAE,oBAAoB,sDAO1D"}
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyF9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heuristic-config.d.ts","sourceRoot":"","sources":["../src/heuristic-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAiBnD;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAK,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"heuristic-config.d.ts","sourceRoot":"","sources":["../src/heuristic-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAiBnD;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAK,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC,CAuDnF"}
|
package/types/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,SAAS,CAAA;AAGjE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAYzF"}
|
package/types/syncer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../src/syncer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAInD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,iBA8E5D"}
|
package/types/types.d.ts
CHANGED
|
@@ -63,6 +63,22 @@ export interface I18nextToolkitConfig {
|
|
|
63
63
|
primaryLanguage?: string;
|
|
64
64
|
/** Secondary languages that get empty values initially */
|
|
65
65
|
secondaryLanguages?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* The format of the output translation files.
|
|
68
|
+
* 'json': Standard JSON file (default)
|
|
69
|
+
* 'js': JavaScript file with ES Module syntax (export default)
|
|
70
|
+
* 'js-esm': JavaScript file with ES Module syntax (export default)
|
|
71
|
+
* 'js-cjs': JavaScript file with CommonJS syntax (module.exports)
|
|
72
|
+
* 'ts': TypeScript file with ES Module syntax and `as const` for type safety
|
|
73
|
+
*/
|
|
74
|
+
outputFormat?: 'json' | 'js' | 'js-esm' | 'js-esm' | 'js-cjs' | 'ts';
|
|
75
|
+
/**
|
|
76
|
+
* If true, all namespaces will be merged into a single file per language.
|
|
77
|
+
* The `output` path should not contain the `{{namespace}}` placeholder.
|
|
78
|
+
* Example output: `locales/en.js`
|
|
79
|
+
* (default: false)
|
|
80
|
+
*/
|
|
81
|
+
mergeNamespaces?: boolean;
|
|
66
82
|
};
|
|
67
83
|
/** Configuration options for TypeScript type generation */
|
|
68
84
|
types?: {
|
package/types/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB,2DAA2D;IAC3D,OAAO,EAAE;QACP,oEAAoE;QACpE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAEzB,mGAAmG;QACnG,MAAM,EAAE,MAAM,CAAC;QAEf,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QAEnB,uEAAuE;QACvE,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;QAErC,8EAA8E;QAC9E,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;QAEpC,oDAAoD;QACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAE1B,mDAAmD;QACnD,eAAe,CAAC,EAAE,MAAM,CAAC;QAEzB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAErB,4EAA4E;QAC5E,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAE3B,0GAA0G;QAC1G,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE/B,kFAAkF;QAClF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE7B,kGAAkG;QAClG,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAEvB,8FAA8F;QAC9F,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;QAEtC,wFAAwF;QACxF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE5B,0EAA0E;QAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;QAEf,yDAAyD;QACzD,WAAW,CAAC,EAAE,MAAM,CAAC;QAErB,2EAA2E;QAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;QAEtB,4EAA4E;QAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;QAEzB,0DAA0D;QAC1D,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB,2DAA2D;IAC3D,OAAO,EAAE;QACP,oEAAoE;QACpE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAEzB,mGAAmG;QACnG,MAAM,EAAE,MAAM,CAAC;QAEf,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QAEnB,uEAAuE;QACvE,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;QAErC,8EAA8E;QAC9E,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;QAEpC,oDAAoD;QACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAE1B,mDAAmD;QACnD,eAAe,CAAC,EAAE,MAAM,CAAC;QAEzB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAErB,4EAA4E;QAC5E,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAE3B,0GAA0G;QAC1G,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE/B,kFAAkF;QAClF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE7B,kGAAkG;QAClG,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAEvB,8FAA8F;QAC9F,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;QAEtC,wFAAwF;QACxF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE5B,0EAA0E;QAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;QAEf,yDAAyD;QACzD,WAAW,CAAC,EAAE,MAAM,CAAC;QAErB,2EAA2E;QAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;QAEtB,4EAA4E;QAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;QAEzB,0DAA0D;QAC1D,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;QAE9B;;;;;;;WAOG;QACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;QAErE;;;;;WAKG;QACH,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,CAAC;IAEF,2DAA2D;IAC3D,KAAK,CAAC,EAAE;QACN,mEAAmE;QACnE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAEzB,0DAA0D;QAC1D,MAAM,EAAE,MAAM,CAAC;QAEf,8EAA8E;QAC9E,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;QAEtC,qDAAqD;QACrD,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IAEF,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB,2CAA2C;IAC3C,MAAM,CAAC,EAAE;QACP,wBAAwB;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;QAEnB,gEAAgE;QAChE,MAAM,CAAC,EAAE,MAAM,CAAC;QAEhB,+CAA+C;QAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;QAEjB,8DAA8D;QAC9D,YAAY,CAAC,EAAE,OAAO,CAAC;QAEvB,8CAA8C;QAC9C,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAE7B,8CAA8C;QAC9C,uBAAuB,CAAC,EAAE,OAAO,CAAC;QAElC,0CAA0C;QAC1C,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,MAAM;IACrB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAElE;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAE3D;;;;;OAKG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7F;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;IAEZ,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,oCAAoC;IACpC,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,UAAU,CAAC;CAChC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IAEb,+DAA+D;IAC/D,OAAO,EAAE,OAAO,CAAC;IAEjB,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAErC,kEAAkE;IAClE,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC3C;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,MAAM;IACrB;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAExC;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,MAAM,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;CACzC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { I18nextToolkitConfig } from '../types';
|
|
1
2
|
/**
|
|
2
3
|
* Ensures that the directory for a given file path exists.
|
|
3
4
|
* Creates all necessary parent directories recursively if they don't exist.
|
|
@@ -57,5 +58,19 @@ export declare function writeFileAsync(filePath: string, data: string): Promise<
|
|
|
57
58
|
* // Returns: 'locales/en/common.json'
|
|
58
59
|
* ```
|
|
59
60
|
*/
|
|
60
|
-
export declare function getOutputPath(template: string, locale: string, namespace
|
|
61
|
+
export declare function getOutputPath(template: string, locale: string, namespace?: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Dynamically loads a translation file, supporting .json, .js, and .ts formats.
|
|
64
|
+
* @param filePath - The path to the translation file.
|
|
65
|
+
* @returns The parsed content of the file, or null if not found or failed to parse.
|
|
66
|
+
*/
|
|
67
|
+
export declare function loadTranslationFile(filePath: string): Promise<Record<string, any> | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Serializes a translation object into a string based on the desired format.
|
|
70
|
+
* @param data - The translation data object.
|
|
71
|
+
* @param format - The desired output format ('json', 'js-esm', etc.).
|
|
72
|
+
* @param indentation - The number of spaces for indentation.
|
|
73
|
+
* @returns The serialized file content as a string.
|
|
74
|
+
*/
|
|
75
|
+
export declare function serializeTranslationFile(data: Record<string, any>, format?: I18nextToolkitConfig['extract']['outputFormat'], indentation?: number): string;
|
|
61
76
|
//# sourceMappingURL=file-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;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,EAAE,MAAM,
|
|
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;AAEpD;;;;;;;;;;;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,CAehG;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,MAAU,GACtB,MAAM,CAgBR"}
|