langie 1.11.0 → 1.12.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.
- package/dist/components/InterfaceLanguageSelect.vue.d.ts +1 -1
- package/dist/components/index.cjs +288 -180
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.css.map +1 -1
- package/dist/components/index.mjs +289 -181
- package/dist/components/index.mjs.map +1 -1
- package/dist/composables/useLangie-core.d.ts +2 -0
- package/dist/core.cjs +10 -3
- package/dist/core.cjs.map +1 -1
- package/dist/core.mjs +10 -3
- package/dist/core.mjs.map +1 -1
- package/dist/index.cjs +333 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +334 -226
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +27 -0
- package/dist/useLangie.d.ts +9 -1
- package/dist/utils/displayNative.d.ts +10 -0
- package/package.json +1 -1
|
@@ -15,7 +15,9 @@ export declare function useLangieCore(options?: TranslatorOptions): {
|
|
|
15
15
|
fetchLanguages: (opts?: {
|
|
16
16
|
force?: boolean;
|
|
17
17
|
country?: string;
|
|
18
|
+
maxAgeMs?: number;
|
|
18
19
|
}) => Promise<TranslatorLanguage[]>;
|
|
20
|
+
invalidateLanguages: () => void;
|
|
19
21
|
clearTranslations: () => void;
|
|
20
22
|
translatorHost: string;
|
|
21
23
|
};
|
package/dist/core.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* langie v1.
|
|
2
|
+
* langie v1.12.0
|
|
3
3
|
* (c) 2026 nlit
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*
|
|
@@ -47,6 +47,13 @@ var API_FIELD_TURNSTILE = "turnstileToken";
|
|
|
47
47
|
|
|
48
48
|
// src/core.ts
|
|
49
49
|
var DEFAULT_TRANSLATOR_HOST = "http://localhost:8081";
|
|
50
|
+
var DEFAULT_API_KEY_ENV = "TRANSLATOR_API_KEY";
|
|
51
|
+
function resolveApiKey(options) {
|
|
52
|
+
if (options.apiKey) return options.apiKey;
|
|
53
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
54
|
+
if (!env) return void 0;
|
|
55
|
+
return env[options.apiKeyEnv || DEFAULT_API_KEY_ENV];
|
|
56
|
+
}
|
|
50
57
|
var translationCache = /* @__PURE__ */ new Map();
|
|
51
58
|
function getTranslationCacheKey(serviceTranslations, options) {
|
|
52
59
|
return JSON.stringify({
|
|
@@ -65,7 +72,7 @@ async function translateBatch(translations = [], options = {}) {
|
|
|
65
72
|
throw new Error("translations must be a non-empty array");
|
|
66
73
|
}
|
|
67
74
|
const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST;
|
|
68
|
-
const apiKey = options
|
|
75
|
+
const apiKey = resolveApiKey(options);
|
|
69
76
|
const serviceTranslations = [];
|
|
70
77
|
const indexMap = [];
|
|
71
78
|
translations.forEach((tr, idx) => {
|
|
@@ -197,7 +204,7 @@ function baseLanguageCode(code) {
|
|
|
197
204
|
}
|
|
198
205
|
async function fetchAvailableLanguages(options = {}) {
|
|
199
206
|
const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST;
|
|
200
|
-
const apiKey = options
|
|
207
|
+
const apiKey = resolveApiKey(options);
|
|
201
208
|
const minPop = options.minPopularity !== void 0 ? Number(options.minPopularity) : 0;
|
|
202
209
|
const queryParams = [];
|
|
203
210
|
if (options.country) queryParams.push(`country=${encodeURIComponent(options.country)}`);
|
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 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/**\n * Vendor/legacy codes whose primary subtag isn't their BCP-47 base language.\n * Folding `kazlat` → `kk` treats Latin and Cyrillic Kazakh as siblings, so a\n * popularity-passing variant keeps its below-floor sibling visible (both scripts\n * stay selectable). It does NOT dedup them — both rows survive.\n */\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"]}
|
|
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'\nconst DEFAULT_API_KEY_ENV = 'TRANSLATOR_API_KEY'\n\n/**\n * Resolve the API key: explicit `apiKey` wins, then a host-configured env var\n * name (`apiKeyEnv`), then the default `TRANSLATOR_API_KEY`.\n */\nfunction resolveApiKey(options: TranslatorOptions): string | undefined {\n if (options.apiKey) return options.apiKey\n const env = typeof process !== 'undefined' ? process.env : undefined\n if (!env) return undefined\n return env[options.apiKeyEnv || DEFAULT_API_KEY_ENV]\n}\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 = resolveApiKey(options)\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/**\n * Vendor/legacy codes whose primary subtag isn't their BCP-47 base language.\n * Folding `kazlat` → `kk` treats Latin and Cyrillic Kazakh as siblings, so a\n * popularity-passing variant keeps its below-floor sibling visible (both scripts\n * stay selectable). It does NOT dedup them — both rows survive.\n */\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 = resolveApiKey(options)\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.
|
|
2
|
+
* langie v1.12.0
|
|
3
3
|
* (c) 2026 nlit
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*
|
|
@@ -18,6 +18,13 @@ var API_FIELD_TURNSTILE = "turnstileToken";
|
|
|
18
18
|
|
|
19
19
|
// src/core.ts
|
|
20
20
|
var DEFAULT_TRANSLATOR_HOST = "http://localhost:8081";
|
|
21
|
+
var DEFAULT_API_KEY_ENV = "TRANSLATOR_API_KEY";
|
|
22
|
+
function resolveApiKey(options) {
|
|
23
|
+
if (options.apiKey) return options.apiKey;
|
|
24
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
25
|
+
if (!env) return void 0;
|
|
26
|
+
return env[options.apiKeyEnv || DEFAULT_API_KEY_ENV];
|
|
27
|
+
}
|
|
21
28
|
var translationCache = /* @__PURE__ */ new Map();
|
|
22
29
|
function getTranslationCacheKey(serviceTranslations, options) {
|
|
23
30
|
return JSON.stringify({
|
|
@@ -36,7 +43,7 @@ async function translateBatch(translations = [], options = {}) {
|
|
|
36
43
|
throw new Error("translations must be a non-empty array");
|
|
37
44
|
}
|
|
38
45
|
const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST;
|
|
39
|
-
const apiKey = options
|
|
46
|
+
const apiKey = resolveApiKey(options);
|
|
40
47
|
const serviceTranslations = [];
|
|
41
48
|
const indexMap = [];
|
|
42
49
|
translations.forEach((tr, idx) => {
|
|
@@ -168,7 +175,7 @@ function baseLanguageCode(code) {
|
|
|
168
175
|
}
|
|
169
176
|
async function fetchAvailableLanguages(options = {}) {
|
|
170
177
|
const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST;
|
|
171
|
-
const apiKey = options
|
|
178
|
+
const apiKey = resolveApiKey(options);
|
|
172
179
|
const minPop = options.minPopularity !== void 0 ? Number(options.minPopularity) : 0;
|
|
173
180
|
const queryParams = [];
|
|
174
181
|
if (options.country) queryParams.push(`country=${encodeURIComponent(options.country)}`);
|
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// 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/**\n * Vendor/legacy codes whose primary subtag isn't their BCP-47 base language.\n * Folding `kazlat` → `kk` treats Latin and Cyrillic Kazakh as siblings, so a\n * popularity-passing variant keeps its below-floor sibling visible (both scripts\n * stay selectable). It does NOT dedup them — both rows survive.\n */\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'\nconst DEFAULT_API_KEY_ENV = 'TRANSLATOR_API_KEY'\n\n/**\n * Resolve the API key: explicit `apiKey` wins, then a host-configured env var\n * name (`apiKeyEnv`), then the default `TRANSLATOR_API_KEY`.\n */\nfunction resolveApiKey(options: TranslatorOptions): string | undefined {\n if (options.apiKey) return options.apiKey\n const env = typeof process !== 'undefined' ? process.env : undefined\n if (!env) return undefined\n return env[options.apiKeyEnv || DEFAULT_API_KEY_ENV]\n}\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 = resolveApiKey(options)\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/**\n * Vendor/legacy codes whose primary subtag isn't their BCP-47 base language.\n * Folding `kazlat` → `kk` treats Latin and Cyrillic Kazakh as siblings, so a\n * popularity-passing variant keeps its below-floor sibling visible (both scripts\n * stay selectable). It does NOT dedup them — both rows survive.\n */\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 = resolveApiKey(options)\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"]}
|