Coverage for app / backend / src / couchers / i18n / localize.py: 95%

34 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-03 06:18 +0000

1""" 

2Defines low-level localization functions for strings, dates, etc. 

3Most code should use the higher-level couchers.i18n.LocalizationContext object. 

4""" 

5 

6from collections.abc import Mapping 

7from datetime import date, datetime, time 

8from functools import lru_cache 

9from pathlib import Path 

10from zoneinfo import ZoneInfo 

11 

12import phonenumbers 

13from google.protobuf.timestamp_pb2 import Timestamp 

14 

15from couchers.i18n.i18next import I18Next 

16from couchers.i18n.locales import load_locales 

17from couchers.utils import to_aware_datetime 

18 

19 

20@lru_cache(maxsize=1) 

21def get_main_i18next() -> I18Next: 

22 """Gets the I18Next instance for the main locales files.""" 

23 return load_locales(Path(__file__).parent / "locales") 

24 

25 

26def localize_string(lang: str | None, key: str, *, substitutions: Mapping[str, str | int] | None = None) -> str: 

27 """ 

28 Retrieves a translated string and performs substitutions. 

29 

30 Args: 

31 lang: Language code (e.g., "en", "pt-BR"). If None, defaults to the default fallback language ("en") 

32 key: The key for the string to be looked up. 

33 substitutions: Dictionary of variable substitutions for the string (e.g., {"hours": 24}) 

34 

35 Returns: 

36 The translated string with substitutions applied 

37 """ 

38 return get_main_i18next().localize(key, lang or "en", substitutions) 

39 

40 

41def localize_date(value: date, locale: str) -> str: 

42 """Formats a time- and timezone-agnostic date for the given locale.""" 

43 # TODO(#7590): Account for locale 

44 return value.strftime("%A %-d %B %Y") 

45 

46 

47def localize_date_from_iso(value: str, locale: str) -> str: 

48 """Formats a date in ISO YYYY-MM-DD format for the given locale.""" 

49 return localize_date(date.fromisoformat(value), locale) 

50 

51 

52def localize_time(value: time, locale: str) -> str: 

53 """Formats a date- and timezone-agnostic time for the given locale.""" 

54 # TODO(#7590): Account for locale 

55 return value.strftime("%-I:%M %p (%H:%M)") 

56 

57 

58def localize_datetime(value: datetime | Timestamp, timezone: ZoneInfo | None, locale: str) -> str: 

59 """ 

60 Formats a date and time for the given locale. 

61 

62 Args: 

63 datetime: The datetime or timestamp to be formatted. 

64 timezone: An optional timezone in which to interpret the date. If None, uses datetime's timezone. 

65 locale: The locale for which to format the date. 

66 

67 Returns: 

68 The localized date and time string. 

69 """ 

70 if isinstance(value, Timestamp): 70 ↛ 74line 70 didn't jump to line 74 because the condition on line 70 was always true

71 value = to_aware_datetime(value) 

72 

73 # A timezone-unaware datetime is almost certainly a bug, so we don't support it. 

74 assert value.tzinfo is not None, "Cannot localize a timezone-unaware datetime." 

75 

76 if timezone is not None: 76 ↛ 79line 76 didn't jump to line 79 because the condition on line 76 was always true

77 value = value.astimezone(timezone) 

78 

79 localized_date = localize_date(value.date(), locale) 

80 localized_time = localize_time(value.time(), locale) 

81 

82 # TODO(#7590): Account for locale 

83 return f"{localized_date} at {localized_time}" 

84 

85 

86def localize_timezone(timezone: ZoneInfo, locale: str) -> str: 

87 # TODO(#7590): Account for locale 

88 return datetime.now(tz=timezone).strftime("%Z/UTC%z") 

89 

90 

91def format_phone_number(value: str) -> str: 

92 """Formats a phone number from E.164 format to the international format.""" 

93 return phonenumbers.format_number(phonenumbers.parse(value), phonenumbers.PhoneNumberFormat.INTERNATIONAL)