Coverage for gws-app/gws/lib/intl/__init__.py: 0%
126 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Intl and localization tools."""
3import babel
4import babel.dates
5import babel.numbers
6import pycountry
8import gws
9import gws.lib.datetimex
11_DEFAULT_UID = 'en_CA' # English with metric units
14# NB in the following code, `name` is a locale or language name (`de` or `de_DE`), `uid` is strictly a uid (`de_DE`)
16def default_locale():
17 """Returns the default locale object (``en_CA``)."""
19 return locale(_DEFAULT_UID, fallback=False)
22def locale(name: str, allowed: list[str] = None, fallback: bool = True) -> gws.Locale:
23 """Locates a Locale object by locale name.
25 If the name is invalid, and ``fallback`` is ``True``, return the first ``allowed`` locale,
26 or the default locale. Otherwise, raise an exception.
28 Args:
29 name: Language or locale name like ``de`` or ``de_DE``.
30 allowed: A list of allowed locale uids.
31 fallback: Fall back to the default locale.
32 """
34 lo = _locale_by_name(name, allowed)
35 if lo:
36 return lo
38 if not fallback:
39 raise gws.Error(f'locale {name!r} not found')
41 if allowed:
42 lo = _locale_by_uid(allowed[0])
43 if lo:
44 return lo
46 return default_locale()
49def _locale_by_name(name, allowed):
50 if not name:
51 return
53 name = name.strip().replace('-', '_')
55 if '_' in name:
56 # name is a uid
57 if allowed and name not in allowed:
58 return
59 return _locale_by_uid(name)
61 # just a lang name, try to find an allowed locale for this lang
62 if allowed:
63 for uid in allowed:
64 if uid.startswith(name):
65 return _locale_by_uid(uid)
67 # try to get a generic locale
68 return _locale_by_uid(name + '_zz')
71def _locale_by_uid(uid):
72 def _get():
73 p = babel.Locale.parse(uid, resolve_likely_subtags=True)
75 lo = gws.Locale()
77 # @TODO script etc
78 lo.uid = p.language + '_' + p.territory
80 lo.language = p.language
81 lo.languageName = p.language_name
83 lg = pycountry.languages.get(alpha_2=lo.language)
84 if not lg:
85 raise ValueError(f'unknown language {lo.language}')
86 lo.language3 = getattr(lg, 'alpha_3', '')
87 lo.languageBib = getattr(lg, 'bibliographic', lo.language3)
88 lo.languageNameEn = getattr(lg, 'name', lo.languageName)
90 lo.territory = p.territory
91 lo.territoryName = p.territory_name
93 lo.dateFormatLong = str(p.date_formats['long'])
94 lo.dateFormatMedium = str(p.date_formats['medium'])
95 lo.dateFormatShort = str(p.date_formats['short'])
96 lo.dateUnits = (
97 p.unit_display_names['duration-year']['narrow'] +
98 p.unit_display_names['duration-month']['narrow'] +
99 p.unit_display_names['duration-day']['narrow'])
101 lo.dayNamesLong = list(p.days['format']['wide'].values())
102 lo.dayNamesNarrow = list(p.days['format']['narrow'].values())
103 lo.dayNamesShort = list(p.days['format']['abbreviated'].values())
105 lo.firstWeekDay = p.first_week_day
107 lo.monthNamesLong = list(p.months['format']['wide'].values())
108 lo.monthNamesNarrow = list(p.months['format']['narrow'].values())
109 lo.monthNamesShort = list(p.months['format']['abbreviated'].values())
111 lo.numberDecimal = p.number_symbols['latn']['decimal']
112 lo.numberGroup = p.number_symbols['latn']['group']
114 return lo
116 try:
117 return gws.u.get_app_global(f'gws.lib.intl.locale.{uid}', _get)
118 except (AttributeError, ValueError, babel.UnknownLocaleError):
119 gws.log.exception()
120 return None
123##
126class _FnStr:
127 """Allow a property to act both as a method and as a string."""
129 def __init__(self, method, arg):
130 self.method = method
131 self.arg = arg
133 def __str__(self):
134 return self.method(self.arg)
136 def __call__(self, a=None):
137 return self.method(self.arg, a)
140# @TODO support RFC 2822
142class DateFormatter(gws.DateFormatter):
143 def __init__(self, loc: gws.Locale):
144 self.locale = loc
145 self.short = _FnStr(self.format, gws.DateTimeFormat.short)
146 self.medium = _FnStr(self.format, gws.DateTimeFormat.medium)
147 self.long = _FnStr(self.format, gws.DateTimeFormat.long)
148 self.iso = _FnStr(self.format, gws.DateTimeFormat.iso)
150 def format(self, fmt: gws.DateTimeFormat, date=None):
151 date = date or gws.lib.datetimex.now()
152 d = gws.lib.datetimex.parse(date)
153 if not d:
154 raise gws.Error(f'invalid {date=}')
155 if fmt == gws.DateTimeFormat.iso:
156 return gws.lib.datetimex.to_iso_date_string(d)
157 return babel.dates.format_date(d, locale=self.locale.uid, format=str(fmt))
160class TimeFormatter(gws.TimeFormatter):
161 def __init__(self, loc: gws.Locale):
162 self.locale = loc
163 self.short = _FnStr(self.format, gws.DateTimeFormat.short)
164 self.medium = _FnStr(self.format, gws.DateTimeFormat.medium)
165 self.long = _FnStr(self.format, gws.DateTimeFormat.long)
166 self.iso = _FnStr(self.format, gws.DateTimeFormat.iso)
168 def format(self, fmt: gws.DateTimeFormat, date=None) -> str:
169 date = date or gws.lib.datetimex.now()
170 d = gws.lib.datetimex.parse(date) or gws.lib.datetimex.parse_time(date)
171 if not d:
172 raise gws.Error(f'invalid {date=}')
173 if fmt == gws.DateTimeFormat.iso:
174 return gws.lib.datetimex.to_iso_time_string(d)
175 return babel.dates.format_time(d, locale=self.locale.uid, format=str(fmt))
178# @TODO scientific, compact...
180class NumberFormatter(gws.NumberFormatter):
181 def __init__(self, loc: gws.Locale):
182 self.locale = loc
183 self.fns = {
184 gws.NumberFormat.decimal: self.decimal,
185 gws.NumberFormat.grouped: self.grouped,
186 gws.NumberFormat.currency: self.currency,
187 gws.NumberFormat.percent: self.percent,
188 }
190 def format(self, fmt, n, *args, **kwargs):
191 fn = self.fns.get(fmt)
192 if not fn:
193 return str(n)
194 return fn(n, *args, **kwargs)
196 def decimal(self, n, *args, **kwargs):
197 return babel.numbers.format_decimal(n, locale=self.locale.uid, group_separator=False, *args, **kwargs)
199 def grouped(self, n, *args, **kwargs):
200 return babel.numbers.format_decimal(n, locale=self.locale.uid, group_separator=True, *args, **kwargs)
202 def currency(self, n, currency, *args, **kwargs):
203 return babel.numbers.format_currency(n, currency=currency, locale=self.locale.uid, *args, **kwargs)
205 def percent(self, n, *args, **kwargs):
206 return babel.numbers.format_percent(n, locale=self.locale.uid, *args, **kwargs)
209##
212def formatters(loc: gws.Locale) -> tuple[DateFormatter, TimeFormatter, NumberFormatter]:
213 """Return a tuple of locale-aware formatters."""
215 def _get():
216 return (
217 DateFormatter(loc),
218 TimeFormatter(loc),
219 NumberFormatter(loc),
220 )
222 return gws.u.get_app_global(f'gws.lib.intl.formatters.{loc.uid}', _get)