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.
@@ -56,6 +56,7 @@
56
56
  --ms-border-color: var(--5b09cc17-THEME_COLORS\.dark\.border);
57
57
  --ms-placeholder-color: var(--5b09cc17-THEME_COLORS\.dark\.placeholder);
58
58
  --ms-ring-color: var(--5b09cc17-THEME_COLORS\.dark\.ring);
59
+ --ms-color: var(--5b09cc17-THEME_COLORS\.dark\.dropdownText);
59
60
  --ms-option-bg-pointed: var(--5b09cc17-THEME_COLORS\.dark\.optionPointed);
60
61
  --ms-option-color-pointed: var(--5b09cc17-THEME_COLORS\.dark\.optionPointedText);
61
62
  --ms-option-bg-selected: var(--5b09cc17-THEME_COLORS\.dark\.optionSelected);
@@ -1 +1 @@
1
- {"version":3,"sources":["/builds/codelits/langie-sdk/src/components/LanguageSelect.vue","/builds/codelits/langie-sdk/src/components/InterfaceLanguageSelect.vue","/builds/codelits/langie-sdk/src/components/lt.vue"],"sourcesContent":["<template>\n <div class=\"language-select\" :class=\"{ 'is-dark': isDark }\">\n <!-- Show multiselect when we have languages OR when not loading -->\n <Multiselect\n v-if=\"validLanguages.length > 0 || !isLoading\"\n :key=\"multiselectKey\"\n v-model=\"selectedLanguage\"\n :options=\"filteredLanguages\"\n :searchable=\"true\"\n @search-change=\"handleSearch\"\n :can-clear=\"false\"\n :allow-empty=\"false\"\n :object=\"true\"\n :placeholder=\"validLanguages.length === 0 ? 'No languages available' : placeholder\"\n :disabled=\"props.disabled || validLanguages.length === 0\"\n :loading=\"isLoading\"\n track-by=\"name\"\n label=\"name\"\n value-prop=\"code\"\n :filter-results=\"false\"\n @keydown=\"handleKeydown\"\n >\n <template #singlelabel=\"{ value }\">\n <div v-if=\"value\" class=\"multiselect-single-label\">\n <img\n v-if=\"value.code\"\n :key=\"selectedLanguageKey\"\n :src=\"getFlagImageUrl(value)\"\n class=\"lang-flag\"\n :alt=\"`${value.native_name || value.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ value.name }}\n <span class=\"native-name\">({{ value.native_name || value.name }})</span></span\n >\n </div>\n </template>\n <template #option=\"{ option }\">\n <div class=\"multiselect-option\" :data-lang-code=\"option.code\">\n <img\n :key=\"`${option.code}-${option.flag_country || 'fallback'}`\"\n :src=\"getFlagImageUrl(option)\"\n class=\"lang-flag\"\n :alt=\"`${option.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ option.name }}\n <span class=\"native-name\">({{ option.native_name || option.name }})</span></span\n >\n </div>\n </template>\n <template #noresults>\n <div class=\"multiselect-no-results\">No languages found.</div>\n </template>\n <template #nooptions>\n <div class=\"multiselect-no-options\">\n {{\n validLanguages.length === 0\n ? 'Please provide languages via the :languages prop'\n : 'No languages available.'\n }}\n </div>\n </template>\n </Multiselect>\n <!-- Only show skeleton loader when actually loading AND we have no languages yet -->\n <div v-else-if=\"isLoading && validLanguages.length === 0\" class=\"skeleton-loader\"></div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport '@vueform/multiselect/themes/default.css'\nimport '../styles/teleport.css'\nimport { ref, computed, nextTick } from 'vue'\nimport type { PropType } from 'vue'\nimport Multiselect from '@vueform/multiselect'\nimport Fuse from 'fuse.js'\nimport { applyLanguageAlias } from '../search-utils'\nimport { THEME_COLORS, COLORS } from '../constants/colors'\nimport type { TranslatorLanguage } from '../types'\n\nconst getFlagCode = (lang: TranslatorLanguage): string => {\n const flagCode = lang.flag_country || lang.code\n return flagCode\n}\n\nconst getFlagImageUrl = (lang: TranslatorLanguage): string => {\n const flagCode = getFlagCode(lang)\n // Use local flags (downloaded during build for offline support and faster loading)\n // If not found, the onerror handler will use CDN fallback\n return `/flags/${flagCode}.svg`\n}\n\nconst props = defineProps({\n modelValue: {\n type: Object as PropType<TranslatorLanguage | null | undefined>,\n default: undefined\n },\n languages: {\n type: Array as PropType<TranslatorLanguage[]>,\n default: () => []\n },\n placeholder: {\n type: String,\n default: 'Select language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\nconst isLoading = computed(() => {\n return props.languages.length <= 0\n})\nconst searchQuery = ref('')\n\nconst validLanguages = computed(() => {\n return props.languages.filter((lang) => lang && lang.code && lang.name && lang.native_name)\n})\n\nconst fuse = computed(() => {\n if (!validLanguages.value.length) return null\n\n return new Fuse(validLanguages.value, {\n keys: ['name', 'native_name', 'code'],\n includeScore: true,\n threshold: 0.6, // Use highest threshold to catch all cases\n isCaseSensitive: false,\n minMatchCharLength: 1 // Allow single character matches\n })\n})\n\nconst multiselectKey = computed(() => {\n // Create a key that changes when the language data structure changes\n const hasApiData = props.languages.length > 0 && props.languages[0].flag_country !== null\n return hasApiData ? 'api-data' : 'fallback-data'\n})\n\nconst selectedLanguageKey = computed(() => {\n if (!props.modelValue || !props.modelValue.code) return 'no-selection'\n const key = `${props.modelValue.code}-${props.modelValue.flag_country || 'fallback'}`\n return key\n})\n\nconst selectedLanguage = computed({\n get: () => props.modelValue || null,\n set: (value) => {\n if (value && value.code) {\n emit('update:modelValue', value)\n }\n }\n})\n\nconst filteredLanguages = computed(() => {\n const query = searchQuery.value.trim()\n\n let results: TranslatorLanguage[]\n if (!query) {\n // When no search query, return all valid languages (bypass Fuse.js)\n results = validLanguages.value\n } else {\n // Only use Fuse.js when there's an actual search query\n if (!fuse.value) return []\n\n // Get the most likely alias or the original term\n const aliasResult = applyLanguageAlias(query)\n const searchTerm = Array.isArray(aliasResult.primary)\n ? aliasResult.primary[0]\n : aliasResult.primary\n\n // Always use Fuse.js for the main search (both original query and alias result)\n const fuseResults = fuse.value.search(searchTerm)\n results = fuseResults.map((result) => result.item)\n\n // If the search term is different from query (alias was applied), also search the original query\n if (searchTerm !== query) {\n const originalResults = fuse.value.search(query)\n originalResults.forEach((result) => {\n if (!results.some((r) => r.code === result.item.code)) {\n results.push(result.item)\n }\n })\n }\n\n // Add suggested languages to expand the results\n if (aliasResult.suggestions.length > 0) {\n aliasResult.suggestions.forEach((suggestion) => {\n const suggestedLang = validLanguages.value.find((lang) => {\n const langName = lang.name.toLowerCase()\n const suggestion_lower = suggestion.toLowerCase()\n\n // Try multiple matching strategies\n return (\n langName === suggestion_lower || // Exact match\n langName.includes(suggestion_lower) || // Contains match\n lang.code.toLowerCase() === suggestion_lower || // Code match\n langName.startsWith(suggestion_lower) || // Starts with match\n // Handle common variations\n (suggestion_lower === 'tatar' && langName.includes('tatar')) ||\n (suggestion_lower === 'belarusian' &&\n (langName.includes('belarus') || langName.includes('belarusian'))) ||\n (suggestion_lower === 'moldovan' &&\n (langName.includes('moldov') || langName.includes('moldova')))\n )\n })\n if (suggestedLang && !results.some((r) => r.code === suggestedLang.code)) {\n results.push(suggestedLang)\n }\n })\n }\n\n // Special handling for very short queries (e.g. \"се\")\n if (query.length === 2) {\n const lowerQuery = query.toLowerCase()\n const manualResults = validLanguages.value.filter(\n (lang) =>\n lang.name.toLowerCase().startsWith(lowerQuery) ||\n lang.native_name.toLowerCase().startsWith(lowerQuery)\n )\n manualResults.forEach((lang) => {\n if (!results.some((r) => r.code === lang.code)) {\n results.push(lang)\n }\n })\n }\n }\n\n // Remove the currently selected language from the options\n const filtered = results.filter(\n (lang) => !props.modelValue || !props.modelValue.code || lang.code !== props.modelValue.code\n )\n\n return filtered\n})\n\nfunction handleSearch(query: string) {\n searchQuery.value = query\n\n // Auto-scroll to first result when typing\n if (query.trim()) {\n nextTick(() => {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption) {\n firstOption.scrollIntoView({ behavior: 'smooth', block: 'nearest' })\n // Highlight the first option\n firstOption.classList.add('multiselect-option-is-pointed')\n }\n })\n }\n}\n\n// Handle keyboard navigation\nfunction handleKeydown(event: Event) {\n const keyboardEvent = event as KeyboardEvent\n if (keyboardEvent.key === 'Tab' || keyboardEvent.key === 'Enter') {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption && searchQuery.value.trim()) {\n // Get the language data from the first option\n const langCode = firstOption.getAttribute('data-lang-code')\n if (langCode) {\n const language = validLanguages.value.find((lang) => lang.code === langCode)\n if (language) {\n selectedLanguage.value = language\n searchQuery.value = ''\n keyboardEvent.preventDefault()\n }\n }\n }\n }\n}\n\nconst onFlagError = (event: Event) => {\n const target = event.target as HTMLImageElement\n const currentSrc = target.src\n\n // If loading from local cache failed, try CDN\n if (currentSrc && currentSrc.includes('/flags/')) {\n const match = currentSrc.match(/\\/flags\\/([a-z]{2})\\.svg/)\n if (match) {\n const countryCode = match[1]\n // Fallback to CDN\n target.src = `https://flagcdn.com/${countryCode}.svg`\n // If CDN also fails, hide the image\n target.onerror = () => {\n target.style.display = 'none'\n }\n return\n }\n }\n\n // If CDN or any other source fails, hide the image\n target.style.display = 'none'\n}\n</script>\n\n<style scoped>\n.language-select {\n position: relative;\n min-height: 40px; /* To prevent layout shift while loading */\n}\n\n.skeleton-loader {\n width: 100%;\n height: 40px;\n background-color: #e2e8f0;\n border-radius: 4px;\n animation: pulse 1.5s infinite ease-in-out;\n}\n\n.is-dark .skeleton-loader {\n background-color: #374151;\n}\n\n@keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n\n:deep(.multiselect) {\n --ms-font-size: 1rem;\n --ms-line-height: 1.5;\n --ms-bg: v-bind('THEME_COLORS.light.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.light.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.light.border');\n --ms-border-width: 1px;\n --ms-radius: 4px;\n --ms-py: 0.3rem;\n --ms-px: 0.875rem;\n --ms-ring-width: 3px;\n --ms-ring-color: v-bind('THEME_COLORS.light.ring');\n --ms-placeholder-color: v-bind('THEME_COLORS.light.placeholder');\n --ms-color: #1f2937; /* Explicit text color for light theme */\n\n /* Options */\n --ms-option-font-size: 1rem;\n --ms-option-line-height: 1;\n --ms-option-py: 0.2rem;\n --ms-option-px: 0.5rem;\n --ms-option-bg-pointed: v-bind('THEME_COLORS.light.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.light.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.light.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.light.optionSelectedText');\n --ms-option-bg-disabled: #fff;\n --ms-option-color-disabled: #d1d5db;\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.light.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.light.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.light.dropdownText');\n\n /* Others */\n --ms-tag-bg: v-bind('THEME_COLORS.light.tagBackground');\n}\n\n.is-dark :deep(.multiselect) {\n --ms-bg: v-bind('THEME_COLORS.dark.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.dark.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.dark.border');\n --ms-placeholder-color: v-bind('THEME_COLORS.dark.placeholder');\n --ms-ring-color: v-bind('THEME_COLORS.dark.ring');\n\n /* Options */\n --ms-option-bg-pointed: v-bind('THEME_COLORS.dark.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.dark.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.dark.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.dark.optionSelectedText');\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.dark.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.dark.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n:deep(.lang-flag) {\n width: 20px;\n height: 15px;\n border: 1px solid v-bind('COLORS.border.flag');\n border-radius: 2px;\n object-fit: cover;\n flex-shrink: 0;\n display: block;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n\n.is-dark :deep(.lang-flag) {\n border-color: v-bind('COLORS.border.dark');\n}\n\n:deep(.multiselect-single-label),\n:deep(.multiselect-option) {\n display: flex !important;\n align-items: center !important;\n min-width: 0 !important;\n width: 100% !important;\n padding: 4px 8px !important;\n gap: 8px !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-option) {\n text-align: left !important;\n transition: background-color 0.15s ease !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.language-text) {\n display: inline-block !important;\n min-width: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n flex: 1 !important;\n line-height: 1.4 !important;\n color: v-bind('THEME_COLORS.light.dropdownText') !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n.is-dark :deep(.language-text) {\n color: v-bind('THEME_COLORS.dark.dropdownText') !important;\n}\n\n:deep(.native-name) {\n color: v-bind('COLORS.text.secondary');\n margin-left: 0.5rem;\n font-size: 0.9em;\n white-space: nowrap !important;\n display: inline !important;\n}\n\n.is-dark :deep(.native-name) {\n color: v-bind('COLORS.neutral.gray400');\n}\n\n:deep(.multiselect) {\n width: 100%;\n min-width: 200px;\n color: v-bind('THEME_COLORS.light.dropdownText');\n position: relative;\n z-index: 1;\n}\n\n.is-dark :deep(.multiselect) {\n color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n/* Lift the open select above sibling selects (each is its own z-index:1 stacking context) */\n:deep(.multiselect.is-active) {\n z-index: 2;\n}\n\n:deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n z-index: 1000 !important;\n}\n\n:deep(.multiselect-dropdown .multiselect-option) {\n padding: 4px 8px !important;\n margin: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-dropdown .language-text) {\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n:deep(.multiselect-no-results),\n:deep(.multiselect-no-options) {\n padding: 0.5rem 0.875rem;\n color: v-bind('COLORS.text.secondary');\n}\n\n.is-dark :deep(.multiselect-no-results),\n.is-dark :deep(.multiselect-no-options) {\n color: v-bind('COLORS.neutral.gray400');\n}\n</style>\n","<template>\n <div class=\"interface-language-select-wrapper\">\n <LanguageSelect\n :key=\"`interface-lang-${effectiveLanguages.length}-${effectiveLanguages[0]?.code || 'empty'}`\"\n :model-value=\"currentLanguageObject\"\n :languages=\"effectiveLanguages\"\n :placeholder=\"props.placeholder\"\n :disabled=\"props.disabled || isChangingLanguage\"\n :is-dark=\"props.isDark\"\n @update:model-value=\"handleLanguageChange\"\n />\n\n <!-- Loading overlay -->\n <div\n v-if=\"isChangingLanguage\"\n class=\"language-change-loader\"\n :class=\"{ 'is-dark': props.isDark }\"\n >\n <div class=\"loader-spinner\"></div>\n <span class=\"loader-text\">{{ lr('Changing language...', 'ui') }}</span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, watch, onMounted, ref } from 'vue'\nimport LanguageSelect from './LanguageSelect.vue'\nimport { useLangie } from '../useLangie'\nimport type { TranslatorLanguage } from '../types'\nimport { getCountryCode } from '../utils/getCountryCode'\n\n// Loading state for language change\nconst isChangingLanguage = ref(false)\n\nconst props = defineProps({\n placeholder: {\n type: String,\n default: 'Select interface language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n },\n translatorHost: {\n type: String,\n default: ''\n },\n apiKey: {\n type: String,\n default: ''\n },\n languages: {\n type: Array as () => TranslatorLanguage[],\n default: () => []\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\n// Use the same SDK instance as the main app\nconst { availableLanguages, currentLanguage, setLanguage, fetchLanguages, lr } = useLangie()\n\n// Use provided languages if available, otherwise use SDK's languages\nconst effectiveLanguages = computed(() => {\n const languages =\n props.languages && props.languages.length > 0 ? props.languages : availableLanguages.value\n\n // Ensure all languages have required fields to prevent jumping\n const processed = languages.map((lang) => ({\n ...lang,\n native_name: lang.native_name || lang.name,\n flag_country: lang.flag_country || lang.code\n }))\n\n // Important: Keep API order (by popularity from language_popularity table)\n // Do NOT sort alphabetically - multiselect may reorder, but we preserve source order\n return processed\n})\n\nconst currentLanguageObject = computed(() => {\n if (!currentLanguage.value) return null\n return effectiveLanguages.value.find((lang) => lang.code === currentLanguage.value) || null\n})\n\n// Function to detect browser language from available languages\nfunction detectBrowserLanguage(languages: TranslatorLanguage[]): string | null {\n if (languages.length === 0) return null\n\n // Get browser languages in order of preference\n const browserLanguages = navigator.languages || [navigator.language || 'en']\n\n // Try to find exact match first\n for (const browserLang of browserLanguages) {\n const langCode = browserLang.toLowerCase().split('-')[0] // Extract language code (e.g., 'en' from 'en-US')\n const exactMatch = languages.find((lang) => lang.code.toLowerCase() === langCode)\n if (exactMatch) {\n return exactMatch.code\n }\n }\n\n // If no exact match, try with full locale (e.g., 'en-US')\n for (const browserLang of browserLanguages) {\n const fullLangCode = browserLang.toLowerCase()\n const localeMatch = languages.find((lang) => lang.code.toLowerCase() === fullLangCode)\n if (localeMatch) {\n return localeMatch.code\n }\n }\n\n return null\n}\n\nasync function handleLanguageChange(selectedLanguage: TranslatorLanguage | null) {\n if (selectedLanguage) {\n // Start loading\n isChangingLanguage.value = true\n\n try {\n setLanguage(selectedLanguage.code)\n // Save to localStorage when user manually changes language\n localStorage.setItem('interface_language', selectedLanguage.code)\n emit('update:modelValue', selectedLanguage)\n } finally {\n // End loading\n isChangingLanguage.value = false\n }\n }\n}\n\n// Watch for current language changes to save to localStorage\nwatch(currentLanguage, (newLangCode) => {\n if (newLangCode) {\n localStorage.setItem('interface_language', newLangCode)\n }\n})\n\n// Watch for changes in effective languages to set browser language\nwatch(\n () => effectiveLanguages.value,\n (newLanguages) => {\n if (newLanguages.length > 0 && !currentLanguage.value) {\n // Only set browser language if no language is currently selected\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode) {\n // Check if saved language exists in the provided languages\n const savedLangExists = newLanguages.find((lang) => lang.code === savedLanguageCode)\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n return\n }\n }\n\n // If no saved language or saved language doesn't exist, detect browser language\n const browserLang = detectBrowserLanguage(newLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n },\n { immediate: true }\n)\n\n// Load saved language from localStorage on initialization\nonMounted(async () => {\n // Only fetch languages if not provided via props\n if (!props.languages || props.languages.length === 0) {\n // Get country code from browser for better language ordering\n const countryCode = await getCountryCode()\n await fetchLanguages({ country: countryCode || undefined })\n }\n\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode && savedLanguageCode !== currentLanguage.value) {\n // Check if saved language exists in current languages\n const currentLanguages = effectiveLanguages.value\n const savedLangExists = currentLanguages.find((lang) => lang.code === savedLanguageCode)\n\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n } else if (currentLanguages.length > 0) {\n // If saved language doesn't exist, try to detect browser language\n const browserLang = detectBrowserLanguage(currentLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n } else if (!currentLanguage.value && effectiveLanguages.value.length > 0) {\n // If no saved language and no current language, detect browser language\n const browserLang = detectBrowserLanguage(effectiveLanguages.value)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n})\n\n// Emit the current language when it changes\nwatch(\n currentLanguageObject,\n (newValue) => {\n if (newValue) {\n emit('update:modelValue', newValue)\n }\n },\n { immediate: true }\n)\n</script>\n\n<style scoped>\n.interface-language-select-wrapper {\n position: relative;\n display: inline-block;\n width: 100%;\n min-width: 200px;\n}\n\n/* Override styles for the language select component */\n.interface-language-select-wrapper :deep(.multiselect-option) {\n padding: 5px 10px !important;\n margin: 0;\n border-radius: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-option > div) {\n padding: 0 !important;\n margin: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n}\n\n.language-change-loader {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.95);\n border-radius: 6px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n z-index: 10;\n backdrop-filter: blur(2px);\n border: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.language-change-loader.is-dark {\n background: rgba(30, 30, 30, 0.95);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.loader-spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top: 3px solid #3b82f6;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin-bottom: 8px;\n}\n\n.language-change-loader.is-dark .loader-spinner {\n border-color: #374151;\n border-top-color: #3b82f6;\n}\n\n.loader-text {\n font-size: 12px;\n color: #6b7280;\n font-weight: 500;\n text-align: center;\n}\n\n.language-change-loader.is-dark .loader-text {\n color: #9ca3af;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n</style>\n","<template>\n <transition name=\"fade\" mode=\"out-in\">\n <span :key=\"translated\">{{ translated }}</span>\n </transition>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, useSlots, watch } from 'vue'\nimport { ref, nextTick } from 'vue'\nimport { useLangie } from '../useLangie'\n\n// Define component name for better debugging\n// Component name for Vue devtools\n// Note: defineOptions requires Vue 3.3+, but we support Vue 3.2+\n// The component will work without explicit name\n\n// Detect if we're running in Nuxt\nconst isNuxt = computed(() => {\n if (typeof window !== 'undefined') {\n return !!(window as any).__NUXT__\n }\n if (typeof process !== 'undefined') {\n return !!(process.env as any).NUXT_SSR_BASE || !!(process.env as any).NUXT_PUBLIC_BASE_URL\n }\n return false\n})\n\nconst props = defineProps({\n // Message key (optional, otherwise slot content is used)\n msg: {\n type: String,\n default: undefined\n },\n // Translation context shorthand\n ctx: {\n type: String,\n required: false,\n default: 'ui'\n },\n // Original language shorthand\n orig: {\n type: String,\n required: false,\n default: undefined\n }\n})\n\nconst slots = useSlots()\nconst { lr, currentLanguage, uiTranslations, translations, getLtDefaults } = useLangie()\n\nconst keyStr = computed(() => {\n if (props.msg) return props.msg\n const slotContent = slots.default\n ? slots\n .default()\n .map((n) => n.children)\n .join('')\n : ''\n return (slotContent || '').trim()\n})\n\n// Force component update when translations change\nconst forceUpdate = ref(0)\nwatch(\n [uiTranslations, translations],\n () => {\n // Small delay to ensure translations are cached before updating\n nextTick(() => {\n forceUpdate.value++\n })\n },\n { deep: true }\n)\n\nconst translated = computed(() => {\n // To prevent SSR hydration mismatches in Nuxt, render the untranslated key on the server\n if (isNuxt.value && typeof window === 'undefined') {\n return keyStr.value\n }\n\n // Get global defaults\n const globalDefaults = getLtDefaults()\n\n // Use props or fall back to global defaults\n const effectiveCtx = props.ctx ?? globalDefaults.ctx\n const effectiveOrig = props.orig ?? globalDefaults.orig\n\n // Force reactivity by depending on currentLanguage and translation caches\n void currentLanguage.value\n void forceUpdate.value // Force recomputation when translations change\n // Access properties to create reactive dependencies\n const cacheKey = `${keyStr.value}|${effectiveCtx}`\n const cache = effectiveCtx === 'ui' ? uiTranslations : translations\n void cache[cacheKey] // This creates a reactive dependency\n\n // For other frameworks, always translate\n const result = lr(keyStr.value, effectiveCtx, effectiveOrig)\n return result\n})\n</script>\n\n<style scoped>\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.25s ease;\n}\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["/builds/codelits/langie-sdk/src/components/LanguageSelect.vue","/builds/codelits/langie-sdk/src/components/InterfaceLanguageSelect.vue","/builds/codelits/langie-sdk/src/components/lt.vue"],"sourcesContent":["<template>\n <div class=\"language-select\" :class=\"{ 'is-dark': isDark }\">\n <!-- Show multiselect when we have languages OR when not loading -->\n <Multiselect\n v-if=\"validLanguages.length > 0 || !isLoading\"\n :key=\"multiselectKey\"\n v-model=\"selectedLanguage\"\n :options=\"filteredLanguages\"\n :searchable=\"true\"\n @search-change=\"handleSearch\"\n :can-clear=\"false\"\n :allow-empty=\"false\"\n :object=\"true\"\n :placeholder=\"validLanguages.length === 0 ? 'No languages available' : placeholder\"\n :disabled=\"props.disabled || validLanguages.length === 0\"\n :loading=\"isLoading\"\n track-by=\"name\"\n label=\"name\"\n value-prop=\"code\"\n :filter-results=\"false\"\n @keydown=\"handleKeydown\"\n >\n <template #singlelabel=\"{ value }\">\n <div v-if=\"value\" class=\"multiselect-single-label\">\n <img\n v-if=\"value.code\"\n :key=\"selectedLanguageKey\"\n :src=\"getFlagImageUrl(value)\"\n class=\"lang-flag\"\n :alt=\"`${value.native_name || value.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ value.name }}\n <span v-if=\"value.native_name && value.native_name !== value.name\" class=\"native-name\"\n >({{ value.native_name }})</span\n ></span\n >\n </div>\n </template>\n <template #option=\"{ option }\">\n <div class=\"multiselect-option\" :data-lang-code=\"option.code\">\n <img\n :key=\"`${option.code}-${option.flag_country || 'fallback'}`\"\n :src=\"getFlagImageUrl(option)\"\n class=\"lang-flag\"\n :alt=\"`${option.name} flag`\"\n @error=\"onFlagError\"\n />\n <span class=\"language-text\"\n >{{ option.name }}\n <span\n v-if=\"option.native_name && option.native_name !== option.name\"\n class=\"native-name\"\n >({{ option.native_name }})</span\n ></span\n >\n </div>\n </template>\n <template #noresults>\n <div class=\"multiselect-no-results\">No languages found.</div>\n </template>\n <template #nooptions>\n <div class=\"multiselect-no-options\">\n {{\n validLanguages.length === 0\n ? 'Please provide languages via the :languages prop'\n : 'No languages available.'\n }}\n </div>\n </template>\n </Multiselect>\n <!-- Only show skeleton loader when actually loading AND we have no languages yet -->\n <div v-else-if=\"isLoading && validLanguages.length === 0\" class=\"skeleton-loader\"></div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport '@vueform/multiselect/themes/default.css'\nimport '../styles/teleport.css'\nimport { ref, computed, nextTick } from 'vue'\nimport type { PropType } from 'vue'\nimport Multiselect from '@vueform/multiselect'\nimport Fuse from 'fuse.js'\nimport type { FuseResult } from 'fuse.js'\nimport { transliterate } from 'transliteration'\nimport { applyLanguageAlias } from '../search-utils'\nimport { THEME_COLORS, COLORS } from '../constants/colors'\nimport type { TranslatorLanguage } from '../types'\n\nconst getFlagCode = (lang: TranslatorLanguage): string => {\n const flagCode = lang.flag_country || lang.code\n return flagCode\n}\n\nconst getFlagImageUrl = (lang: TranslatorLanguage): string => {\n const flagCode = getFlagCode(lang)\n // Use local flags (downloaded during build for offline support and faster loading)\n // If not found, the onerror handler will use CDN fallback\n return `/flags/${flagCode}.svg`\n}\n\nconst props = defineProps({\n modelValue: {\n type: Object as PropType<TranslatorLanguage | null | undefined>,\n default: undefined\n },\n languages: {\n type: Array as PropType<TranslatorLanguage[]>,\n default: () => []\n },\n placeholder: {\n type: String,\n default: 'Select language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\nconst isLoading = computed(() => {\n return props.languages.length <= 0\n})\nconst searchQuery = ref('')\n\nconst validLanguages = computed(() => {\n return props.languages.filter((lang) => lang && lang.code && lang.name && lang.native_name)\n})\n\nconst fuse = computed(() => {\n if (!validLanguages.value.length) return null\n\n return new Fuse(validLanguages.value, {\n keys: ['name', 'native_name', 'search_alias', 'code'],\n includeScore: true,\n threshold: 0.6, // Use highest threshold to catch all cases\n isCaseSensitive: false,\n minMatchCharLength: 1 // Allow single character matches\n })\n})\n\nconst multiselectKey = computed(() => {\n // Create a key that changes when the language data structure changes\n const hasApiData = props.languages.length > 0 && props.languages[0].flag_country !== null\n return hasApiData ? 'api-data' : 'fallback-data'\n})\n\nconst selectedLanguageKey = computed(() => {\n if (!props.modelValue || !props.modelValue.code) return 'no-selection'\n const key = `${props.modelValue.code}-${props.modelValue.flag_country || 'fallback'}`\n return key\n})\n\nconst selectedLanguage = computed({\n get: () => props.modelValue || null,\n set: (value) => {\n if (value && value.code) {\n emit('update:modelValue', value)\n }\n }\n})\n\nconst filteredLanguages = computed(() => {\n const query = searchQuery.value.trim()\n\n let results: TranslatorLanguage[]\n if (!query) {\n // When no search query, return all valid languages (bypass Fuse.js)\n results = validLanguages.value\n } else {\n // Only use Fuse.js when there's an actual search query\n if (!fuse.value) return []\n\n // Get the most likely alias or the original term\n const aliasResult = applyLanguageAlias(query)\n const searchTerm = Array.isArray(aliasResult.primary)\n ? aliasResult.primary[0]\n : aliasResult.primary\n\n // Merge Fuse hits into `results`, skipping languages already present.\n const mergeByCode = (hits: FuseResult<TranslatorLanguage>[]) => {\n hits.forEach((hit) => {\n if (!results.some((r) => r.code === hit.item.code)) results.push(hit.item)\n })\n }\n\n // Always use Fuse.js for the main search (both original query and alias result)\n results = fuse.value.search(searchTerm).map((result) => result.item)\n\n // If the search term is different from query (alias was applied), also search the original query\n if (searchTerm !== query) {\n mergeByCode(fuse.value.search(query))\n }\n\n // Transliterate the query to Latin so a foreign-script keyboard still matches the\n // Latin `name`/`native_name` when no localized `search_alias` exists (e.g. English\n // interface). Helps cognates: Cyrillic \"браз\" → \"braz\" → Brazilian.\n const latinQuery = transliterate(query)\n if (latinQuery && latinQuery.toLowerCase() !== query.toLowerCase()) {\n mergeByCode(fuse.value.search(latinQuery))\n }\n\n // Add suggested languages to expand the results\n if (aliasResult.suggestions.length > 0) {\n aliasResult.suggestions.forEach((suggestion) => {\n const suggestedLang = validLanguages.value.find((lang) => {\n const langName = lang.name.toLowerCase()\n const suggestion_lower = suggestion.toLowerCase()\n\n // Try multiple matching strategies\n return (\n langName === suggestion_lower || // Exact match\n langName.includes(suggestion_lower) || // Contains match\n lang.code.toLowerCase() === suggestion_lower || // Code match\n langName.startsWith(suggestion_lower) || // Starts with match\n // Handle common variations\n (suggestion_lower === 'tatar' && langName.includes('tatar')) ||\n (suggestion_lower === 'belarusian' &&\n (langName.includes('belarus') || langName.includes('belarusian'))) ||\n (suggestion_lower === 'moldovan' &&\n (langName.includes('moldov') || langName.includes('moldova')))\n )\n })\n if (suggestedLang && !results.some((r) => r.code === suggestedLang.code)) {\n results.push(suggestedLang)\n }\n })\n }\n\n // Special handling for very short queries (e.g. \"се\")\n if (query.length === 2) {\n const lowerQuery = query.toLowerCase()\n const manualResults = validLanguages.value.filter(\n (lang) =>\n lang.name.toLowerCase().startsWith(lowerQuery) ||\n lang.native_name.toLowerCase().startsWith(lowerQuery)\n )\n manualResults.forEach((lang) => {\n if (!results.some((r) => r.code === lang.code)) {\n results.push(lang)\n }\n })\n }\n }\n\n // Remove the currently selected language from the options\n const filtered = results.filter(\n (lang) => !props.modelValue || !props.modelValue.code || lang.code !== props.modelValue.code\n )\n\n return filtered\n})\n\nfunction handleSearch(query: string) {\n searchQuery.value = query\n\n // Auto-scroll to first result when typing\n if (query.trim()) {\n nextTick(() => {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption) {\n firstOption.scrollIntoView({ behavior: 'smooth', block: 'nearest' })\n // Highlight the first option\n firstOption.classList.add('multiselect-option-is-pointed')\n }\n })\n }\n}\n\n// Handle keyboard navigation\nfunction handleKeydown(event: Event) {\n const keyboardEvent = event as KeyboardEvent\n if (keyboardEvent.key === 'Tab' || keyboardEvent.key === 'Enter') {\n const dropdown = document.querySelector('.multiselect-dropdown')\n const firstOption = dropdown?.querySelector('.multiselect-option')\n if (firstOption && searchQuery.value.trim()) {\n // Get the language data from the first option\n const langCode = firstOption.getAttribute('data-lang-code')\n if (langCode) {\n const language = validLanguages.value.find((lang) => lang.code === langCode)\n if (language) {\n selectedLanguage.value = language\n searchQuery.value = ''\n keyboardEvent.preventDefault()\n }\n }\n }\n }\n}\n\nconst onFlagError = (event: Event) => {\n const target = event.target as HTMLImageElement\n const currentSrc = target.src\n\n // If loading from local cache failed, try CDN\n if (currentSrc && currentSrc.includes('/flags/')) {\n const match = currentSrc.match(/\\/flags\\/([a-z]{2})\\.svg/)\n if (match) {\n const countryCode = match[1]\n // Fallback to CDN\n target.src = `https://flagcdn.com/${countryCode}.svg`\n // If CDN also fails, hide the image\n target.onerror = () => {\n target.style.display = 'none'\n }\n return\n }\n }\n\n // If CDN or any other source fails, hide the image\n target.style.display = 'none'\n}\n</script>\n\n<style scoped>\n.language-select {\n position: relative;\n min-height: 40px; /* To prevent layout shift while loading */\n}\n\n.skeleton-loader {\n width: 100%;\n height: 40px;\n background-color: #e2e8f0;\n border-radius: 4px;\n animation: pulse 1.5s infinite ease-in-out;\n}\n\n.is-dark .skeleton-loader {\n background-color: #374151;\n}\n\n@keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n\n:deep(.multiselect) {\n --ms-font-size: 1rem;\n --ms-line-height: 1.5;\n --ms-bg: v-bind('THEME_COLORS.light.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.light.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.light.border');\n --ms-border-width: 1px;\n --ms-radius: 4px;\n --ms-py: 0.3rem;\n --ms-px: 0.875rem;\n --ms-ring-width: 3px;\n --ms-ring-color: v-bind('THEME_COLORS.light.ring');\n --ms-placeholder-color: v-bind('THEME_COLORS.light.placeholder');\n --ms-color: #1f2937; /* Explicit text color for light theme */\n\n /* Options */\n --ms-option-font-size: 1rem;\n --ms-option-line-height: 1;\n --ms-option-py: 0.2rem;\n --ms-option-px: 0.5rem;\n --ms-option-bg-pointed: v-bind('THEME_COLORS.light.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.light.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.light.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.light.optionSelectedText');\n --ms-option-bg-disabled: #fff;\n --ms-option-color-disabled: #d1d5db;\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.light.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.light.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.light.dropdownText');\n\n /* Others */\n --ms-tag-bg: v-bind('THEME_COLORS.light.tagBackground');\n}\n\n.is-dark :deep(.multiselect) {\n --ms-bg: v-bind('THEME_COLORS.dark.background');\n --ms-bg-disabled: v-bind('THEME_COLORS.dark.backgroundDisabled');\n --ms-border-color: v-bind('THEME_COLORS.dark.border');\n --ms-placeholder-color: v-bind('THEME_COLORS.dark.placeholder');\n --ms-ring-color: v-bind('THEME_COLORS.dark.ring');\n --ms-color: v-bind('THEME_COLORS.dark.dropdownText'); /* typed search text; base is dark #1f2937 → invisible on dark bg */\n\n /* Options */\n --ms-option-bg-pointed: v-bind('THEME_COLORS.dark.optionPointed');\n --ms-option-color-pointed: v-bind('THEME_COLORS.dark.optionPointedText');\n --ms-option-bg-selected: v-bind('THEME_COLORS.dark.optionSelected');\n --ms-option-color-selected: v-bind('THEME_COLORS.dark.optionSelectedText');\n\n /* Dropdown */\n --ms-dropdown-bg: v-bind('THEME_COLORS.dark.dropdownBackground');\n --ms-dropdown-border-color: v-bind('THEME_COLORS.dark.dropdownBorder');\n --ms-dropdown-color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n:deep(.lang-flag) {\n width: 20px;\n height: 15px;\n border: 1px solid v-bind('COLORS.border.flag');\n border-radius: 2px;\n object-fit: cover;\n flex-shrink: 0;\n display: block;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n\n.is-dark :deep(.lang-flag) {\n border-color: v-bind('COLORS.border.dark');\n}\n\n:deep(.multiselect-single-label),\n:deep(.multiselect-option) {\n display: flex !important;\n align-items: center !important;\n min-width: 0 !important;\n width: 100% !important;\n padding: 4px 8px !important;\n gap: 8px !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-option) {\n text-align: left !important;\n transition: background-color 0.15s ease !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.language-text) {\n display: inline-block !important;\n min-width: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n flex: 1 !important;\n line-height: 1.4 !important;\n color: v-bind('THEME_COLORS.light.dropdownText') !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n.is-dark :deep(.language-text) {\n color: v-bind('THEME_COLORS.dark.dropdownText') !important;\n}\n\n:deep(.native-name) {\n color: v-bind('COLORS.text.secondary');\n margin-left: 0.5rem;\n font-size: 0.9em;\n white-space: nowrap !important;\n display: inline !important;\n}\n\n.is-dark :deep(.native-name) {\n color: v-bind('COLORS.neutral.gray400');\n}\n\n:deep(.multiselect) {\n width: 100%;\n min-width: 200px;\n color: v-bind('THEME_COLORS.light.dropdownText');\n position: relative;\n z-index: 1;\n}\n\n.is-dark :deep(.multiselect) {\n color: v-bind('THEME_COLORS.dark.dropdownText');\n}\n\n/* Lift the open select above sibling selects (each is its own z-index:1 stacking context) */\n:deep(.multiselect.is-active) {\n z-index: 2;\n}\n\n:deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n z-index: 1000 !important;\n}\n\n:deep(.multiselect-dropdown .multiselect-option) {\n padding: 4px 8px !important;\n margin: 0 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n flex-wrap: nowrap !important;\n}\n\n:deep(.multiselect-dropdown .language-text) {\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n word-break: keep-all !important;\n word-wrap: normal !important;\n word-spacing: normal !important;\n max-width: 100% !important;\n}\n\n:deep(.multiselect-no-results),\n:deep(.multiselect-no-options) {\n padding: 0.5rem 0.875rem;\n color: v-bind('COLORS.text.secondary');\n}\n\n.is-dark :deep(.multiselect-no-results),\n.is-dark :deep(.multiselect-no-options) {\n color: v-bind('COLORS.neutral.gray400');\n}\n</style>\n","<template>\n <div class=\"interface-language-select-wrapper\">\n <LanguageSelect\n :key=\"`interface-lang-${effectiveLanguages.length}-${effectiveLanguages[0]?.code || 'empty'}`\"\n :model-value=\"currentLanguageObject\"\n :languages=\"effectiveLanguages\"\n :placeholder=\"props.placeholder\"\n :disabled=\"props.disabled || isChangingLanguage\"\n :is-dark=\"props.isDark\"\n @update:model-value=\"handleLanguageChange\"\n />\n\n <!-- Loading overlay -->\n <div\n v-if=\"isChangingLanguage\"\n class=\"language-change-loader\"\n :class=\"{ 'is-dark': props.isDark }\"\n >\n <div class=\"loader-spinner\"></div>\n <span class=\"loader-text\">{{ lr('Changing language...', 'ui') }}</span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, watch, onMounted, ref } from 'vue'\nimport LanguageSelect from './LanguageSelect.vue'\nimport { useLangie } from '../useLangie'\nimport type { TranslatorLanguage } from '../types'\nimport { getCountryCode } from '../utils/getCountryCode'\n\n// Loading state for language change\nconst isChangingLanguage = ref(false)\n\nconst props = defineProps({\n placeholder: {\n type: String,\n default: 'Select interface language'\n },\n disabled: {\n type: Boolean,\n default: false\n },\n isDark: {\n type: Boolean,\n default: false\n },\n translatorHost: {\n type: String,\n default: ''\n },\n apiKey: {\n type: String,\n default: ''\n },\n languages: {\n type: Array as () => TranslatorLanguage[],\n default: () => []\n }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\n// Use the same SDK instance as the main app\nconst { availableLanguages, currentLanguage, setLanguage, fetchLanguages, lr } = useLangie()\n\n// Use provided languages if available, otherwise use SDK's languages\nconst effectiveLanguages = computed(() => {\n const languages =\n props.languages && props.languages.length > 0 ? props.languages : availableLanguages.value\n\n // Ensure all languages have required fields to prevent jumping\n const processed = languages.map((lang) => ({\n ...lang,\n native_name: lang.native_name || lang.name,\n flag_country: lang.flag_country || lang.code\n }))\n\n // Important: Keep API order (by popularity from language_popularity table)\n // Do NOT sort alphabetically - multiselect may reorder, but we preserve source order\n return processed\n})\n\nconst currentLanguageObject = computed(() => {\n if (!currentLanguage.value) return null\n return effectiveLanguages.value.find((lang) => lang.code === currentLanguage.value) || null\n})\n\n// Function to detect browser language from available languages\nfunction detectBrowserLanguage(languages: TranslatorLanguage[]): string | null {\n if (languages.length === 0) return null\n\n // Get browser languages in order of preference\n const browserLanguages = navigator.languages || [navigator.language || 'en']\n\n // Try to find exact match first\n for (const browserLang of browserLanguages) {\n const langCode = browserLang.toLowerCase().split('-')[0] // Extract language code (e.g., 'en' from 'en-US')\n const exactMatch = languages.find((lang) => lang.code.toLowerCase() === langCode)\n if (exactMatch) {\n return exactMatch.code\n }\n }\n\n // If no exact match, try with full locale (e.g., 'en-US')\n for (const browserLang of browserLanguages) {\n const fullLangCode = browserLang.toLowerCase()\n const localeMatch = languages.find((lang) => lang.code.toLowerCase() === fullLangCode)\n if (localeMatch) {\n return localeMatch.code\n }\n }\n\n return null\n}\n\nasync function handleLanguageChange(selectedLanguage: TranslatorLanguage | null) {\n if (selectedLanguage) {\n // Start loading\n isChangingLanguage.value = true\n\n try {\n setLanguage(selectedLanguage.code)\n // Save to localStorage when user manually changes language\n localStorage.setItem('interface_language', selectedLanguage.code)\n emit('update:modelValue', selectedLanguage)\n } finally {\n // End loading\n isChangingLanguage.value = false\n }\n }\n}\n\n// Watch for current language changes to save to localStorage\nwatch(currentLanguage, (newLangCode) => {\n if (newLangCode) {\n localStorage.setItem('interface_language', newLangCode)\n }\n})\n\n// Watch for changes in effective languages to set browser language\nwatch(\n () => effectiveLanguages.value,\n (newLanguages) => {\n if (newLanguages.length > 0 && !currentLanguage.value) {\n // Only set browser language if no language is currently selected\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode) {\n // Check if saved language exists in the provided languages\n const savedLangExists = newLanguages.find((lang) => lang.code === savedLanguageCode)\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n return\n }\n }\n\n // If no saved language or saved language doesn't exist, detect browser language\n const browserLang = detectBrowserLanguage(newLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n },\n { immediate: true }\n)\n\n// Load saved language from localStorage on initialization\nonMounted(async () => {\n // Only fetch languages if not provided via props\n if (!props.languages || props.languages.length === 0) {\n // Get country code from browser for better language ordering\n const countryCode = await getCountryCode()\n await fetchLanguages({ country: countryCode || undefined })\n }\n\n const savedLanguageCode = localStorage.getItem('interface_language')\n\n if (savedLanguageCode && savedLanguageCode !== currentLanguage.value) {\n // Check if saved language exists in current languages\n const currentLanguages = effectiveLanguages.value\n const savedLangExists = currentLanguages.find((lang) => lang.code === savedLanguageCode)\n\n if (savedLangExists) {\n setLanguage(savedLanguageCode)\n } else if (currentLanguages.length > 0) {\n // If saved language doesn't exist, try to detect browser language\n const browserLang = detectBrowserLanguage(currentLanguages)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n } else if (!currentLanguage.value && effectiveLanguages.value.length > 0) {\n // If no saved language and no current language, detect browser language\n const browserLang = detectBrowserLanguage(effectiveLanguages.value)\n if (browserLang) {\n setLanguage(browserLang)\n }\n }\n})\n\n// Emit the current language when it changes\nwatch(\n currentLanguageObject,\n (newValue) => {\n if (newValue) {\n emit('update:modelValue', newValue)\n }\n },\n { immediate: true }\n)\n</script>\n\n<style scoped>\n.interface-language-select-wrapper {\n position: relative;\n display: inline-block;\n width: 100%;\n min-width: 200px;\n}\n\n/* Override styles for the language select component */\n.interface-language-select-wrapper :deep(.multiselect-option) {\n padding: 5px 10px !important;\n margin: 0;\n border-radius: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-option > div) {\n padding: 0 !important;\n margin: 0;\n}\n\n.interface-language-select-wrapper :deep(.multiselect-dropdown) {\n padding: 2px 0 !important;\n}\n\n.language-change-loader {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.95);\n border-radius: 6px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n z-index: 10;\n backdrop-filter: blur(2px);\n border: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.language-change-loader.is-dark {\n background: rgba(30, 30, 30, 0.95);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.loader-spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top: 3px solid #3b82f6;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin-bottom: 8px;\n}\n\n.language-change-loader.is-dark .loader-spinner {\n border-color: #374151;\n border-top-color: #3b82f6;\n}\n\n.loader-text {\n font-size: 12px;\n color: #6b7280;\n font-weight: 500;\n text-align: center;\n}\n\n.language-change-loader.is-dark .loader-text {\n color: #9ca3af;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n</style>\n","<template>\n <transition name=\"fade\" mode=\"out-in\">\n <span :key=\"translated\">{{ translated }}</span>\n </transition>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, useSlots, watch } from 'vue'\nimport { ref, nextTick } from 'vue'\nimport { useLangie } from '../useLangie'\n\n// Define component name for better debugging\n// Component name for Vue devtools\n// Note: defineOptions requires Vue 3.3+, but we support Vue 3.2+\n// The component will work without explicit name\n\n// Detect if we're running in Nuxt\nconst isNuxt = computed(() => {\n if (typeof window !== 'undefined') {\n return !!(window as any).__NUXT__\n }\n if (typeof process !== 'undefined') {\n return !!(process.env as any).NUXT_SSR_BASE || !!(process.env as any).NUXT_PUBLIC_BASE_URL\n }\n return false\n})\n\nconst props = defineProps({\n // Message key (optional, otherwise slot content is used)\n msg: {\n type: String,\n default: undefined\n },\n // Translation context shorthand\n ctx: {\n type: String,\n required: false,\n default: 'ui'\n },\n // Original language shorthand\n orig: {\n type: String,\n required: false,\n default: undefined\n }\n})\n\nconst slots = useSlots()\nconst { lr, currentLanguage, uiTranslations, translations, getLtDefaults } = useLangie()\n\nconst keyStr = computed(() => {\n if (props.msg) return props.msg\n const slotContent = slots.default\n ? slots\n .default()\n .map((n) => n.children)\n .join('')\n : ''\n return (slotContent || '').trim()\n})\n\n// Force component update when translations change\nconst forceUpdate = ref(0)\nwatch(\n [uiTranslations, translations],\n () => {\n // Small delay to ensure translations are cached before updating\n nextTick(() => {\n forceUpdate.value++\n })\n },\n { deep: true }\n)\n\nconst translated = computed(() => {\n // To prevent SSR hydration mismatches in Nuxt, render the untranslated key on the server\n if (isNuxt.value && typeof window === 'undefined') {\n return keyStr.value\n }\n\n // Get global defaults\n const globalDefaults = getLtDefaults()\n\n // Use props or fall back to global defaults\n const effectiveCtx = props.ctx ?? globalDefaults.ctx\n const effectiveOrig = props.orig ?? globalDefaults.orig\n\n // Force reactivity by depending on currentLanguage and translation caches\n void currentLanguage.value\n void forceUpdate.value // Force recomputation when translations change\n // Access properties to create reactive dependencies\n const cacheKey = `${keyStr.value}|${effectiveCtx}`\n const cache = effectiveCtx === 'ui' ? uiTranslations : translations\n void cache[cacheKey] // This creates a reactive dependency\n\n // For other frameworks, always translate\n const result = lr(keyStr.value, effectiveCtx, effectiveOrig)\n return result\n})\n</script>\n\n<style scoped>\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.25s ease;\n}\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n"],"mappings":"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","names":[]}
@@ -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
  *
@@ -9,7 +9,7 @@
9
9
 
10
10
  // vue-script:/builds/codelits/langie-sdk/src/components/LanguageSelect.vue?type=script
11
11
  import { useCssVars as _useCssVars, defineComponent as _defineComponent } from "vue";
12
- import { createCommentVNode as _createCommentVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, unref as _unref, withCtx as _withCtx, createBlock as _createBlock, Fragment as _Fragment, normalizeClass as _normalizeClass } from "vue";
12
+ import { createCommentVNode as _createCommentVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, unref as _unref, withCtx as _withCtx, createBlock as _createBlock, Fragment as _Fragment, normalizeClass as _normalizeClass } from "vue";
13
13
  import "@vueform/multiselect/themes/default.css";
14
14
 
15
15
  // #style-inject:#style-inject
@@ -41,6 +41,7 @@ styleInject(".multiselect-single-label,\n.multiselect-option {\n display: flex;
41
41
  import { ref, computed, nextTick } from "vue";
42
42
  import Multiselect from "@vueform/multiselect";
43
43
  import Fuse from "fuse.js";
44
+ import { transliterate } from "transliteration";
44
45
 
45
46
  // src/language-aliases/european.ts
46
47
  var EUROPEAN_LANGUAGES = [
@@ -158,7 +159,19 @@ var EUROPEAN_LANGUAGES = [
158
159
  },
159
160
  {
160
161
  lang: "serbian",
161
- match: ["srp", "sr", "ser", "serbia", "serbian", "srpski", "\u0441\u0440\u0431\u0438\u0458\u0430", "\u0441\u0440\u043F\u0441\u043A\u0438"],
162
+ match: [
163
+ "srp",
164
+ "sr",
165
+ "ser",
166
+ "serbia",
167
+ "serbian",
168
+ "srpski",
169
+ "\u0441\u0440\u0431\u0438\u0458\u0430",
170
+ "\u0441\u0440\u043F\u0441\u043A\u0438",
171
+ "\u0441\u0435\u0440\u0431",
172
+ "\u0441\u0435\u0440\u0431\u0441\u043A\u0438\u0439",
173
+ "\u0441\u0435\u0440\u0431\u0441"
174
+ ],
162
175
  suggest: ["croatian", "bosnian", "montenegrin"]
163
176
  },
164
177
  {
@@ -324,7 +337,7 @@ var ASIAN_LANGUAGES = [
324
337
  // Central Asian & Turkic Languages
325
338
  {
326
339
  lang: "kazakh",
327
- match: ["kaz", "kk", "kazakh", "kazakhstan"],
340
+ 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"],
328
341
  suggest: ["russian", "kyrgyz", "uzbek"]
329
342
  },
330
343
  {
@@ -590,11 +603,17 @@ var _hoisted_1 = {
590
603
  };
591
604
  var _hoisted_2 = ["src", "alt"];
592
605
  var _hoisted_3 = { class: "language-text" };
593
- var _hoisted_4 = { class: "native-name" };
606
+ var _hoisted_4 = {
607
+ key: 0,
608
+ class: "native-name"
609
+ };
594
610
  var _hoisted_5 = ["data-lang-code"];
595
611
  var _hoisted_6 = ["src", "alt"];
596
612
  var _hoisted_7 = { class: "language-text" };
597
- var _hoisted_8 = { class: "native-name" };
613
+ var _hoisted_8 = {
614
+ key: 0,
615
+ class: "native-name"
616
+ };
598
617
  var _hoisted_9 = { class: "multiselect-no-options" };
599
618
  var LanguageSelect_default = /* @__PURE__ */ _defineComponent({
600
619
  __name: "LanguageSelect",
@@ -641,13 +660,13 @@ var LanguageSelect_default = /* @__PURE__ */ _defineComponent({
641
660
  "5b09cc17-THEME_COLORS.dark.border": _unref(THEME_COLORS).dark.border,
642
661
  "5b09cc17-THEME_COLORS.dark.placeholder": _unref(THEME_COLORS).dark.placeholder,
643
662
  "5b09cc17-THEME_COLORS.dark.ring": _unref(THEME_COLORS).dark.ring,
663
+ "5b09cc17-THEME_COLORS.dark.dropdownText": _unref(THEME_COLORS).dark.dropdownText,
644
664
  "5b09cc17-THEME_COLORS.dark.optionPointed": _unref(THEME_COLORS).dark.optionPointed,
645
665
  "5b09cc17-THEME_COLORS.dark.optionPointedText": _unref(THEME_COLORS).dark.optionPointedText,
646
666
  "5b09cc17-THEME_COLORS.dark.optionSelected": _unref(THEME_COLORS).dark.optionSelected,
647
667
  "5b09cc17-THEME_COLORS.dark.optionSelectedText": _unref(THEME_COLORS).dark.optionSelectedText,
648
668
  "5b09cc17-THEME_COLORS.dark.dropdownBackground": _unref(THEME_COLORS).dark.dropdownBackground,
649
669
  "5b09cc17-THEME_COLORS.dark.dropdownBorder": _unref(THEME_COLORS).dark.dropdownBorder,
650
- "5b09cc17-THEME_COLORS.dark.dropdownText": _unref(THEME_COLORS).dark.dropdownText,
651
670
  "5b09cc17-COLORS.border.flag": _unref(COLORS).border.flag,
652
671
  "5b09cc17-COLORS.border.dark": _unref(COLORS).border.dark,
653
672
  "5b09cc17-COLORS.text.secondary": _unref(COLORS).text.secondary,
@@ -673,7 +692,7 @@ var LanguageSelect_default = /* @__PURE__ */ _defineComponent({
673
692
  const fuse = computed(() => {
674
693
  if (!validLanguages.value.length) return null;
675
694
  return new Fuse(validLanguages.value, {
676
- keys: ["name", "native_name", "code"],
695
+ keys: ["name", "native_name", "search_alias", "code"],
677
696
  includeScore: true,
678
697
  threshold: 0.6,
679
698
  // Use highest threshold to catch all cases
@@ -708,15 +727,18 @@ var LanguageSelect_default = /* @__PURE__ */ _defineComponent({
708
727
  if (!fuse.value) return [];
709
728
  const aliasResult = applyLanguageAlias(query);
710
729
  const searchTerm = Array.isArray(aliasResult.primary) ? aliasResult.primary[0] : aliasResult.primary;
711
- const fuseResults = fuse.value.search(searchTerm);
712
- results = fuseResults.map((result) => result.item);
713
- if (searchTerm !== query) {
714
- const originalResults = fuse.value.search(query);
715
- originalResults.forEach((result) => {
716
- if (!results.some((r) => r.code === result.item.code)) {
717
- results.push(result.item);
718
- }
730
+ const mergeByCode = (hits) => {
731
+ hits.forEach((hit) => {
732
+ if (!results.some((r) => r.code === hit.item.code)) results.push(hit.item);
719
733
  });
734
+ };
735
+ results = fuse.value.search(searchTerm).map((result) => result.item);
736
+ if (searchTerm !== query) {
737
+ mergeByCode(fuse.value.search(query));
738
+ }
739
+ const latinQuery = transliterate(query);
740
+ if (latinQuery && latinQuery.toLowerCase() !== query.toLowerCase()) {
741
+ mergeByCode(fuse.value.search(latinQuery));
720
742
  }
721
743
  if (aliasResult.suggestions.length > 0) {
722
744
  aliasResult.suggestions.forEach((suggestion) => {
@@ -841,13 +863,13 @@ var LanguageSelect_default = /* @__PURE__ */ _defineComponent({
841
863
  1
842
864
  /* TEXT */
843
865
  ),
844
- _createElementVNode(
866
+ value.native_name && value.native_name !== value.name ? (_openBlock(), _createElementBlock(
845
867
  "span",
846
868
  _hoisted_4,
847
- "(" + _toDisplayString(value.native_name || value.name) + ")",
869
+ "(" + _toDisplayString(value.native_name) + ")",
848
870
  1
849
871
  /* TEXT */
850
- )
872
+ )) : _createCommentVNode("v-if", true)
851
873
  ])
852
874
  ])) : _createCommentVNode("v-if", true)
853
875
  ]),
@@ -869,13 +891,13 @@ var LanguageSelect_default = /* @__PURE__ */ _defineComponent({
869
891
  1
870
892
  /* TEXT */
871
893
  ),
872
- _createElementVNode(
894
+ option.native_name && option.native_name !== option.name ? (_openBlock(), _createElementBlock(
873
895
  "span",
874
896
  _hoisted_8,
875
- "(" + _toDisplayString(option.native_name || option.name) + ")",
897
+ "(" + _toDisplayString(option.native_name) + ")",
876
898
  1
877
899
  /* TEXT */
878
- )
900
+ )) : _createCommentVNode("v-if", true)
879
901
  ])
880
902
  ], 8, _hoisted_5)
881
903
  ]),
@@ -1191,6 +1213,181 @@ var clearCache = (pattern) => {
1191
1213
  return cacheManager.clear(pattern);
1192
1214
  };
1193
1215
 
1216
+ // src/core.ts
1217
+ var DEFAULT_TRANSLATOR_HOST = "http://localhost:8081";
1218
+ var translationCache = /* @__PURE__ */ new Map();
1219
+ function getTranslationCacheKey(serviceTranslations, options) {
1220
+ return JSON.stringify({
1221
+ translations: serviceTranslations.map((t) => ({
1222
+ [API_FIELD_TEXT]: t[API_FIELD_TEXT],
1223
+ [API_FIELD_FROM]: t[API_FIELD_FROM],
1224
+ [API_FIELD_TO]: t[API_FIELD_TO],
1225
+ [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
1226
+ })),
1227
+ host: options.translatorHost || DEFAULT_TRANSLATOR_HOST,
1228
+ context: options[API_FIELD_CTX] || "ui"
1229
+ });
1230
+ }
1231
+ async function translateBatch(translations2 = [], options = {}) {
1232
+ if (!Array.isArray(translations2) || translations2.length === 0) {
1233
+ throw new Error("translations must be a non-empty array");
1234
+ }
1235
+ const translatorHost = options.translatorHost || DEFAULT_TRANSLATOR_HOST;
1236
+ const apiKey = options.apiKey || process.env.TRANSLATOR_API_KEY;
1237
+ const serviceTranslations = [];
1238
+ const indexMap = [];
1239
+ translations2.forEach((tr, idx) => {
1240
+ const from = (tr[API_FIELD_FROM] || "").toLowerCase();
1241
+ const to = (tr[API_FIELD_TO] || "").toLowerCase();
1242
+ if (from === to) {
1243
+ return;
1244
+ }
1245
+ serviceTranslations.push(tr);
1246
+ indexMap.push(idx);
1247
+ });
1248
+ let serviceResults = [];
1249
+ if (serviceTranslations.length > 0) {
1250
+ const cacheKey = getTranslationCacheKey(serviceTranslations, options);
1251
+ if (translationCache.has(cacheKey)) {
1252
+ serviceResults = await translationCache.get(cacheKey);
1253
+ } else {
1254
+ const translationPromise = (async () => {
1255
+ try {
1256
+ const turnstileToken = options.getTurnstileToken ? await options.getTurnstileToken() : void 0;
1257
+ const controller = new AbortController();
1258
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
1259
+ const resp = await fetch(`${translatorHost}/translate`, {
1260
+ method: "POST",
1261
+ headers: {
1262
+ "Content-Type": "application/json",
1263
+ ...apiKey ? { Authorization: `Bearer ${apiKey}`, "X-Api-Key": apiKey } : {}
1264
+ },
1265
+ body: JSON.stringify({
1266
+ [API_FIELD_TRANSLATIONS]: serviceTranslations.map((t) => ({
1267
+ [API_FIELD_TEXT]: t[API_FIELD_TEXT],
1268
+ [API_FIELD_FROM]: t[API_FIELD_FROM],
1269
+ [API_FIELD_TO]: t[API_FIELD_TO],
1270
+ [API_FIELD_CTX]: t[API_FIELD_CTX] || options[API_FIELD_CTX] || "ui"
1271
+ })),
1272
+ [API_FIELD_CTX]: options[API_FIELD_CTX] || "ui",
1273
+ ...turnstileToken ? { [API_FIELD_TURNSTILE]: turnstileToken } : {}
1274
+ }),
1275
+ signal: controller.signal
1276
+ });
1277
+ clearTimeout(timeoutId);
1278
+ if (!resp.ok) {
1279
+ console.error(
1280
+ "[translator-sdk] Translator error response:",
1281
+ resp.status,
1282
+ resp.statusText
1283
+ );
1284
+ throw new Error(`Translator service error: ${resp.status} ${resp.statusText}`);
1285
+ }
1286
+ let parsed;
1287
+ try {
1288
+ parsed = await resp.clone().json();
1289
+ } catch (jsonErr) {
1290
+ console.error("[translator-sdk] Failed to parse JSON response:", jsonErr);
1291
+ throw new Error(`Failed to parse JSON response: ${jsonErr}`);
1292
+ }
1293
+ const data = parsed || {};
1294
+ if (data[API_FIELD_ERROR]) {
1295
+ console.warn(`[translator-sdk] Top-level API error:`, data[API_FIELD_ERROR]);
1296
+ return serviceTranslations.map((translation) => ({
1297
+ [API_FIELD_TEXT]: translation[API_FIELD_TEXT],
1298
+ [API_FIELD_ERROR]: data[API_FIELD_ERROR]
1299
+ }));
1300
+ }
1301
+ const results = Array.isArray(data[API_FIELD_TRANSLATIONS]) ? data[API_FIELD_TRANSLATIONS].map((translation, index) => {
1302
+ const originalText = serviceTranslations[index]?.[API_FIELD_TEXT] || "";
1303
+ if (translation[API_FIELD_ERROR]) {
1304
+ console.warn(
1305
+ `[translator-sdk] Translation error for "${originalText}":`,
1306
+ translation[API_FIELD_ERROR]
1307
+ );
1308
+ return {
1309
+ [API_FIELD_TEXT]: originalText,
1310
+ // Return original text on error
1311
+ [API_FIELD_ERROR]: translation[API_FIELD_ERROR]
1312
+ };
1313
+ }
1314
+ if (translation[API_FIELD_FROM] && !translation[API_FIELD_TEXT]) {
1315
+ return {
1316
+ [API_FIELD_TEXT]: originalText,
1317
+ // For detection, return original text
1318
+ [API_FIELD_FROM]: translation[API_FIELD_FROM]
1319
+ };
1320
+ }
1321
+ return {
1322
+ [API_FIELD_TEXT]: translation[API_FIELD_TEXT] || originalText
1323
+ };
1324
+ }) : data[API_FIELD_TEXT] ? [{ [API_FIELD_TEXT]: data[API_FIELD_TEXT] }] : [];
1325
+ return results;
1326
+ } catch (error) {
1327
+ translationCache.delete(cacheKey);
1328
+ if (error instanceof Error && error.name === "AbortError") {
1329
+ const message2 = `Translator request to ${translatorHost} timed out after 5 seconds.`;
1330
+ console.error(message2);
1331
+ throw new Error(message2);
1332
+ }
1333
+ const message = `Failed to connect to translator at ${translatorHost}. Is the service running?`;
1334
+ console.error(message);
1335
+ throw new Error(message);
1336
+ }
1337
+ })();
1338
+ translationCache.set(cacheKey, translationPromise);
1339
+ serviceResults = await translationPromise;
1340
+ }
1341
+ }
1342
+ const final = translations2.map((tr, idx) => {
1343
+ const from = (tr[API_FIELD_FROM] || "").toLowerCase();
1344
+ const to = (tr[API_FIELD_TO] || "").toLowerCase();
1345
+ if (from === to) return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1346
+ const svcIdx = indexMap.indexOf(idx);
1347
+ if (svcIdx !== -1) return serviceResults[svcIdx] || { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1348
+ return { [API_FIELD_TEXT]: tr[API_FIELD_TEXT] };
1349
+ });
1350
+ return final;
1351
+ }
1352
+
1353
+ // src/utils/localizeLanguageNames.ts
1354
+ var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1355
+ var NAME_CTX = "language name";
1356
+ async function translateNames(names, targetLang, options) {
1357
+ const requests = names.map((name) => ({
1358
+ [API_FIELD_TEXT]: name,
1359
+ [API_FIELD_FROM]: "en",
1360
+ [API_FIELD_TO]: targetLang,
1361
+ [API_FIELD_CTX]: NAME_CTX
1362
+ }));
1363
+ try {
1364
+ const results = await translateBatch(requests, options);
1365
+ const map = {};
1366
+ names.forEach((name, i) => {
1367
+ const translated = results[i]?.[API_FIELD_TEXT];
1368
+ if (translated) map[name] = translated;
1369
+ });
1370
+ return map;
1371
+ } catch (error) {
1372
+ devDebug("[localizeLanguageNames] translate failed:", error);
1373
+ return null;
1374
+ }
1375
+ }
1376
+ async function localizeLanguageNames(languages, targetLang, options = {}) {
1377
+ const to = (targetLang || "").toLowerCase();
1378
+ if (!to || to === "en" || languages.length === 0) return languages;
1379
+ const cacheKey = `langie_langnames_${to}`;
1380
+ let map = getCache(cacheKey);
1381
+ if (!map) {
1382
+ const names = [...new Set(languages.map((l) => l.name).filter(Boolean))];
1383
+ map = await translateNames(names, to, options);
1384
+ if (map && Object.keys(map).length) setCache(cacheKey, map, CACHE_TTL_MS);
1385
+ }
1386
+ if (!map) return languages;
1387
+ const resolved = map;
1388
+ return languages.map((l) => ({ ...l, search_alias: resolved[l.name] || l.search_alias }));
1389
+ }
1390
+
1194
1391
  // src/composables/useLangie-core.ts
1195
1392
  var availableLanguages = ref2([]);
1196
1393
  var translations = reactive({});
@@ -1220,8 +1417,7 @@ function useLangieCore(options = {}) {
1220
1417
  const cachedLanguages = getCache("langie_languages_cache");
1221
1418
  if (cachedLanguages && availableLanguages.value.length === 0) {
1222
1419
  availableLanguages.value = cachedLanguages;
1223
- _languagesCache = cachedLanguages;
1224
- devDebug("[useLangie] Loaded languages from cache:", cachedLanguages.length);
1420
+ devDebug("[useLangie] Seeded languages from cache (will revalidate):", cachedLanguages.length);
1225
1421
  } else if (cachedLanguages) {
1226
1422
  devDebug(
1227
1423
  "[useLangie] Cache exists but languages already loaded:",
@@ -1231,6 +1427,16 @@ function useLangieCore(options = {}) {
1231
1427
  devDebug("[useLangie] No cached languages found");
1232
1428
  }
1233
1429
  }
1430
+ const applyLocalizedNames = (lang) => {
1431
+ const base = availableLanguages.value;
1432
+ if (!base.length) return;
1433
+ localizeLanguageNames(base, lang, { translatorHost }).then((localized) => {
1434
+ if (localized !== base && availableLanguages.value === base) {
1435
+ availableLanguages.value = localized;
1436
+ }
1437
+ }).catch(() => {
1438
+ });
1439
+ };
1234
1440
  const setLanguage = (lang) => {
1235
1441
  if (lang && lang !== currentLanguage.value) {
1236
1442
  }
@@ -1238,6 +1444,7 @@ function useLangieCore(options = {}) {
1238
1444
  if (typeof window !== "undefined") {
1239
1445
  localStorage.setItem("interface_language", lang);
1240
1446
  }
1447
+ applyLocalizedNames(lang);
1241
1448
  };
1242
1449
  const fetchLanguages = async (opts = {}) => {
1243
1450
  const { force = false, country: explicitCountry } = opts;
@@ -1338,6 +1545,7 @@ function useLangieCore(options = {}) {
1338
1545
  }
1339
1546
  _languagesCache = filtered;
1340
1547
  _languagesPromise = null;
1548
+ applyLocalizedNames(currentLanguage.value);
1341
1549
  return filtered;
1342
1550
  } catch (error) {
1343
1551
  devDebug("[useLangie] Language fetch error:", { error });