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