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::gettext; using blocale::translate; } // 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< std::locale const&, std::ranges::range_const_reference_t>; // 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