From 52b3cd46c76fd4be18aeb8422a08a073484b9fad Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Sat, 29 Aug 2026 12:01:42 +0200 Subject: More module implementation partition units --- server/src/locale.cpp | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 server/src/locale.cpp (limited to 'server/src/locale.cpp') 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 @@ +module; + +#include +#include + +module routemon:locale$impl; + +import :locale; + +namespace routemon::locale { + +struct locale_priority +{ + float weight; + std::size_t original_index; +}; + +auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool +{ + if (lhs.weight != rhs.weight) + return lhs.weight > rhs.weight; + return lhs.original_index < rhs.original_index; +} + +struct icu_locale_hash +{ + std::size_t operator()(icu::Locale const& l) const noexcept + { + static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); + std::int32_t hash = l.hashCode(); + if (hash < 0) + { + return static_cast(std::numeric_limits::max()) + + static_cast(-hash) + 1; + } + else + { + return static_cast(hash); + } + } +}; + +using icu_locale_priority_map = + std::unordered_map; +using icu_priority_locale = std::pair; + +auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) + -> bool +{ + return lhs.second < rhs.second; +} + +class icu_priority_locale_vec_iterator : public icu::Locale::Iterator +{ + std::size_t i_ = 0uz; + std::vector ls_; + +public: + explicit icu_priority_locale_vec_iterator( + std::vector&& ls) + : ls_(std::move(ls)) + { + } + + auto hasNext() const -> UBool override { return i_ < ls_.size(); } + + auto next() -> icu::Locale const& override { return ls_[i_++].first; } + + ~icu_priority_locale_vec_iterator() override = default; +}; + +// Trimming optional whitespace as defined in RFC 9110, ยง 12.4.2. +auto selector::ltrim_ows(std::string_view s) -> std::string_view +{ + if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) + s.remove_prefix(i); + return s; +} +auto selector::rtrim_ows(std::string_view s) -> std::string_view +{ + if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) + return s.substr(0, i + 1); + return s; +} +auto selector::trim_ows(std::string_view s) -> std::string_view +{ + return rtrim_ows(ltrim_ows(s)); +} + +auto selector::from_icu_locale(icu::Locale const& l) const -> std::locale +{ + auto posix_name = std::string{l.getLanguage()}; + if (l.getScript() && std::strlen(l.getScript()) > 0) + { + posix_name += "_"; + posix_name += l.getScript(); + } + if (l.getCountry() && std::strlen(l.getCountry()) > 0) + { + posix_name += "_"; + posix_name += l.getCountry(); + } + posix_name += ".UTF-8"; + auto added_at = false; + if (l.getVariant() && std::strlen(l.getVariant()) > 0) + { + added_at = true; + posix_name += "@"; + posix_name += l.getVariant(); + } + auto ec = UErrorCode::U_ZERO_ERROR; + auto* keywords = l.createKeywords(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to create keywords"}; + if (keywords) + { + std::int32_t kw_len = 0; + char const* kw = nullptr; + while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) + { + auto value = + l.getKeywordValue(icu::StringPiece(kw, kw_len), ec); + if (!added_at) + { + posix_name += "@"; + added_at = true; + } + else + { + posix_name += ";"; + } + posix_name += kw; + posix_name += "="; + posix_name += value; + } + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to iterate over keywords"}; + delete keywords; + } + return lgen_->generate(posix_name); +} + +auto selector::select(std::string_view accept_language) const -> std::locale +{ + using namespace std::literals::string_view_literals; + // NOTE: can also contain a *;q=0.1 + // q should have at most 3 digits after period + auto dlpm = icu_locale_priority_map{}; + for (auto const [i, lang_prio] : + accept_language | std::views::split(","sv) | std::views::enumerate) + { + auto [lang_range_ut, mweight_ut] = + util::split_on(std::string_view{lang_prio}, ';'); + auto lang_range_str = trim_ows(lang_range_ut); + auto mweight_str = mweight_ut.transform(trim_ows); + if (lang_range_str == "*") + break; + + auto ec = UErrorCode::U_ZERO_ERROR; + auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); + if (U_FAILURE(ec) || icu_locale.isBogus()) + continue; // ignore this locale + + auto weight = 1.0f; + if (mweight_str && mweight_str->starts_with("q=")) + { + auto weight_str = mweight_str->substr(2, 4); + if (auto mweight = + util::parse_float(weight_str, std::chars_format::fixed); + mweight && 0.0f < *mweight && *mweight < 1.0f) + { + weight = *mweight; + } + } + + if (weight > 0.0f) + { + dlpm[icu_locale] = { + .weight = weight, + .original_index = static_cast(i), + }; + } + else + { + dlpm.erase(icu_locale); + } + } + + auto desired_locales = + std::vector{dlpm.begin(), dlpm.end()}; + std::sort(desired_locales.begin(), desired_locales.end()); + auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; + auto ec = UErrorCode::U_ZERO_ERROR; + auto res = matcher_.getBestMatchResult(it, ec); + if (U_FAILURE(ec)) + return default_; + auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? + if (U_FAILURE(ec)) + return from_icu_locale(*res.getSupportedLocale()); + return from_icu_locale(resolved); +} + +auto to_bcp47_lang_tag(std::locale locale) -> std::optional +{ + auto const& locale_info = std::use_facet(locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto bcp47_lang_tag = + icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); + if (U_FAILURE(ec)) + return std::nullopt; + return bcp47_lang_tag; +} + +#ifdef LOCALEDIR +#define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) +#define LOCALEDIR_AUX_STR(s) #s +constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; +#undef LOCALEDIR_AUX_STR +#undef LOCALEDIR_AUX_XSTR +#else // ifdef LOCALEDIR +constexpr auto messages_path = std::string_view{"locale/dev"}; +#endif // ifdef LOCALEDIR + +export auto make_generator() -> std::shared_ptr +{ + auto lgen = std::make_shared(); + lgen->add_messages_path(std::string{messages_path}); + lgen->add_messages_domain("routemon"); + return std::static_pointer_cast(lgen); +} + +} // namespace routemon::locale -- cgit v1.3