eska_presentation/localization/
localizer.rs1use std::{error::Error, fmt};
4
5use fluent::{FluentArgs, FluentBundle, FluentResource, FluentValue};
6
7use super::Locale;
8
9const EN_US_RESOURCES: &[&str] = &[
10 include_str!("../../locales/en-US/main.ftl"),
11 include_str!("../../locales/en-US/platform-8.3.27.ftl"),
12 include_str!("../../locales/en-US/platform-8.5.1.ftl"),
13 include_str!("../../locales/en-US/platform-values.ftl"),
14 include_str!("../../locales/en-US/platform-presentation.ftl"),
15];
16const RU_RU_RESOURCES: &[&str] = &[
17 include_str!("../../locales/ru-RU/main.ftl"),
18 include_str!("../../locales/ru-RU/platform-8.3.27.ftl"),
19 include_str!("../../locales/ru-RU/platform-8.5.1.ftl"),
20 include_str!("../../locales/ru-RU/platform-values.ftl"),
21 include_str!("../../locales/ru-RU/platform-presentation.ftl"),
22];
23
24#[derive(Clone, Copy, Debug)]
26pub enum LocalizationValue<'a> {
27 Text(&'a str),
28 Number(i64),
29}
30
31pub struct Localizer {
33 locale: Locale,
34 bundle: FluentBundle<FluentResource>,
35}
36
37impl Localizer {
38 pub fn try_new(locale: Locale) -> Result<Self, LocalizationError> {
45 let sources = match locale {
46 Locale::RuRu => RU_RU_RESOURCES,
47 Locale::EnUs => EN_US_RESOURCES,
48 };
49 let language_id = locale.as_str().parse().map_err(|error| {
50 LocalizationError::new(format!(
51 "invalid locale identifier {}: {error}",
52 locale.as_str()
53 ))
54 })?;
55 let mut bundle = FluentBundle::new(vec![language_id]);
56 for source in sources {
57 let resource =
58 FluentResource::try_new((*source).to_owned()).map_err(|(_, errors)| {
59 LocalizationError::new(format!(
60 "invalid embedded {} resource: {errors:?}",
61 locale.as_str()
62 ))
63 })?;
64 bundle.add_resource(resource).map_err(|errors| {
65 LocalizationError::new(format!(
66 "invalid embedded {} bundle: {errors:?}",
67 locale.as_str()
68 ))
69 })?;
70 }
71
72 Ok(Self { locale, bundle })
73 }
74
75 #[must_use]
77 pub const fn locale(&self) -> Locale {
78 self.locale
79 }
80
81 #[must_use]
83 pub fn text(&self, key: &str) -> String {
84 self.format(key, &[])
85 }
86
87 pub(super) fn optional_text(&self, key: &str) -> Option<String> {
89 self.bundle.get_message(key)?.value()?;
90 Some(self.text(key))
91 }
92
93 #[must_use]
95 pub fn format(&self, key: &str, values: &[(&str, LocalizationValue<'_>)]) -> String {
96 let Some(message) = self.bundle.get_message(key) else {
97 debug_assert!(false, "missing localization key: {key}");
98 return key.to_owned();
99 };
100 let Some(pattern) = message.value() else {
101 debug_assert!(false, "localization key has no value: {key}");
102 return key.to_owned();
103 };
104
105 let mut args = FluentArgs::new();
106 for (name, value) in values {
107 let value = match value {
108 LocalizationValue::Text(value) => FluentValue::from(*value),
109 LocalizationValue::Number(value) => FluentValue::from(*value),
110 };
111 args.set(*name, value);
112 }
113
114 let mut errors = Vec::new();
115 let value = self
116 .bundle
117 .format_pattern(pattern, Some(&args), &mut errors);
118 debug_assert!(
119 errors.is_empty(),
120 "failed to format localization key {key}: {errors:?}"
121 );
122 value.into_owned()
123 }
124}
125
126#[derive(Debug)]
128pub struct LocalizationError {
129 message: String,
130}
131
132impl LocalizationError {
133 const fn new(message: String) -> Self {
134 Self { message }
135 }
136}
137
138impl fmt::Display for LocalizationError {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 formatter.write_str(&self.message)
141 }
142}
143
144impl Error for LocalizationError {}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn embedded_resources_have_matching_keys_and_are_valid() {
152 fn keys(resource: &str) -> Vec<&str> {
153 let mut keys: Vec<_> = resource
154 .lines()
155 .filter_map(|line| line.split_once(" = ").map(|(key, _)| key))
156 .collect();
157 keys.sort_unstable();
158 keys
159 }
160
161 for (en, ru) in EN_US_RESOURCES.iter().zip(RU_RU_RESOURCES) {
162 assert_eq!(keys(en), keys(ru));
163 }
164 assert!(Localizer::try_new(Locale::EnUs).is_ok());
165 assert!(Localizer::try_new(Locale::RuRu).is_ok());
166 }
167}