use crate::locale_catalogues::{self as locales, LocaleCatalogue};
use ortho_config::LanguageIdentifier;
use ortho_config::{FluentLocalizer, FluentLocalizerBuilder, Localizer, NoOpLocalizer};
use std::str::FromStr;
struct LayeredLocalizer {
primary: Box<dyn Localizer>,
fallback: Box<dyn Localizer>,
}
impl LayeredLocalizer {
fn new(primary: Box<dyn Localizer>, fallback: Box<dyn Localizer>) -> Self {
Self { primary, fallback }
}
}
impl Localizer for LayeredLocalizer {
fn lookup(
&self,
id: &str,
args: Option<&ortho_config::LocalizationArgs<'_>>,
) -> Option<String> {
self.primary
.lookup(id, args)
.or_else(|| self.fallback.lookup(id, args))
}
}
fn parse_locale_identifier(locale: &str) -> Option<LanguageIdentifier> {
LanguageIdentifier::from_str(locale).ok()
}
fn build_en_localizer() -> Box<dyn Localizer> {
match FluentLocalizer::with_en_us_defaults([locales::source_catalogue().resource()]) {
Ok(localizer) => Box::new(localizer) as Box<dyn Localizer>,
Err(err) => {
tracing::warn!(error = %err, "failed to load default localization resources");
Box::new(NoOpLocalizer::new()) as Box<dyn Localizer>
}
}
}
fn build_consumer_localizer(
builder: FluentLocalizerBuilder,
tag: &'static str,
resource: &'static str,
) -> Option<Box<dyn Localizer>> {
match builder
.with_consumer_resources([resource])
.disable_defaults()
.try_build()
{
Ok(localizer) => Some(Box::new(localizer) as Box<dyn Localizer>),
Err(err) => {
tracing::warn!(
locale = tag,
error = %err,
"failed to load locale catalogue; falling back to the source locale"
);
None
}
}
}
fn build_layered_localizer(
requested: &LanguageIdentifier,
catalogue: &'static LocaleCatalogue,
fallback: Box<dyn Localizer>,
) -> Box<dyn Localizer> {
let locale = parse_locale_identifier(catalogue.tag()).unwrap_or_else(|| requested.clone());
let builder = FluentLocalizer::builder(locale);
match build_consumer_localizer(builder, catalogue.tag(), catalogue.resource()) {
Some(primary) => Box::new(LayeredLocalizer::new(primary, fallback)),
None => fallback,
}
}
fn fell_back_from_another_language(
locale: &LanguageIdentifier,
catalogue: &LocaleCatalogue,
) -> bool {
if catalogue.tag() != locales::SOURCE_LOCALE {
return false;
}
if *locale == locales::SOURCE_LOCALE {
return false;
}
locale.language.as_str() != locales::tag_language(locales::SOURCE_LOCALE)
}
#[must_use]
pub fn build_localizer(preferred_locale: Option<&str>) -> Box<dyn Localizer> {
let fallback = build_en_localizer();
let Some(preferred) = preferred_locale else {
return fallback;
};
let Some(locale) = parse_locale_identifier(preferred) else {
tracing::warn!(
requested = preferred,
effective = locales::SOURCE_LOCALE,
reason = "unparseable",
"locale request did not parse; falling back to the source locale"
);
return fallback;
};
let catalogue = locales::resolve_catalogue(&locale);
if fell_back_from_another_language(&locale, catalogue) {
tracing::warn!(
requested = preferred,
effective = locales::SOURCE_LOCALE,
reason = "unsupported",
"no catalogue for the requested locale; falling back to the source locale"
);
return fallback;
}
tracing::debug!(
requested = preferred,
effective = catalogue.tag(),
"resolved locale catalogue"
);
if catalogue.tag() == locales::SOURCE_LOCALE {
return fallback;
}
build_layered_localizer(&locale, catalogue, fallback)
}
#[cfg(test)]
#[path = "cli_localization_tracing_tests.rs"]
mod tracing_tests;