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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * langie v1.11.0
2
+ * langie v1.12.0
3
3
  * (c) 2026 nlit
4
4
  * @license Apache-2.0
5
5
  *
@@ -88,6 +88,8 @@ var EUROPEAN_LANGUAGES = [
88
88
  match: [
89
89
  "eng",
90
90
  "en",
91
+ "\u0430\u043D\u0433\u043B\u0438\u0439\u0441\u043A\u0438\u0439",
92
+ "\u0430\u043D\u0433\u043B",
91
93
  "united states",
92
94
  "united states of america",
93
95
  "usa",
@@ -101,6 +103,31 @@ var EUROPEAN_LANGUAGES = [
101
103
  ],
102
104
  suggest: ["irish", "scottish", "welsh"]
103
105
  },
106
+ // English variants: country name / abbreviation → the specific variant CODE.
107
+ // `lang` here is a variant code (not a language name) so Fuse matches it against
108
+ // the `code` key and ranks that exact variant first (e.g. "сша" → en-US #1).
109
+ // Latin "usa"/"us" stay on the `english` entry above (surface the whole group);
110
+ // these cover the Cyrillic exonyms the translated names never contain.
111
+ {
112
+ lang: "en-us",
113
+ match: ["\u0441\u0448\u0430", "\u0448\u0442\u0430\u0442\u044B", "\u0430\u043C\u0435\u0440\u0438\u043A\u0430", "\u0441\u043E\u0435\u0434\u0438\u043D\u0451\u043D\u043D\u044B\u0435 \u0448\u0442\u0430\u0442\u044B", "\u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u043D\u044B\u0435 \u0448\u0442\u0430\u0442\u044B"],
114
+ suggest: []
115
+ },
116
+ {
117
+ lang: "en-au",
118
+ match: ["\u0430\u0432\u0441\u0442\u0440\u0430\u043B\u0438\u044F"],
119
+ suggest: []
120
+ },
121
+ {
122
+ lang: "en-ca",
123
+ match: ["\u043A\u0430\u043D\u0430\u0434\u0430"],
124
+ suggest: []
125
+ },
126
+ {
127
+ lang: "en-ie",
128
+ match: ["\u0438\u0440\u043B\u0430\u043D\u0434\u0438\u044F"],
129
+ suggest: []
130
+ },
104
131
  {
105
132
  lang: "spanish",
106
133
  match: [
@@ -562,6 +589,12 @@ function applyLanguageAlias(term = "") {
562
589
  return { primary, suggestions: Array.from(suggestions) };
563
590
  }
564
591
 
592
+ // src/utils/displayNative.ts
593
+ var TRAILING_PAREN = /\s*[((][^()()]*[))]\s*$/;
594
+ function displayNative(name = "", native = "") {
595
+ return name !== native && TRAILING_PAREN.test(name) && TRAILING_PAREN.test(native) ? native.replace(TRAILING_PAREN, "") : native;
596
+ }
597
+
565
598
  // src/constants/colors.ts
566
599
  var COLORS = {
567
600
  // Primary colors
@@ -719,6 +752,7 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
719
752
  };
720
753
  const props = __props;
721
754
  const emit = __emit;
755
+ const multiselectRef = (0, import_vue3.ref)(null);
722
756
  const isLoading = (0, import_vue3.computed)(() => {
723
757
  return props.languages.length <= 0;
724
758
  });
@@ -731,8 +765,10 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
731
765
  return new import_fuse.default(validLanguages.value, {
732
766
  keys: ["name", "native_name", "search_alias", "code"],
733
767
  includeScore: true,
734
- threshold: 0.6,
735
- // Use highest threshold to catch all cases
768
+ threshold: 0.4,
769
+ // Trim fuzzy noise; short abbreviations (e.g. "сша") no longer rank below it
770
+ ignoreLocation: true,
771
+ // Match anywhere in the field, not just near position 0
736
772
  isCaseSensitive: false,
737
773
  minMatchCharLength: 1
738
774
  // Allow single character matches
@@ -813,33 +849,26 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
813
849
  });
814
850
  function handleSearch(query) {
815
851
  searchQuery.value = query;
816
- if (query.trim()) {
817
- (0, import_vue3.nextTick)(() => {
818
- const dropdown = document.querySelector(".multiselect-dropdown");
819
- const firstOption = dropdown?.querySelector(".multiselect-option");
820
- if (firstOption) {
821
- firstOption.scrollIntoView({ behavior: "smooth", block: "nearest" });
822
- firstOption.classList.add("multiselect-option-is-pointed");
823
- }
824
- });
825
- }
852
+ if (!query.trim()) return;
853
+ (0, import_vue3.nextTick)(() => {
854
+ multiselectRef.value?.setPointerFirst?.();
855
+ const dropdown = document.querySelector(".multiselect-dropdown");
856
+ if (dropdown) dropdown.scrollTop = 0;
857
+ });
826
858
  }
827
859
  function handleKeydown(event) {
828
860
  const keyboardEvent = event;
829
- if (keyboardEvent.key === "Tab" || keyboardEvent.key === "Enter") {
830
- const dropdown = document.querySelector(".multiselect-dropdown");
831
- const firstOption = dropdown?.querySelector(".multiselect-option");
832
- if (firstOption && searchQuery.value.trim()) {
833
- const langCode = firstOption.getAttribute("data-lang-code");
834
- if (langCode) {
835
- const language = validLanguages.value.find((lang) => lang.code === langCode);
836
- if (language) {
837
- selectedLanguage.value = language;
838
- searchQuery.value = "";
839
- keyboardEvent.preventDefault();
840
- }
841
- }
842
- }
861
+ if (keyboardEvent.key !== "Tab" && keyboardEvent.key !== "Enter") return;
862
+ if (!searchQuery.value.trim()) return;
863
+ const dropdown = document.querySelector(".multiselect-dropdown");
864
+ const pointed = dropdown?.querySelector(".multiselect-option.is-pointed [data-lang-code]") || dropdown?.querySelector(".multiselect-option [data-lang-code]");
865
+ const langCode = pointed?.getAttribute("data-lang-code");
866
+ if (!langCode) return;
867
+ const language = validLanguages.value.find((lang) => lang.code === langCode);
868
+ if (language) {
869
+ selectedLanguage.value = language;
870
+ searchQuery.value = "";
871
+ keyboardEvent.preventDefault();
843
872
  }
844
873
  }
845
874
  const onFlagError = (event) => {
@@ -867,6 +896,8 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
867
896
  [
868
897
  (0, import_vue2.createCommentVNode)(" Show multiselect when we have languages OR when not loading "),
869
898
  validLanguages.value.length > 0 || !isLoading.value ? ((0, import_vue2.openBlock)(), (0, import_vue2.createBlock)((0, import_vue2.unref)(import_multiselect.default), {
899
+ ref_key: "multiselectRef",
900
+ ref: multiselectRef,
870
901
  key: multiselectKey.value,
871
902
  modelValue: selectedLanguage.value,
872
903
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => selectedLanguage.value = $event),
@@ -879,7 +910,7 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
879
910
  placeholder: validLanguages.value.length === 0 ? "No languages available" : __props.placeholder,
880
911
  disabled: props.disabled || validLanguages.value.length === 0,
881
912
  loading: isLoading.value,
882
- "track-by": "name",
913
+ "track-by": "code",
883
914
  label: "name",
884
915
  "value-prop": "code",
885
916
  "filter-results": false,
@@ -903,7 +934,7 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
903
934
  value.native_name && value.native_name !== value.name ? ((0, import_vue2.openBlock)(), (0, import_vue2.createElementBlock)(
904
935
  "span",
905
936
  _hoisted_4,
906
- "(" + (0, import_vue2.toDisplayString)(value.native_name) + ")",
937
+ "(" + (0, import_vue2.toDisplayString)((0, import_vue2.unref)(displayNative)(value.name, value.native_name)) + ")",
907
938
  1
908
939
  /* TEXT */
909
940
  )) : (0, import_vue2.createCommentVNode)("v-if", true)
@@ -931,7 +962,7 @@ var LanguageSelect_default = /* @__PURE__ */ (0, import_vue.defineComponent)({
931
962
  option.native_name && option.native_name !== option.name ? ((0, import_vue2.openBlock)(), (0, import_vue2.createElementBlock)(
932
963
  "span",
933
964
  _hoisted_8,
934
- "(" + (0, import_vue2.toDisplayString)(option.native_name) + ")",
965
+ "(" + (0, import_vue2.toDisplayString)((0, import_vue2.unref)(displayNative)(option.name, option.native_name)) + ")",
935
966
  1
936
967
  /* TEXT */
937
968
  )) : (0, import_vue2.createCommentVNode)("v-if", true)
@@ -994,9 +1025,6 @@ var import_vue8 = require("vue");
994
1025
  // src/useLangie.ts
995
1026
  var import_vue5 = require("vue");
996
1027
 
997
- // src/composables/useLangie-core.ts
998
- var import_vue4 = require("vue");
999
-
1000
1028
  // src/constants.ts
1001
1029
  var DEFAULT_API_HOST = "https://api.langie.uk/v1";
1002
1030
  var API_FIELD_TEXT = "t";
@@ -1007,6 +1035,153 @@ var API_FIELD_TRANSLATIONS = "translations";
1007
1035
  var API_FIELD_ERROR = "error";
1008
1036
  var API_FIELD_TURNSTILE = "turnstileToken";
1009
1037
 
1038
+ // src/core.ts
1039
+ var DEFAULT_TRANSLATOR_HOST = "http://localhost:8081";
1040
+ var DEFAULT_API_KEY_ENV = "TRANSLATOR_API_KEY";
1041
+ function resolveApiKey(options) {
1042
+ if (options.apiKey) return options.apiKey;
1043
+ const env = typeof process !== "undefined" ? process.env : void 0;
1044
+ if (!env) return void 0;
1045
+ return env[options.apiKeyEnv || DEFAULT_API_KEY_ENV];
1046
+ }
1047
+ var translationCache = /* @__PURE__ */ new Map();
1048
+ function getTranslationCacheKey(serviceTranslations, options) {
1049
+ return JSON.stringify({
1050
+ translations: serviceTranslations.map((t) => ({
1051
+ [API_FIELD_TEXT]: t[API_FIELD_TEXT],
1052
+ [API_FIELD_FROM]: t[API_FIELD_FROM],
1053
+ [API_FIELD_TO]: t[API_FIELD_TO],
1054
+ [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
1055
+ })),
1056
+ host: options.translatorHost || DEFAULT_TRANSLATOR_HOST,
1057
+ context: options[API_FIELD_CTX] || "ui"
1058
+ });
1059
+ }
1060
+ async function translateBatch(translations2 = [], options = {}) {
1061
+ if (!Array.isArray(translations2) || translations2.length === 0) {
1062
+ throw new Error("translations must be a non-empty array");
1063
+ }
1064
+ const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST;
1065
+ const apiKey = resolveApiKey(options);
1066
+ const serviceTranslations = [];
1067
+ const indexMap = [];
1068
+ translations2.forEach((tr, idx) => {
1069
+ const from = (tr[API_FIELD_FROM] || "").toLowerCase();
1070
+ const to = (tr[API_FIELD_TO] || "").toLowerCase();
1071
+ if (from === to) {
1072
+ return;
1073
+ }
1074
+ serviceTranslations.push(tr);
1075
+ indexMap.push(idx);
1076
+ });
1077
+ let serviceResults = [];
1078
+ if (serviceTranslations.length > 0) {
1079
+ const cacheKey = getTranslationCacheKey(serviceTranslations, options);
1080
+ if (translationCache.has(cacheKey)) {
1081
+ serviceResults = await translationCache.get(cacheKey);
1082
+ } else {
1083
+ const translationPromise = (async () => {
1084
+ try {
1085
+ const turnstileToken = options.getTurnstileToken ? await options.getTurnstileToken() : void 0;
1086
+ const controller = new AbortController();
1087
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
1088
+ const resp = await fetch(`${translatorHost}/translate`, {
1089
+ method: "POST",
1090
+ headers: {
1091
+ "Content-Type": "application/json",
1092
+ ...apiKey ? { Authorization: `Bearer ${apiKey}`, "X-Api-Key": apiKey } : {}
1093
+ },
1094
+ body: JSON.stringify({
1095
+ [API_FIELD_TRANSLATIONS]: serviceTranslations.map((t) => ({
1096
+ [API_FIELD_TEXT]: t[API_FIELD_TEXT],
1097
+ [API_FIELD_FROM]: t[API_FIELD_FROM],
1098
+ [API_FIELD_TO]: t[API_FIELD_TO],
1099
+ [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
1100
+ })),
1101
+ [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui",
1102
+ ...turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {}
1103
+ }),
1104
+ signal: controller.signal
1105
+ });
1106
+ clearTimeout(timeoutId);
1107
+ if (!resp.ok) {
1108
+ console.error(
1109
+ "[translator-sdk] Translator error response:",
1110
+ resp.status,
1111
+ resp.statusText
1112
+ );
1113
+ throw new Error(`Translator service error: ${resp.status} ${resp.statusText}`);
1114
+ }
1115
+ let parsed;
1116
+ try {
1117
+ parsed = await resp.clone().json();
1118
+ } catch (jsonErr) {
1119
+ console.error("[translator-sdk] Failed to parse JSON response:", jsonErr);
1120
+ throw new Error(`Failed to parse JSON response: ${jsonErr}`);
1121
+ }
1122
+ const data = parsed || {};
1123
+ if (data[API_FIELD_ERROR]) {
1124
+ console.warn(`[translator-sdk] Top-level API error:`, data[API_FIELD_ERROR]);
1125
+ return serviceTranslations.map((translation) => ({
1126
+ [API_FIELD_TEXT]: translation[API_FIELD_TEXT],
1127
+ [API_FIELD_ERROR]: data[API_FIELD_ERROR]
1128
+ }));
1129
+ }
1130
+ const results = Array.isArray(data[API_FIELD_TRANSLATIONS]) ? data[API_FIELD_TRANSLATIONS].map((translation, index) => {
1131
+ const originalText = serviceTranslations[index]?.[API_FIELD_TEXT] || "";
1132
+ if (translation[API_FIELD_ERROR]) {
1133
+ console.warn(
1134
+ `[translator-sdk] Translation error for "${originalText}":`,
1135
+ translation[API_FIELD_ERROR]
1136
+ );
1137
+ return {
1138
+ [API_FIELD_TEXT]: originalText,
1139
+ // Return original text on error
1140
+ [API_FIELD_ERROR]: translation[API_FIELD_ERROR]
1141
+ };
1142
+ }
1143
+ if (translation[API_FIELD_FROM] && !translation[API_FIELD_TEXT]) {
1144
+ return {
1145
+ [API_FIELD_TEXT]: originalText,
1146
+ // For detection, return original text
1147
+ [API_FIELD_FROM]: translation[API_FIELD_FROM]
1148
+ };
1149
+ }
1150
+ return {
1151
+ [API_FIELD_TEXT]: translation[API_FIELD_TEXT] || originalText
1152
+ };
1153
+ }) : data[API_FIELD_TEXT] ? [{ [API_FIELD_TEXT]: data[API_FIELD_TEXT] }] : [];
1154
+ return results;
1155
+ } catch (error) {
1156
+ translationCache.delete(cacheKey);
1157
+ if (error instanceof Error && error.name === "AbortError") {
1158
+ const message2 = `Translator request to ${translatorHost} timed out after 5 seconds.`;
1159
+ console.error(message2);
1160
+ throw new Error(message2);
1161
+ }
1162
+ const message = `Failed to connect to translator at ${translatorHost}. Is the service running?`;
1163
+ console.error(message);
1164
+ throw new Error(message);
1165
+ }
1166
+ })();
1167
+ translationCache.set(cacheKey, translationPromise);
1168
+ serviceResults = await translationPromise;
1169
+ }
1170
+ }
1171
+ const final = translations2.map((tr, idx) => {
1172
+ const from = (tr[API_FIELD_FROM] || "").toLowerCase();
1173
+ const to = (tr[API_FIELD_TO] || "").toLowerCase();
1174
+ if (from === to) return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1175
+ const svcIdx = indexMap.indexOf(idx);
1176
+ if (svcIdx !== -1) return serviceResults[svcIdx] || { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1177
+ return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1178
+ });
1179
+ return final;
1180
+ }
1181
+
1182
+ // src/composables/useLangie-core.ts
1183
+ var import_vue4 = require("vue");
1184
+
1010
1185
  // src/utils/debug.ts
1011
1186
  var import_meta = {};
1012
1187
  function devDebug(...args) {
@@ -1251,143 +1426,6 @@ var clearCache = (pattern) => {
1251
1426
  return cacheManager.clear(pattern);
1252
1427
  };
1253
1428
 
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
1429
  // src/utils/localizeLanguageNames.ts
1392
1430
  var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1393
1431
  var NAME_CTX = "language name";
@@ -1434,7 +1472,9 @@ var currentLanguage = (0, import_vue4.ref)("en");
1434
1472
  var _translatorHost = DEFAULT_API_HOST;
1435
1473
  var _autoSelected = false;
1436
1474
  var _languagesCache = null;
1475
+ var _languagesCacheAt = 0;
1437
1476
  var _languagesPromise = null;
1477
+ var LANGUAGES_CACHE_KEY = "langie_languages_cache";
1438
1478
  function useLangieCore(options = {}) {
1439
1479
  if (options.translatorHost) {
1440
1480
  _translatorHost = options.translatorHost;
@@ -1452,7 +1492,7 @@ function useLangieCore(options = {}) {
1452
1492
  }
1453
1493
  }
1454
1494
  if (typeof window !== "undefined") {
1455
- const cachedLanguages = getCache("langie_languages_cache");
1495
+ const cachedLanguages = getCache(LANGUAGES_CACHE_KEY);
1456
1496
  if (cachedLanguages && availableLanguages.value.length === 0) {
1457
1497
  availableLanguages.value = cachedLanguages;
1458
1498
  devDebug("[useLangie] Seeded languages from cache (will revalidate):", cachedLanguages.length);
@@ -1485,15 +1525,18 @@ function useLangieCore(options = {}) {
1485
1525
  applyLocalizedNames(lang);
1486
1526
  };
1487
1527
  const fetchLanguages = async (opts = {}) => {
1488
- const { force = false, country: explicitCountry } = opts;
1528
+ const { force = false, country: explicitCountry, maxAgeMs } = opts;
1529
+ const cacheFresh = !!_languagesCache && (maxAgeMs === void 0 || Date.now() - _languagesCacheAt < maxAgeMs);
1489
1530
  devDebug("[useLangie] fetchLanguages called:", {
1490
1531
  force,
1532
+ maxAgeMs,
1533
+ cacheFresh,
1491
1534
  hasCache: !!_languagesCache,
1492
1535
  hasPromise: !!_languagesPromise,
1493
1536
  availableLanguagesLength: availableLanguages.value.length
1494
1537
  });
1495
1538
  if (!force) {
1496
- if (_languagesCache) {
1539
+ if (cacheFresh) {
1497
1540
  devDebug("[useLangie] Returning cached languages:", _languagesCache.length);
1498
1541
  return _languagesCache;
1499
1542
  }
@@ -1551,7 +1594,7 @@ function useLangieCore(options = {}) {
1551
1594
  availableLanguages.value = filtered;
1552
1595
  devDebug("[useLangie] Set availableLanguages:", filtered.length);
1553
1596
  if (typeof window !== "undefined") {
1554
- const saved = setCache("langie_languages_cache", filtered, 7 * 24 * 60 * 60 * 1e3);
1597
+ const saved = setCache(LANGUAGES_CACHE_KEY, filtered, 7 * 24 * 60 * 60 * 1e3);
1555
1598
  devDebug(
1556
1599
  "[useLangie] Saved languages to cache:",
1557
1600
  saved ? "success" : "failed",
@@ -1582,6 +1625,7 @@ function useLangieCore(options = {}) {
1582
1625
  _autoSelected = true;
1583
1626
  }
1584
1627
  _languagesCache = filtered;
1628
+ _languagesCacheAt = Date.now();
1585
1629
  _languagesPromise = null;
1586
1630
  applyLocalizedNames(currentLanguage.value);
1587
1631
  return filtered;
@@ -1590,6 +1634,14 @@ function useLangieCore(options = {}) {
1590
1634
  return [];
1591
1635
  }
1592
1636
  };
1637
+ const invalidateLanguages = () => {
1638
+ _languagesCache = null;
1639
+ _languagesCacheAt = 0;
1640
+ _languagesPromise = null;
1641
+ if (typeof window !== "undefined") {
1642
+ clearCache(LANGUAGES_CACHE_KEY);
1643
+ }
1644
+ };
1593
1645
  const clearTranslations = () => {
1594
1646
  Object.keys(translations).forEach((key) => delete translations[key]);
1595
1647
  Object.keys(uiTranslations).forEach((key) => delete uiTranslations[key]);
@@ -1606,6 +1658,7 @@ function useLangieCore(options = {}) {
1606
1658
  isLoading,
1607
1659
  setLanguage,
1608
1660
  fetchLanguages,
1661
+ invalidateLanguages,
1609
1662
  clearTranslations,
1610
1663
  translatorHost
1611
1664
  };
@@ -1923,6 +1976,7 @@ function createLangieInstance(options = {}) {
1923
1976
  isLoading,
1924
1977
  setLanguage,
1925
1978
  fetchLanguages,
1979
+ invalidateLanguages,
1926
1980
  translatorHost,
1927
1981
  clearTranslations
1928
1982
  } = core;
@@ -1934,9 +1988,17 @@ function createLangieInstance(options = {}) {
1934
1988
  Object.assign(ltDefaults, defaults);
1935
1989
  };
1936
1990
  const getLtDefaults = () => ({ ...ltDefaults });
1991
+ const seed = {};
1992
+ if (options.ssrTranslations) seedTranslations(options.ssrTranslations);
1993
+ function seedTranslations(entries) {
1994
+ for (const lang of Object.keys(entries)) {
1995
+ seed[lang] = { ...seed[lang] || {}, ...entries[lang] };
1996
+ }
1997
+ }
1998
+ const getSeed = (lang, text, ctx) => seed[lang]?.[`${text}|${ctx}`];
1937
1999
  const CACHE_KEY = "langie_translations_cache";
1938
2000
  const UI_CACHE_KEY = "langie_ui_translations_cache";
1939
- const LANGUAGES_CACHE_KEY = "langie_languages_cache";
2001
+ const LANGUAGES_CACHE_KEY2 = "langie_languages_cache";
1940
2002
  const loadCachedTranslations = () => {
1941
2003
  if (typeof window === "undefined") return;
1942
2004
  const cachedTranslations = getCache(CACHE_KEY);
@@ -1964,7 +2026,7 @@ function createLangieInstance(options = {}) {
1964
2026
  };
1965
2027
  const loadCachedLanguages = () => {
1966
2028
  if (typeof window === "undefined") return;
1967
- const cachedLanguages = getCache(LANGUAGES_CACHE_KEY);
2029
+ const cachedLanguages = getCache(LANGUAGES_CACHE_KEY2);
1968
2030
  if (cachedLanguages) {
1969
2031
  availableLanguages2.value = cachedLanguages;
1970
2032
  }
@@ -2078,6 +2140,10 @@ function createLangieInstance(options = {}) {
2078
2140
  if (cache[cacheKey]) {
2079
2141
  return cache[cacheKey];
2080
2142
  }
2143
+ const seeded = getSeed(to, text, effectiveCtx);
2144
+ if (seeded) {
2145
+ return seeded;
2146
+ }
2081
2147
  const errorKey = `${text}|${effectiveCtx}|${from}|${to}`;
2082
2148
  if (translationErrors.has(errorKey)) {
2083
2149
  return text;
@@ -2086,6 +2152,7 @@ function createLangieInstance(options = {}) {
2086
2152
  if (recentlyQueued.has(languageCacheKey)) {
2087
2153
  return text;
2088
2154
  }
2155
+ options.onTranslationMiss?.({ text, ctx: effectiveCtx, from, to });
2089
2156
  batching.queueTranslation(text, effectiveCtx, from, to, cacheKey, toLang !== void 0);
2090
2157
  recentlyQueued.add(languageCacheKey);
2091
2158
  const clearDelay = 100;
@@ -2098,6 +2165,42 @@ function createLangieInstance(options = {}) {
2098
2165
  };
2099
2166
  const l = (text, ctx, originalLang, toLang) => translateInternal(text, ctx, originalLang, toLang, false);
2100
2167
  const lr = (text, ctx, originalLang, toLang) => translateInternal(text, ctx, originalLang, toLang, true);
2168
+ const peekTranslation = (text, ctx, toLang) => {
2169
+ const effectiveCtx = ctx !== void 0 ? ctx : ltDefaults.ctx || "ui";
2170
+ const from = ltDefaults.orig || "";
2171
+ const to = toLang || currentLanguage2.value;
2172
+ if (from === to) return text;
2173
+ const baseKey = `${text}|${effectiveCtx}`;
2174
+ const cacheKey = to !== currentLanguage2.value ? `${baseKey}|${from}|${to}` : baseKey;
2175
+ const cache = effectiveCtx === "ui" ? uiTranslations2 : translations2;
2176
+ return cache[cacheKey] || getSeed(to, text, effectiveCtx) || text;
2177
+ };
2178
+ const warmTranslations = async (keys, lang, from = ltDefaults.orig || "") => {
2179
+ const valid = keys.filter((k) => k && k.text);
2180
+ if (valid.length === 0) return {};
2181
+ const items = valid.map((k) => ({
2182
+ [API_FIELD_TEXT]: k.text,
2183
+ [API_FIELD_FROM]: from,
2184
+ [API_FIELD_TO]: lang,
2185
+ [API_FIELD_CTX]: k.ctx ?? ltDefaults.ctx ?? "ui"
2186
+ }));
2187
+ const results = await translateBatch(items, {
2188
+ translatorHost,
2189
+ apiKey: options.apiKey,
2190
+ apiKeyEnv: options.apiKeyEnv
2191
+ });
2192
+ const resolved = {};
2193
+ results.forEach((res, i) => {
2194
+ const src = valid[i];
2195
+ const translated = res?.[API_FIELD_TEXT];
2196
+ if (src && translated && translated !== src.text) {
2197
+ const ctx = src.ctx ?? ltDefaults.ctx ?? "ui";
2198
+ resolved[`${src.text}|${ctx}`] = translated;
2199
+ }
2200
+ });
2201
+ if (Object.keys(resolved).length > 0) seedTranslations({ [lang]: resolved });
2202
+ return resolved;
2203
+ };
2101
2204
  const fetchAndCacheBatch = async (items, from, to = currentLanguage2.value, globalCtx) => {
2102
2205
  if (items.length === 0) return;
2103
2206
  const effectiveFrom = from || ltDefaults.orig || "";
@@ -2227,11 +2330,16 @@ function createLangieInstance(options = {}) {
2227
2330
  isLoading,
2228
2331
  setLanguage,
2229
2332
  fetchLanguages,
2333
+ invalidateLanguages,
2230
2334
  translatorHost,
2231
2335
  // Translation functions
2232
2336
  l,
2233
2337
  lr,
2338
+ peekTranslation,
2234
2339
  fetchAndCacheBatch,
2340
+ // SSR translation seam
2341
+ seedTranslations,
2342
+ warmTranslations,
2235
2343
  // Error handling
2236
2344
  getTranslationError: (text, ctx, from, to) => {
2237
2345
  const effectiveCtx = ctx !== void 0 ? ctx : ltDefaults.ctx || "ui";
@@ -2509,7 +2617,7 @@ var lt_default = /* @__PURE__ */ (0, import_vue9.defineComponent)({
2509
2617
  });
2510
2618
  const props = __props;
2511
2619
  const slots = (0, import_vue11.useSlots)();
2512
- const { lr, currentLanguage: currentLanguage2, uiTranslations: uiTranslations2, translations: translations2, getLtDefaults } = useLangie();
2620
+ const { lr, peekTranslation, currentLanguage: currentLanguage2, uiTranslations: uiTranslations2, translations: translations2, getLtDefaults } = useLangie();
2513
2621
  const keyStr = (0, import_vue11.computed)(() => {
2514
2622
  if (props.msg) return props.msg;
2515
2623
  const slotContent = slots.default ? slots.default().map((n) => n.children).join("") : "";
@@ -2526,12 +2634,12 @@ var lt_default = /* @__PURE__ */ (0, import_vue9.defineComponent)({
2526
2634
  { deep: true }
2527
2635
  );
2528
2636
  const translated = (0, import_vue11.computed)(() => {
2529
- if (isNuxt.value && typeof window === "undefined") {
2530
- return keyStr.value;
2531
- }
2532
2637
  const globalDefaults = getLtDefaults();
2533
2638
  const effectiveCtx = props.ctx ?? globalDefaults.ctx;
2534
2639
  const effectiveOrig = props.orig ?? globalDefaults.orig;
2640
+ if (isNuxt.value && typeof window === "undefined") {
2641
+ return peekTranslation(keyStr.value, effectiveCtx);
2642
+ }
2535
2643
  void currentLanguage2.value;
2536
2644
  void forceUpdate.value;
2537
2645
  const cacheKey = `${keyStr.value}|${effectiveCtx}`;