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
« 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"""
6from collections.abc import Mapping
7from datetime import date, datetime, time
8from functools import lru_cache
9from pathlib import Path
10from zoneinfo import ZoneInfo
12import phonenumbers
13from google.protobuf.timestamp_pb2 import Timestamp
15from couchers.i18n.i18next import I18Next
16from couchers.i18n.locales import load_locales
17from couchers.utils import to_aware_datetime
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")
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.
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})
35 Returns:
36 The translated string with substitutions applied
37 """
38 return get_main_i18next().localize(key, lang or "en", substitutions)
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")
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)
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)")
58def localize_datetime(value: datetime | Timestamp, timezone: ZoneInfo | None, locale: str) -> str:
59 """
60 Formats a date and time for the given locale.
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.
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)
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."
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)
79 localized_date = localize_date(value.date(), locale)
80 localized_time = localize_time(value.time(), locale)
82 # TODO(#7590): Account for locale
83 return f"{localized_date} at {localized_time}"
86def localize_timezone(timezone: ZoneInfo, locale: str) -> str:
87 # TODO(#7590): Account for locale
88 return datetime.now(tz=timezone).strftime("%Z/UTC%z")
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)