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

1import json 

2from pathlib import Path 

3 

4from couchers.i18n.i18next import I18Next 

5 

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" 

9 

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} 

20 

21 

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] 

29 

30 

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

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

33 

34 i18next = I18Next() 

35 

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" 

39 

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

41 translations = json.load(f) 

42 

43 translation = i18next.add_translation(locale) 

44 translation.load_json_dict(translations) 

45 

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 

51 

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

56 

57 return i18next