vbss-translator 1.1.0 → 1.1.2
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/README.md +17 -21
- package/dist/cli/index.js +1 -1
- package/dist/vbss-translator.js +1704 -2151
- package/dist/vbss-translator.umd.cjs +9 -35
- package/package.json +111 -90
package/README.md
CHANGED
|
@@ -146,10 +146,6 @@ const externalTranslation = {
|
|
|
146
146
|
ttlMs: 30 * 60 * 1000,
|
|
147
147
|
maxEntries: 500,
|
|
148
148
|
},
|
|
149
|
-
glossary: {
|
|
150
|
-
// Optional terminology map forwarded to providers that support glossaries
|
|
151
|
-
BRAND_A: "Marca A",
|
|
152
|
-
},
|
|
153
149
|
alwaysExternalKeys: ["product.description"],
|
|
154
150
|
shouldTranslate: ({ key, text }) => !text.includes("SECRET"),
|
|
155
151
|
onExternalTranslation: ({ key, text }) => {
|
|
@@ -172,8 +168,7 @@ const externalTranslation = {
|
|
|
172
168
|
| `debug` | `boolean` | `false` | Emits structured logs for cache hits, deduped requests, retries, etc. |
|
|
173
169
|
| `provider` | `ProviderConfig` | `{ id: "google" }` | Google or custom provider config. See provider sections below. |
|
|
174
170
|
| `cache` | `CacheConfig` | `{ enabled: false, ttlMs: 3_600_000 }` | In-memory cache with TTL and optional LRU size limit (`maxEntries`). |
|
|
175
|
-
| `
|
|
176
|
-
| `alwaysExternalKeys` | `ReadonlySet` | `new Set()` | Automatically merged with strings registered at runtime. |
|
|
171
|
+
| `alwaysExternalKeys` | `ReadonlyArray<string> \| ReadonlySet<string>` | `new Set()` | Automatically merged with strings registered at runtime. |
|
|
177
172
|
| `shouldTranslate` | `(request) => boolean` | `undefined` | Synchronous guard invoked before caching/dedup. Exceptions default to `true`. |
|
|
178
173
|
| `onExternalTranslation` | `(request) => void \| boolean` | `undefined` | Async-friendly hook after `shouldTranslate` but before the network call. Returning `false` cancels the request. |
|
|
179
174
|
| `onTranslationError` | `(event) => void` | `undefined` | Receives normalized provider errors with retry metadata. |
|
|
@@ -197,15 +192,13 @@ Supply either an `implementation` (a pre-built provider instance) or a `factory`
|
|
|
197
192
|
import {
|
|
198
193
|
TranslatorProvider,
|
|
199
194
|
type TranslationProvider,
|
|
200
|
-
type TranslateRequest,
|
|
201
|
-
type TranslateResult,
|
|
202
195
|
} from "vbss-translator";
|
|
203
196
|
|
|
204
197
|
// Custom provider implementation
|
|
205
198
|
const myCustomProvider: TranslationProvider = {
|
|
206
199
|
type: "custom",
|
|
207
|
-
|
|
208
|
-
translate: async (request
|
|
200
|
+
isAvailable: () => ({ available: true }),
|
|
201
|
+
translate: async (request) => {
|
|
209
202
|
// Your custom translation logic here
|
|
210
203
|
const response = await fetch("https://my-translation-api.com/translate", {
|
|
211
204
|
method: "POST",
|
|
@@ -250,13 +243,13 @@ const providerFactory = () => {
|
|
|
250
243
|
|
|
251
244
|
return {
|
|
252
245
|
type: "custom",
|
|
253
|
-
|
|
246
|
+
isAvailable: () => {
|
|
254
247
|
if (!apiKey || !endpoint) {
|
|
255
248
|
return { available: false, reason: "Missing configuration" };
|
|
256
249
|
}
|
|
257
250
|
return { available: true };
|
|
258
251
|
},
|
|
259
|
-
translate: async (request
|
|
252
|
+
translate: async (request) => {
|
|
260
253
|
const response = await fetch(endpoint, {
|
|
261
254
|
method: "POST",
|
|
262
255
|
headers: {
|
|
@@ -299,16 +292,19 @@ Your implementation must satisfy the `TranslationProvider` interface:
|
|
|
299
292
|
|
|
300
293
|
```typescript
|
|
301
294
|
interface TranslationProvider {
|
|
302
|
-
type:
|
|
303
|
-
|
|
304
|
-
|
|
295
|
+
type: ProviderId;
|
|
296
|
+
translate: (
|
|
297
|
+
request: TranslateRequest,
|
|
298
|
+
options?: TranslationProviderTranslateOptions
|
|
299
|
+
) => Promise<TranslateResult>;
|
|
300
|
+
isAvailable?: () => ProviderAvailability;
|
|
305
301
|
normalizeError?: (error: unknown) => ProviderError;
|
|
306
302
|
}
|
|
307
303
|
```
|
|
308
304
|
|
|
309
|
-
- `type`:
|
|
310
|
-
- `
|
|
311
|
-
- `
|
|
305
|
+
- `type`: Provider identifier (`"google"` or `"custom"`).
|
|
306
|
+
- `translate`: Accepts `TranslateRequest` (text, source/target languages, optional glossary) plus optional `TranslationProviderTranslateOptions` (`signal`, `timeoutMs`, `headers`) and returns `TranslateResult` (translated text, optional metadata).
|
|
307
|
+
- `isAvailable` (optional): Synchronously validates provider readiness (e.g., credentials, network).
|
|
312
308
|
- `normalizeError` (optional): Converts provider-specific errors into structured `ProviderError` with retryable flags.
|
|
313
309
|
|
|
314
310
|
#### Switching Between Providers
|
|
@@ -350,7 +346,7 @@ const activeConfig = useGoogleTranslate ? googleConfig : customConfig;
|
|
|
350
346
|
Validate your custom provider before production:
|
|
351
347
|
|
|
352
348
|
```typescript
|
|
353
|
-
import { createTranslationProvider } from "vbss-translator
|
|
349
|
+
import { createTranslationProvider } from "vbss-translator";
|
|
354
350
|
|
|
355
351
|
const provider = createTranslationProvider({
|
|
356
352
|
id: "custom",
|
|
@@ -358,8 +354,8 @@ const provider = createTranslationProvider({
|
|
|
358
354
|
});
|
|
359
355
|
|
|
360
356
|
// Test availability
|
|
361
|
-
const availability =
|
|
362
|
-
console.log("Provider available:", availability
|
|
357
|
+
const availability = provider.isAvailable?.();
|
|
358
|
+
console.log("Provider available:", availability?.available);
|
|
363
359
|
|
|
364
360
|
// Test translation
|
|
365
361
|
const result = await provider.translate({
|
package/dist/cli/index.js
CHANGED
|
@@ -31,7 +31,7 @@ ${s}
|
|
|
31
31
|
|
|
32
32
|
export default uniqueTranslations;
|
|
33
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:"❌"
|
|
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:"❌"};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
35
|
${g.blue}vbss-translator generate${g.reset}
|
|
36
36
|
Generate translation index files from translation JSON files.
|
|
37
37
|
${g.yellow}Usage:${g.reset}
|