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

1import json 

2from pathlib import Path 

3 

4from couchers.i18n.i18next import I18Next 

5from couchers.i18n.plurals import PluralRules 

6 

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" 

10 

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} 

21 

22 

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] 

30 

31 

32def load_locales(directory: Path) -> I18Next: 

33 """Load all translation files from a locales directory and apply fallbacks.""" 

34 

35 i18next = I18Next() 

36 

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" 

40 

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

42 translations = json.load(f) 

43 

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) 

47 

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 

53 

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

58 

59 return i18next