Coverage for app / backend / src / couchers / i18n / locales.py: 95%
27 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
1import json
2from pathlib import Path
4from couchers.i18n.i18next import I18Next
6# The default locale if a language or string is unavailable.
7# Note: "en" is a valid locale even if it doesn't include a region.
8DEFAULT_LOCALE = "en"
10# Locale fallbacks (for those that don't fallback to English).
11# If a string is not found in the requested language, we try the provided one before English
12# Some mutually intelligible language variants fallback to each other.
13_LOCALE_FALLBACKS: dict[str, str] = {
14 "pt-BR": "pt",
15 "pt": "pt-BR",
16 "es-419": "es",
17 "es": "es-419",
18 "fr-CA": "fr",
19}
22def get_locale_fallbacks(locale: str) -> list[str]:
23 """Gets the list of locales to which to fallback to if the given one is unavailable."""
24 if fallback := _LOCALE_FALLBACKS.get(locale):
25 return [fallback, DEFAULT_LOCALE]
26 if locale == DEFAULT_LOCALE:
27 return []
28 return [DEFAULT_LOCALE]
31def load_locales(directory: Path) -> I18Next:
32 """Load all translation files from a locales directory and apply fallbacks."""
34 i18next = I18Next()
36 # Load all locale JSON files from the locales directory
37 for locale_file in directory.glob("*.json"):
38 locale = locale_file.stem # e.g., "en" from "en.json"
40 with open(locale_file, "r", encoding="utf-8") as f:
41 translations = json.load(f)
43 translation = i18next.add_translation(locale)
44 translation.load_json_dict(translations)
46 # English is our default for undefined languages
47 default_translation = i18next.translations_by_locale.get(DEFAULT_LOCALE)
48 if default_translation is None: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 raise RuntimeError("English translations must be loaded")
50 i18next.default_translation = default_translation
52 # Apply fallbacks
53 for translation in i18next.translations_by_locale.values():
54 for fallback_locale in get_locale_fallbacks(translation.locale):
55 translation.fallbacks.append(i18next.translations_by_locale[fallback_locale])
57 return i18next