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 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
- | `glossary` | `Record<string, string>` | `undefined` | Optional term overrides sent when the provider supports them. |
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
- checkAvailability: async () => ({ available: true }),
208
- translate: async (request: TranslateRequest): Promise<TranslateResult> => {
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
- checkAvailability: async () => {
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: TranslateRequest): Promise<TranslateResult> => {
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: string;
303
- checkAvailability: () => Promise<ProviderAvailability>;
304
- translate: (request: TranslateRequest) => Promise<TranslateResult>;
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`: String identifier for your provider (typically `"custom"`).
310
- - `checkAvailability`: Validates provider readiness (e.g., credentials, network).
311
- - `translate`: Accepts `TranslateRequest` (text, source/target languages, optional glossary) and returns `TranslateResult` (translated text, optional metadata).
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/factory";
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 = await provider.checkAvailability();
362
- console.log("Provider available:", availability.available);
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:"❌",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=`
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}