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; }; 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; } auto bcp47_tag::id = std::locale::id{}; bcp47_tag::bcp47_tag(std::locale locale) { auto mtag = to_bcp47_lang_tag(locale); if (!mtag) throw std::invalid_argument{ "locale name cannot be expressed as a BCP 47 language tag" }; tag_ = *mtag; } auto bcp47_tag::tag() const -> std::string_view { return tag_; } auto selector::make_matcher(std::vector const& supported_locales) -> icu::LocaleMatcher { 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); } builder.setNoDefaultLocale(); 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. 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::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 i = res.getSupportedIndex(); if (i == -1) return default_; return supported_[util::size_from_int(i).value()]; } #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