langie 1.10.0 → 1.11.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * langie v1.10.0
2
+ * langie v1.11.0
3
3
  * (c) 2026 nlit
4
4
  * @license Apache-2.0
5
5
  *
@@ -78,6 +78,7 @@ styleInject(".multiselect-single-label,\n.multiselect-option {\n display: flex;
78
78
  var import_vue3 = require("vue");
79
79
  var import_multiselect = __toESM(require("@vueform/multiselect"));
80
80
  var import_fuse = __toESM(require("fuse.js"));
81
+ var import_transliteration = require("transliteration");
81
82
 
82
83
  // src/language-aliases/european.ts
83
84
  var EUROPEAN_LANGUAGES = [
@@ -195,7 +196,19 @@ var EUROPEAN_LANGUAGES = [
195
196
  },
196
197
  {
197
198
  lang: "serbian",
198
- match: ["srp", "sr", "ser", "serbia", "serbian", "srpski", "\u0441\u0440\u0431\u0438\u0458\u0430", "\u0441\u0440\u043F\u0441\u043A\u0438"],
199
+ match: [
200
+ "srp",
201
+ "sr",
202
+ "ser",
203
+ "serbia",
204
+ "serbian",
205
+ "srpski",
206
+ "\u0441\u0440\u0431\u0438\u0458\u0430",
207
+ "\u0441\u0440\u043F\u0441\u043A\u0438",
208
+ "\u0441\u0435\u0440\u0431",
209
+ "\u0441\u0435\u0440\u0431\u0441\u043A\u0438\u0439",
210
+ "\u0441\u0435\u0440\u0431\u0441"
211
+ ],
199
212
  suggest: ["croatian", "bosnian", "montenegrin"]
200
213
  },
201
214
  {
@@ -361,7 +374,7 @@ var ASIAN_LANGUAGES = [
361
374
  // Central Asian & Turkic Languages
362
375
  {
363
376
  lang: "kazakh",
364
- match: ["kaz", "kk", "kazakh", "kazakhstan"],
377
+ match: ["kaz", "kk", "kazlat", "kazakh", "kazakhstan", "\u043A\u0430\u0437\u0430\u0445", "\u043A\u0430\u0437\u0430\u0445\u0441\u043A\u0438\u0439", "\u043A\u0430\u0437\u0430\u0445\u0441"],
365
378
  suggest: ["russian", "kyrgyz", "uzbek"]
366
379
  },
367
380
  {
@@ -627,11 +640,17 @@ var _hoisted_1 = {
627
640
  };
628
641
  var _hoisted_2 = ["src", "alt"];
629
642
  var _hoisted_3 = { class: "language-text" };
630
- var _hoisted_4 = { class: "native-name" };
643
+ var _hoisted_4 = {
644
+ key: 0,
645
+ class: "native-name"
646
+ };
631
647
  var _hoisted_5 = ["data-lang-code"];
632
648
  var _hoisted_6 = ["src", "alt"];
633
649
  var _hoisted_7 = { class: "language-text" };
634
- var _hoisted_8 = { class: "native-name" };
650
+ var _hoisted_8 = {
651
+ key: 0,
652
+ class: "native-name"
653
+ };
635
654
  var _hoisted_9 = { class: "multiselect-no-options" };
636
655
  var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
637
656
  __name: "LanguageSelect",
@@ -678,13 +697,13 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
678
697
  "5b09cc17-THEME_COLORS.dark.border": (0, import_vue2.unref)(THEME_COLORS).dark.border,
679
698
  "5b09cc17-THEME_COLORS.dark.placeholder": (0, import_vue2.unref)(THEME_COLORS).dark.placeholder,
680
699
  "5b09cc17-THEME_COLORS.dark.ring": (0, import_vue2.unref)(THEME_COLORS).dark.ring,
700
+ "5b09cc17-THEME_COLORS.dark.dropdownText": (0, import_vue2.unref)(THEME_COLORS).dark.dropdownText,
681
701
  "5b09cc17-THEME_COLORS.dark.optionPointed": (0, import_vue2.unref)(THEME_COLORS).dark.optionPointed,
682
702
  "5b09cc17-THEME_COLORS.dark.optionPointedText": (0, import_vue2.unref)(THEME_COLORS).dark.optionPointedText,
683
703
  "5b09cc17-THEME_COLORS.dark.optionSelected": (0, import_vue2.unref)(THEME_COLORS).dark.optionSelected,
684
704
  "5b09cc17-THEME_COLORS.dark.optionSelectedText": (0, import_vue2.unref)(THEME_COLORS).dark.optionSelectedText,
685
705
  "5b09cc17-THEME_COLORS.dark.dropdownBackground": (0, import_vue2.unref)(THEME_COLORS).dark.dropdownBackground,
686
706
  "5b09cc17-THEME_COLORS.dark.dropdownBorder": (0, import_vue2.unref)(THEME_COLORS).dark.dropdownBorder,
687
- "5b09cc17-THEME_COLORS.dark.dropdownText": (0, import_vue2.unref)(THEME_COLORS).dark.dropdownText,
688
707
  "5b09cc17-COLORS.border.flag": (0, import_vue2.unref)(COLORS).border.flag,
689
708
  "5b09cc17-COLORS.border.dark": (0, import_vue2.unref)(COLORS).border.dark,
690
709
  "5b09cc17-COLORS.text.secondary": (0, import_vue2.unref)(COLORS).text.secondary,
@@ -710,7 +729,7 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
710
729
  const fuse = (0, import_vue3.computed)(() => {
711
730
  if (!validLanguages.value.length) return null;
712
731
  return new import_fuse.default(validLanguages.value, {
713
- keys: ["name", "native_name", "code"],
732
+ keys: ["name", "native_name", "search_alias", "code"],
714
733
  includeScore: true,
715
734
  threshold: 0.6,
716
735
  // Use highest threshold to catch all cases
@@ -745,15 +764,18 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
745
764
  if (!fuse.value) return [];
746
765
  const aliasResult = applyLanguageAlias(query);
747
766
  const searchTerm = Array.isArray(aliasResult.primary) ? aliasResult.primary[0] : aliasResult.primary;
748
- const fuseResults = fuse.value.search(searchTerm);
749
- results = fuseResults.map((result) => result.item);
750
- if (searchTerm !== query) {
751
- const originalResults = fuse.value.search(query);
752
- originalResults.forEach((result) => {
753
- if (!results.some((r) => r.code === result.item.code)) {
754
- results.push(result.item);
755
- }
767
+ const mergeByCode = (hits) => {
768
+ hits.forEach((hit) => {
769
+ if (!results.some((r) => r.code === hit.item.code)) results.push(hit.item);
756
770
  });
771
+ };
772
+ results = fuse.value.search(searchTerm).map((result) => result.item);
773
+ if (searchTerm !== query) {
774
+ mergeByCode(fuse.value.search(query));
775
+ }
776
+ const latinQuery = (0, import_transliteration.transliterate)(query);
777
+ if (latinQuery && latinQuery.toLowerCase() !== query.toLowerCase()) {
778
+ mergeByCode(fuse.value.search(latinQuery));
757
779
  }
758
780
  if (aliasResult.suggestions.length > 0) {
759
781
  aliasResult.suggestions.forEach((suggestion) => {
@@ -878,13 +900,13 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
878
900
  1
879
901
  /* TEXT */
880
902
  ),
881
- (0, import_vue2.createElementVNode)(
903
+ value.native_name && value.native_name !== value.name ? ((0, import_vue2.openBlock)(), (0, import_vue2.createElementBlock)(
882
904
  "span",
883
905
  _hoisted_4,
884
- "(" + (0, import_vue2.toDisplayString)(value.native_name || value.name) + ")",
906
+ "(" + (0, import_vue2.toDisplayString)(value.native_name) + ")",
885
907
  1
886
908
  /* TEXT */
887
- )
909
+ )) : (0, import_vue2.createCommentVNode)("v-if", true)
888
910
  ])
889
911
  ])) : (0, import_vue2.createCommentVNode)("v-if", true)
890
912
  ]),
@@ -906,13 +928,13 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
906
928
  1
907
929
  /* TEXT */
908
930
  ),
909
- (0, import_vue2.createElementVNode)(
931
+ option.native_name && option.native_name !== option.name ? ((0, import_vue2.openBlock)(), (0, import_vue2.createElementBlock)(
910
932
  "span",
911
933
  _hoisted_8,
912
- "(" + (0, import_vue2.toDisplayString)(option.native_name || option.name) + ")",
934
+ "(" + (0, import_vue2.toDisplayString)(option.native_name) + ")",
913
935
  1
914
936
  /* TEXT */
915
- )
937
+ )) : (0, import_vue2.createCommentVNode)("v-if", true)
916
938
  ])
917
939
  ], 8, _hoisted_5)
918
940
  ]),
@@ -1229,6 +1251,181 @@ var clearCache = (pattern) => {
1229
1251
  return cacheManager.clear(pattern);
1230
1252
  };
1231
1253
 
1254
+ // src/core.ts
1255
+ var DEFAULT_TRANSLATOR_HOST = "http://localhost:8081";
1256
+ var translationCache = /* @__PURE__ */ new Map();
1257
+ function getTranslationCacheKey(serviceTranslations, options) {
1258
+ return JSON.stringify({
1259
+ translations: serviceTranslations.map((t) => ({
1260
+ [API_FIELD_TEXT]: t[API_FIELD_TEXT],
1261
+ [API_FIELD_FROM]: t[API_FIELD_FROM],
1262
+ [API_FIELD_TO]: t[API_FIELD_TO],
1263
+ [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
1264
+ })),
1265
+ host: options.translatorHost || DEFAULT_TRANSLATOR_HOST,
1266
+ context: options[API_FIELD_CTX] || "ui"
1267
+ });
1268
+ }
1269
+ async function translateBatch(translations2 = [], options = {}) {
1270
+ if (!Array.isArray(translations2) || translations2.length === 0) {
1271
+ throw new Error("translations must be a non-empty array");
1272
+ }
1273
+ const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST;
1274
+ const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY;
1275
+ const serviceTranslations = [];
1276
+ const indexMap = [];
1277
+ translations2.forEach((tr, idx) => {
1278
+ const from = (tr[API_FIELD_FROM] || "").toLowerCase();
1279
+ const to = (tr[API_FIELD_TO] || "").toLowerCase();
1280
+ if (from === to) {
1281
+ return;
1282
+ }
1283
+ serviceTranslations.push(tr);
1284
+ indexMap.push(idx);
1285
+ });
1286
+ let serviceResults = [];
1287
+ if (serviceTranslations.length > 0) {
1288
+ const cacheKey = getTranslationCacheKey(serviceTranslations, options);
1289
+ if (translationCache.has(cacheKey)) {
1290
+ serviceResults = await translationCache.get(cacheKey);
1291
+ } else {
1292
+ const translationPromise = (async () => {
1293
+ try {
1294
+ const turnstileToken = options.getTurnstileToken ? await options.getTurnstileToken() : void 0;
1295
+ const controller = new AbortController();
1296
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
1297
+ const resp = await fetch(`${translatorHost}/translate`, {
1298
+ method: "POST",
1299
+ headers: {
1300
+ "Content-Type": "application/json",
1301
+ ...apiKey ? { Authorization: `Bearer ${apiKey}`, "X-Api-Key": apiKey } : {}
1302
+ },
1303
+ body: JSON.stringify({
1304
+ [API_FIELD_TRANSLATIONS]: serviceTranslations.map((t) => ({
1305
+ [API_FIELD_TEXT]: t[API_FIELD_TEXT],
1306
+ [API_FIELD_FROM]: t[API_FIELD_FROM],
1307
+ [API_FIELD_TO]: t[API_FIELD_TO],
1308
+ [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
1309
+ })),
1310
+ [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui",
1311
+ ...turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {}
1312
+ }),
1313
+ signal: controller.signal
1314
+ });
1315
+ clearTimeout(timeoutId);
1316
+ if (!resp.ok) {
1317
+ console.error(
1318
+ "[translator-sdk] Translator error response:",
1319
+ resp.status,
1320
+ resp.statusText
1321
+ );
1322
+ throw new Error(`Translator service error: ${resp.status} ${resp.statusText}`);
1323
+ }
1324
+ let parsed;
1325
+ try {
1326
+ parsed = await resp.clone().json();
1327
+ } catch (jsonErr) {
1328
+ console.error("[translator-sdk] Failed to parse JSON response:", jsonErr);
1329
+ throw new Error(`Failed to parse JSON response: ${jsonErr}`);
1330
+ }
1331
+ const data = parsed || {};
1332
+ if (data[API_FIELD_ERROR]) {
1333
+ console.warn(`[translator-sdk] Top-level API error:`, data[API_FIELD_ERROR]);
1334
+ return serviceTranslations.map((translation) => ({
1335
+ [API_FIELD_TEXT]: translation[API_FIELD_TEXT],
1336
+ [API_FIELD_ERROR]: data[API_FIELD_ERROR]
1337
+ }));
1338
+ }
1339
+ const results = Array.isArray(data[API_FIELD_TRANSLATIONS]) ? data[API_FIELD_TRANSLATIONS].map((translation, index) => {
1340
+ const originalText = serviceTranslations[index]?.[API_FIELD_TEXT] || "";
1341
+ if (translation[API_FIELD_ERROR]) {
1342
+ console.warn(
1343
+ `[translator-sdk] Translation error for "${originalText}":`,
1344
+ translation[API_FIELD_ERROR]
1345
+ );
1346
+ return {
1347
+ [API_FIELD_TEXT]: originalText,
1348
+ // Return original text on error
1349
+ [API_FIELD_ERROR]: translation[API_FIELD_ERROR]
1350
+ };
1351
+ }
1352
+ if (translation[API_FIELD_FROM] && !translation[API_FIELD_TEXT]) {
1353
+ return {
1354
+ [API_FIELD_TEXT]: originalText,
1355
+ // For detection, return original text
1356
+ [API_FIELD_FROM]: translation[API_FIELD_FROM]
1357
+ };
1358
+ }
1359
+ return {
1360
+ [API_FIELD_TEXT]: translation[API_FIELD_TEXT] || originalText
1361
+ };
1362
+ }) : data[API_FIELD_TEXT] ? [{ [API_FIELD_TEXT]: data[API_FIELD_TEXT] }] : [];
1363
+ return results;
1364
+ } catch (error) {
1365
+ translationCache.delete(cacheKey);
1366
+ if (error instanceof Error && error.name === "AbortError") {
1367
+ const message2 = `Translator request to ${translatorHost} timed out after 5 seconds.`;
1368
+ console.error(message2);
1369
+ throw new Error(message2);
1370
+ }
1371
+ const message = `Failed to connect to translator at ${translatorHost}. Is the service running?`;
1372
+ console.error(message);
1373
+ throw new Error(message);
1374
+ }
1375
+ })();
1376
+ translationCache.set(cacheKey, translationPromise);
1377
+ serviceResults = await translationPromise;
1378
+ }
1379
+ }
1380
+ const final = translations2.map((tr, idx) => {
1381
+ const from = (tr[API_FIELD_FROM] || "").toLowerCase();
1382
+ const to = (tr[API_FIELD_TO] || "").toLowerCase();
1383
+ if (from === to) return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1384
+ const svcIdx = indexMap.indexOf(idx);
1385
+ if (svcIdx !== -1) return serviceResults[svcIdx] || { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1386
+ return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1387
+ });
1388
+ return final;
1389
+ }
1390
+
1391
+ // src/utils/localizeLanguageNames.ts
1392
+ var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1393
+ var NAME_CTX = "language name";
1394
+ async function translateNames(names, targetLang, options) {
1395
+ const requests = names.map((name) => ({
1396
+ [API_FIELD_TEXT]: name,
1397
+ [API_FIELD_FROM]: "en",
1398
+ [API_FIELD_TO]: targetLang,
1399
+ [API_FIELD_CTX]: NAME_CTX
1400
+ }));
1401
+ try {
1402
+ const results = await translateBatch(requests, options);
1403
+ const map = {};
1404
+ names.forEach((name, i) => {
1405
+ const translated = results[i]?.[API_FIELD_TEXT];
1406
+ if (translated) map[name] = translated;
1407
+ });
1408
+ return map;
1409
+ } catch (error) {
1410
+ devDebug("[localizeLanguageNames] translate failed:", error);
1411
+ return null;
1412
+ }
1413
+ }
1414
+ async function localizeLanguageNames(languages, targetLang, options = {}) {
1415
+ const to = (targetLang || "").toLowerCase();
1416
+ if (!to || to === "en" || languages.length === 0) return languages;
1417
+ const cacheKey = `langie_langnames_${to}`;
1418
+ let map = getCache(cacheKey);
1419
+ if (!map) {
1420
+ const names = [...new Set(languages.map((l) => l.name).filter(Boolean))];
1421
+ map = await translateNames(names, to, options);
1422
+ if (map && Object.keys(map).length) setCache(cacheKey, map, CACHE_TTL_MS);
1423
+ }
1424
+ if (!map) return languages;
1425
+ const resolved = map;
1426
+ return languages.map((l) => ({ ...l, search_alias: resolved[l.name] || l.search_alias }));
1427
+ }
1428
+
1232
1429
  // src/composables/useLangie-core.ts
1233
1430
  var availableLanguages = (0, import_vue4.ref)([]);
1234
1431
  var translations = (0, import_vue4.reactive)({});
@@ -1258,8 +1455,7 @@ function useLangieCore(options = {}) {
1258
1455
  const cachedLanguages = getCache("langie_languages_cache");
1259
1456
  if (cachedLanguages && availableLanguages.value.length === 0) {
1260
1457
  availableLanguages.value = cachedLanguages;
1261
- _languagesCache = cachedLanguages;
1262
- devDebug("[useLangie] Loaded languages from cache:", cachedLanguages.length);
1458
+ devDebug("[useLangie] Seeded languages from cache (will revalidate):", cachedLanguages.length);
1263
1459
  } else if (cachedLanguages) {
1264
1460
  devDebug(
1265
1461
  "[useLangie] Cache exists but languages already loaded:",
@@ -1269,6 +1465,16 @@ function useLangieCore(options = {}) {
1269
1465
  devDebug("[useLangie] No cached languages found");
1270
1466
  }
1271
1467
  }
1468
+ const applyLocalizedNames = (lang) => {
1469
+ const base = availableLanguages.value;
1470
+ if (!base.length) return;
1471
+ localizeLanguageNames(base, lang, { translatorHost }).then((localized) => {
1472
+ if (localized !== base && availableLanguages.value === base) {
1473
+ availableLanguages.value = localized;
1474
+ }
1475
+ }).catch(() => {
1476
+ });
1477
+ };
1272
1478
  const setLanguage = (lang) => {
1273
1479
  if (lang && lang !== currentLanguage.value) {
1274
1480
  }
@@ -1276,6 +1482,7 @@ function useLangieCore(options = {}) {
1276
1482
  if (typeof window !== "undefined") {
1277
1483
  localStorage.setItem("interface_language", lang);
1278
1484
  }
1485
+ applyLocalizedNames(lang);
1279
1486
  };
1280
1487
  const fetchLanguages = async (opts = {}) => {
1281
1488
  const { force = false, country: explicitCountry } = opts;
@@ -1376,6 +1583,7 @@ function useLangieCore(options = {}) {
1376
1583
  }
1377
1584
  _languagesCache = filtered;
1378
1585
  _languagesPromise = null;
1586
+ applyLocalizedNames(currentLanguage.value);
1379
1587
  return filtered;
1380
1588
  } catch (error) {
1381
1589
  devDebug("[useLangie] Language fetch error:", { error });