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

1import json 

2from collections.abc import Mapping 

3from functools import lru_cache 

4from pathlib import Path 

5 

6from couchers.i18n.constants import LANGUAGE_FALLBACKS 

7from couchers.i18n.i18next import I18Next 

8from couchers.i18n.plurals import PluralRules 

9 

10 

11@lru_cache(maxsize=1) 

12def get_i18next() -> I18Next: 

13 """ 

14 Load all translation files from the locales directory and apply fallbacks. 

15 

16 Returns: 

17 An I18Next object that can localize the application strings. 

18 

19 The result is cached so that translations are only loaded once per process. 

20 """ 

21 

22 i18next = I18Next() 

23 

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" 

28 

29 with open(locale_file, "r", encoding="utf-8") as f: 

30 translations = json.load(f) 

31 

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) 

35 

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 

41 

42 # Apply fallbacks 

43 for language in i18next.languages_by_code.values(): 

44 if language == en: 

45 continue # English has no fallback 

46 

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 

51 

52 return i18next 

53 

54 

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. 

58 

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}) 

63 

64 Returns: 

65 The translated string with substitutions applied 

66 """ 

67 return get_i18next().localize(key, lang or "en", substitutions)