summaryrefslogtreecommitdiffstats
path: root/server/src/locale.cppm
diff options
context:
space:
mode:
authorRutger Broekhoff2026-08-29 12:01:42 +0200
committerRutger Broekhoff2026-08-29 12:01:42 +0200
commit52b3cd46c76fd4be18aeb8422a08a073484b9fad (patch)
treeb4f23957c096e6bce5369bb94a4e8e23de71761a /server/src/locale.cppm
parent35878b76049b9c3e752480e9f298b7e2da6fdf1f (diff)
downloadroutemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.tar.gz
routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.zip
More module implementation partition units
Diffstat (limited to 'server/src/locale.cppm')
-rw-r--r--server/src/locale.cppm227
1 files changed, 11 insertions, 216 deletions
diff --git a/server/src/locale.cppm b/server/src/locale.cppm
index da80f76..263de1d 100644
--- a/server/src/locale.cppm
+++ b/server/src/locale.cppm
@@ -11,78 +11,20 @@ import :util;
11export namespace blocale = boost::locale; 11export namespace blocale = boost::locale;
12 12
13export namespace routemon { 13export namespace routemon {
14
14using lformat = blocale::format; 15using lformat = blocale::format;
15using blocale::gettext; 16using blocale::gettext;
16using blocale::translate; 17using blocale::translate;
18
17} // namespace routemon 19} // namespace routemon
18 20
19namespace routemon::locale { 21namespace routemon::locale {
20 22
21struct locale_priority
22{
23 float weight;
24 std::size_t original_index;
25};
26
27auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool
28{
29 if (lhs.weight != rhs.weight)
30 return lhs.weight > rhs.weight;
31 return lhs.original_index < rhs.original_index;
32}
33
34struct icu_locale_hash
35{
36 std::size_t operator()(icu::Locale const& l) const noexcept
37 {
38 static_assert(sizeof(std::int32_t) < sizeof(std::size_t));
39 std::int32_t hash = l.hashCode();
40 if (hash < 0)
41 {
42 return static_cast<std::size_t>(std::numeric_limits<int>::max())
43 + static_cast<std::size_t>(-hash) + 1;
44 }
45 else
46 {
47 return static_cast<std::size_t>(hash);
48 }
49 }
50};
51
52using icu_locale_priority_map =
53 std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>;
54using icu_priority_locale = std::pair<icu::Locale, locale_priority>;
55
56auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs)
57 -> bool
58{
59 return lhs.second < rhs.second;
60}
61
62class icu_priority_locale_vec_iterator : public icu::Locale::Iterator
63{
64 std::size_t i_ = 0uz;
65 std::vector<icu_priority_locale> ls_;
66
67public:
68 explicit icu_priority_locale_vec_iterator(
69 std::vector<icu_priority_locale>&& ls)
70 : ls_(std::move(ls))
71 {
72 }
73
74 auto hasNext() const -> UBool override { return i_ < ls_.size(); }
75
76 auto next() -> icu::Locale const& override { return ls_[i_++].first; }
77
78 ~icu_priority_locale_vec_iterator() override = default;
79};
80
81export template <class T> 23export template <class T>
82concept locale_input_range = 24concept locale_input_range =
83 std::ranges::input_range<T> 25 std::ranges::input_range<T>
84 && std::same_as< 26 && std::
85 std::locale const&, std::ranges::range_const_reference_t<T>>; 27 same_as<std::locale const&, std::ranges::range_const_reference_t<T>>;
86 28
87// Helps select a locale based on the Accept-Language header in an 29// Helps select a locale based on the Accept-Language header in an
88// HTTP request. 30// HTTP request.
@@ -122,75 +64,11 @@ export class selector
122 } 64 }
123 65
124 // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. 66 // Trimming optional whitespace as defined in RFC 9110, § 12.4.2.
125 static auto ltrim_ows(std::string_view s) -> std::string_view 67 static auto ltrim_ows(std::string_view s) -> std::string_view;
126 { 68 static auto rtrim_ows(std::string_view s) -> std::string_view;
127 if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) 69 static auto trim_ows(std::string_view s) -> std::string_view;
128 s.remove_prefix(i);
129 return s;
130 }
131 static auto rtrim_ows(std::string_view s) -> std::string_view
132 {
133 if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos)
134 return s.substr(0, i + 1);
135 return s;
136 }
137 static auto trim_ows(std::string_view s) -> std::string_view
138 {
139 return rtrim_ows(ltrim_ows(s));
140 }
141 70
142 auto from_icu_locale(icu::Locale const& l) const -> std::locale 71 auto from_icu_locale(icu::Locale const& l) const -> std::locale;
143 {
144 auto posix_name = std::string{l.getLanguage()};
145 if (l.getScript() && std::strlen(l.getScript()) > 0)
146 {
147 posix_name += "_";
148 posix_name += l.getScript();
149 }
150 if (l.getCountry() && std::strlen(l.getCountry()) > 0)
151 {
152 posix_name += "_";
153 posix_name += l.getCountry();
154 }
155 posix_name += ".UTF-8";
156 auto added_at = false;
157 if (l.getVariant() && std::strlen(l.getVariant()) > 0)
158 {
159 added_at = true;
160 posix_name += "@";
161 posix_name += l.getVariant();
162 }
163 auto ec = UErrorCode::U_ZERO_ERROR;
164 auto* keywords = l.createKeywords(ec);
165 if (U_FAILURE(ec))
166 throw std::runtime_error{"failed to create keywords"};
167 if (keywords)
168 {
169 std::int32_t kw_len = 0;
170 char const* kw = nullptr;
171 while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw)
172 {
173 auto value =
174 l.getKeywordValue<std::string>(icu::StringPiece(kw, kw_len), ec);
175 if (!added_at)
176 {
177 posix_name += "@";
178 added_at = true;
179 }
180 else
181 {
182 posix_name += ";";
183 }
184 posix_name += kw;
185 posix_name += "=";
186 posix_name += value;
187 }
188 if (U_FAILURE(ec))
189 throw std::runtime_error{"failed to iterate over keywords"};
190 delete keywords;
191 }
192 return lgen_->generate(posix_name);
193 }
194 72
195public: 73public:
196 // Note: lgen must live at least as long as the selector constructed here! 74 // Note: lgen must live at least as long as the selector constructed here!
@@ -202,94 +80,11 @@ public:
202 { 80 {
203 } 81 }
204 82
205 auto select(std::string_view accept_language) const -> std::locale 83 auto select(std::string_view accept_language) const -> std::locale;
206 {
207 using namespace std::literals::string_view_literals;
208 // NOTE: can also contain a *;q=0.1
209 // q should have at most 3 digits after period
210 auto dlpm = icu_locale_priority_map{};
211 for (auto const [i, lang_prio] :
212 accept_language | std::views::split(","sv) | std::views::enumerate)
213 {
214 auto [lang_range_ut, mweight_ut] =
215 util::split_on(std::string_view{lang_prio}, ';');
216 auto lang_range_str = trim_ows(lang_range_ut);
217 auto mweight_str = mweight_ut.transform(trim_ows);
218 if (lang_range_str == "*")
219 break;
220
221 auto ec = UErrorCode::U_ZERO_ERROR;
222 auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec);
223 if (U_FAILURE(ec) || icu_locale.isBogus())
224 continue; // ignore this locale
225
226 auto weight = 1.0f;
227 if (mweight_str && mweight_str->starts_with("q="))
228 {
229 auto weight_str = mweight_str->substr(2, 4);
230 if (auto mweight =
231 util::parse_float(weight_str, std::chars_format::fixed);
232 mweight && 0.0f < *mweight && *mweight < 1.0f)
233 {
234 weight = *mweight;
235 }
236 }
237
238 if (weight > 0.0f)
239 {
240 dlpm[icu_locale] = {
241 .weight = weight,
242 .original_index = static_cast<std::size_t>(i),
243 };
244 }
245 else
246 {
247 dlpm.erase(icu_locale);
248 }
249 }
250
251 auto desired_locales =
252 std::vector<icu_priority_locale>{dlpm.begin(), dlpm.end()};
253 std::sort(desired_locales.begin(), desired_locales.end());
254 auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)};
255 auto ec = UErrorCode::U_ZERO_ERROR;
256 auto res = matcher_.getBestMatchResult(it, ec);
257 if (U_FAILURE(ec))
258 return default_;
259 auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't?
260 if (U_FAILURE(ec))
261 return from_icu_locale(*res.getSupportedLocale());
262 return from_icu_locale(resolved);
263 }
264}; 84};
265 85
266export auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> 86auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string>;
267{
268 auto const& locale_info = std::use_facet<blocale::info>(locale);
269 auto ec = UErrorCode::U_ZERO_ERROR;
270 auto bcp47_lang_tag =
271 icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec);
272 if (U_FAILURE(ec))
273 return std::nullopt;
274 return bcp47_lang_tag;
275}
276
277#ifdef LOCALEDIR
278#define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s)
279#define LOCALEDIR_AUX_STR(s) #s
280constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)};
281#undef LOCALEDIR_AUX_STR
282#undef LOCALEDIR_AUX_XSTR
283#else // ifdef LOCALEDIR
284constexpr auto messages_path = std::string_view{"locale/dev"};
285#endif // ifdef LOCALEDIR
286 87
287export auto make_generator() -> std::shared_ptr<blocale::generator const> 88export auto make_generator() -> std::shared_ptr<blocale::generator const>;
288{
289 auto lgen = std::make_shared<blocale::generator>();
290 lgen->add_messages_path(std::string{messages_path});
291 lgen->add_messages_domain("routemon");
292 return std::static_pointer_cast<blocale::generator const>(lgen);
293}
294 89
295} // namespace routemon::locale 90} // namespace routemon::locale