Coverage for src / couchers / i18n / i18n.py: 95%
32 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-07 16:21 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-07 16:21 +0000
1import json
2from collections.abc import Mapping
3from functools import lru_cache
4from pathlib import Path
6from couchers.i18n.constants import LANGUAGE_FALLBACKS
7from couchers.i18n.i18next import I18Next
8from couchers.i18n.plurals import PluralRules
11@lru_cache(maxsize=1)
12def get_i18next() -> I18Next:
13 """
14 Load all translation files from the locales directory and apply fallbacks.
16 Returns:
17 An I18Next object that can localize the application strings.
19 The result is cached so that translations are only loaded once per process.
20 """
22 i18next = I18Next()
24 # Load all locale JSON files from the locales directory
25 locales_dir = Path(__file__).parent / "locales"
26 for locale_file in locales_dir.glob("*.json"):
27 lang_code = locale_file.stem # e.g., "en" from "en.json"
29 with open(locale_file, "r", encoding="utf-8") as f:
30 translations = json.load(f)
32 plural_rule = PluralRules.for_language(lang_code) or PluralRules.en
33 language = i18next.add_language(lang_code, plural_rule)
34 language.load_json_dict(translations)
36 # English is our default for undefined languages
37 en = i18next.languages_by_code.get("en")
38 if en is None: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true
39 raise RuntimeError("English translations must be loaded")
40 i18next.default_language = en
42 # Apply fallbacks
43 for language in i18next.languages_by_code.values():
44 if language == en:
45 continue # English has no fallback
47 fallback_language = en
48 if fallback_code := LANGUAGE_FALLBACKS.get(language.code):
49 fallback_language = i18next.languages_by_code[fallback_code]
50 language.fallback = fallback_language
52 return i18next
55def localize_string(lang: str | None, key: str, *, substitutions: Mapping[str, str | int] | None = None) -> str:
56 """
57 Retrieves a translated string and performs substitutions.
59 Args:
60 lang: Language code (e.g., "en", "pt-BR"). If None, defaults to the default fallback language ("en")
61 key: The key for the string to be looked up.
62 substitutions: Dictionary of variable substitutions for the string (e.g., {"hours": 24})
64 Returns:
65 The translated string with substitutions applied
66 """
67 return get_i18next().localize(key, lang or "en", substitutions)