langie 1.9.26 → 1.10.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.
@@ -5,6 +5,12 @@ export interface BatchingOptions {
5
5
  followupBatchDelay?: number;
6
6
  maxBatchSize?: number;
7
7
  maxWaitTime?: number;
8
+ /**
9
+ * Supplies a fresh Cloudflare Turnstile token per batch request. Required by the
10
+ * keyless `/frontend/translate` proxy when Turnstile is enabled server-side.
11
+ * Single-use tokens, so it is invoked once per outgoing request.
12
+ */
13
+ getTurnstileToken?: () => string | Promise<string>;
8
14
  }
9
15
  export interface BatchRequest {
10
16
  [API_FIELD_TEXT]: string;
@@ -27,3 +27,4 @@ export declare const API_FIELD_CTX = "ctx";
27
27
  export declare const API_FIELD_TRANSLATED = "t";
28
28
  export declare const API_FIELD_TRANSLATIONS = "translations";
29
29
  export declare const API_FIELD_ERROR = "error";
30
+ export declare const API_FIELD_TURNSTILE = "turnstileToken";
package/dist/core.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * langie v1.9.26
2
+ * langie v1.10.0
3
3
  * (c) 2026 nlit
4
4
  * @license Apache-2.0
5
5
  *
@@ -43,6 +43,7 @@ var API_FIELD_TO = "to";
43
43
  var API_FIELD_CTX = "ctx";
44
44
  var API_FIELD_TRANSLATIONS = "translations";
45
45
  var API_FIELD_ERROR = "error";
46
+ var API_FIELD_TURNSTILE = "turnstileToken";
46
47
 
47
48
  // src/core.ts
48
49
  var DEFAULT_TRANSLATOR_HOST = "http://localhost:8081";
@@ -84,6 +85,7 @@ async function translateBatch(translations = [], options = {}) {
84
85
  } else {
85
86
  const translationPromise = (async () => {
86
87
  try {
88
+ const turnstileToken = options.getTurnstileToken ? await options.getTurnstileToken() : void 0;
87
89
  const controller = new AbortController();
88
90
  const timeoutId = setTimeout(() => controller.abort(), 5e3);
89
91
  const resp = await fetch(`${translatorHost}/translate`, {
@@ -99,7 +101,8 @@ async function translateBatch(translations = [], options = {}) {
99
101
  [API_FIELD_TO]: t[API_FIELD_TO],
100
102
  [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
101
103
  })),
102
- [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui"
104
+ [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui",
105
+ ...turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {}
103
106
  }),
104
107
  signal: controller.signal
105
108
  });
package/dist/core.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.ts","../src/constants.ts"],"sourcesContent":["/**\n * Core translation functions\n */\nimport type {\n TranslateRequestBody,\n TranslateServiceResponse,\n TranslatorLanguage,\n TranslatorOptions\n} from './types'\nimport {\n API_FIELD_TEXT,\n API_FIELD_FROM,\n API_FIELD_TO,\n API_FIELD_CTX,\n API_FIELD_TRANSLATIONS,\n API_FIELD_ERROR\n} from './constants'\n\nconst DEFAULT_TRANSLATOR_HOST = 'http://localhost:8081'\n\n// --- Translation cache ---\nconst translationCache = new Map<string, Promise<TranslateServiceResponse[]>>()\n\nfunction getTranslationCacheKey(\n serviceTranslations: TranslateRequestBody[],\n options: TranslatorOptions\n): string {\n // Create a stable cache key based on translations and options\n return JSON.stringify({\n translations: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n host: options.translatorHost || DEFAULT_TRANSLATOR_HOST,\n context: options[API_FIELD_CTX] || 'ui'\n })\n}\n\n/**\n * Translates a batch of texts\n *\n * @param translations Array of translation requests\n * @param options Configuration options\n * @returns Array of translated texts\n */\nexport async function translateBatch(\n translations: TranslateRequestBody[] = [],\n options: TranslatorOptions = {}\n): Promise<TranslateServiceResponse[]> {\n // const startTime = Date.now()\n\n // try {\n // console.debug('[translator-sdk] translateBatch called', {\n // count: translations.length,\n // sample: translations.slice(0, 3).map((t) => ({\n // text: t.text?.slice?.(0, 30) ?? '',\n // from: t.from_lang,\n // to: t.to_lang\n // })),\n // opts: { ...options }\n // })\n // } catch (_) {\n // // Ignore logging errors – never block translation\n // }\n\n if (!Array.isArray(translations) || translations.length === 0) {\n // console.error('[translator-sdk] Invalid translations input:', { translations })\n throw new Error('translations must be a non-empty array')\n }\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n\n // console.debug('[translator-sdk] Configuration', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // apiKeyLength: apiKey?.length || 0\n // })\n\n // Partition translations: identical language pairs can be returned as-is\n const serviceTranslations: TranslateRequestBody[] = []\n const indexMap: number[] = []\n\n translations.forEach((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) {\n // console.debug('[translator-sdk] Skipping translation (same language)', { from, to, text: tr.t?.slice(0, 20) })\n return // no need to translate\n }\n serviceTranslations.push(tr)\n indexMap.push(idx)\n })\n\n // console.debug('[translator-sdk] Translation partitioning', {\n // total: translations.length,\n // needsTranslation: serviceTranslations.length,\n // skipped: translations.length - serviceTranslations.length\n // })\n\n let serviceResults: TranslateServiceResponse[] = []\n if (serviceTranslations.length > 0) {\n // Check cache first\n const cacheKey = getTranslationCacheKey(serviceTranslations, options)\n if (translationCache.has(cacheKey)) {\n // console.debug('[translator-sdk] Cache hit for translation batch')\n serviceResults = await translationCache.get(cacheKey)!\n } else {\n // const requestStartTime = Date.now()\n // console.debug('[translator-sdk] Sending request to translator', {\n // host: translatorHost,\n // apiKeySnippet: apiKey ? `${apiKey.slice(0, 4)}…${apiKey.slice(-4)}` : 'none',\n // batchCount: serviceTranslations.length,\n // requestSize: JSON.stringify({ translations: serviceTranslations }).length\n // })\n\n const translationPromise = (async () => {\n try {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 5000) // 5s timeout\n\n const resp = await fetch(`${translatorHost}/translate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n },\n body: JSON.stringify({\n [API_FIELD_TRANSLATIONS]: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n [API_FIELD_CTX]: options[API_FIELD_CTX] || 'ui'\n }),\n signal: controller.signal\n })\n\n clearTimeout(timeoutId)\n // const requestDuration = Date.now() - requestStartTime\n\n // console.debug('[translator-sdk] Translator response received', {\n // status: resp.status,\n // statusText: resp.statusText,\n // duration: `${requestDuration}ms`,\n // contentType: resp.headers.get('content-type'),\n // contentLength: resp.headers.get('content-length')\n // })\n\n if (!resp.ok) {\n console.error(\n '[translator-sdk] Translator error response:',\n resp.status,\n resp.statusText\n )\n throw new Error(`Translator service error: ${resp.status} ${resp.statusText}`)\n }\n\n let parsed: {\n translations?: TranslateServiceResponse[]\n t?: string\n error?: string\n } | null\n try {\n parsed = await resp.clone().json()\n // console.debug('[translator-sdk] Response parsed successfully', {\n // hasTranslations: !!parsed?.translations,\n // translationCount: parsed?.translations?.length || 0,\n // hasLegacyT: !!parsed?.t\n // })\n } catch (jsonErr) {\n console.error('[translator-sdk] Failed to parse JSON response:', jsonErr)\n throw new Error(`Failed to parse JSON response: ${jsonErr}`)\n }\n\n // if (parsed) {\n // console.debug(\n // '[translator-sdk] Translator response sample',\n // JSON.stringify(parsed).slice(0, 500)\n // )\n // }\n\n const data = parsed || {}\n\n // Handle top-level error responses\n if (data[API_FIELD_ERROR]) {\n console.warn(`[translator-sdk] Top-level API error:`, data[API_FIELD_ERROR])\n\n // Create error responses for all translations\n return serviceTranslations.map((translation) => ({\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT],\n [API_FIELD_ERROR]: data[API_FIELD_ERROR]\n }))\n }\n\n const results = Array.isArray(data[API_FIELD_TRANSLATIONS])\n ? data[API_FIELD_TRANSLATIONS].map((translation, index) => {\n const originalText = serviceTranslations[index]?.[API_FIELD_TEXT] || ''\n\n // Handle error responses\n if (translation[API_FIELD_ERROR]) {\n console.warn(\n `[translator-sdk] Translation error for \"${originalText}\":`,\n translation[API_FIELD_ERROR]\n )\n return {\n [API_FIELD_TEXT]: originalText, // Return original text on error\n [API_FIELD_ERROR]: translation[API_FIELD_ERROR]\n }\n }\n\n // Handle language detection response\n if (translation[API_FIELD_FROM] && !translation[API_FIELD_TEXT]) {\n return {\n [API_FIELD_TEXT]: originalText, // For detection, return original text\n [API_FIELD_FROM]: translation[API_FIELD_FROM]\n }\n }\n\n // Handle translation response\n return {\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT] || originalText\n }\n })\n : data[API_FIELD_TEXT]\n ? [{ [API_FIELD_TEXT]: data[API_FIELD_TEXT] }]\n : []\n\n // console.debug('[translator-sdk] Service results processed', {\n // resultCount: results.length,\n // expectedCount: serviceTranslations.length\n // })\n\n return results\n } catch (error: unknown) {\n // Remove from cache on error\n translationCache.delete(cacheKey)\n\n if (error instanceof Error && error.name === 'AbortError') {\n const message = `Translator request to ${translatorHost} timed out after 5 seconds.`\n console.error(message)\n throw new Error(message)\n }\n const message = `Failed to connect to translator at ${translatorHost}. Is the service running?`\n console.error(message)\n throw new Error(message)\n }\n })()\n\n // Cache the promise\n translationCache.set(cacheKey, translationPromise)\n serviceResults = await translationPromise\n }\n }\n\n // Assemble final results in original order\n const final = translations.map((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n\n const svcIdx = indexMap.indexOf(idx)\n if (svcIdx !== -1) return serviceResults[svcIdx] || { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n })\n\n // const totalDuration = Date.now() - startTime\n // console.debug('[translator-sdk] translateBatch completed', {\n // totalDuration: `${totalDuration}ms`,\n // resultCount: final.length,\n // successCount: final.filter(r => r.translated && r.translated !== r.text).length\n // })\n\n return final as TranslateServiceResponse[]\n}\n\n// --- Cache management functions ---\nexport function clearTranslationCache(): void {\n translationCache.clear()\n}\n\nexport function getTranslationCacheSize(): number {\n return translationCache.size\n}\n\n/** Vendor/legacy codes whose primary subtag isn't their BCP-47 base language. */\nconst BASE_CODE_ALIASES: Record<string, string> = {\n kazlat: 'kk' // Kazakh (Latin) — vendor code, not \"kk-latn\"\n}\n\n/**\n * Reduce a language code to its BCP-47 primary subtag, so script/region variants\n * share a base: `sr-cyrl`/`sr-latn` → `sr`, `kk`/`kazlat` → `kk`.\n */\nexport function baseLanguageCode(code: string): string {\n const c = (code || '').trim().toLowerCase().replace(/_/g, '-')\n return BASE_CODE_ALIASES[c] ?? (c.split('-')[0] || c)\n}\n\n/**\n * Fetches available languages from the translation service\n *\n * @param options Configuration options\n * @returns Array of available languages\n */\nexport async function fetchAvailableLanguages(\n options: TranslatorOptions = {}\n): Promise<TranslatorLanguage[]> {\n // const startTime = Date.now()\n\n // console.debug('[translator-sdk] fetchAvailableLanguages called', { options })\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n // Popularity filtering is opt-in. Default 0 returns the full catalog so the\n // caller (e.g. a searchable picker) gets every language and every script\n // variant; a host env var must not silently prune the list.\n const minPop = options.minPopularity !== undefined ? Number(options.minPopularity) : 0\n\n // console.debug('[translator-sdk] Languages request config', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // minPopularity: minPop,\n // country: options.country,\n // region: options.region\n // })\n\n const queryParams = []\n if (options.country) queryParams.push(`country=${encodeURIComponent(options.country)}`)\n if (options.region) queryParams.push(`region=${encodeURIComponent(options.region)}`)\n const queryStr = queryParams.length ? `?${queryParams.join('&')}` : ''\n\n const requestUrl = `${translatorHost}/languages${queryStr}`\n\n try {\n const resp = await fetch(requestUrl, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n }\n })\n\n if (!resp.ok) {\n throw new Error(`Translator languages error: ${resp.status}`)\n }\n\n const data = await resp.json()\n const languages: TranslatorLanguage[] = Array.isArray(data) ? data : data.languages || []\n\n if (minPop <= 0) return languages\n\n const passes = (lang: TranslatorLanguage): boolean =>\n lang.popularity === undefined || lang.popularity === null || Number(lang.popularity) >= minPop\n\n // Keep every script/region variant of a base language that has at least one\n // passing entry, so a low-popularity variant (e.g. Kazakh Cyrillic with\n // popularity 0) survives next to its popular sibling and the UI can still\n // offer the \"which variant?\" choice. Otherwise a flat floor splits the pair.\n const keptBases = new Set(languages.filter(passes).map((l) => baseLanguageCode(l.code)))\n return languages.filter((lang) => passes(lang) || keptBases.has(baseLanguageCode(lang.code)))\n } catch (error) {\n console.error('[translator-sdk] Languages fetch error:', error)\n throw error\n }\n}\n","/**\n * Default API configuration constants\n */\nexport const DEFAULT_API_HOST = 'https://api.langie.uk/v1'\n\n/**\n * Development/Demo API host for local testing.\n * Uses the keyless first-party `/frontend` proxy (the backend injects\n * PAGES_API_KEY); the raw `/v1` endpoints require a client API key.\n */\nexport const DEV_API_HOST = 'http://localhost:8081/frontend'\n\n/**\n * Default translator options\n */\nexport const DEFAULT_TRANSLATOR_OPTIONS = {\n translatorHost: DEFAULT_API_HOST,\n defaultLanguage: 'en',\n fallbackLanguage: 'en'\n} as const\n\n/**\n * API field constants\n * Used throughout the SDK to ensure consistency\n */\n\n// Translation text field\nexport const API_FIELD_TEXT = 't'\n\n// Source language field\nexport const API_FIELD_FROM = 'from'\n\n// Target language field\nexport const API_FIELD_TO = 'to'\n\n// Context field\nexport const API_FIELD_CTX = 'ctx'\n\n// Translated text field (same as text field for response)\nexport const API_FIELD_TRANSLATED = 't'\n\n// Translations array field\nexport const API_FIELD_TRANSLATIONS = 'translations'\n\n// Error field for API responses\nexport const API_FIELD_ERROR = 'error'\n"],"mappings":"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","names":["message"]}
1
+ {"version":3,"sources":["../src/core.ts","../src/constants.ts"],"sourcesContent":["/**\n * Core translation functions\n */\nimport type {\n TranslateRequestBody,\n TranslateServiceResponse,\n TranslatorLanguage,\n TranslatorOptions\n} from './types'\nimport {\n API_FIELD_TEXT,\n API_FIELD_FROM,\n API_FIELD_TO,\n API_FIELD_CTX,\n API_FIELD_TRANSLATIONS,\n API_FIELD_ERROR,\n API_FIELD_TURNSTILE\n} from './constants'\n\nconst DEFAULT_TRANSLATOR_HOST = 'http://localhost:8081'\n\n// --- Translation cache ---\nconst translationCache = new Map<string, Promise<TranslateServiceResponse[]>>()\n\nfunction getTranslationCacheKey(\n serviceTranslations: TranslateRequestBody[],\n options: TranslatorOptions\n): string {\n // Create a stable cache key based on translations and options\n return JSON.stringify({\n translations: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n host: options.translatorHost || DEFAULT_TRANSLATOR_HOST,\n context: options[API_FIELD_CTX] || 'ui'\n })\n}\n\n/**\n * Translates a batch of texts\n *\n * @param translations Array of translation requests\n * @param options Configuration options\n * @returns Array of translated texts\n */\nexport async function translateBatch(\n translations: TranslateRequestBody[] = [],\n options: TranslatorOptions = {}\n): Promise<TranslateServiceResponse[]> {\n // const startTime = Date.now()\n\n // try {\n // console.debug('[translator-sdk] translateBatch called', {\n // count: translations.length,\n // sample: translations.slice(0, 3).map((t) => ({\n // text: t.text?.slice?.(0, 30) ?? '',\n // from: t.from_lang,\n // to: t.to_lang\n // })),\n // opts: { ...options }\n // })\n // } catch (_) {\n // // Ignore logging errors – never block translation\n // }\n\n if (!Array.isArray(translations) || translations.length === 0) {\n // console.error('[translator-sdk] Invalid translations input:', { translations })\n throw new Error('translations must be a non-empty array')\n }\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n\n // console.debug('[translator-sdk] Configuration', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // apiKeyLength: apiKey?.length || 0\n // })\n\n // Partition translations: identical language pairs can be returned as-is\n const serviceTranslations: TranslateRequestBody[] = []\n const indexMap: number[] = []\n\n translations.forEach((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) {\n // console.debug('[translator-sdk] Skipping translation (same language)', { from, to, text: tr.t?.slice(0, 20) })\n return // no need to translate\n }\n serviceTranslations.push(tr)\n indexMap.push(idx)\n })\n\n // console.debug('[translator-sdk] Translation partitioning', {\n // total: translations.length,\n // needsTranslation: serviceTranslations.length,\n // skipped: translations.length - serviceTranslations.length\n // })\n\n let serviceResults: TranslateServiceResponse[] = []\n if (serviceTranslations.length > 0) {\n // Check cache first\n const cacheKey = getTranslationCacheKey(serviceTranslations, options)\n if (translationCache.has(cacheKey)) {\n // console.debug('[translator-sdk] Cache hit for translation batch')\n serviceResults = await translationCache.get(cacheKey)!\n } else {\n // const requestStartTime = Date.now()\n // console.debug('[translator-sdk] Sending request to translator', {\n // host: translatorHost,\n // apiKeySnippet: apiKey ? `${apiKey.slice(0, 4)}…${apiKey.slice(-4)}` : 'none',\n // batchCount: serviceTranslations.length,\n // requestSize: JSON.stringify({ translations: serviceTranslations }).length\n // })\n\n const translationPromise = (async () => {\n try {\n // Fresh Turnstile token per flush (single-use); only the keyless\n // /frontend/translate proxy requires it, other hosts ignore the field.\n const turnstileToken = options.getTurnstileToken\n ? await options.getTurnstileToken()\n : undefined\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 5000) // 5s timeout\n\n const resp = await fetch(`${translatorHost}/translate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n },\n body: JSON.stringify({\n [API_FIELD_TRANSLATIONS]: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n [API_FIELD_CTX]: options[API_FIELD_CTX] || 'ui',\n ...(turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {})\n }),\n signal: controller.signal\n })\n\n clearTimeout(timeoutId)\n // const requestDuration = Date.now() - requestStartTime\n\n // console.debug('[translator-sdk] Translator response received', {\n // status: resp.status,\n // statusText: resp.statusText,\n // duration: `${requestDuration}ms`,\n // contentType: resp.headers.get('content-type'),\n // contentLength: resp.headers.get('content-length')\n // })\n\n if (!resp.ok) {\n console.error(\n '[translator-sdk] Translator error response:',\n resp.status,\n resp.statusText\n )\n throw new Error(`Translator service error: ${resp.status} ${resp.statusText}`)\n }\n\n let parsed: {\n translations?: TranslateServiceResponse[]\n t?: string\n error?: string\n } | null\n try {\n parsed = await resp.clone().json()\n // console.debug('[translator-sdk] Response parsed successfully', {\n // hasTranslations: !!parsed?.translations,\n // translationCount: parsed?.translations?.length || 0,\n // hasLegacyT: !!parsed?.t\n // })\n } catch (jsonErr) {\n console.error('[translator-sdk] Failed to parse JSON response:', jsonErr)\n throw new Error(`Failed to parse JSON response: ${jsonErr}`)\n }\n\n // if (parsed) {\n // console.debug(\n // '[translator-sdk] Translator response sample',\n // JSON.stringify(parsed).slice(0, 500)\n // )\n // }\n\n const data = parsed || {}\n\n // Handle top-level error responses\n if (data[API_FIELD_ERROR]) {\n console.warn(`[translator-sdk] Top-level API error:`, data[API_FIELD_ERROR])\n\n // Create error responses for all translations\n return serviceTranslations.map((translation) => ({\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT],\n [API_FIELD_ERROR]: data[API_FIELD_ERROR]\n }))\n }\n\n const results = Array.isArray(data[API_FIELD_TRANSLATIONS])\n ? data[API_FIELD_TRANSLATIONS].map((translation, index) => {\n const originalText = serviceTranslations[index]?.[API_FIELD_TEXT] || ''\n\n // Handle error responses\n if (translation[API_FIELD_ERROR]) {\n console.warn(\n `[translator-sdk] Translation error for \"${originalText}\":`,\n translation[API_FIELD_ERROR]\n )\n return {\n [API_FIELD_TEXT]: originalText, // Return original text on error\n [API_FIELD_ERROR]: translation[API_FIELD_ERROR]\n }\n }\n\n // Handle language detection response\n if (translation[API_FIELD_FROM] && !translation[API_FIELD_TEXT]) {\n return {\n [API_FIELD_TEXT]: originalText, // For detection, return original text\n [API_FIELD_FROM]: translation[API_FIELD_FROM]\n }\n }\n\n // Handle translation response\n return {\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT] || originalText\n }\n })\n : data[API_FIELD_TEXT]\n ? [{ [API_FIELD_TEXT]: data[API_FIELD_TEXT] }]\n : []\n\n // console.debug('[translator-sdk] Service results processed', {\n // resultCount: results.length,\n // expectedCount: serviceTranslations.length\n // })\n\n return results\n } catch (error: unknown) {\n // Remove from cache on error\n translationCache.delete(cacheKey)\n\n if (error instanceof Error && error.name === 'AbortError') {\n const message = `Translator request to ${translatorHost} timed out after 5 seconds.`\n console.error(message)\n throw new Error(message)\n }\n const message = `Failed to connect to translator at ${translatorHost}. Is the service running?`\n console.error(message)\n throw new Error(message)\n }\n })()\n\n // Cache the promise\n translationCache.set(cacheKey, translationPromise)\n serviceResults = await translationPromise\n }\n }\n\n // Assemble final results in original order\n const final = translations.map((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n\n const svcIdx = indexMap.indexOf(idx)\n if (svcIdx !== -1) return serviceResults[svcIdx] || { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n })\n\n // const totalDuration = Date.now() - startTime\n // console.debug('[translator-sdk] translateBatch completed', {\n // totalDuration: `${totalDuration}ms`,\n // resultCount: final.length,\n // successCount: final.filter(r => r.translated && r.translated !== r.text).length\n // })\n\n return final as TranslateServiceResponse[]\n}\n\n// --- Cache management functions ---\nexport function clearTranslationCache(): void {\n translationCache.clear()\n}\n\nexport function getTranslationCacheSize(): number {\n return translationCache.size\n}\n\n/** Vendor/legacy codes whose primary subtag isn't their BCP-47 base language. */\nconst BASE_CODE_ALIASES: Record<string, string> = {\n kazlat: 'kk' // Kazakh (Latin) — vendor code, not \"kk-latn\"\n}\n\n/**\n * Reduce a language code to its BCP-47 primary subtag, so script/region variants\n * share a base: `sr-cyrl`/`sr-latn` → `sr`, `kk`/`kazlat` → `kk`.\n */\nexport function baseLanguageCode(code: string): string {\n const c = (code || '').trim().toLowerCase().replace(/_/g, '-')\n return BASE_CODE_ALIASES[c] ?? (c.split('-')[0] || c)\n}\n\n/**\n * Fetches available languages from the translation service\n *\n * @param options Configuration options\n * @returns Array of available languages\n */\nexport async function fetchAvailableLanguages(\n options: TranslatorOptions = {}\n): Promise<TranslatorLanguage[]> {\n // const startTime = Date.now()\n\n // console.debug('[translator-sdk] fetchAvailableLanguages called', { options })\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n // Popularity filtering is opt-in. Default 0 returns the full catalog so the\n // caller (e.g. a searchable picker) gets every language and every script\n // variant; a host env var must not silently prune the list.\n const minPop = options.minPopularity !== undefined ? Number(options.minPopularity) : 0\n\n // console.debug('[translator-sdk] Languages request config', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // minPopularity: minPop,\n // country: options.country,\n // region: options.region\n // })\n\n const queryParams = []\n if (options.country) queryParams.push(`country=${encodeURIComponent(options.country)}`)\n if (options.region) queryParams.push(`region=${encodeURIComponent(options.region)}`)\n const queryStr = queryParams.length ? `?${queryParams.join('&')}` : ''\n\n const requestUrl = `${translatorHost}/languages${queryStr}`\n\n try {\n const resp = await fetch(requestUrl, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n }\n })\n\n if (!resp.ok) {\n throw new Error(`Translator languages error: ${resp.status}`)\n }\n\n const data = await resp.json()\n const languages: TranslatorLanguage[] = Array.isArray(data) ? data : data.languages || []\n\n if (minPop <= 0) return languages\n\n const passes = (lang: TranslatorLanguage): boolean =>\n lang.popularity === undefined || lang.popularity === null || Number(lang.popularity) >= minPop\n\n // Keep every script/region variant of a base language that has at least one\n // passing entry, so a low-popularity variant (e.g. Kazakh Cyrillic with\n // popularity 0) survives next to its popular sibling and the UI can still\n // offer the \"which variant?\" choice. Otherwise a flat floor splits the pair.\n const keptBases = new Set(languages.filter(passes).map((l) => baseLanguageCode(l.code)))\n return languages.filter((lang) => passes(lang) || keptBases.has(baseLanguageCode(lang.code)))\n } catch (error) {\n console.error('[translator-sdk] Languages fetch error:', error)\n throw error\n }\n}\n","/**\n * Default API configuration constants\n */\nexport const DEFAULT_API_HOST = 'https://api.langie.uk/v1'\n\n/**\n * Development/Demo API host for local testing.\n * Uses the keyless first-party `/frontend` proxy (the backend injects\n * PAGES_API_KEY); the raw `/v1` endpoints require a client API key.\n */\nexport const DEV_API_HOST = 'http://localhost:8081/frontend'\n\n/**\n * Default translator options\n */\nexport const DEFAULT_TRANSLATOR_OPTIONS = {\n translatorHost: DEFAULT_API_HOST,\n defaultLanguage: 'en',\n fallbackLanguage: 'en'\n} as const\n\n/**\n * API field constants\n * Used throughout the SDK to ensure consistency\n */\n\n// Translation text field\nexport const API_FIELD_TEXT = 't'\n\n// Source language field\nexport const API_FIELD_FROM = 'from'\n\n// Target language field\nexport const API_FIELD_TO = 'to'\n\n// Context field\nexport const API_FIELD_CTX = 'ctx'\n\n// Translated text field (same as text field for response)\nexport const API_FIELD_TRANSLATED = 't'\n\n// Translations array field\nexport const API_FIELD_TRANSLATIONS = 'translations'\n\n// Error field for API responses\nexport const API_FIELD_ERROR = 'error'\n\n// Cloudflare Turnstile token field — anti-abuse on the keyless /frontend/translate proxy\nexport const API_FIELD_TURNSTILE = 'turnstileToken'\n"],"mappings":"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","names":["message"]}
package/dist/core.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * langie v1.9.26
2
+ * langie v1.10.0
3
3
  * (c) 2026 nlit
4
4
  * @license Apache-2.0
5
5
  *
@@ -14,6 +14,7 @@ var API_FIELD_TO = "to";
14
14
  var API_FIELD_CTX = "ctx";
15
15
  var API_FIELD_TRANSLATIONS = "translations";
16
16
  var API_FIELD_ERROR = "error";
17
+ var API_FIELD_TURNSTILE = "turnstileToken";
17
18
 
18
19
  // src/core.ts
19
20
  var DEFAULT_TRANSLATOR_HOST = "http://localhost:8081";
@@ -55,6 +56,7 @@ async function translateBatch(translations = [], options = {}) {
55
56
  } else {
56
57
  const translationPromise = (async () => {
57
58
  try {
59
+ const turnstileToken = options.getTurnstileToken ? await options.getTurnstileToken() : void 0;
58
60
  const controller = new AbortController();
59
61
  const timeoutId = setTimeout(() => controller.abort(), 5e3);
60
62
  const resp = await fetch(`${translatorHost}/translate`, {
@@ -70,7 +72,8 @@ async function translateBatch(translations = [], options = {}) {
70
72
  [API_FIELD_TO]: t[API_FIELD_TO],
71
73
  [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
72
74
  })),
73
- [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui"
75
+ [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui",
76
+ ...turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {}
74
77
  }),
75
78
  signal: controller.signal
76
79
  });
package/dist/core.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/constants.ts","../src/core.ts"],"sourcesContent":["/**\n * Default API configuration constants\n */\nexport const DEFAULT_API_HOST = 'https://api.langie.uk/v1'\n\n/**\n * Development/Demo API host for local testing.\n * Uses the keyless first-party `/frontend` proxy (the backend injects\n * PAGES_API_KEY); the raw `/v1` endpoints require a client API key.\n */\nexport const DEV_API_HOST = 'http://localhost:8081/frontend'\n\n/**\n * Default translator options\n */\nexport const DEFAULT_TRANSLATOR_OPTIONS = {\n translatorHost: DEFAULT_API_HOST,\n defaultLanguage: 'en',\n fallbackLanguage: 'en'\n} as const\n\n/**\n * API field constants\n * Used throughout the SDK to ensure consistency\n */\n\n// Translation text field\nexport const API_FIELD_TEXT = 't'\n\n// Source language field\nexport const API_FIELD_FROM = 'from'\n\n// Target language field\nexport const API_FIELD_TO = 'to'\n\n// Context field\nexport const API_FIELD_CTX = 'ctx'\n\n// Translated text field (same as text field for response)\nexport const API_FIELD_TRANSLATED = 't'\n\n// Translations array field\nexport const API_FIELD_TRANSLATIONS = 'translations'\n\n// Error field for API responses\nexport const API_FIELD_ERROR = 'error'\n","/**\n * Core translation functions\n */\nimport type {\n TranslateRequestBody,\n TranslateServiceResponse,\n TranslatorLanguage,\n TranslatorOptions\n} from './types'\nimport {\n API_FIELD_TEXT,\n API_FIELD_FROM,\n API_FIELD_TO,\n API_FIELD_CTX,\n API_FIELD_TRANSLATIONS,\n API_FIELD_ERROR\n} from './constants'\n\nconst DEFAULT_TRANSLATOR_HOST = 'http://localhost:8081'\n\n// --- Translation cache ---\nconst translationCache = new Map<string, Promise<TranslateServiceResponse[]>>()\n\nfunction getTranslationCacheKey(\n serviceTranslations: TranslateRequestBody[],\n options: TranslatorOptions\n): string {\n // Create a stable cache key based on translations and options\n return JSON.stringify({\n translations: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n host: options.translatorHost || DEFAULT_TRANSLATOR_HOST,\n context: options[API_FIELD_CTX] || 'ui'\n })\n}\n\n/**\n * Translates a batch of texts\n *\n * @param translations Array of translation requests\n * @param options Configuration options\n * @returns Array of translated texts\n */\nexport async function translateBatch(\n translations: TranslateRequestBody[] = [],\n options: TranslatorOptions = {}\n): Promise<TranslateServiceResponse[]> {\n // const startTime = Date.now()\n\n // try {\n // console.debug('[translator-sdk] translateBatch called', {\n // count: translations.length,\n // sample: translations.slice(0, 3).map((t) => ({\n // text: t.text?.slice?.(0, 30) ?? '',\n // from: t.from_lang,\n // to: t.to_lang\n // })),\n // opts: { ...options }\n // })\n // } catch (_) {\n // // Ignore logging errors – never block translation\n // }\n\n if (!Array.isArray(translations) || translations.length === 0) {\n // console.error('[translator-sdk] Invalid translations input:', { translations })\n throw new Error('translations must be a non-empty array')\n }\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n\n // console.debug('[translator-sdk] Configuration', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // apiKeyLength: apiKey?.length || 0\n // })\n\n // Partition translations: identical language pairs can be returned as-is\n const serviceTranslations: TranslateRequestBody[] = []\n const indexMap: number[] = []\n\n translations.forEach((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) {\n // console.debug('[translator-sdk] Skipping translation (same language)', { from, to, text: tr.t?.slice(0, 20) })\n return // no need to translate\n }\n serviceTranslations.push(tr)\n indexMap.push(idx)\n })\n\n // console.debug('[translator-sdk] Translation partitioning', {\n // total: translations.length,\n // needsTranslation: serviceTranslations.length,\n // skipped: translations.length - serviceTranslations.length\n // })\n\n let serviceResults: TranslateServiceResponse[] = []\n if (serviceTranslations.length > 0) {\n // Check cache first\n const cacheKey = getTranslationCacheKey(serviceTranslations, options)\n if (translationCache.has(cacheKey)) {\n // console.debug('[translator-sdk] Cache hit for translation batch')\n serviceResults = await translationCache.get(cacheKey)!\n } else {\n // const requestStartTime = Date.now()\n // console.debug('[translator-sdk] Sending request to translator', {\n // host: translatorHost,\n // apiKeySnippet: apiKey ? `${apiKey.slice(0, 4)}…${apiKey.slice(-4)}` : 'none',\n // batchCount: serviceTranslations.length,\n // requestSize: JSON.stringify({ translations: serviceTranslations }).length\n // })\n\n const translationPromise = (async () => {\n try {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 5000) // 5s timeout\n\n const resp = await fetch(`${translatorHost}/translate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n },\n body: JSON.stringify({\n [API_FIELD_TRANSLATIONS]: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n [API_FIELD_CTX]: options[API_FIELD_CTX] || 'ui'\n }),\n signal: controller.signal\n })\n\n clearTimeout(timeoutId)\n // const requestDuration = Date.now() - requestStartTime\n\n // console.debug('[translator-sdk] Translator response received', {\n // status: resp.status,\n // statusText: resp.statusText,\n // duration: `${requestDuration}ms`,\n // contentType: resp.headers.get('content-type'),\n // contentLength: resp.headers.get('content-length')\n // })\n\n if (!resp.ok) {\n console.error(\n '[translator-sdk] Translator error response:',\n resp.status,\n resp.statusText\n )\n throw new Error(`Translator service error: ${resp.status} ${resp.statusText}`)\n }\n\n let parsed: {\n translations?: TranslateServiceResponse[]\n t?: string\n error?: string\n } | null\n try {\n parsed = await resp.clone().json()\n // console.debug('[translator-sdk] Response parsed successfully', {\n // hasTranslations: !!parsed?.translations,\n // translationCount: parsed?.translations?.length || 0,\n // hasLegacyT: !!parsed?.t\n // })\n } catch (jsonErr) {\n console.error('[translator-sdk] Failed to parse JSON response:', jsonErr)\n throw new Error(`Failed to parse JSON response: ${jsonErr}`)\n }\n\n // if (parsed) {\n // console.debug(\n // '[translator-sdk] Translator response sample',\n // JSON.stringify(parsed).slice(0, 500)\n // )\n // }\n\n const data = parsed || {}\n\n // Handle top-level error responses\n if (data[API_FIELD_ERROR]) {\n console.warn(`[translator-sdk] Top-level API error:`, data[API_FIELD_ERROR])\n\n // Create error responses for all translations\n return serviceTranslations.map((translation) => ({\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT],\n [API_FIELD_ERROR]: data[API_FIELD_ERROR]\n }))\n }\n\n const results = Array.isArray(data[API_FIELD_TRANSLATIONS])\n ? data[API_FIELD_TRANSLATIONS].map((translation, index) => {\n const originalText = serviceTranslations[index]?.[API_FIELD_TEXT] || ''\n\n // Handle error responses\n if (translation[API_FIELD_ERROR]) {\n console.warn(\n `[translator-sdk] Translation error for \"${originalText}\":`,\n translation[API_FIELD_ERROR]\n )\n return {\n [API_FIELD_TEXT]: originalText, // Return original text on error\n [API_FIELD_ERROR]: translation[API_FIELD_ERROR]\n }\n }\n\n // Handle language detection response\n if (translation[API_FIELD_FROM] && !translation[API_FIELD_TEXT]) {\n return {\n [API_FIELD_TEXT]: originalText, // For detection, return original text\n [API_FIELD_FROM]: translation[API_FIELD_FROM]\n }\n }\n\n // Handle translation response\n return {\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT] || originalText\n }\n })\n : data[API_FIELD_TEXT]\n ? [{ [API_FIELD_TEXT]: data[API_FIELD_TEXT] }]\n : []\n\n // console.debug('[translator-sdk] Service results processed', {\n // resultCount: results.length,\n // expectedCount: serviceTranslations.length\n // })\n\n return results\n } catch (error: unknown) {\n // Remove from cache on error\n translationCache.delete(cacheKey)\n\n if (error instanceof Error && error.name === 'AbortError') {\n const message = `Translator request to ${translatorHost} timed out after 5 seconds.`\n console.error(message)\n throw new Error(message)\n }\n const message = `Failed to connect to translator at ${translatorHost}. Is the service running?`\n console.error(message)\n throw new Error(message)\n }\n })()\n\n // Cache the promise\n translationCache.set(cacheKey, translationPromise)\n serviceResults = await translationPromise\n }\n }\n\n // Assemble final results in original order\n const final = translations.map((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n\n const svcIdx = indexMap.indexOf(idx)\n if (svcIdx !== -1) return serviceResults[svcIdx] || { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n })\n\n // const totalDuration = Date.now() - startTime\n // console.debug('[translator-sdk] translateBatch completed', {\n // totalDuration: `${totalDuration}ms`,\n // resultCount: final.length,\n // successCount: final.filter(r => r.translated && r.translated !== r.text).length\n // })\n\n return final as TranslateServiceResponse[]\n}\n\n// --- Cache management functions ---\nexport function clearTranslationCache(): void {\n translationCache.clear()\n}\n\nexport function getTranslationCacheSize(): number {\n return translationCache.size\n}\n\n/** Vendor/legacy codes whose primary subtag isn't their BCP-47 base language. */\nconst BASE_CODE_ALIASES: Record<string, string> = {\n kazlat: 'kk' // Kazakh (Latin) — vendor code, not \"kk-latn\"\n}\n\n/**\n * Reduce a language code to its BCP-47 primary subtag, so script/region variants\n * share a base: `sr-cyrl`/`sr-latn` → `sr`, `kk`/`kazlat` → `kk`.\n */\nexport function baseLanguageCode(code: string): string {\n const c = (code || '').trim().toLowerCase().replace(/_/g, '-')\n return BASE_CODE_ALIASES[c] ?? (c.split('-')[0] || c)\n}\n\n/**\n * Fetches available languages from the translation service\n *\n * @param options Configuration options\n * @returns Array of available languages\n */\nexport async function fetchAvailableLanguages(\n options: TranslatorOptions = {}\n): Promise<TranslatorLanguage[]> {\n // const startTime = Date.now()\n\n // console.debug('[translator-sdk] fetchAvailableLanguages called', { options })\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n // Popularity filtering is opt-in. Default 0 returns the full catalog so the\n // caller (e.g. a searchable picker) gets every language and every script\n // variant; a host env var must not silently prune the list.\n const minPop = options.minPopularity !== undefined ? Number(options.minPopularity) : 0\n\n // console.debug('[translator-sdk] Languages request config', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // minPopularity: minPop,\n // country: options.country,\n // region: options.region\n // })\n\n const queryParams = []\n if (options.country) queryParams.push(`country=${encodeURIComponent(options.country)}`)\n if (options.region) queryParams.push(`region=${encodeURIComponent(options.region)}`)\n const queryStr = queryParams.length ? `?${queryParams.join('&')}` : ''\n\n const requestUrl = `${translatorHost}/languages${queryStr}`\n\n try {\n const resp = await fetch(requestUrl, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n }\n })\n\n if (!resp.ok) {\n throw new Error(`Translator languages error: ${resp.status}`)\n }\n\n const data = await resp.json()\n const languages: TranslatorLanguage[] = Array.isArray(data) ? data : data.languages || []\n\n if (minPop <= 0) return languages\n\n const passes = (lang: TranslatorLanguage): boolean =>\n lang.popularity === undefined || lang.popularity === null || Number(lang.popularity) >= minPop\n\n // Keep every script/region variant of a base language that has at least one\n // passing entry, so a low-popularity variant (e.g. Kazakh Cyrillic with\n // popularity 0) survives next to its popular sibling and the UI can still\n // offer the \"which variant?\" choice. Otherwise a flat floor splits the pair.\n const keptBases = new Set(languages.filter(passes).map((l) => baseLanguageCode(l.code)))\n return languages.filter((lang) => passes(lang) || keptBases.has(baseLanguageCode(lang.code)))\n } catch (error) {\n console.error('[translator-sdk] Languages fetch error:', error)\n throw error\n }\n}\n"],"mappings":"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","names":["message"]}
1
+ {"version":3,"sources":["../src/constants.ts","../src/core.ts"],"sourcesContent":["/**\n * Default API configuration constants\n */\nexport const DEFAULT_API_HOST = 'https://api.langie.uk/v1'\n\n/**\n * Development/Demo API host for local testing.\n * Uses the keyless first-party `/frontend` proxy (the backend injects\n * PAGES_API_KEY); the raw `/v1` endpoints require a client API key.\n */\nexport const DEV_API_HOST = 'http://localhost:8081/frontend'\n\n/**\n * Default translator options\n */\nexport const DEFAULT_TRANSLATOR_OPTIONS = {\n translatorHost: DEFAULT_API_HOST,\n defaultLanguage: 'en',\n fallbackLanguage: 'en'\n} as const\n\n/**\n * API field constants\n * Used throughout the SDK to ensure consistency\n */\n\n// Translation text field\nexport const API_FIELD_TEXT = 't'\n\n// Source language field\nexport const API_FIELD_FROM = 'from'\n\n// Target language field\nexport const API_FIELD_TO = 'to'\n\n// Context field\nexport const API_FIELD_CTX = 'ctx'\n\n// Translated text field (same as text field for response)\nexport const API_FIELD_TRANSLATED = 't'\n\n// Translations array field\nexport const API_FIELD_TRANSLATIONS = 'translations'\n\n// Error field for API responses\nexport const API_FIELD_ERROR = 'error'\n\n// Cloudflare Turnstile token field — anti-abuse on the keyless /frontend/translate proxy\nexport const API_FIELD_TURNSTILE = 'turnstileToken'\n","/**\n * Core translation functions\n */\nimport type {\n TranslateRequestBody,\n TranslateServiceResponse,\n TranslatorLanguage,\n TranslatorOptions\n} from './types'\nimport {\n API_FIELD_TEXT,\n API_FIELD_FROM,\n API_FIELD_TO,\n API_FIELD_CTX,\n API_FIELD_TRANSLATIONS,\n API_FIELD_ERROR,\n API_FIELD_TURNSTILE\n} from './constants'\n\nconst DEFAULT_TRANSLATOR_HOST = 'http://localhost:8081'\n\n// --- Translation cache ---\nconst translationCache = new Map<string, Promise<TranslateServiceResponse[]>>()\n\nfunction getTranslationCacheKey(\n serviceTranslations: TranslateRequestBody[],\n options: TranslatorOptions\n): string {\n // Create a stable cache key based on translations and options\n return JSON.stringify({\n translations: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n host: options.translatorHost || DEFAULT_TRANSLATOR_HOST,\n context: options[API_FIELD_CTX] || 'ui'\n })\n}\n\n/**\n * Translates a batch of texts\n *\n * @param translations Array of translation requests\n * @param options Configuration options\n * @returns Array of translated texts\n */\nexport async function translateBatch(\n translations: TranslateRequestBody[] = [],\n options: TranslatorOptions = {}\n): Promise<TranslateServiceResponse[]> {\n // const startTime = Date.now()\n\n // try {\n // console.debug('[translator-sdk] translateBatch called', {\n // count: translations.length,\n // sample: translations.slice(0, 3).map((t) => ({\n // text: t.text?.slice?.(0, 30) ?? '',\n // from: t.from_lang,\n // to: t.to_lang\n // })),\n // opts: { ...options }\n // })\n // } catch (_) {\n // // Ignore logging errors – never block translation\n // }\n\n if (!Array.isArray(translations) || translations.length === 0) {\n // console.error('[translator-sdk] Invalid translations input:', { translations })\n throw new Error('translations must be a non-empty array')\n }\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n\n // console.debug('[translator-sdk] Configuration', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // apiKeyLength: apiKey?.length || 0\n // })\n\n // Partition translations: identical language pairs can be returned as-is\n const serviceTranslations: TranslateRequestBody[] = []\n const indexMap: number[] = []\n\n translations.forEach((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) {\n // console.debug('[translator-sdk] Skipping translation (same language)', { from, to, text: tr.t?.slice(0, 20) })\n return // no need to translate\n }\n serviceTranslations.push(tr)\n indexMap.push(idx)\n })\n\n // console.debug('[translator-sdk] Translation partitioning', {\n // total: translations.length,\n // needsTranslation: serviceTranslations.length,\n // skipped: translations.length - serviceTranslations.length\n // })\n\n let serviceResults: TranslateServiceResponse[] = []\n if (serviceTranslations.length > 0) {\n // Check cache first\n const cacheKey = getTranslationCacheKey(serviceTranslations, options)\n if (translationCache.has(cacheKey)) {\n // console.debug('[translator-sdk] Cache hit for translation batch')\n serviceResults = await translationCache.get(cacheKey)!\n } else {\n // const requestStartTime = Date.now()\n // console.debug('[translator-sdk] Sending request to translator', {\n // host: translatorHost,\n // apiKeySnippet: apiKey ? `${apiKey.slice(0, 4)}…${apiKey.slice(-4)}` : 'none',\n // batchCount: serviceTranslations.length,\n // requestSize: JSON.stringify({ translations: serviceTranslations }).length\n // })\n\n const translationPromise = (async () => {\n try {\n // Fresh Turnstile token per flush (single-use); only the keyless\n // /frontend/translate proxy requires it, other hosts ignore the field.\n const turnstileToken = options.getTurnstileToken\n ? await options.getTurnstileToken()\n : undefined\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 5000) // 5s timeout\n\n const resp = await fetch(`${translatorHost}/translate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n },\n body: JSON.stringify({\n [API_FIELD_TRANSLATIONS]: serviceTranslations.map((t) => ({\n [API_FIELD_TEXT]: t[API_FIELD_TEXT],\n [API_FIELD_FROM]: t[API_FIELD_FROM],\n [API_FIELD_TO]: t[API_FIELD_TO],\n [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || 'ui'\n })),\n [API_FIELD_CTX]: options[API_FIELD_CTX] || 'ui',\n ...(turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {})\n }),\n signal: controller.signal\n })\n\n clearTimeout(timeoutId)\n // const requestDuration = Date.now() - requestStartTime\n\n // console.debug('[translator-sdk] Translator response received', {\n // status: resp.status,\n // statusText: resp.statusText,\n // duration: `${requestDuration}ms`,\n // contentType: resp.headers.get('content-type'),\n // contentLength: resp.headers.get('content-length')\n // })\n\n if (!resp.ok) {\n console.error(\n '[translator-sdk] Translator error response:',\n resp.status,\n resp.statusText\n )\n throw new Error(`Translator service error: ${resp.status} ${resp.statusText}`)\n }\n\n let parsed: {\n translations?: TranslateServiceResponse[]\n t?: string\n error?: string\n } | null\n try {\n parsed = await resp.clone().json()\n // console.debug('[translator-sdk] Response parsed successfully', {\n // hasTranslations: !!parsed?.translations,\n // translationCount: parsed?.translations?.length || 0,\n // hasLegacyT: !!parsed?.t\n // })\n } catch (jsonErr) {\n console.error('[translator-sdk] Failed to parse JSON response:', jsonErr)\n throw new Error(`Failed to parse JSON response: ${jsonErr}`)\n }\n\n // if (parsed) {\n // console.debug(\n // '[translator-sdk] Translator response sample',\n // JSON.stringify(parsed).slice(0, 500)\n // )\n // }\n\n const data = parsed || {}\n\n // Handle top-level error responses\n if (data[API_FIELD_ERROR]) {\n console.warn(`[translator-sdk] Top-level API error:`, data[API_FIELD_ERROR])\n\n // Create error responses for all translations\n return serviceTranslations.map((translation) => ({\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT],\n [API_FIELD_ERROR]: data[API_FIELD_ERROR]\n }))\n }\n\n const results = Array.isArray(data[API_FIELD_TRANSLATIONS])\n ? data[API_FIELD_TRANSLATIONS].map((translation, index) => {\n const originalText = serviceTranslations[index]?.[API_FIELD_TEXT] || ''\n\n // Handle error responses\n if (translation[API_FIELD_ERROR]) {\n console.warn(\n `[translator-sdk] Translation error for \"${originalText}\":`,\n translation[API_FIELD_ERROR]\n )\n return {\n [API_FIELD_TEXT]: originalText, // Return original text on error\n [API_FIELD_ERROR]: translation[API_FIELD_ERROR]\n }\n }\n\n // Handle language detection response\n if (translation[API_FIELD_FROM] && !translation[API_FIELD_TEXT]) {\n return {\n [API_FIELD_TEXT]: originalText, // For detection, return original text\n [API_FIELD_FROM]: translation[API_FIELD_FROM]\n }\n }\n\n // Handle translation response\n return {\n [API_FIELD_TEXT]: translation[API_FIELD_TEXT] || originalText\n }\n })\n : data[API_FIELD_TEXT]\n ? [{ [API_FIELD_TEXT]: data[API_FIELD_TEXT] }]\n : []\n\n // console.debug('[translator-sdk] Service results processed', {\n // resultCount: results.length,\n // expectedCount: serviceTranslations.length\n // })\n\n return results\n } catch (error: unknown) {\n // Remove from cache on error\n translationCache.delete(cacheKey)\n\n if (error instanceof Error && error.name === 'AbortError') {\n const message = `Translator request to ${translatorHost} timed out after 5 seconds.`\n console.error(message)\n throw new Error(message)\n }\n const message = `Failed to connect to translator at ${translatorHost}. Is the service running?`\n console.error(message)\n throw new Error(message)\n }\n })()\n\n // Cache the promise\n translationCache.set(cacheKey, translationPromise)\n serviceResults = await translationPromise\n }\n }\n\n // Assemble final results in original order\n const final = translations.map((tr, idx) => {\n const from = (tr[API_FIELD_FROM] || '').toLowerCase()\n const to = (tr[API_FIELD_TO] || '').toLowerCase()\n if (from === to) return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n\n const svcIdx = indexMap.indexOf(idx)\n if (svcIdx !== -1) return serviceResults[svcIdx] || { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] }\n })\n\n // const totalDuration = Date.now() - startTime\n // console.debug('[translator-sdk] translateBatch completed', {\n // totalDuration: `${totalDuration}ms`,\n // resultCount: final.length,\n // successCount: final.filter(r => r.translated && r.translated !== r.text).length\n // })\n\n return final as TranslateServiceResponse[]\n}\n\n// --- Cache management functions ---\nexport function clearTranslationCache(): void {\n translationCache.clear()\n}\n\nexport function getTranslationCacheSize(): number {\n return translationCache.size\n}\n\n/** Vendor/legacy codes whose primary subtag isn't their BCP-47 base language. */\nconst BASE_CODE_ALIASES: Record<string, string> = {\n kazlat: 'kk' // Kazakh (Latin) — vendor code, not \"kk-latn\"\n}\n\n/**\n * Reduce a language code to its BCP-47 primary subtag, so script/region variants\n * share a base: `sr-cyrl`/`sr-latn` → `sr`, `kk`/`kazlat` → `kk`.\n */\nexport function baseLanguageCode(code: string): string {\n const c = (code || '').trim().toLowerCase().replace(/_/g, '-')\n return BASE_CODE_ALIASES[c] ?? (c.split('-')[0] || c)\n}\n\n/**\n * Fetches available languages from the translation service\n *\n * @param options Configuration options\n * @returns Array of available languages\n */\nexport async function fetchAvailableLanguages(\n options: TranslatorOptions = {}\n): Promise<TranslatorLanguage[]> {\n // const startTime = Date.now()\n\n // console.debug('[translator-sdk] fetchAvailableLanguages called', { options })\n\n const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST\n const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY\n // Popularity filtering is opt-in. Default 0 returns the full catalog so the\n // caller (e.g. a searchable picker) gets every language and every script\n // variant; a host env var must not silently prune the list.\n const minPop = options.minPopularity !== undefined ? Number(options.minPopularity) : 0\n\n // console.debug('[translator-sdk] Languages request config', {\n // translatorHost,\n // hasApiKey: !!apiKey,\n // minPopularity: minPop,\n // country: options.country,\n // region: options.region\n // })\n\n const queryParams = []\n if (options.country) queryParams.push(`country=${encodeURIComponent(options.country)}`)\n if (options.region) queryParams.push(`region=${encodeURIComponent(options.region)}`)\n const queryStr = queryParams.length ? `?${queryParams.join('&')}` : ''\n\n const requestUrl = `${translatorHost}/languages${queryStr}`\n\n try {\n const resp = await fetch(requestUrl, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}`, 'X-Api-Key': apiKey } : {})\n }\n })\n\n if (!resp.ok) {\n throw new Error(`Translator languages error: ${resp.status}`)\n }\n\n const data = await resp.json()\n const languages: TranslatorLanguage[] = Array.isArray(data) ? data : data.languages || []\n\n if (minPop <= 0) return languages\n\n const passes = (lang: TranslatorLanguage): boolean =>\n lang.popularity === undefined || lang.popularity === null || Number(lang.popularity) >= minPop\n\n // Keep every script/region variant of a base language that has at least one\n // passing entry, so a low-popularity variant (e.g. Kazakh Cyrillic with\n // popularity 0) survives next to its popular sibling and the UI can still\n // offer the \"which variant?\" choice. Otherwise a flat floor splits the pair.\n const keptBases = new Set(languages.filter(passes).map((l) => baseLanguageCode(l.code)))\n return languages.filter((lang) => passes(lang) || keptBases.has(baseLanguageCode(lang.code)))\n } catch (error) {\n console.error('[translator-sdk] Languages fetch error:', error)\n throw error\n }\n}\n"],"mappings":"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","names":["message"]}
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * langie v1.9.26
2
+ * langie v1.10.0
3
3
  * (c) 2026 nlit
4
4
  * @license Apache-2.0
5
5
  *
@@ -93,6 +93,7 @@ var API_FIELD_CTX = "ctx";
93
93
  var API_FIELD_TRANSLATED = "t";
94
94
  var API_FIELD_TRANSLATIONS = "translations";
95
95
  var API_FIELD_ERROR = "error";
96
+ var API_FIELD_TURNSTILE = "turnstileToken";
96
97
 
97
98
  // src/utils/debug.ts
98
99
  var import_meta = {};
@@ -676,21 +677,29 @@ var TranslationBatching = class {
676
677
  "items",
677
678
  batchRequests
678
679
  );
679
- const response = await fetch(`${this.translatorHost}/translate`, {
680
+ const buildBody = (turnstileToken) => JSON.stringify({
681
+ translations: batchRequests.map((req) => ({
682
+ [API_FIELD_TEXT]: req[API_FIELD_TEXT],
683
+ ...useGlobalContext ? {} : { [API_FIELD_CTX]: req[API_FIELD_CTX] }
684
+ })),
685
+ [API_FIELD_FROM]: from,
686
+ [API_FIELD_TO]: to,
687
+ ...useGlobalContext ? { [API_FIELD_CTX]: "ui" } : {},
688
+ ...turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {}
689
+ });
690
+ const postTranslate = (turnstileToken) => fetch(`${this.translatorHost}/translate`, {
680
691
  method: "POST",
681
- headers: {
682
- "Content-Type": "application/json"
683
- },
684
- body: JSON.stringify({
685
- translations: batchRequests.map((req) => ({
686
- [API_FIELD_TEXT]: req[API_FIELD_TEXT],
687
- ...useGlobalContext ? {} : { [API_FIELD_CTX]: req[API_FIELD_CTX] }
688
- })),
689
- [API_FIELD_FROM]: from,
690
- [API_FIELD_TO]: to,
691
- ...useGlobalContext ? { [API_FIELD_CTX]: "ui" } : {}
692
- })
692
+ // Carry the /frontend session cookie the proxy issues after one
693
+ // Turnstile check, so we don't solve a challenge on every request.
694
+ credentials: "include",
695
+ headers: { "Content-Type": "application/json" },
696
+ body: buildBody(turnstileToken)
693
697
  });
698
+ let response = await postTranslate();
699
+ if ((response.status === 401 || response.status === 400) && this.options.getTurnstileToken) {
700
+ const turnstileToken = await this.options.getTurnstileToken();
701
+ response = await postTranslate(turnstileToken);
702
+ }
694
703
  let result;
695
704
  try {
696
705
  result = await response.json();
@@ -880,7 +889,8 @@ function createLangieInstance(options = {}) {
880
889
  initialBatchDelay: options.initialBatchDelay,
881
890
  followupBatchDelay: options.followupBatchDelay,
882
891
  maxBatchSize: options.maxBatchSize,
883
- maxWaitTime: options.maxWaitTime
892
+ maxWaitTime: options.maxWaitTime,
893
+ getTurnstileToken: options.getTurnstileToken
884
894
  },
885
895
  translatorHost,
886
896
  () => currentLanguage2.value,
@@ -942,7 +952,7 @@ function createLangieInstance(options = {}) {
942
952
  return;
943
953
  }
944
954
  const effectiveCtx = originalCtx;
945
- const cacheKey = `${originalText}|${effectiveCtx}`;
955
+ const cacheKey = request.cacheKey || `${originalText}|${effectiveCtx}`;
946
956
  const cache = effectiveCtx === "ui" ? uiTranslations2 : translations2;
947
957
  cache[cacheKey] = translatedText;
948
958
  devDebug("[useLangie] Cached translation:", {
@@ -970,7 +980,8 @@ function createLangieInstance(options = {}) {
970
980
  return text;
971
981
  }
972
982
  const effectiveCtx = ctx !== void 0 ? ctx : ltDefaults.ctx || "ui";
973
- const cacheKey = `${text}|${effectiveCtx}`;
983
+ const baseKey = `${text}|${effectiveCtx}`;
984
+ const cacheKey = to !== currentLanguage2.value ? `${baseKey}|${from}|${to}` : baseKey;
974
985
  const cache = effectiveCtx === "ui" ? uiTranslations2 : translations2;
975
986
  if (cache[cacheKey]) {
976
987
  return cache[cacheKey];
@@ -979,7 +990,7 @@ function createLangieInstance(options = {}) {
979
990
  if (translationErrors.has(errorKey)) {
980
991
  return text;
981
992
  }
982
- const languageCacheKey = `${cacheKey}|${from}|${to}`;
993
+ const languageCacheKey = `${baseKey}|${from}|${to}`;
983
994
  if (recentlyQueued.has(languageCacheKey)) {
984
995
  return text;
985
996
  }
@@ -1236,6 +1247,7 @@ async function translateBatch(translations2 = [], options = {}) {
1236
1247
  } else {
1237
1248
  const translationPromise = (async () => {
1238
1249
  try {
1250
+ const turnstileToken = options.getTurnstileToken ? await options.getTurnstileToken() : void 0;
1239
1251
  const controller = new AbortController();
1240
1252
  const timeoutId = setTimeout(() => controller.abort(), 5e3);
1241
1253
  const resp = await fetch(`${translatorHost}/translate`, {
@@ -1251,7 +1263,8 @@ async function translateBatch(translations2 = [], options = {}) {
1251
1263
  [API_FIELD_TO]: t[API_FIELD_TO],
1252
1264
  [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
1253
1265
  })),
1254
- [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui"
1266
+ [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui",
1267
+ ...turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {}
1255
1268
  }),
1256
1269
  signal: controller.signal
1257
1270
  });