From 973aec43ea54bbf95b64fbcb636403401d1ca60e Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Fri, 28 Aug 2026 18:03:05 +0200 Subject: Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14 --- server/src/locale.cppm | 245 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 server/src/locale.cppm (limited to 'server/src/locale.cppm') 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 @@ +module; + +#include +#include + +export module routemon:locale; + +import std; +import :util; + +export namespace blocale = boost::locale; + +export namespace routemon { + using lformat = blocale::format; + using blocale::translate; + using blocale::gettext; +} // namespace routemon + +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; + }; + + export template + concept locale_input_range = + std::ranges::input_range && + std::same_as>; + + // Helps select a locale based on the Accept-Language header in an + // HTTP request. + export class selector { + std::locale default_; + icu::LocaleMatcher matcher_; + std::shared_ptr lgen_; + + auto make_matcher(locale_input_range auto supported_locales, std::locale default_locale) { + auto builder = icu::LocaleMatcher::Builder{}; + for (auto const& supported_locale : supported_locales) { + auto const& supported_locale_info = std::use_facet(supported_locale); + auto supported_icu_locale = icu::Locale{supported_locale_info.name().c_str()}; + if (supported_icu_locale.isBogus()) + throw std::runtime_error{"supported locale gives rise to bogus ICU locale"}; + builder.addSupportedLocale(supported_icu_locale); + } + auto const& default_locale_info = std::use_facet(default_locale); + auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; + if (default_icu_locale.isBogus()) + throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; + builder.setDefaultLocale(&default_icu_locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto matcher = builder.build(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to build icu::LocaleMatcher"}; + return matcher; + } + + // Trimming optional whitespace as defined in RFC 9110, ยง 12.4.2. + static auto 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; + } + static auto 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; + } + static auto trim_ows(std::string_view s) -> std::string_view { + return rtrim_ows(ltrim_ows(s)); + } + + auto 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); + } + + public: + // Note: lgen must live at least as long as the selector constructed here! + // It is unfortunately not possible to copy/move a blocale::generator. + explicit selector(locale_input_range auto locales, std::locale default_, std::shared_ptr lgen) + : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} + {} + + auto 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); + } + }; + + export 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