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:
copilot-swe-agent[bot]
2026-03-13 21:31:16 +00:00
parent d27140f809
commit 3557590679
83 changed files with 358 additions and 49 deletions
+7
View File
@@ -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
View File
@@ -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)
# ---------------------------------------------------------------------------
+5
View File
@@ -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)
+5 -2
View File
@@ -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
+56 -15
View File
@@ -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 %}
<!-- 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="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 }}')"
: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">{{ 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 %}
<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>
{% endfor %}
</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>
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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
View File
@@ -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