1from collections import OrderedDict
2from copy import deepcopy
3from importlib import import_module
4from itertools import zip_longest
5
6import regex as re
7
8from ..data import language_locale_dict, language_order
9from .locale import Locale
10
11LOCALE_SPLIT_PATTERN = re.compile(r"-(?=[A-Z0-9]+$)")
12
13
14def _isvalidlocale(locale):
15 language = LOCALE_SPLIT_PATTERN.split(locale)[0]
16 if language not in language_order:
17 return False
18 else:
19 locales_list = language_locale_dict[language]
20 if locale == language or locale in locales_list:
21 return True
22 else:
23 return False
24
25
26def _filter_valid_locales(locales):
27 return [locale for locale in locales if _isvalidlocale(locale)]
28
29
30def _construct_locales(languages, region):
31 if region:
32 possible_locales = [language + "-" + region for language in languages]
33 locales = _filter_valid_locales(possible_locales)
34 else:
35 locales = languages
36 return locales
37
38
39class LocaleDataLoader:
40 """Class that handles loading of locale instances."""
41
42 _loaded_languages = {}
43 _loaded_locales = {}
44
45 def get_locale_map(
46 self,
47 languages=None,
48 locales=None,
49 region=None,
50 use_given_order=False,
51 allow_conflicting_locales=False,
52 ):
53 """
54 Get an ordered mapping with locale codes as keys
55 and corresponding locale instances as values.
56
57 :param languages:
58 A list of language codes, e.g. ['en', 'es', 'zh-Hant'].
59 If locales are not given, languages and region are
60 used to construct locales to load.
61 :type languages: list
62
63 :param locales:
64 A list of codes of locales which are to be loaded,
65 e.g. ['fr-PF', 'qu-EC', 'af-NA']
66 :type locales: list
67
68 :param region:
69 A region code, e.g. 'IN', '001', 'NE'.
70 If locales are not given, languages and region are
71 used to construct locales to load.
72 :type region: str
73
74 :param use_given_order:
75 If True, the returned mapping is ordered in the order locales are given.
76 :type use_given_order: bool
77
78 :param allow_conflicting_locales:
79 if True, locales with same language and different region can be loaded.
80 :type allow_conflicting_locales: bool
81
82 :return: ordered locale code to locale instance mapping
83 """
84 return OrderedDict(
85 self._load_data(
86 languages=languages,
87 locales=locales,
88 region=region,
89 use_given_order=use_given_order,
90 allow_conflicting_locales=allow_conflicting_locales,
91 )
92 )
93
94 def get_locales(
95 self,
96 languages=None,
97 locales=None,
98 region=None,
99 use_given_order=False,
100 allow_conflicting_locales=False,
101 ):
102 """
103 Yield locale instances.
104
105 :param languages:
106 A list of language codes, e.g. ['en', 'es', 'zh-Hant'].
107 If locales are not given, languages and region are
108 used to construct locales to load.
109 :type languages: list
110
111 :param locales:
112 A list of codes of locales which are to be loaded,
113 e.g. ['fr-PF', 'qu-EC', 'af-NA']
114 :type locales: list
115
116 :param region:
117 A region code, e.g. 'IN', '001', 'NE'.
118 If locales are not given, languages and region are
119 used to construct locales to load.
120 :type region: str
121
122 :param use_given_order:
123 If True, the returned mapping is ordered in the order locales are given.
124 :type use_given_order: bool
125
126 :param allow_conflicting_locales:
127 if True, locales with same language and different region can be loaded.
128 :type allow_conflicting_locales: bool
129
130 :yield: locale instances
131 """
132 for _, locale in self._load_data(
133 languages=languages,
134 locales=locales,
135 region=region,
136 use_given_order=use_given_order,
137 allow_conflicting_locales=allow_conflicting_locales,
138 ):
139 yield locale
140
141 def get_locale(self, shortname):
142 """
143 Get a locale instance.
144
145 :param shortname:
146 A locale code, e.g. 'fr-PF', 'qu-EC', 'af-NA'.
147 :type shortname: str
148
149 :return: locale instance
150 """
151 return list(self.get_locales(locales=[shortname]))[0]
152
153 def _load_data(
154 self,
155 languages=None,
156 locales=None,
157 region=None,
158 use_given_order=False,
159 allow_conflicting_locales=False,
160 ):
161 locale_dict = OrderedDict()
162 if locales:
163 invalid_locales = []
164 for locale in locales:
165 lang_reg = LOCALE_SPLIT_PATTERN.split(locale)
166 if len(lang_reg) == 1:
167 lang_reg.append("")
168 locale_dict[locale] = tuple(lang_reg)
169 if not _isvalidlocale(locale):
170 invalid_locales.append(locale)
171 if invalid_locales:
172 raise ValueError(
173 "Unknown locale(s): %s" % ", ".join(map(repr, invalid_locales))
174 )
175
176 if not allow_conflicting_locales:
177 if len(set(locales)) > len({t[0] for t in locale_dict.values()}):
178 raise ValueError(
179 "Locales should not have same language and different region"
180 )
181
182 else:
183 if languages is None:
184 languages = language_order
185 unsupported_languages = set(languages) - set(language_order)
186 if unsupported_languages:
187 raise ValueError(
188 "Unknown language(s): %s"
189 % ", ".join(map(repr, unsupported_languages))
190 )
191 if region is None:
192 region = ""
193 locales = _construct_locales(languages, region)
194 locale_dict.update(
195 zip_longest(
196 locales, tuple(zip_longest(languages, [], fillvalue=region))
197 )
198 )
199
200 if not use_given_order:
201 locale_dict = OrderedDict(
202 sorted(locale_dict.items(), key=lambda x: language_order.index(x[1][0]))
203 )
204
205 for shortname, lang_reg in locale_dict.items():
206 if shortname not in self._loaded_locales:
207 lang, reg = lang_reg
208 if lang in self._loaded_languages:
209 locale = Locale(
210 shortname, language_info=deepcopy(self._loaded_languages[lang])
211 )
212 self._loaded_locales[shortname] = locale
213 else:
214 language_info = getattr(
215 import_module("dateparser.data.date_translation_data." + lang),
216 "info",
217 )
218 locale = Locale(shortname, language_info=deepcopy(language_info))
219 self._loaded_languages[lang] = language_info
220 self._loaded_locales[shortname] = locale
221 yield shortname, self._loaded_locales[shortname]
222
223
224default_loader = LocaleDataLoader()