vbss-translator 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import{dirname as w,resolve as d,normalize as p,relative as $}from"path";import{glob as T}from"glob";import{mkdir as G,writeFile as O,readFile as F}from"fs/promises";import{watch as U,existsSync as _}from"fs";async function x(o){try{const t=await T(o,{cwd:process.cwd(),absolute:!1}),e=process.cwd();return t.map(n=>({path:d(e,n),relativePath:p(n).replace(/\\/g,"/"),translations:[]}))}catch(t){const e=t instanceof Error?t.message:"Unknown error";return[{path:"",relativePath:"",translations:[],error:{file:o,message:`Failed to discover files: ${e}`,type:"file_system"}}]}}async function j(o){const t=o.map(async e=>{if(e.error)return e;try{const n=await F(e.path,"utf-8"),s=JSON.parse(n),a=[];if(Array.isArray(s))for(const r of s){if(typeof r!="object"||r===null)return{...e,translations:[],error:{file:e.path,message:"Translation array must contain objects only",type:"validation"}};const u={};for(const[l,i]of Object.entries(r)){if(typeof i!="string")return{...e,translations:[],error:{file:e.path,message:`Translation value for key "${l}" must be a string, got ${typeof i}`,type:"validation"}};u[l]=i}a.push(u)}else if(typeof s=="object"&&s!==null){const r={};for(const[u,l]of Object.entries(s)){if(typeof l!="string")return{...e,translations:[],error:{file:e.path,message:`Translation value for key "${u}" must be a string, got ${typeof l}`,type:"validation"}};r[u]=l}return a.push(r),{...e,translations:a,wasSingleObject:!0}}else return{...e,translations:[],error:{file:e.path,message:"Translation file must contain a JSON object or array of objects",type:"validation"}};return{...e,translations:a}}catch(n){if(n instanceof SyntaxError)return{...e,translations:[],error:{file:e.path,message:`Invalid JSON: ${n.message}`,type:"invalid_json"}};const s=n instanceof Error?n.message:"Unknown error";return{...e,translations:[],error:{file:e.path,message:`Failed to read file: ${s}`,type:"file_system"}}}});return Promise.all(t)}function S(o){for(const t of o)if(!t.error&&t.translations.length>0)return Object.keys(t.translations[0]);return null}function B(o,t){const e=[];for(const n of o){if(n.error)continue;if(n.translations.length===0){e.push({file:n.path,message:"File has no translations",type:"validation"});continue}const s=Object.keys(n.translations[0]);for(let i=1;i<n.translations.length;i++){const f=Object.keys(n.translations[i]);if(JSON.stringify([...f].sort())!==JSON.stringify([...s].sort())){e.push({file:n.path,message:`Translation ${i+1} in file has different language keys`,type:"validation"});continue}}const a=[...s].sort(),r=[...t].sort(),u=r.filter(i=>!a.includes(i)),l=a.filter(i=>!r.includes(i));if(u.length>0||l.length>0){const i=[];u.length>0&&i.push(`missing languages: ${u.join(", ")}`),l.length>0&&i.push(`extra languages: ${l.join(", ")}`),e.push({file:n.path,message:`Language mismatch: ${i.join(", ")}. Expected: ${t.join(", ")}`,type:"missing_languages"})}}return e}function k(o,t){const e=new Set,n=[];for(const s of o){const a=s[t];if(a===void 0){n.push(s);continue}const r=a;e.has(r)||(e.add(r),n.push(s))}return n}function L(o,t){const e=w(o);let s=$(e,t);return s=p(s).replace(/\\/g,"/"),s.startsWith(".")||(s="./"+s),s}function P(o,t,e){const n=o.filter(f=>!f.error&&f.translations.length>0).sort((f,h)=>f.path.localeCompare(h.path));if(n.length===0)return`export default []
3
+ `;const s=n.map((f,h)=>{const c=L(t,f.path);return`import translations${h} from '${c}' assert { type: 'json' };`}).join(`
4
+ `),a=n.map((f,h)=>f.wasSingleObject?` ...(Array.isArray(translations${h}) ? translations${h} : [translations${h}])`:` ...translations${h}`).join(`,
5
+ `),u=`export interface Translation {
6
+ ${e.map(f=>` ${f}: string;`).join(`
7
+ `)}
8
+ [key: string]: string;
9
+ }`,l=e[0]||"en",i=n.length>0?`
10
+ const uniqueTranslations = allTranslations.filter(
11
+ (translation, index, self) => index === self.findIndex((t) => t.${l} === translation.${l})
12
+ );`:"";return`${s}
13
+
14
+ ${u}
15
+
16
+ const allTranslations: Translation[] = [
17
+ ${a}
18
+ ];${i}
19
+
20
+ export default uniqueTranslations;
21
+ `}function A(o){const t=o.filter(r=>!r.error&&r.translations.length>0).sort((r,u)=>r.path.localeCompare(u.path));if(t.length===0)return`export default []
22
+ `;const e=[];for(const r of t)r.wasSingleObject&&r.translations.length===1?e.push(r.translations[0]):e.push(...r.translations);const n=e.length>0?Object.keys(e[0])[0]:"en",s=e.map(r=>JSON.stringify(r,null,2).split(`
23
+ `).map(i=>` ${i}`).join(`
24
+ `)).join(`,
25
+ `),a=`
26
+ const uniqueTranslations = allTranslations.filter(
27
+ (translation, index, self) => index === self.findIndex((t) => t.${n} === translation.${n})
28
+ );`;return`const allTranslations = [
29
+ ${s}
30
+ ];${a}
31
+
32
+ export default uniqueTranslations;
33
+ `}function C(o,t,e){return P(o,t,e)}async function E(o){const t=[],e=Date.now(),n=await x(o.pattern);if(n.length===0)return{success:!0,filesFound:0,translationsGenerated:0,errors:[],outputPath:o.outputPath};for(const c of n)c.error&&t.push(c.error);const s=await j(n);for(const c of s)c.error&&t.push(c.error);const a=S(s);if((!a||a.length===0)&&s.filter(m=>!m.error&&m.translations.length>0).length===0)return{success:!1,filesFound:n.length,translationsGenerated:0,errors:t.length>0?t:[{file:o.pattern,message:"No valid translation files found",type:"validation"}],outputPath:o.outputPath};if(a&&a.length>0){const c=B(s,a);if(t.push(...c),c.length>0)return{success:!1,filesFound:n.length,translationsGenerated:0,errors:t,outputPath:o.outputPath}}const r=[];for(const c of s)!c.error&&c.translations.length>0&&r.push(...c.translations);const u=o.referenceLanguage||a?.[0]||"en",l=k(r,u);let i;const f=s.filter(c=>!c.error&&c.translations.length>0);switch(o.outputFormat){case"ts":i=P(f,o.outputPath,a||[]);break;case"tsx":i=C(f,o.outputPath,a||[]);break;case"js":i=A(f);break;default:return{success:!1,filesFound:n.length,translationsGenerated:0,errors:[...t,{file:o.outputPath,message:`Unsupported output format: ${o.outputFormat}`,type:"validation"}],outputPath:o.outputPath}}try{const c=w(o.outputPath);await G(c,{recursive:!0}),await O(o.outputPath,i,"utf-8")}catch(c){const m=c instanceof Error?c.message:"Unknown error";return t.push({file:o.outputPath,message:`Failed to write output file: ${m}`,type:"file_system"}),{success:!1,filesFound:n.length,translationsGenerated:l.length,errors:t,outputPath:o.outputPath}}const h=Date.now()-e;return h>1e3&&n.length<100&&console.warn(`⚠️ Generation took ${h}ms for ${n.length} files`),{success:t.length===0,filesFound:n.length,translationsGenerated:l.length,errors:t,outputPath:o.outputPath}}const R="src/**/translations.json",W="src/translations/index.ts",N="ts";class I{async loadFromFile(t){try{const e=d(t),n=await F(e,"utf-8");return JSON.parse(n)}catch(e){if(e&&typeof e=="object"&&"code"in e&&e.code==="ENOENT")return null;throw e instanceof SyntaxError?new Error(`Invalid JSON in config file: ${e.message}`):e}}loadFromArgs(t){const e={};return typeof t.pattern=="string"&&(e.pattern=t.pattern),typeof t.output=="string"&&(e.outputPath=t.output),typeof t.format=="string"&&(t.format==="ts"||t.format==="js"||t.format==="tsx")&&(e.outputFormat=t.format),typeof t["reference-language"]=="string"&&(e.referenceLanguage=t["reference-language"]),e}merge(t,e){const n={...t,...e};return{pattern:n.pattern??R,outputPath:n.outputPath??W,outputFormat:n.outputFormat??N,referenceLanguage:n.referenceLanguage}}}const y=new I,D=300;function J(o,t,e){try{const n=p(o),s=p(e);let a;try{a=$(s,n)}catch{a=$(e,o)}a=a.replace(/\\/g,"/");let r=t.replace(/\\/g,"/");r=r.replace(/\*\*\//g,"___GLOB_DOUBLE_STAR_SLASH___"),r=r.replace(/\*\*/g,"___GLOB_DOUBLE_STAR___"),r=r.replace(/\./g,"\\."),r=r.replace(/\*/g,"[^/]*"),r=r.replace(/\?/g,"[^/]"),r=r.replace(/___GLOB_DOUBLE_STAR_SLASH___/g,"(.*/)?"),r=r.replace(/___GLOB_DOUBLE_STAR___/g,".*");const l=new RegExp(`^${r}$`).test(a);return console.log(`[DEBUG] Pattern matching: "${a}" against "${r}" = ${l}`),l}catch(n){console.log("[DEBUG] Pattern matching failed, using fallback:",n);const s=$(e,o).replace(/\\/g,"/"),a=s.split("/"),r=a[a.length-1]==="translations.json"&&a[0]==="src";return console.log(`[DEBUG] Fallback pattern matching: "${s}" = ${r}`),r}}function M(o){const t=new Set;for(const e of o)t.add(w(e));return Array.from(t)}class z{watchers=new Map;debounceTimer=null;isWatching=!1;generatorOptions=null;callback=null;pattern="";cwd="";async watch(t,e){if(this.isWatching)throw new Error("Watch service is already watching");this.pattern=t,this.callback=e,this.cwd=p(process.cwd()),this.isWatching=!0;const n=await T(t,{cwd:this.cwd,absolute:!0}),s=M(n);s.length===0&&s.push(this.cwd);const a=s.map(u=>p(u));console.log(`[DEBUG] Watching ${a.length} directory(ies) for pattern: ${t}`);for(const u of a)console.log(`[DEBUG] Setting up watch for directory: ${u}`),this.watchDirectory(u);const r=p(this.cwd);this.watchers.has(r)||(console.log(`[DEBUG] Setting up watch for root directory: ${r}`),this.watchDirectory(r)),console.log(`[DEBUG] Total watchers active: ${this.watchers.size}`)}watchDirectory(t){const e=p(t);if(this.watchers.has(e)){console.log(`[DEBUG] Already watching directory: ${e}`);return}console.log(`[DEBUG] Starting watch on directory: ${e}`);const n=U(e,{recursive:!0},(s,a)=>{if(!a){console.log(`[DEBUG] Event received but filename is empty, eventType: ${s}`);return}console.log(`[DEBUG] File system event: ${s} on ${a} in ${e}`);let r;try{r=d(e,a),r=p(r)}catch{r=p(a)}console.log(`[DEBUG] Resolved file path: ${r}`),console.log(`[DEBUG] Pattern: ${this.pattern}, CWD: ${this.cwd}`);const u=J(r,this.pattern,this.cwd);if(console.log(`[DEBUG] Pattern match result: ${u}`),!u){console.log(`[DEBUG] File does not match pattern, ignoring: ${r}`);return}console.log("[DEBUG] File matches pattern! Processing event...");let l;if(s==="rename"){const i=_(r);l=i?"add":"delete",console.log(`[DEBUG] Rename event, file exists: ${i}, normalized to: ${l}`)}else s==="change"?(l="change",console.log("[DEBUG] Change event detected")):(l="change",console.log(`[DEBUG] Unknown event type: ${s}, defaulting to change`));if(l==="add"){const i=w(r),f=p(i);!this.watchers.has(f)&&f!==e&&(console.log(`[DEBUG] New file detected, ensuring directory is watched: ${f}`),this.watchDirectory(f))}console.log(`[DEBUG] Triggering regeneration for: ${r} (${l})`),this.debounceRegeneration(r,l)});this.watchers.set(e,n),n.on("error",s=>{console.error(`[DEBUG] Watch error in ${e}:`,s)}),console.log(`[DEBUG] Watch successfully set up for: ${e}`)}debounceRegeneration(t,e){console.log(`[DEBUG] debounceRegeneration called for: ${t} (${e})`),this.debounceTimer&&(console.log("[DEBUG] Clearing existing debounce timer"),clearTimeout(this.debounceTimer)),console.log(`[DEBUG] Setting new debounce timer (${D}ms)`),this.debounceTimer=setTimeout(async()=>{if(console.log("[DEBUG] Debounce timer fired, starting regeneration..."),!this.generatorOptions||!this.callback){console.log("[DEBUG] Missing generatorOptions or callback, aborting regeneration");return}try{console.log("[DEBUG] Calling generate with options:",{pattern:this.generatorOptions.pattern,outputPath:this.generatorOptions.outputPath,outputFormat:this.generatorOptions.outputFormat});const n=await E(this.generatorOptions),s=e;this.callback&&this.callback({type:s,file:t}),n.success?console.log(`✅ Regenerated translations (${n.translationsGenerated} translations from ${n.filesFound} file(s))`):console.error(`❌ Regeneration failed: ${n.errors.map(a=>a.message).join(", ")}`)}catch(n){const s=n instanceof Error?n.message:"Unknown error";console.error(`❌ Watch regeneration error: ${s}`)}},D)}setGeneratorOptions(t){this.generatorOptions=t}stop(){if(this.isWatching){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null);for(const[,t]of this.watchers.entries())t.close();this.watchers.clear(),this.isWatching=!1,this.generatorOptions=null,this.callback=null}}}async function q(o){const t=new z;t.setGeneratorOptions(o),console.log("🔍 Watching for translation file changes...");const e=await E(o);e.success?console.log(`✅ Initial generation: ${e.translationsGenerated} translations from ${e.filesFound} file(s)`):console.error(`❌ Initial generation failed: ${e.errors.map(s=>s.message).join(", ")}`),await t.watch(o.pattern,()=>{});const n=()=>{console.log(`
34
+ 👋 Stopping watch mode...`),t.stop(),process.exit(0)};return process.on("SIGINT",n),process.on("SIGTERM",n),new Promise(()=>{})}var H={};const g={reset:"\x1B[0m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m"},b={searching:"🔍",success:"✅",error:"❌",warning:"⚠️"};function V(o){const t={};let e=0;for(;e<o.length;){const n=o[e];if(n.startsWith("--")&&n.includes("=")){const[s,...a]=n.slice(2).split("="),r=a.join("=");t[s]=r||!0,e++;continue}if(n.startsWith("--")){const s=n.slice(2);e+1<o.length&&!o[e+1].startsWith("--")?(t[s]=o[e+1],e+=2):(t[s]=!0,e++);continue}if(n.startsWith("-")&&!n.startsWith("--")){const s=n.slice(1);t[s]=!0,e++;continue}e++}return t}function K(){const o=`
35
+ ${g.blue}vbss-translator generate${g.reset}
36
+ Generate translation index files from translation JSON files.
37
+ ${g.yellow}Usage:${g.reset}
38
+ npx vbss-translator generate [options]
39
+ npm run generate-translations [options]
40
+ ${g.yellow}Options:${g.reset}
41
+ --pattern <glob> Glob pattern to search for translation files
42
+ (default: "src/**/translations.json")
43
+ --output <path> Output file path
44
+ (default: "src/translations/index.ts")
45
+ --format <ts|js|tsx> Output format (default: "ts")
46
+ --watch, -w Enable watch mode (runs continuously)
47
+ --config <path> Path to config file
48
+ (default: "vbss-translator.config.json")
49
+ --reference-language <lang> Reference language for deduplication
50
+ --help, -h Show this help message
51
+ ${g.yellow}Examples:${g.reset}
52
+ npx vbss-translator generate
53
+ npx vbss-translator generate --pattern "src/**/*.json" --output "src/translations.ts"
54
+ npx vbss-translator generate --format js --reference-language en
55
+ ${g.yellow}Configuration:${g.reset}
56
+ Create a "vbss-translator.config.json" file in your project root:
57
+ {
58
+ "pattern": "src/**/translations.json",
59
+ "outputPath": "src/translations/index.ts",
60
+ "outputFormat": "ts",
61
+ "referenceLanguage": "en"
62
+ }
63
+ CLI flags take precedence over config file settings.
64
+ `;console.log(o)}function v(o){return`${g.red}${b.error} ${o}${g.reset}`}function X(o){return`${g.green}${b.success} ${o}${g.reset}`}function Y(o){return`${g.blue}${b.searching} ${o}${g.reset}`}async function Q(){const o=process.argv.slice(2),t=V(o);(t.help||t.h)&&(K(),process.exit(0));try{const e=typeof t.config=="string"?t.config:"vbss-translator.config.json",n=d(process.cwd(),e),s=await y.loadFromFile(n),a=y.loadFromArgs({pattern:t.pattern,output:t.output,format:t.format,"reference-language":t["reference-language"]}),r=y.merge(s||{},a);console.log(Y(`Searching for translation files matching: ${r.pattern}`));const u={pattern:r.pattern,outputPath:d(process.cwd(),r.outputPath),outputFormat:r.outputFormat,referenceLanguage:r.referenceLanguage,watch:t.watch===!0||t.w===!0};if(u.watch){await q(u);return}const l=Date.now(),i=await E(u),f=Date.now()-l;if(i.success)console.log(X(`Generated ${i.translationsGenerated} translations from ${i.filesFound} file(s)`)),console.log(`${g.green}Output:${g.reset} ${i.outputPath}`),console.log(`${g.green}Time:${g.reset} ${f}ms`),process.exit(0);else{console.error(v("Generation failed with errors:"));for(const c of i.errors)console.error(`${g.red} ${c.type}:${g.reset} ${c.file}`),console.error(` ${c.message}`);const h=i.errors.some(c=>c.type==="validation"||c.type==="missing_languages");process.exit(h?2:1)}}catch(e){const n=e instanceof Error?e.message:"Unknown error occurred";console.error(v(n)),e instanceof Error&&e.stack&&(console.error(`${g.red}Stack trace:${g.reset}`),console.error(e.stack)),process.exit(1)}}H.JEST_WORKER_ID||process.argv[1]&&(process.argv[1].includes("cli/index")||process.argv[1].includes("vbss-translator"))&&Q().catch(t=>{const e=t instanceof Error?t.message:"Unknown error occurred";console.error(v(`Fatal error: ${e}`)),process.exit(1)});
@@ -0,0 +1,32 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("glob"),d=require("fs/promises"),h=require("path");async function v(n){try{const r=await y.glob(n,{cwd:process.cwd(),absolute:!1}),t=process.cwd();return r.map(e=>({path:h.resolve(t,e),relativePath:h.normalize(e).replace(/\\/g,"/"),translations:[]}))}catch(r){const t=r instanceof Error?r.message:"Unknown error";return[{path:"",relativePath:"",translations:[],error:{file:n,message:`Failed to discover files: ${t}`,type:"file_system"}}]}}async function $(n){const r=n.map(async t=>{if(t.error)return t;try{const e=await d.readFile(t.path,"utf-8"),a=JSON.parse(e),o=[];if(Array.isArray(a))for(const s of a){if(typeof s!="object"||s===null)return{...t,translations:[],error:{file:t.path,message:"Translation array must contain objects only",type:"validation"}};const c={};for(const[u,i]of Object.entries(s)){if(typeof i!="string")return{...t,translations:[],error:{file:t.path,message:`Translation value for key "${u}" must be a string, got ${typeof i}`,type:"validation"}};c[u]=i}o.push(c)}else if(typeof a=="object"&&a!==null){const s={};for(const[c,u]of Object.entries(a)){if(typeof u!="string")return{...t,translations:[],error:{file:t.path,message:`Translation value for key "${c}" must be a string, got ${typeof u}`,type:"validation"}};s[c]=u}return o.push(s),{...t,translations:o,wasSingleObject:!0}}else return{...t,translations:[],error:{file:t.path,message:"Translation file must contain a JSON object or array of objects",type:"validation"}};return{...t,translations:o}}catch(e){if(e instanceof SyntaxError)return{...t,translations:[],error:{file:t.path,message:`Invalid JSON: ${e.message}`,type:"invalid_json"}};const a=e instanceof Error?e.message:"Unknown error";return{...t,translations:[],error:{file:t.path,message:`Failed to read file: ${a}`,type:"file_system"}}}});return Promise.all(r)}function j(n){for(const r of n)if(!r.error&&r.translations.length>0)return Object.keys(r.translations[0]);return null}function P(n,r){const t=[];for(const e of n){if(e.error)continue;if(e.translations.length===0){t.push({file:e.path,message:"File has no translations",type:"validation"});continue}const a=Object.keys(e.translations[0]);for(let i=1;i<e.translations.length;i++){const f=Object.keys(e.translations[i]);if(JSON.stringify([...f].sort())!==JSON.stringify([...a].sort())){t.push({file:e.path,message:`Translation ${i+1} in file has different language keys`,type:"validation"});continue}}const o=[...a].sort(),s=[...r].sort(),c=s.filter(i=>!o.includes(i)),u=o.filter(i=>!s.includes(i));if(c.length>0||u.length>0){const i=[];c.length>0&&i.push(`missing languages: ${c.join(", ")}`),u.length>0&&i.push(`extra languages: ${u.join(", ")}`),t.push({file:e.path,message:`Language mismatch: ${i.join(", ")}. Expected: ${r.join(", ")}`,type:"missing_languages"})}}return t}function T(n,r){const t=new Set,e=[];for(const a of n){const o=a[r];if(o===void 0){e.push(a);continue}const s=o;t.has(s)||(t.add(s),e.push(a))}return e}function b(n,r){const t=h.dirname(n),e=r;let a=h.relative(t,e);return a=h.normalize(a).replace(/\\/g,"/"),a.startsWith(".")||(a="./"+a),a}function m(n,r,t){const e=n.filter(f=>!f.error&&f.translations.length>0).sort((f,g)=>f.path.localeCompare(g.path));if(e.length===0)return`export default []
2
+ `;const a=e.map((f,g)=>{const l=b(r,f.path);return`import translations${g} from '${l}' assert { type: 'json' };`}).join(`
3
+ `),o=e.map((f,g)=>f.wasSingleObject?` ...(Array.isArray(translations${g}) ? translations${g} : [translations${g}])`:` ...translations${g}`).join(`,
4
+ `),c=`export interface Translation {
5
+ ${t.map(f=>` ${f}: string;`).join(`
6
+ `)}
7
+ [key: string]: string;
8
+ }`,u=t[0]||"en",i=e.length>0?`
9
+ const uniqueTranslations = allTranslations.filter(
10
+ (translation, index, self) => index === self.findIndex((t) => t.${u} === translation.${u})
11
+ );`:"";return`${a}
12
+
13
+ ${c}
14
+
15
+ const allTranslations: Translation[] = [
16
+ ${o}
17
+ ];${i}
18
+
19
+ export default uniqueTranslations;
20
+ `}function F(n){const r=n.filter(s=>!s.error&&s.translations.length>0).sort((s,c)=>s.path.localeCompare(c.path));if(r.length===0)return`export default []
21
+ `;const t=[];for(const s of r)s.wasSingleObject&&s.translations.length===1?t.push(s.translations[0]):t.push(...s.translations);const e=t.length>0?Object.keys(t[0])[0]:"en",a=t.map(s=>JSON.stringify(s,null,2).split(`
22
+ `).map(i=>` ${i}`).join(`
23
+ `)).join(`,
24
+ `),o=`
25
+ const uniqueTranslations = allTranslations.filter(
26
+ (translation, index, self) => index === self.findIndex((t) => t.${e} === translation.${e})
27
+ );`;return`const allTranslations = [
28
+ ${a}
29
+ ];${o}
30
+
31
+ export default uniqueTranslations;
32
+ `}function w(n,r,t){return m(n,r,t)}async function k(n){const r=[],t=Date.now(),e=await v(n.pattern);if(e.length===0)return{success:!0,filesFound:0,translationsGenerated:0,errors:[],outputPath:n.outputPath};for(const l of e)l.error&&r.push(l.error);const a=await $(e);for(const l of a)l.error&&r.push(l.error);const o=j(a);if((!o||o.length===0)&&a.filter(p=>!p.error&&p.translations.length>0).length===0)return{success:!1,filesFound:e.length,translationsGenerated:0,errors:r.length>0?r:[{file:n.pattern,message:"No valid translation files found",type:"validation"}],outputPath:n.outputPath};if(o&&o.length>0){const l=P(a,o);if(r.push(...l),l.length>0)return{success:!1,filesFound:e.length,translationsGenerated:0,errors:r,outputPath:n.outputPath}}const s=[];for(const l of a)!l.error&&l.translations.length>0&&s.push(...l.translations);const c=n.referenceLanguage||(o==null?void 0:o[0])||"en",u=T(s,c);let i;const f=a.filter(l=>!l.error&&l.translations.length>0);switch(n.outputFormat){case"ts":i=m(f,n.outputPath,o||[]);break;case"tsx":i=w(f,n.outputPath,o||[]);break;case"js":i=F(f);break;default:return{success:!1,filesFound:e.length,translationsGenerated:0,errors:[...r,{file:n.outputPath,message:`Unsupported output format: ${n.outputFormat}`,type:"validation"}],outputPath:n.outputPath}}try{const l=h.dirname(n.outputPath);await d.mkdir(l,{recursive:!0}),await d.writeFile(n.outputPath,i,"utf-8")}catch(l){const p=l instanceof Error?l.message:"Unknown error";return r.push({file:n.outputPath,message:`Failed to write output file: ${p}`,type:"file_system"}),{success:!1,filesFound:e.length,translationsGenerated:u.length,errors:r,outputPath:n.outputPath}}const g=Date.now()-t;return g>1e3&&e.length<100&&console.warn(`⚠️ Generation took ${g}ms for ${e.length} files`),{success:r.length===0,filesFound:e.length,translationsGenerated:u.length,errors:r,outputPath:n.outputPath}}exports.generate=k;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Programmatic API for vbss-translator generator
3
+ *
4
+ * This module is Node.js only and should not be imported in browser environments.
5
+ * Use this for build tool integrations (Vite plugins, Webpack loaders, etc.)
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { generate } from 'vbss-translator/generator';
10
+ *
11
+ * const result = await generate({
12
+ * pattern: 'src/**\/translations.json',
13
+ * outputPath: 'src/translations/index.ts',
14
+ * outputFormat: 'ts',
15
+ * });
16
+ * ```
17
+ */
18
+ export { generate } from './cli/generator';
19
+ export type { GeneratorOptions, GenerationResult, GenerationError, OutputFormat, TranslationRecord, } from './cli/types';
@@ -0,0 +1,360 @@
1
+ import { glob as y } from "glob";
2
+ import { mkdir as v, writeFile as $, readFile as j } from "fs/promises";
3
+ import { dirname as p, resolve as F, normalize as d, relative as P } from "path";
4
+ async function T(n) {
5
+ try {
6
+ const r = await y(n, { cwd: process.cwd(), absolute: !1 }), t = process.cwd();
7
+ return r.map((e) => ({
8
+ path: F(t, e),
9
+ relativePath: d(e).replace(/\\/g, "/"),
10
+ translations: []
11
+ }));
12
+ } catch (r) {
13
+ const t = r instanceof Error ? r.message : "Unknown error";
14
+ return [
15
+ {
16
+ path: "",
17
+ relativePath: "",
18
+ translations: [],
19
+ error: {
20
+ file: n,
21
+ message: `Failed to discover files: ${t}`,
22
+ type: "file_system"
23
+ }
24
+ }
25
+ ];
26
+ }
27
+ }
28
+ async function b(n) {
29
+ const r = n.map(async (t) => {
30
+ if (t.error)
31
+ return t;
32
+ try {
33
+ const e = await j(t.path, "utf-8"), a = JSON.parse(e), o = [];
34
+ if (Array.isArray(a))
35
+ for (const s of a) {
36
+ if (typeof s != "object" || s === null)
37
+ return {
38
+ ...t,
39
+ translations: [],
40
+ error: {
41
+ file: t.path,
42
+ message: "Translation array must contain objects only",
43
+ type: "validation"
44
+ }
45
+ };
46
+ const c = {};
47
+ for (const [u, i] of Object.entries(s)) {
48
+ if (typeof i != "string")
49
+ return {
50
+ ...t,
51
+ translations: [],
52
+ error: {
53
+ file: t.path,
54
+ message: `Translation value for key "${u}" must be a string, got ${typeof i}`,
55
+ type: "validation"
56
+ }
57
+ };
58
+ c[u] = i;
59
+ }
60
+ o.push(c);
61
+ }
62
+ else if (typeof a == "object" && a !== null) {
63
+ const s = {};
64
+ for (const [c, u] of Object.entries(a)) {
65
+ if (typeof u != "string")
66
+ return {
67
+ ...t,
68
+ translations: [],
69
+ error: {
70
+ file: t.path,
71
+ message: `Translation value for key "${c}" must be a string, got ${typeof u}`,
72
+ type: "validation"
73
+ }
74
+ };
75
+ s[c] = u;
76
+ }
77
+ return o.push(s), {
78
+ ...t,
79
+ translations: o,
80
+ wasSingleObject: !0
81
+ };
82
+ } else
83
+ return {
84
+ ...t,
85
+ translations: [],
86
+ error: {
87
+ file: t.path,
88
+ message: "Translation file must contain a JSON object or array of objects",
89
+ type: "validation"
90
+ }
91
+ };
92
+ return {
93
+ ...t,
94
+ translations: o
95
+ };
96
+ } catch (e) {
97
+ if (e instanceof SyntaxError)
98
+ return {
99
+ ...t,
100
+ translations: [],
101
+ error: {
102
+ file: t.path,
103
+ message: `Invalid JSON: ${e.message}`,
104
+ type: "invalid_json"
105
+ }
106
+ };
107
+ const a = e instanceof Error ? e.message : "Unknown error";
108
+ return {
109
+ ...t,
110
+ translations: [],
111
+ error: {
112
+ file: t.path,
113
+ message: `Failed to read file: ${a}`,
114
+ type: "file_system"
115
+ }
116
+ };
117
+ }
118
+ });
119
+ return Promise.all(r);
120
+ }
121
+ function w(n) {
122
+ for (const r of n)
123
+ if (!r.error && r.translations.length > 0)
124
+ return Object.keys(r.translations[0]);
125
+ return null;
126
+ }
127
+ function k(n, r) {
128
+ const t = [];
129
+ for (const e of n) {
130
+ if (e.error)
131
+ continue;
132
+ if (e.translations.length === 0) {
133
+ t.push({
134
+ file: e.path,
135
+ message: "File has no translations",
136
+ type: "validation"
137
+ });
138
+ continue;
139
+ }
140
+ const a = Object.keys(e.translations[0]);
141
+ for (let i = 1; i < e.translations.length; i++) {
142
+ const f = Object.keys(e.translations[i]);
143
+ if (JSON.stringify([...f].sort()) !== JSON.stringify([...a].sort())) {
144
+ t.push({
145
+ file: e.path,
146
+ message: `Translation ${i + 1} in file has different language keys`,
147
+ type: "validation"
148
+ });
149
+ continue;
150
+ }
151
+ }
152
+ const o = [...a].sort(), s = [...r].sort(), c = s.filter(
153
+ (i) => !o.includes(i)
154
+ ), u = o.filter(
155
+ (i) => !s.includes(i)
156
+ );
157
+ if (c.length > 0 || u.length > 0) {
158
+ const i = [];
159
+ c.length > 0 && i.push(`missing languages: ${c.join(", ")}`), u.length > 0 && i.push(`extra languages: ${u.join(", ")}`), t.push({
160
+ file: e.path,
161
+ message: `Language mismatch: ${i.join(
162
+ ", "
163
+ )}. Expected: ${r.join(", ")}`,
164
+ type: "missing_languages"
165
+ });
166
+ }
167
+ }
168
+ return t;
169
+ }
170
+ function O(n, r) {
171
+ const t = /* @__PURE__ */ new Set(), e = [];
172
+ for (const a of n) {
173
+ const o = a[r];
174
+ if (o === void 0) {
175
+ e.push(a);
176
+ continue;
177
+ }
178
+ const s = o;
179
+ t.has(s) || (t.add(s), e.push(a));
180
+ }
181
+ return e;
182
+ }
183
+ function S(n, r) {
184
+ const t = p(n);
185
+ let a = P(t, r);
186
+ return a = d(a).replace(/\\/g, "/"), a.startsWith(".") || (a = "./" + a), a;
187
+ }
188
+ function m(n, r, t) {
189
+ const e = n.filter((f) => !f.error && f.translations.length > 0).sort((f, g) => f.path.localeCompare(g.path));
190
+ if (e.length === 0)
191
+ return `export default []
192
+ `;
193
+ const a = e.map((f, g) => {
194
+ const l = S(r, f.path);
195
+ return `import translations${g} from '${l}' assert { type: 'json' };`;
196
+ }).join(`
197
+ `), o = e.map((f, g) => f.wasSingleObject ? ` ...(Array.isArray(translations${g}) ? translations${g} : [translations${g}])` : ` ...translations${g}`).join(`,
198
+ `), c = `export interface Translation {
199
+ ${t.map((f) => ` ${f}: string;`).join(`
200
+ `)}
201
+ [key: string]: string;
202
+ }`, u = t[0] || "en", i = e.length > 0 ? `
203
+ const uniqueTranslations = allTranslations.filter(
204
+ (translation, index, self) => index === self.findIndex((t) => t.${u} === translation.${u})
205
+ );` : "";
206
+ return `${a}
207
+
208
+ ${c}
209
+
210
+ const allTranslations: Translation[] = [
211
+ ${o}
212
+ ];${i}
213
+
214
+ export default uniqueTranslations;
215
+ `;
216
+ }
217
+ function x(n) {
218
+ const r = n.filter((s) => !s.error && s.translations.length > 0).sort((s, c) => s.path.localeCompare(c.path));
219
+ if (r.length === 0)
220
+ return `export default []
221
+ `;
222
+ const t = [];
223
+ for (const s of r)
224
+ s.wasSingleObject && s.translations.length === 1 ? t.push(s.translations[0]) : t.push(...s.translations);
225
+ const e = t.length > 0 ? Object.keys(t[0])[0] : "en", a = t.map((s) => JSON.stringify(s, null, 2).split(`
226
+ `).map((i) => ` ${i}`).join(`
227
+ `)).join(`,
228
+ `), o = `
229
+ const uniqueTranslations = allTranslations.filter(
230
+ (translation, index, self) => index === self.findIndex((t) => t.${e} === translation.${e})
231
+ );`;
232
+ return `const allTranslations = [
233
+ ${a}
234
+ ];${o}
235
+
236
+ export default uniqueTranslations;
237
+ `;
238
+ }
239
+ function L(n, r, t) {
240
+ return m(n, r, t);
241
+ }
242
+ async function N(n) {
243
+ const r = [], t = Date.now(), e = await T(n.pattern);
244
+ if (e.length === 0)
245
+ return {
246
+ success: !0,
247
+ filesFound: 0,
248
+ translationsGenerated: 0,
249
+ errors: [],
250
+ outputPath: n.outputPath
251
+ };
252
+ for (const l of e)
253
+ l.error && r.push(l.error);
254
+ const a = await b(e);
255
+ for (const l of a)
256
+ l.error && r.push(l.error);
257
+ const o = w(a);
258
+ if ((!o || o.length === 0) && a.filter(
259
+ (h) => !h.error && h.translations.length > 0
260
+ ).length === 0)
261
+ return {
262
+ success: !1,
263
+ filesFound: e.length,
264
+ translationsGenerated: 0,
265
+ errors: r.length > 0 ? r : [
266
+ {
267
+ file: n.pattern,
268
+ message: "No valid translation files found",
269
+ type: "validation"
270
+ }
271
+ ],
272
+ outputPath: n.outputPath
273
+ };
274
+ if (o && o.length > 0) {
275
+ const l = k(a, o);
276
+ if (r.push(...l), l.length > 0)
277
+ return {
278
+ success: !1,
279
+ filesFound: e.length,
280
+ translationsGenerated: 0,
281
+ errors: r,
282
+ outputPath: n.outputPath
283
+ };
284
+ }
285
+ const s = [];
286
+ for (const l of a)
287
+ !l.error && l.translations.length > 0 && s.push(...l.translations);
288
+ const c = n.referenceLanguage || (o == null ? void 0 : o[0]) || "en", u = O(
289
+ s,
290
+ c
291
+ );
292
+ let i;
293
+ const f = a.filter(
294
+ (l) => !l.error && l.translations.length > 0
295
+ );
296
+ switch (n.outputFormat) {
297
+ case "ts":
298
+ i = m(
299
+ f,
300
+ n.outputPath,
301
+ o || []
302
+ );
303
+ break;
304
+ case "tsx":
305
+ i = L(
306
+ f,
307
+ n.outputPath,
308
+ o || []
309
+ );
310
+ break;
311
+ case "js":
312
+ i = x(f);
313
+ break;
314
+ default:
315
+ return {
316
+ success: !1,
317
+ filesFound: e.length,
318
+ translationsGenerated: 0,
319
+ errors: [
320
+ ...r,
321
+ {
322
+ file: n.outputPath,
323
+ message: `Unsupported output format: ${n.outputFormat}`,
324
+ type: "validation"
325
+ }
326
+ ],
327
+ outputPath: n.outputPath
328
+ };
329
+ }
330
+ try {
331
+ const l = p(n.outputPath);
332
+ await v(l, { recursive: !0 }), await $(n.outputPath, i, "utf-8");
333
+ } catch (l) {
334
+ const h = l instanceof Error ? l.message : "Unknown error";
335
+ return r.push({
336
+ file: n.outputPath,
337
+ message: `Failed to write output file: ${h}`,
338
+ type: "file_system"
339
+ }), {
340
+ success: !1,
341
+ filesFound: e.length,
342
+ translationsGenerated: u.length,
343
+ errors: r,
344
+ outputPath: n.outputPath
345
+ };
346
+ }
347
+ const g = Date.now() - t;
348
+ return g > 1e3 && e.length < 100 && console.warn(
349
+ `⚠️ Generation took ${g}ms for ${e.length} files`
350
+ ), {
351
+ success: r.length === 0,
352
+ filesFound: e.length,
353
+ translationsGenerated: u.length,
354
+ errors: r,
355
+ outputPath: n.outputPath
356
+ };
357
+ }
358
+ export {
359
+ N as generate
360
+ };
@@ -1,25 +1,138 @@
1
1
  import { JSX as JSX_2 } from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
2
3
 
3
- declare type Translation = {
4
- [key: string]: string;
4
+ declare type CacheConfig = {
5
+ enabled: boolean;
6
+ ttlMs: number;
7
+ maxEntries?: number;
8
+ };
9
+
10
+ export declare const createTranslationProvider: (config: ProviderConfig) => TranslationProvider;
11
+
12
+ declare type CustomProviderConfig = {
13
+ readonly id: "custom";
14
+ readonly implementation?: TranslationProvider;
15
+ readonly factory?: CustomProviderFactory;
16
+ readonly options?: Record<string, unknown>;
17
+ };
18
+
19
+ declare type CustomProviderFactory = () => TranslationProvider;
20
+
21
+ declare type ExternalTranslationCallbacks = {
22
+ shouldTranslate?: (request: ExternalTranslationRequest) => boolean;
23
+ onExternalTranslation?: (request: ExternalTranslationRequest) => void | boolean | Promise<void | boolean>;
24
+ onTranslationError?: (event: ExternalTranslationErrorEvent) => void;
25
+ onTranslationComplete?: (result: TranslateResult) => void;
26
+ };
27
+
28
+ declare type ExternalTranslationConfig = ExternalTranslationCallbacks & {
29
+ enabled: boolean;
30
+ provider: ProviderConfig;
31
+ cache: CacheConfig;
32
+ alwaysExternalKeys: ReadonlySet<string>;
33
+ timeoutMs: number;
34
+ debug?: boolean;
35
+ };
36
+
37
+ declare type ExternalTranslationConfigInput = Partial<Omit<ExternalTranslationConfig, "alwaysExternalKeys">> & {
38
+ alwaysExternalKeys?: ReadonlyArray<string> | ReadonlySet<string>;
39
+ };
40
+
41
+ declare type ExternalTranslationErrorEvent = {
42
+ key: string;
43
+ language: string;
44
+ error: ProviderError;
45
+ originalText: string;
46
+ };
47
+
48
+ declare type ExternalTranslationRequest = {
49
+ key: string;
50
+ text: string;
51
+ targetLanguage: string;
52
+ sourceLanguage?: string;
53
+ };
54
+
55
+ declare type GoogleProviderConfig = {
56
+ readonly id: "google";
57
+ readonly apiKey?: string;
58
+ readonly endpoint?: string;
59
+ readonly projectId?: string;
60
+ readonly region?: string;
61
+ readonly headers?: Record<string, string>;
62
+ };
63
+
64
+ declare type ProviderAvailability = {
65
+ available: boolean;
66
+ reason?: string;
67
+ };
68
+
69
+ declare type ProviderConfig = GoogleProviderConfig | CustomProviderConfig;
70
+
71
+ declare type ProviderError = {
72
+ code: string;
73
+ message: string;
74
+ retryable: boolean;
75
+ details?: unknown;
76
+ };
77
+
78
+ declare type ProviderId = "google" | "custom";
79
+
80
+ declare type TranslateOptions = {
81
+ preferExternal?: boolean;
82
+ sourceLanguage?: string;
83
+ fallbackValue?: string;
84
+ signal?: AbortSignal;
85
+ };
86
+
87
+ declare type TranslateRequest = {
88
+ text: string;
89
+ sourceLanguage: string;
90
+ targetLanguage: string;
91
+ glossary?: Record<string, string>;
92
+ };
93
+
94
+ declare type TranslateResult = {
95
+ translatedText: string;
96
+ detectedSourceLanguage?: string;
97
+ providerMetadata?: Record<string, string>;
98
+ };
99
+
100
+ declare type Translation = Record<string, string>;
101
+
102
+ export declare interface TranslationProvider {
103
+ readonly type: ProviderId;
104
+ translate(request: TranslateRequest, options?: TranslationProviderTranslateOptions): Promise<TranslateResult>;
105
+ isAvailable?(): ProviderAvailability;
106
+ normalizeError?(error: unknown): ProviderError;
107
+ }
108
+
109
+ declare type TranslationProviderTranslateOptions = {
110
+ readonly signal?: AbortSignal;
111
+ readonly timeoutMs?: number;
112
+ readonly headers?: Record<string, string>;
5
113
  };
6
114
 
7
115
  declare type TranslatorContextType = {
8
- t: (key: string) => string;
116
+ t: (key: string, options?: TranslateOptions) => string;
9
117
  language: string;
10
118
  languages: string[];
11
119
  setLanguage: (language: string) => void;
120
+ isTranslating: Record<string, boolean>;
121
+ isTranslatingAny: boolean;
122
+ registerExternalKey: (key: string) => void;
123
+ externalConfig: ExternalTranslationConfig;
12
124
  };
13
125
 
14
- export declare const TranslatorProvider: ({ children, translations, defaultLanguage, persist, persistKey, autoDetectLanguage }: TranslatorProviderType) => JSX_2.Element;
126
+ export declare const TranslatorProvider: ({ children, translations, defaultLanguage, persist, persistKey, autoDetectLanguage, externalTranslation, }: TranslatorProviderType) => JSX_2.Element;
15
127
 
16
128
  declare type TranslatorProviderType = {
17
- children: React.ReactNode;
129
+ children: ReactNode;
18
130
  translations: Translation[];
19
131
  defaultLanguage?: string;
20
132
  persist?: boolean;
21
133
  persistKey?: string;
22
134
  autoDetectLanguage?: boolean;
135
+ externalTranslation?: ExternalTranslationConfigInput;
23
136
  };
24
137
 
25
138
  export declare const useTranslator: () => TranslatorContextType;