summaryrefslogtreecommitdiffstats
path: root/server/src/locale.cppm
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/locale.cppm')
-rw-r--r--server/src/locale.cppm245
1 files changed, 245 insertions, 0 deletions
diff --git a/server/src/locale.cppm b/server/src/locale.cppm
new file mode 100644
index 0000000..65ae2ea
--- /dev/null
+++ b/server/src/locale.cppm
@@ -0,0 +1,245 @@
1module;
2
3#include <boost/locale.hpp>
4#include <unicode/localematcher.h>
5
6export module routemon:locale;
7
8import std;
9import :util;
10
11export namespace blocale = boost::locale;
12
13export namespace routemon {
14 using lformat = blocale::format;
15 using blocale::translate;
16 using blocale::gettext;
17} // namespace routemon
18
19namespace routemon::locale {
20
21 struct locale_priority {
22 float weight;
23 std::size_t original_index;
24 };
25
26 auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool {
27 if (lhs.weight != rhs.weight)
28 return lhs.weight > rhs.weight;
29 return lhs.original_index < rhs.original_index;
30 }
31
32 struct icu_locale_hash {
33 std::size_t operator()(icu::Locale const& l) const noexcept {
34 static_assert(sizeof(std::int32_t) < sizeof(std::size_t));
35 std::int32_t hash = l.hashCode();
36 if (hash < 0) {
37 return static_cast<std::size_t>(std::numeric_limits<int>::max()) + static_cast<std::size_t>(-hash) + 1;
38 } else {
39 return static_cast<std::size_t>(hash);
40 }
41 }
42 };
43
44 using icu_locale_priority_map = std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>;
45 using icu_priority_locale = std::pair<icu::Locale, locale_priority>;
46
47 auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) -> bool {
48 return lhs.second < rhs.second;
49 }
50
51 class icu_priority_locale_vec_iterator : public icu::Locale::Iterator {
52 std::size_t i_ = 0uz;
53 std::vector<icu_priority_locale> ls_;
54
55 public:
56 explicit icu_priority_locale_vec_iterator(std::vector<icu_priority_locale>&& ls)
57 : ls_(std::move(ls))
58 {}
59
60 auto hasNext() const -> UBool override {
61 return i_ < ls_.size();
62 }
63
64 auto next() -> icu::Locale const& override {
65 return ls_[i_++].first;
66 }
67
68 ~icu_priority_locale_vec_iterator() override = default;
69 };
70
71 export template<class T>
72 concept locale_input_range =
73 std::ranges::input_range<T> &&
74 std::same_as<std::locale const&, std::ranges::range_const_reference_t<T>>;
75
76 // Helps select a locale based on the Accept-Language header in an
77 // HTTP request.
78 export class selector {
79 std::locale default_;
80 icu::LocaleMatcher matcher_;
81 std::shared_ptr<blocale::generator const> lgen_;
82
83 auto make_matcher(locale_input_range auto supported_locales, std::locale default_locale) {
84 auto builder = icu::LocaleMatcher::Builder{};
85 for (auto const& supported_locale : supported_locales) {
86 auto const& supported_locale_info = std::use_facet<blocale::info>(supported_locale);
87 auto supported_icu_locale = icu::Locale{supported_locale_info.name().c_str()};
88 if (supported_icu_locale.isBogus())
89 throw std::runtime_error{"supported locale gives rise to bogus ICU locale"};
90 builder.addSupportedLocale(supported_icu_locale);
91 }
92 auto const& default_locale_info = std::use_facet<blocale::info>(default_locale);
93 auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()};
94 if (default_icu_locale.isBogus())
95 throw std::runtime_error{"default locale gives rise to bogus ICU locale"};
96 builder.setDefaultLocale(&default_icu_locale);
97 auto ec = UErrorCode::U_ZERO_ERROR;
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 }
103
104 // Trimming optional whitespace as defined in RFC 9110, ยง 12.4.2.
105 static auto ltrim_ows(std::string_view s) -> std::string_view {
106 if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos)
107 s.remove_prefix(i);
108 return s;
109 }
110 static auto rtrim_ows(std::string_view s) -> std::string_view {
111 if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos)
112 return s.substr(0, i + 1);
113 return s;
114 }
115 static auto trim_ows(std::string_view s) -> std::string_view {
116 return rtrim_ows(ltrim_ows(s));
117 }
118
119 auto from_icu_locale(icu::Locale const& l) const -> std::locale {
120 auto posix_name = std::string{l.getLanguage()};
121 if (l.getScript() && std::strlen(l.getScript()) > 0) {
122 posix_name += "_";
123 posix_name += l.getScript();
124 }
125 if (l.getCountry() && std::strlen(l.getCountry()) > 0) {
126 posix_name += "_";
127 posix_name += l.getCountry();
128 }
129 posix_name += ".UTF-8";
130 auto added_at = false;
131 if (l.getVariant() && std::strlen(l.getVariant()) > 0) {
132 added_at = true;
133 posix_name += "@";
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 }
155 if (U_FAILURE(ec))
156 throw std::runtime_error{"failed to iterate over keywords"};
157 delete keywords;
158 }
159 return lgen_->generate(posix_name);
160 }
161
162 public:
163 // 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.
165 explicit selector(locale_input_range auto locales, std::locale default_, std::shared_ptr<blocale::generator const> lgen)
166 : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen}
167 {}
168
169 auto select(std::string_view accept_language) const -> std::locale {
170 using namespace std::literals::string_view_literals;
171 // NOTE: can also contain a *;q=0.1
172 // q should have at most 3 digits after period
173 auto dlpm = icu_locale_priority_map{};
174 for (auto const [i, lang_prio] : accept_language | std::views::split(","sv) | std::views::enumerate) {
175 auto [lang_range_ut, mweight_ut] = util::split_on(std::string_view{lang_prio}, ';');
176 auto lang_range_str = trim_ows(lang_range_ut);
177 auto mweight_str = mweight_ut.transform(trim_ows);
178 if (lang_range_str == "*")
179 break;
180
181 auto ec = UErrorCode::U_ZERO_ERROR;
182 auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec);
183 if (U_FAILURE(ec) || icu_locale.isBogus())
184 continue; // ignore this locale
185
186 auto weight = 1.0f;
187 if (mweight_str && mweight_str->starts_with("q=")) {
188 auto weight_str = mweight_str->substr(2, 4);
189 if (auto mweight = util::parse_float(weight_str, std::chars_format::fixed);
190 mweight && 0.0f < *mweight && *mweight < 1.0f) {
191 weight = *mweight;
192 }
193 }
194
195 if (weight > 0.0f) {
196 dlpm[icu_locale] = {
197 .weight = weight,
198 .original_index = static_cast<std::size_t>(i),
199 };
200 } else {
201 dlpm.erase(icu_locale);
202 }
203 }
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 }
217 };
218
219 export auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> {
220 auto const& locale_info = std::use_facet<blocale::info>(locale);
221 auto ec = UErrorCode::U_ZERO_ERROR;
222 auto bcp47_lang_tag = icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec);
223 if (U_FAILURE(ec))
224 return std::nullopt;
225 return bcp47_lang_tag;
226 }
227
228#ifdef LOCALEDIR
229# define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s)
230# define LOCALEDIR_AUX_STR(s) #s
231 constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)};
232# undef LOCALEDIR_AUX_STR
233# undef LOCALEDIR_AUX_XSTR
234#else // ifdef LOCALEDIR
235 constexpr auto messages_path = std::string_view{"locale/dev"};
236#endif // ifdef LOCALEDIR
237
238 export auto make_generator() -> std::shared_ptr<blocale::generator const> {
239 auto lgen = std::make_shared<blocale::generator>();
240 lgen->add_messages_path(std::string{messages_path});
241 lgen->add_messages_domain("routemon");
242 return std::static_pointer_cast<blocale::generator const>(lgen);
243 }
244
245} // namespace routemon::locale