use std::{error::Error, fmt};
use fluent::{FluentArgs, FluentBundle, FluentResource, FluentValue};
use super::Locale;
const EN_US_RESOURCES: &[&str] = &[
include_str!("../../locales/en-US/main.ftl"),
include_str!("../../locales/en-US/platform-8.3.27.ftl"),
include_str!("../../locales/en-US/platform-8.5.1.ftl"),
include_str!("../../locales/en-US/platform-values.ftl"),
include_str!("../../locales/en-US/platform-presentation.ftl"),
];
const RU_RU_RESOURCES: &[&str] = &[
include_str!("../../locales/ru-RU/main.ftl"),
include_str!("../../locales/ru-RU/platform-8.3.27.ftl"),
include_str!("../../locales/ru-RU/platform-8.5.1.ftl"),
include_str!("../../locales/ru-RU/platform-values.ftl"),
include_str!("../../locales/ru-RU/platform-presentation.ftl"),
];
#[derive(Clone, Copy, Debug)]
pub enum LocalizationValue<'a> {
Text(&'a str),
Number(i64),
}
pub struct Localizer {
locale: Locale,
bundle: FluentBundle<FluentResource>,
}
impl Localizer {
pub fn try_new(locale: Locale) -> Result<Self, LocalizationError> {
let sources = match locale {
Locale::RuRu => RU_RU_RESOURCES,
Locale::EnUs => EN_US_RESOURCES,
};
let language_id = locale.as_str().parse().map_err(|error| {
LocalizationError::new(format!(
"invalid locale identifier {}: {error}",
locale.as_str()
))
})?;
let mut bundle = FluentBundle::new(vec![language_id]);
for source in sources {
let resource =
FluentResource::try_new((*source).to_owned()).map_err(|(_, errors)| {
LocalizationError::new(format!(
"invalid embedded {} resource: {errors:?}",
locale.as_str()
))
})?;
bundle.add_resource(resource).map_err(|errors| {
LocalizationError::new(format!(
"invalid embedded {} bundle: {errors:?}",
locale.as_str()
))
})?;
}
Ok(Self { locale, bundle })
}
#[must_use]
pub const fn locale(&self) -> Locale {
self.locale
}
#[must_use]
pub fn text(&self, key: &str) -> String {
self.format(key, &[])
}
pub(super) fn optional_text(&self, key: &str) -> Option<String> {
self.bundle.get_message(key)?.value()?;
Some(self.text(key))
}
#[must_use]
pub fn format(&self, key: &str, values: &[(&str, LocalizationValue<'_>)]) -> String {
let Some(message) = self.bundle.get_message(key) else {
debug_assert!(false, "missing localization key: {key}");
return key.to_owned();
};
let Some(pattern) = message.value() else {
debug_assert!(false, "localization key has no value: {key}");
return key.to_owned();
};
let mut args = FluentArgs::new();
for (name, value) in values {
let value = match value {
LocalizationValue::Text(value) => FluentValue::from(*value),
LocalizationValue::Number(value) => FluentValue::from(*value),
};
args.set(*name, value);
}
let mut errors = Vec::new();
let value = self
.bundle
.format_pattern(pattern, Some(&args), &mut errors);
debug_assert!(
errors.is_empty(),
"failed to format localization key {key}: {errors:?}"
);
value.into_owned()
}
}
#[derive(Debug)]
pub struct LocalizationError {
message: String,
}
impl LocalizationError {
const fn new(message: String) -> Self {
Self { message }
}
}
impl fmt::Display for LocalizationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl Error for LocalizationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_resources_have_matching_keys_and_are_valid() {
fn keys(resource: &str) -> Vec<&str> {
let mut keys: Vec<_> = resource
.lines()
.filter_map(|line| line.split_once(" = ").map(|(key, _)| key))
.collect();
keys.sort_unstable();
keys
}
for (en, ru) in EN_US_RESOURCES.iter().zip(RU_RU_RESOURCES) {
assert_eq!(keys(en), keys(ru));
}
assert!(Localizer::try_new(Locale::EnUs).is_ok());
assert!(Localizer::try_new(Locale::RuRu).is_ok());
}
}