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

1"""Locale and timezone resolution helpers for per-request i18n.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.admin.i18n.translator import _parse_accept_language 

8 

9_LOCALE_COOKIE = "admin_locale" 

10_LOCALE_HEADER = "accept-language" 

11 

12 

13def convert_to_timezone(dt: Any, timezone: str) -> Any: 

14 """Convert *dt* to the given IANA *timezone*. 

15 

16 Args: 

17 dt: A timezone-aware ``datetime.datetime`` instance. 

18 timezone: IANA timezone name (e.g. ``"Europe/Paris"``). 

19 

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 

26 

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 

33 

34 

35def get_user_timezone(request: Any, *, default: str = "UTC") -> str: 

36 """Resolve the user's preferred timezone from a request. 

37 

38 Resolution order: 

39 1. ``request.state.timezone`` (set by middleware or profile) 

40 2. Cookie named ``admin_timezone`` 

41 3. *default* (``"UTC"``) 

42 

43 Args: 

44 request: Starlette-compatible request object. 

45 default: Fallback timezone name. 

46 

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) 

53 

54 cookies = getattr(request, "cookies", {}) 

55 if isinstance(cookies, dict) and "admin_timezone" in cookies: 

56 return cookies["admin_timezone"] 

57 

58 return default 

59 

60 

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. 

68 

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* 

74 

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. 

79 

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) 

87 

88 # 2. Cookie 

89 cookies = getattr(request, "cookies", {}) 

90 if isinstance(cookies, dict) and cookie_name in cookies: 

91 return cookies[cookie_name] 

92 

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] 

104 

105 return default 

106 

107 

108__all__ = [ 

109 "convert_to_timezone", 

110 "get_locale", 

111 "get_user_timezone", 

112]