Coverage for src / lexigram / admin / i18n / locale.py: 0%
37 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 02:25 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 02:25 +0800
1"""Locale and timezone resolution helpers for per-request i18n."""
3from __future__ import annotations
5from typing import Any
7from lexigram.admin.i18n.translator import _parse_accept_language
9_LOCALE_COOKIE = "admin_locale"
10_LOCALE_HEADER = "accept-language"
13def convert_to_timezone(dt: Any, timezone: str) -> Any:
14 """Convert *dt* to the given IANA *timezone*.
16 Args:
17 dt: A timezone-aware ``datetime.datetime`` instance.
18 timezone: IANA timezone name (e.g. ``"Europe/Paris"``).
20 Returns:
21 The datetime shifted into *timezone*, or *dt* unchanged if conversion
22 fails.
23 """
24 from datetime import datetime as _datetime
25 from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
27 if not isinstance(dt, _datetime):
28 return dt
29 try:
30 return dt.astimezone(ZoneInfo(timezone))
31 except (ZoneInfoNotFoundError, ValueError, AttributeError):
32 return dt
35def get_user_timezone(request: Any, *, default: str = "UTC") -> str:
36 """Resolve the user's preferred timezone from a request.
38 Resolution order:
39 1. ``request.state.timezone`` (set by middleware or profile)
40 2. Cookie named ``admin_timezone``
41 3. *default* (``"UTC"``)
43 Args:
44 request: Starlette-compatible request object.
45 default: Fallback timezone name.
47 Returns:
48 IANA timezone string.
49 """
50 state_tz = getattr(getattr(request, "state", None), "timezone", None)
51 if state_tz:
52 return str(state_tz)
54 cookies = getattr(request, "cookies", {})
55 if isinstance(cookies, dict) and "admin_timezone" in cookies:
56 return cookies["admin_timezone"]
58 return default
61def get_locale(
62 request: Any,
63 *,
64 default: str = "en",
65 cookie_name: str = _LOCALE_COOKIE,
66) -> str:
67 """Resolve the best locale for a request.
69 Resolution order:
70 1. ``request.state.locale`` (set by middleware or login flow)
71 2. Cookie named *cookie_name*
72 3. ``Accept-Language`` header (first match)
73 4. *default*
75 Args:
76 request: Starlette-compatible request object.
77 default: Fallback locale when nothing else is found.
78 cookie_name: Name of the locale cookie.
80 Returns:
81 BCP 47 locale tag.
82 """
83 # 1. Explicit state override
84 state_locale = getattr(getattr(request, "state", None), "locale", None)
85 if state_locale:
86 return str(state_locale)
88 # 2. Cookie
89 cookies = getattr(request, "cookies", {})
90 if isinstance(cookies, dict) and cookie_name in cookies:
91 return cookies[cookie_name]
93 # 3. Accept-Language header
94 headers = getattr(request, "headers", {})
95 accept = (
96 headers.get(_LOCALE_HEADER, "")
97 if isinstance(headers, dict)
98 else getattr(headers, "get", lambda _k, d="": d)(_LOCALE_HEADER, "")
99 )
100 if accept:
101 locales = _parse_accept_language(accept)
102 if locales:
103 return locales[0]
105 return default
108__all__ = [
109 "convert_to_timezone",
110 "get_locale",
111 "get_user_timezone",
112]