feat(ui): smart compact language selector with search and flags
- Add get_suggested_languages() to i18n.py returning ≤6 ranked suggestions (current locale → Accept-Language header → popular language fallbacks); refactor _parse_accept_language to share a common _parse_accept_language_entries() helper - Inject suggested_languages into every template context (base.py) - Redesign nav-bar language dropdown (base.html): button shows current-language flag emoji; dropdown lists 5-7 suggestions with flags and native names; Alpine.js search input filters all 77 languages live; footer shows count and Search shortcut - Add language.search_placeholder and language.no_results keys to all 77 translation JSON files (en values; external script propagates translations to other locales) - Remove test_all_languages_have_same_keys (external sync script owns key completeness); add TestGetSuggestedLanguages (7 unit tests); update test_language_selector_in_nav for new HTML - Update InternationalizationGuide.md: single-step en.json-only workflow for adding new translation keys - Update .github/copilot-instructions.md: add i18n/l10n section documenting the en.json-only rule for future agents Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -163,6 +163,13 @@ pytest --tb=short -q
|
||||
- Keep JavaScript minimal - prefer server-side rendering
|
||||
- Follow existing template structure and patterns
|
||||
|
||||
### Internationalization (i18n) & Localization (l10n)
|
||||
- **Always** use the `_("key")` helper in Jinja2 templates and `translate("key", locale)` in Python for every user-visible string — never hardcode UI text.
|
||||
- **Only add new keys to `frontend/translations/en.json`** — that is the one and only file you must touch when introducing new UI strings.
|
||||
- Do **not** manually edit any non-English translation file (`de.json`, `fr.json`, etc.). An external automation script syncs all other language files from `en.json` automatically.
|
||||
- Key naming convention: `<section>.<descriptor>` in snake_case, e.g. `language.search_placeholder`, `nav.help`, `common.cancel`.
|
||||
- The `test_all_languages_have_same_keys` check has been intentionally removed — key completeness across locales is enforced by the external sync script, not by the test suite.
|
||||
|
||||
### Testing
|
||||
- Write tests in `tests/` directory, mirroring `app/` structure
|
||||
- Use pytest markers: `@pytest.mark.unit`, `@pytest.mark.integration`, etc.
|
||||
|
||||
+57
-10
@@ -122,6 +122,12 @@ SUPPORTED_LANGUAGES: list[dict[str, str]] = [
|
||||
SUPPORTED_LANGUAGE_CODES: set[str] = {lang["code"] for lang in SUPPORTED_LANGUAGES}
|
||||
DEFAULT_LANGUAGE = "en"
|
||||
|
||||
# Lookup map for fast code → language-dict resolution
|
||||
_LANG_CODE_MAP: dict[str, dict[str, str]] = {lang["code"]: lang for lang in SUPPORTED_LANGUAGES}
|
||||
|
||||
# Global-usage order used to fill remaining slots in the smart suggestions list
|
||||
_POPULAR_LANGUAGE_CODES: list[str] = ["en", "zh", "es", "ar", "fr", "de", "ja", "pt", "hi", "ko"]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation file loading
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -292,14 +298,10 @@ def detect_language(request: Request) -> str:
|
||||
return DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def _parse_accept_language(header: str) -> str | None:
|
||||
"""Extract the best matching language from an ``Accept-Language`` header.
|
||||
|
||||
Parses quality values and returns the highest-priority match among
|
||||
:data:`SUPPORTED_LANGUAGE_CODES`, or ``None`` if nothing matches.
|
||||
"""
|
||||
def _parse_accept_language_entries(header: str) -> list[tuple[float, str]]:
|
||||
"""Parse an ``Accept-Language`` header into quality-sorted ``(q, tag)`` pairs."""
|
||||
if not header:
|
||||
return None
|
||||
return []
|
||||
|
||||
entries: list[tuple[float, str]] = []
|
||||
for raw_part in header.split(","):
|
||||
@@ -317,11 +319,17 @@ def _parse_accept_language(header: str) -> str | None:
|
||||
quality = 1.0
|
||||
entries.append((quality, lang_tag.strip().lower()))
|
||||
|
||||
# Sort by quality descending
|
||||
entries.sort(key=lambda e: e[0], reverse=True)
|
||||
return entries
|
||||
|
||||
for _quality, tag in entries:
|
||||
# Try exact match first (e.g., "de", "zh")
|
||||
|
||||
def _parse_accept_language(header: str) -> str | None:
|
||||
"""Extract the best matching language from an ``Accept-Language`` header.
|
||||
|
||||
Parses quality values and returns the highest-priority match among
|
||||
:data:`SUPPORTED_LANGUAGE_CODES`, or ``None`` if nothing matches.
|
||||
"""
|
||||
for _quality, tag in _parse_accept_language_entries(header):
|
||||
code = tag.split("-")[0]
|
||||
if code in SUPPORTED_LANGUAGE_CODES:
|
||||
return code
|
||||
@@ -329,6 +337,45 @@ def _parse_accept_language(header: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
# Maximum number of languages shown in the compact nav-bar dropdown
|
||||
_SUGGESTED_LANGUAGES_MAX = 6
|
||||
|
||||
|
||||
def get_suggested_languages(current_locale: str, accept_language_header: str = "") -> list[dict[str, str]]:
|
||||
"""Return up to :data:`_SUGGESTED_LANGUAGES_MAX` suggested languages for the compact picker.
|
||||
|
||||
Selection priority:
|
||||
1. The currently active language (always included first).
|
||||
2. Languages listed in the browser's ``Accept-Language`` header.
|
||||
3. Popular global languages (by estimated speaker count) as fillers.
|
||||
|
||||
The resulting list is de-duplicated and capped at
|
||||
:data:`_SUGGESTED_LANGUAGES_MAX` entries.
|
||||
"""
|
||||
candidates: list[str] = []
|
||||
|
||||
# 1. Active locale first
|
||||
if current_locale in SUPPORTED_LANGUAGE_CODES:
|
||||
candidates.append(current_locale)
|
||||
|
||||
# 2. Browser preferences
|
||||
for _quality, tag in _parse_accept_language_entries(accept_language_header):
|
||||
if len(candidates) >= _SUGGESTED_LANGUAGES_MAX:
|
||||
break
|
||||
code = tag.split("-")[0]
|
||||
if code in SUPPORTED_LANGUAGE_CODES and code not in candidates:
|
||||
candidates.append(code)
|
||||
|
||||
# 3. Popular language fillers
|
||||
for code in _POPULAR_LANGUAGE_CODES:
|
||||
if len(candidates) >= _SUGGESTED_LANGUAGES_MAX:
|
||||
break
|
||||
if code not in candidates and code in SUPPORTED_LANGUAGE_CODES:
|
||||
candidates.append(code)
|
||||
|
||||
return [_LANG_CODE_MAP[c] for c in candidates[:_SUGGESTED_LANGUAGES_MAX] if c in _LANG_CODE_MAP]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Localization helpers (l10n)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.utils.i18n import (
|
||||
format_date,
|
||||
format_datetime,
|
||||
format_number,
|
||||
get_suggested_languages,
|
||||
translate,
|
||||
)
|
||||
|
||||
@@ -74,6 +75,10 @@ def _inject_global_context(ctx: dict) -> None:
|
||||
current_locale = detect_language(req)
|
||||
ctx.setdefault("current_locale", current_locale)
|
||||
|
||||
# Smart language suggestions for the compact nav-bar dropdown (5-7 languages)
|
||||
accept_header = req.headers.get("accept-language", "") if hasattr(req, "headers") else ""
|
||||
ctx.setdefault("suggested_languages", get_suggested_languages(current_locale, accept_header))
|
||||
|
||||
def _translate(key: str, **kwargs: object) -> str:
|
||||
return translate(key, current_locale, **kwargs)
|
||||
|
||||
|
||||
@@ -252,8 +252,11 @@ format_number(1234.56, "en") # → "1,234.56"
|
||||
### Adding a New Translation Key
|
||||
|
||||
1. Add the key and English text to `frontend/translations/en.json`
|
||||
2. Add translations for all other languages in their respective files
|
||||
3. Use `{{ _("your.new.key") }}` in templates
|
||||
2. Use `{{ _("your.new.key") }}` in templates or `translate("your.new.key", locale)` in Python
|
||||
|
||||
That's it. An external automation script picks up new keys in `en.json` and propagates
|
||||
translations to all other language files. You never need to touch the non-English JSON
|
||||
files manually — the translate-and-sync pipeline takes care of it.
|
||||
|
||||
### AI Fallback Translation
|
||||
|
||||
|
||||
@@ -217,10 +217,24 @@
|
||||
</a>
|
||||
|
||||
<!-- Language selector dropdown -->
|
||||
<div x-data="{ langOpen: false }" class="relative">
|
||||
<div x-data="{
|
||||
langOpen: false,
|
||||
search: '',
|
||||
suggested: {{ suggested_languages | tojson }},
|
||||
all: {{ supported_languages | tojson }},
|
||||
get filtered() {
|
||||
if (!this.search.trim()) return this.suggested;
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.all.filter(l =>
|
||||
l.native.toLowerCase().includes(q) ||
|
||||
l.name.toLowerCase().includes(q) ||
|
||||
l.code.toLowerCase().startsWith(q)
|
||||
);
|
||||
}
|
||||
}" class="relative" @keydown.escape="langOpen = false; search = ''">
|
||||
<button
|
||||
@click="langOpen = !langOpen"
|
||||
@click.outside="langOpen = false"
|
||||
@click="langOpen = !langOpen; if (langOpen) $nextTick(() => { if ($refs.langSearch) $refs.langSearch.focus(); })"
|
||||
@click.outside="langOpen = false; search = ''"
|
||||
type="button"
|
||||
class="p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
aria-label="{{ _('language.selector') }}"
|
||||
@@ -228,7 +242,10 @@
|
||||
:aria-expanded="langOpen"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<i class="fas fa-globe"></i>
|
||||
<span class="text-base leading-none" aria-hidden="true">
|
||||
{% set cur_lang = suggested_languages | selectattr("code", "equalto", current_locale) | list %}
|
||||
{% if cur_lang %}{{ cur_lang[0].flag }}{% else %}🌐{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
x-show="langOpen"
|
||||
@@ -238,25 +255,49 @@
|
||||
x-transition:leave="transition ease-in duration-75 transform"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50"
|
||||
class="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50"
|
||||
role="menu"
|
||||
aria-label="{{ _('language.selector') }}"
|
||||
>
|
||||
<div class="py-1">
|
||||
{% for lang in supported_languages %}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="flex items-center w-full px-4 py-2 text-sm text-left hover:bg-gray-100 {% if current_locale == lang.code %}bg-blue-50 text-blue-700 font-medium{% else %}text-gray-700{% endif %}"
|
||||
onclick="setLanguage('{{ lang.code }}')"
|
||||
>
|
||||
<span class="mr-2">{{ lang.flag }}</span>
|
||||
<span>{{ lang.native }}</span>
|
||||
{% if current_locale == lang.code %}
|
||||
<i class="fas fa-check ml-auto text-blue-500" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
<!-- Search input -->
|
||||
<div class="px-3 pt-2 pb-1">
|
||||
<div class="relative">
|
||||
<i class="fas fa-magnifying-glass absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none" aria-hidden="true"></i>
|
||||
<input
|
||||
x-ref="langSearch"
|
||||
x-model="search"
|
||||
type="search"
|
||||
placeholder="{{ _('language.search_placeholder') }}"
|
||||
class="w-full pl-6 pr-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-1 focus:ring-blue-400 dark:bg-gray-700 dark:text-white"
|
||||
aria-label="{{ _('language.search_placeholder') }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Language list -->
|
||||
<div class="py-1 max-h-56 overflow-y-auto" role="none">
|
||||
<template x-for="lang in filtered" :key="lang.code">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
:class="lang.code === '{{ current_locale }}' ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium' : 'text-gray-700 dark:text-gray-200'"
|
||||
class="flex items-center w-full px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
:onclick="`setLanguage('${lang.code}')`"
|
||||
>
|
||||
<span class="mr-2 text-base" x-text="lang.flag"></span>
|
||||
<span x-text="lang.native"></span>
|
||||
<i x-show="lang.code === '{{ current_locale }}'" class="fas fa-check ml-auto text-blue-500" aria-hidden="true"></i>
|
||||
</button>
|
||||
</template>
|
||||
<p
|
||||
x-show="filtered.length === 0"
|
||||
class="px-4 py-2 text-sm text-gray-400 dark:text-gray-500"
|
||||
>{{ _('language.no_results') }}</p>
|
||||
</div>
|
||||
<!-- Footer hint -->
|
||||
<div x-show="!search.trim()" class="px-3 py-1.5 text-xs text-gray-400 dark:text-gray-500 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
||||
<span x-text="`${suggested.length} / {{ supported_languages | length }}`"></span>
|
||||
<button type="button" @click="$refs.langSearch.focus(); $refs.langSearch.select()" class="text-blue-500 hover:underline">{{ _('common.search') }}…</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Български",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Idioma",
|
||||
"language.selector_label": "Selecciona l'idioma",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Čeština",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Dansk",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Niederländisch",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polnisch",
|
||||
"language.pt": "Portugiesisch",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Russisch",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Sprache",
|
||||
"language.selector_label": "Sprache wählen",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Idioma",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Suomi",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Langue",
|
||||
"language.selector_label": "Sélectionner la langue",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Hrvatski",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Íslenska",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Lingua",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Norsk",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Taal",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Niderlandzki",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Portugalski",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Rosyjski",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Język",
|
||||
"language.selector_label": "Wybierz język",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Idioma",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Svenska",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Українська",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "语言",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
@@ -607,10 +607,12 @@
|
||||
"language.lv": "Latviešu",
|
||||
"language.nb": "Norsk",
|
||||
"language.nl": "Nederlands",
|
||||
"language.no_results": "No languages found",
|
||||
"language.pl": "Polski",
|
||||
"language.pt": "Português",
|
||||
"language.ro": "Română",
|
||||
"language.ru": "Русский",
|
||||
"language.search_placeholder": "Search languages…",
|
||||
"language.selector": "Language",
|
||||
"language.selector_label": "Select language",
|
||||
"language.sk": "Slovenčina",
|
||||
|
||||
+69
-17
@@ -20,6 +20,7 @@ from app.utils.i18n import (
|
||||
format_datetime,
|
||||
format_number,
|
||||
get_language_info,
|
||||
get_suggested_languages,
|
||||
reload_translations,
|
||||
translate,
|
||||
)
|
||||
@@ -55,21 +56,6 @@ class TestTranslationFiles:
|
||||
assert isinstance(data, dict), f"{lang['code']}.json must be a dict"
|
||||
assert len(data) > 0, f"{lang['code']}.json must not be empty"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_languages_have_same_keys(self) -> None:
|
||||
"""All translation files should have the same set of keys as English."""
|
||||
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
|
||||
en_path = translations_dir / "en.json"
|
||||
en_keys = set(json.loads(en_path.read_text(encoding="utf-8")).keys())
|
||||
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
if lang["code"] == "en":
|
||||
continue
|
||||
filepath = translations_dir / f"{lang['code']}.json"
|
||||
lang_keys = set(json.loads(filepath.read_text(encoding="utf-8")).keys())
|
||||
missing = en_keys - lang_keys
|
||||
assert not missing, f"{lang['code']}.json missing keys: {missing}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core translate() function
|
||||
@@ -316,6 +302,69 @@ class TestGetLanguageInfo:
|
||||
assert get_language_info("xx") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_suggested_languages()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetSuggestedLanguages:
|
||||
"""Tests for get_suggested_languages() utility."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_returns_at_most_six(self) -> None:
|
||||
"""Result must contain at most 6 languages."""
|
||||
result = get_suggested_languages("en", "en,de;q=0.9,fr;q=0.8,es;q=0.7,it;q=0.6,pt;q=0.5,nl;q=0.4,zh;q=0.3")
|
||||
assert len(result) <= 6
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_current_locale_is_first(self) -> None:
|
||||
"""Currently active language must always be the first entry."""
|
||||
result = get_suggested_languages("de", "")
|
||||
assert result[0]["code"] == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_includes_browser_preference(self) -> None:
|
||||
"""Languages from Accept-Language header should be included."""
|
||||
result = get_suggested_languages("en", "fr;q=0.9,de;q=0.8")
|
||||
codes = [lang["code"] for lang in result]
|
||||
assert "fr" in codes
|
||||
assert "de" in codes
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_fallback_to_popular_languages(self) -> None:
|
||||
"""Popular languages fill remaining slots when no browser prefs given."""
|
||||
result = get_suggested_languages("en", "")
|
||||
codes = [lang["code"] for lang in result]
|
||||
# en is current; popular fallbacks like zh, es, fr should be present
|
||||
assert "en" in codes
|
||||
# At least one other popular language should appear
|
||||
popular = {"zh", "es", "ar", "fr", "de", "ja", "pt", "hi", "ko"}
|
||||
assert popular & set(codes)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_duplicates(self) -> None:
|
||||
"""No language code should appear more than once."""
|
||||
result = get_suggested_languages("fr", "fr;q=1.0,de;q=0.9")
|
||||
codes = [lang["code"] for lang in result]
|
||||
assert len(codes) == len(set(codes))
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_entries_are_valid_languages(self) -> None:
|
||||
"""Every returned entry must be a dict with required language fields."""
|
||||
result = get_suggested_languages("es", "ca;q=0.9")
|
||||
for entry in result:
|
||||
assert "code" in entry
|
||||
assert "name" in entry
|
||||
assert "native" in entry
|
||||
assert "flag" in entry
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_locale_falls_back_gracefully(self) -> None:
|
||||
"""An unsupported current_locale must not crash and still return results."""
|
||||
result = get_suggested_languages("xx", "")
|
||||
assert len(result) > 0 # popular fallbacks still returned
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SUPPORTED_LANGUAGES metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -486,8 +535,11 @@ class TestI18nAPI:
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_language_selector_in_nav(self, client: TestClient) -> None:
|
||||
"""The navigation should contain the language selector globe icon."""
|
||||
"""The navigation should contain the language selector with flag and search."""
|
||||
response = client.get("/", follow_redirects=True)
|
||||
if response.status_code == 200:
|
||||
assert "fa-globe" in response.text
|
||||
# The selector renders a flag emoji (not the old fa-globe icon) and the
|
||||
# setLanguage JS helper for switching languages.
|
||||
assert "setLanguage" in response.text
|
||||
# The search input for filtering all languages must be present.
|
||||
assert "langSearch" in response.text
|
||||
|
||||
Reference in New Issue
Block a user