1use std::{
2 collections::{BTreeMap, BTreeSet},
3 rc::Rc,
4 sync::Arc,
5};
6
7use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
8use fluent_langneg::{NegotiationStrategy, negotiate_languages};
9use fluent_syntax::ast::Entry;
10use unic_langid::LanguageIdentifier;
11
12use crate::{
13 Argument, Locale, LocalizationError, Message, locale::preview_text, message::fluent_args,
14};
15
16type Resources = BTreeMap<LanguageIdentifier, BTreeMap<Arc<str>, Vec<Arc<FluentResource>>>>;
17type Prepared = BTreeMap<Arc<str>, Vec<FluentBundle<Arc<FluentResource>>>>;
18
19#[derive(Clone, Copy)]
21pub struct Resource<'a> {
22 pub locale: &'a str,
24 pub namespace: &'a str,
26 pub source: &'a str,
28}
29
30struct CatalogData {
31 fallback: Locale,
32 languages: Vec<LanguageIdentifier>,
33 resources: Resources,
34 parents: Vec<Catalog>,
35}
36
37#[derive(Clone)]
39pub struct Catalog(Arc<CatalogData>, Arc<[Language]>);
40
41#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct Language {
44 tag: Arc<str>,
45 name: Arc<str>,
46 locale: Locale,
47}
48
49impl Language {
50 pub fn tag(&self) -> &str {
52 &self.tag
53 }
54
55 pub fn name(&self) -> &str {
57 &self.name
58 }
59
60 pub fn locale(&self) -> &Locale {
62 &self.locale
63 }
64}
65
66impl PartialEq for Catalog {
67 fn eq(&self, other: &Self) -> bool {
68 Arc::ptr_eq(&self.0, &other.0) && (Arc::ptr_eq(&self.1, &other.1) || self.1 == other.1)
69 }
70}
71
72impl Catalog {
73 pub fn from_resources(
76 fallback: &str,
77 resources: &[Resource<'_>],
78 ) -> Result<Self, LocalizationError> {
79 let fallback = Locale::parse(fallback)?;
80 let mut parsed: Resources = BTreeMap::new();
81 for resource in resources {
82 let language = Locale::parse(resource.locale)?.language;
83 let source = FluentResource::try_new(resource.source.to_owned())
84 .map_err(|(_, errors)| resource_error(resource, format!("{errors:?}")))?;
85 parsed
86 .entry(language)
87 .or_default()
88 .entry(Arc::from(resource.namespace))
89 .or_default()
90 .push(Arc::new(source));
91 }
92 validate_duplicates(&parsed)?;
93 let languages: Vec<_> = parsed.keys().cloned().collect();
94 let choices = languages
95 .iter()
96 .map(|language| {
97 let tag: Arc<str> = language.to_string().into();
98 Language {
99 name: tag.clone(),
100 tag,
101 locale: Locale {
102 language: language.clone(),
103 preview: crate::PreviewMode::None,
104 },
105 }
106 })
107 .collect();
108 Ok(Self(
109 Arc::new(CatalogData {
110 fallback,
111 languages,
112 resources: parsed,
113 parents: Vec::new(),
114 }),
115 choices,
116 ))
117 }
118
119 pub fn with_languages(mut self, entries: &[(&str, &str)]) -> Result<Self, LocalizationError> {
123 let mut seen = BTreeSet::new();
124 let mut choices = Vec::with_capacity(entries.len());
125 for (tag, name) in entries {
126 let locale = Locale::parse(tag)?;
127 if name.trim().is_empty()
128 || !self.0.languages.contains(&locale.language)
129 || !seen.insert(locale.language.clone())
130 {
131 return Err(LocalizationError::InvalidResource {
132 resource: "localization.toml".into(),
133 detail: format!("invalid or duplicate language `{tag}`"),
134 });
135 }
136 choices.push(Language {
137 tag: locale.to_string().into(),
138 name: (*name).into(),
139 locale,
140 });
141 }
142 if seen.len() != self.0.languages.len() {
143 return Err(LocalizationError::InvalidResource {
144 resource: "localization.toml".into(),
145 detail: "language declaration must include every catalog language".into(),
146 });
147 }
148 self.1 = choices.into();
149 Ok(self)
150 }
151
152 pub fn languages(&self) -> &[Language] {
154 &self.1
155 }
156
157 pub fn with_fallback(self, library: &Catalog) -> Self {
160 let choices = self.1.clone();
161 let mut languages = self.0.languages.clone();
162 languages.extend(library.0.languages.iter().cloned());
163 languages.sort();
164 languages.dedup();
165 Self(
166 Arc::new(CatalogData {
167 fallback: self.0.fallback.clone(),
168 languages,
169 resources: BTreeMap::new(),
170 parents: vec![self, library.clone()],
171 }),
172 choices,
173 )
174 }
175
176 fn prepare(&self, language: &LanguageIdentifier, output: &mut Prepared) {
177 if let Some(namespaces) = self.0.resources.get(language) {
178 for (namespace, sources) in namespaces {
179 let mut bundle = crate::new_bundle(language.clone());
180 for resource in sources {
181 bundle
182 .add_resource(Arc::clone(resource))
183 .expect("catalog rejects duplicate entries");
184 }
185 output
186 .entry(Arc::clone(namespace))
187 .or_default()
188 .push(bundle);
189 }
190 }
191 for parent in &self.0.parents {
192 parent.prepare(language, output);
193 }
194 }
195
196 pub fn translator(&self, locale: Locale) -> Translator {
198 self.negotiate(std::slice::from_ref(&locale))
199 }
200
201 pub fn negotiate(&self, preferences: &[Locale]) -> Translator {
205 let requested: Vec<_> = preferences.iter().map(|locale| &locale.language).collect();
206 let languages = negotiate_languages(
207 &requested,
208 &self.0.languages,
209 Some(&self.0.fallback.language),
210 NegotiationStrategy::Filtering,
211 );
212 let prepared = languages
213 .iter()
214 .map(|language| {
215 let mut prepared = Prepared::new();
216 self.prepare(language, &mut prepared);
217 prepared
218 })
219 .collect();
220 let locale = Locale {
221 language: languages.first().map_or_else(
222 || self.0.fallback.language.clone(),
223 |language| (*language).clone(),
224 ),
225 preview: preferences.first().map_or_default(|locale| locale.preview),
226 };
227 Translator(Rc::new(TranslatorData {
228 catalog: self.clone(),
229 locale,
230 prepared,
231 preferences: preferences.to_vec(),
232 #[cfg(feature = "formatting")]
233 formatters: std::cell::OnceCell::new(),
234 }))
235 }
236
237 pub fn fallback_locale(&self) -> &Locale {
239 &self.0.fallback
240 }
241}
242
243struct TranslatorData {
244 catalog: Catalog,
245 locale: Locale,
246 prepared: Vec<Prepared>,
247 preferences: Vec<Locale>,
248 #[cfg(feature = "formatting")]
249 formatters: std::cell::OnceCell<Result<Rc<crate::LocaleFormatters>, crate::FormatError>>,
250}
251
252#[derive(Clone)]
255pub struct Translator(Rc<TranslatorData>);
256
257impl PartialEq for Translator {
258 fn eq(&self, other: &Self) -> bool {
259 Rc::ptr_eq(&self.0, &other.0)
260 || (self.0.catalog == other.0.catalog && self.0.preferences == other.0.preferences)
261 }
262}
263
264impl Translator {
265 pub fn formatting_locale(&self) -> &Locale {
269 self.0
270 .preferences
271 .iter()
272 .find(|locale| locale.language.language == self.0.locale.language.language)
273 .unwrap_or(&self.0.locale)
274 }
275
276 #[cfg(feature = "formatting")]
278 pub fn formatters(&self) -> Result<Rc<crate::LocaleFormatters>, crate::FormatError> {
279 self.0
280 .formatters
281 .get_or_init(|| crate::LocaleFormatters::new(self.formatting_locale()).map(Rc::new))
282 .clone()
283 }
284 pub fn locale(&self) -> &Locale {
287 &self.0.locale
288 }
289
290 pub fn with_fallback(&self, library: &Catalog) -> Self {
292 self.0
293 .catalog
294 .clone()
295 .with_fallback(library)
296 .negotiate(&self.0.preferences)
297 }
298
299 pub fn format(
302 &self,
303 message: &Message,
304 arguments: &[Argument<'_>],
305 ) -> Result<String, LocalizationError> {
306 let args = fluent_args(arguments);
307 let text = self
308 .translated(message, &args)
309 .map_or_else(|| message.source(&args), Ok)?;
310 Ok(preview_text(text, self.0.locale.preview))
311 }
312
313 fn translated(&self, message: &Message, args: &FluentArgs<'_>) -> Option<String> {
314 for prepared in &self.0.prepared {
315 let Some(bundles) = prepared.get(message.namespace()) else {
316 continue;
317 };
318 for bundle in bundles {
319 let Some(pattern) = bundle.get_message(message.id()).and_then(|msg| msg.value())
320 else {
321 continue;
322 };
323 let mut errors = Vec::new();
324 let text = bundle.format_pattern(pattern, Some(args), &mut errors);
325 if errors.is_empty() {
326 return Some(text.into_owned());
327 }
328 }
329 }
330 None
331 }
332}
333
334fn validate_duplicates(resources: &Resources) -> Result<(), LocalizationError> {
335 for (language, namespaces) in resources {
336 for (namespace, sources) in namespaces {
337 let mut identifiers = BTreeSet::from(["NUMBER"]);
338 for source in sources {
339 for entry in source.entries() {
340 let key = match entry {
341 Entry::Message(message) => message.id.name,
342 Entry::Term(term) => term.id.name,
343 _ => continue,
344 };
345 if !identifiers.insert(key) {
346 return Err(LocalizationError::InvalidResource {
347 resource: format!("{language}/{namespace}"),
348 detail: format!("duplicate or reserved entry `{key}`"),
349 });
350 }
351 }
352 }
353 }
354 }
355 Ok(())
356}
357
358fn resource_error(resource: &Resource<'_>, detail: String) -> LocalizationError {
359 LocalizationError::InvalidResource {
360 resource: format!("{}/{}", resource.locale, resource.namespace),
361 detail,
362 }
363}