summaryrefslogtreecommitdiffstats
path: root/server/src/locale.cpp
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.cpp
parent35878b76049b9c3e752480e9f298b7e2da6fdf1f (diff)
downloadroutemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.tar.gz
routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.zip
More module implementation partition units
Diffstat (limited to 'server/src/locale.cpp')
-rw-r--r--server/src/locale.cpp232
1 files changed, 232 insertions, 0 deletions
diff --git a/server/src/locale.cpp b/server/src/locale.cpp
new file mode 100644
index 0000000..652349c
--- /dev/null
+++ b/server/src/locale.cpp
@@ -0,0 +1,232 @@
1module;
2
3#include <boost/locale.hpp>
4#include <unicode/localematcher.h>
5
6module routemon:locale$impl;
7
8import :locale;
9
10namespace routemon::locale {
11
12struct locale_priority
13{
14 float weight;
15 std::size_t original_index;
16};
17
18auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool
19{
20 if (lhs.weight != rhs.weight)
21 return lhs.weight > rhs.weight;
22 return lhs.original_index < rhs.original_index;
23}
24
25struct icu_locale_hash
26{
27 std::size_t operator()(icu::Locale const& l) const noexcept
28 {
29 static_assert(sizeof(std::int32_t) < sizeof(std::size_t));
30 std::int32_t hash = l.hashCode();
31 if (hash < 0)
32 {
33 return static_cast<std::size_t>(std::numeric_limits<int>::max())
34 + static_cast<std::size_t>(-hash) + 1;
35 }
36 else
37 {
38 return static_cast<std::size_t>(hash);
39 }
40 }
41};
42
43using icu_locale_priority_map =
44 std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>;
45using icu_priority_locale = std::pair<icu::Locale, locale_priority>;
46
47auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs)
48 -> bool
49{
50 return lhs.second < rhs.second;
51}
52
53class icu_priority_locale_vec_iterator : public icu::Locale::Iterator
54{
55 std::size_t i_ = 0uz;
56 std::vector<icu_priority_locale> ls_;
57
58public:
59 explicit icu_priority_locale_vec_iterator(
60 std::vector<icu_priority_locale>&& ls)
61 : ls_(std::move(ls))
62 {
63 }
64
65 auto hasNext() const -> UBool override { return i_ < ls_.size(); }
66
67 auto next() -> icu::Locale const& override { return ls_[i_++].first; }
68
69 ~icu_priority_locale_vec_iterator() override = default;
70};
71
72// Trimming optional whitespace as defined in RFC 9110, ยง 12.4.2.
73auto selector::ltrim_ows(std::string_view s) -> std::string_view
74{
75 if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos)
76 s.remove_prefix(i);
77 return s;
78}
79auto selector::rtrim_ows(std::string_view s) -> std::string_view
80{
81 if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos)
82 return s.substr(0, i + 1);
83 return s;
84}
85auto selector::trim_ows(std::string_view s) -> std::string_view
86{
87 return rtrim_ows(ltrim_ows(s));
88}
89
90auto selector::from_icu_locale(icu::Locale const& l) const -> std::locale
91{
92 auto posix_name = std::string{l.getLanguage()};
93 if (l.getScript() && std::strlen(l.getScript()) > 0)
94 {
95 posix_name += "_";
96 posix_name += l.getScript();
97 }
98 if (l.getCountry() && std::strlen(l.getCountry()) > 0)
99 {
100 posix_name += "_";
101 posix_name += l.getCountry();
102 }
103 posix_name += ".UTF-8";
104 auto added_at = false;
105 if (l.getVariant() && std::strlen(l.getVariant()) > 0)
106 {
107 added_at = true;
108 posix_name += "@";
109 posix_name += l.getVariant();
110 }
111 auto ec = UErrorCode::U_ZERO_ERROR;
112 auto* keywords = l.createKeywords(ec);
113 if (U_FAILURE(ec))
114 throw std::runtime_error{"failed to create keywords"};
115 if (keywords)
116 {
117 std::int32_t kw_len = 0;
118 char const* kw = nullptr;
119 while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw)
120 {
121 auto value =
122 l.getKeywordValue<std::string>(icu::StringPiece(kw, kw_len), ec);
123 if (!added_at)
124 {
125 posix_name += "@";
126 added_at = true;
127 }
128 else
129 {
130 posix_name += ";";
131 }
132 posix_name += kw;
133 posix_name += "=";
134 posix_name += value;
135 }
136 if (U_FAILURE(ec))
137 throw std::runtime_error{"failed to iterate over keywords"};
138 delete keywords;
139 }
140 return lgen_->generate(posix_name);
141}
142
143auto selector::select(std::string_view accept_language) const -> std::locale
144{
145 using namespace std::literals::string_view_literals;
146 // NOTE: can also contain a *;q=0.1
147 // q should have at most 3 digits after period
148 auto dlpm = icu_locale_priority_map{};
149 for (auto const [i, lang_prio] :
150 accept_language | std::views::split(","sv) | std::views::enumerate)
151 {
152 auto [lang_range_ut, mweight_ut] =
153 util::split_on(std::string_view{lang_prio}, ';');
154 auto lang_range_str = trim_ows(lang_range_ut);
155 auto mweight_str = mweight_ut.transform(trim_ows);
156 if (lang_range_str == "*")
157 break;
158
159 auto ec = UErrorCode::U_ZERO_ERROR;
160 auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec);
161 if (U_FAILURE(ec) || icu_locale.isBogus())
162 continue; // ignore this locale
163
164 auto weight = 1.0f;
165 if (mweight_str && mweight_str->starts_with("q="))
166 {
167 auto weight_str = mweight_str->substr(2, 4);
168 if (auto mweight =
169 util::parse_float(weight_str, std::chars_format::fixed);
170 mweight && 0.0f < *mweight && *mweight < 1.0f)
171 {
172 weight = *mweight;
173 }
174 }
175
176 if (weight > 0.0f)
177 {
178 dlpm[icu_locale] = {
179 .weight = weight,
180 .original_index = static_cast<std::size_t>(i),
181 };
182 }
183 else
184 {
185 dlpm.erase(icu_locale);
186 }
187 }
188
189 auto desired_locales =
190 std::vector<icu_priority_locale>{dlpm.begin(), dlpm.end()};
191 std::sort(desired_locales.begin(), desired_locales.end());
192 auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)};
193 auto ec = UErrorCode::U_ZERO_ERROR;
194 auto res = matcher_.getBestMatchResult(it, ec);
195 if (U_FAILURE(ec))
196 return default_;
197 auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't?
198 if (U_FAILURE(ec))
199 return from_icu_locale(*res.getSupportedLocale());
200 return from_icu_locale(resolved);
201}
202
203auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string>
204{
205 auto const& locale_info = std::use_facet<blocale::info>(locale);
206 auto ec = UErrorCode::U_ZERO_ERROR;
207 auto bcp47_lang_tag =
208 icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec);
209 if (U_FAILURE(ec))
210 return std::nullopt;
211 return bcp47_lang_tag;
212}
213
214#ifdef LOCALEDIR
215#define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s)
216#define LOCALEDIR_AUX_STR(s) #s
217constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)};
218#undef LOCALEDIR_AUX_STR
219#undef LOCALEDIR_AUX_XSTR
220#else // ifdef LOCALEDIR
221constexpr auto messages_path = std::string_view{"locale/dev"};
222#endif // ifdef LOCALEDIR
223
224export auto make_generator() -> std::shared_ptr<blocale::generator const>
225{
226 auto lgen = std::make_shared<blocale::generator>();
227 lgen->add_messages_path(std::string{messages_path});
228 lgen->add_messages_domain("routemon");
229 return std::static_pointer_cast<blocale::generator const>(lgen);
230}
231
232} // namespace routemon::locale