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