1use crate::{Culture, TranslationError};
2use std::{
3 cell::{Cell, RefCell},
4 collections::HashMap,
5 rc::Rc,
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ProTranslateDiagnosticKind {
10 MissingTranslation,
11 ProviderFailure,
12 FormatFailure,
13 RegionProfileFailure,
14}
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ProTranslateDiagnosticSeverity {
17 Information,
18 Warning,
19 Error,
20}
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ProTranslateDiagnostic {
23 pub kind: ProTranslateDiagnosticKind,
24 pub severity: ProTranslateDiagnosticSeverity,
25 pub message: String,
26 pub key: Option<String>,
27 pub culture: Option<Culture>,
28 pub provider_name: Option<String>,
29 pub error: Option<TranslationError>,
30}
31impl ProTranslateDiagnostic {
32 pub(crate) fn provider(
33 key: &str,
34 culture: &Culture,
35 provider: &str,
36 error: TranslationError,
37 ) -> Self {
38 Self {
39 kind: ProTranslateDiagnosticKind::ProviderFailure,
40 severity: ProTranslateDiagnosticSeverity::Error,
41 message: format!("Provider '{provider}' failed for '{key}' in '{culture}': {error}"),
42 key: Some(key.into()),
43 culture: Some(culture.clone()),
44 provider_name: Some(provider.into()),
45 error: Some(error),
46 }
47 }
48}
49pub trait ProTranslateDiagnosticSink {
50 fn report(&self, diagnostic: &ProTranslateDiagnostic);
51}
52impl<F: Fn(&ProTranslateDiagnostic)> ProTranslateDiagnosticSink for F {
53 fn report(&self, diagnostic: &ProTranslateDiagnostic) {
54 self(diagnostic);
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct LocalizedString {
60 pub key: String,
61 pub value: String,
62 pub culture: Culture,
63 pub resource_not_found: bool,
64 pub provider_name: String,
65 pub diagnostics: Vec<ProTranslateDiagnostic>,
66}
67impl LocalizedString {
68 pub fn found(
69 key: impl Into<String>,
70 value: impl Into<String>,
71 culture: Culture,
72 provider_name: impl Into<String>,
73 ) -> Self {
74 Self {
75 key: key.into(),
76 value: value.into(),
77 culture,
78 resource_not_found: false,
79 provider_name: provider_name.into(),
80 diagnostics: Vec::new(),
81 }
82 }
83 pub fn missing(
84 key: impl Into<String>,
85 culture: Culture,
86 provider_name: impl Into<String>,
87 ) -> Self {
88 let key = key.into();
89 Self {
90 value: key.clone(),
91 key,
92 culture,
93 resource_not_found: true,
94 provider_name: provider_name.into(),
95 diagnostics: Vec::new(),
96 }
97 }
98}
99impl std::fmt::Display for LocalizedString {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.write_str(&self.value)
102 }
103}
104pub trait TranslationProvider {
107 fn name(&self) -> &str;
108 fn get_string(&self, key: &str, culture: &Culture)
109 -> Result<LocalizedString, TranslationError>;
110 fn revision(&self) -> u64 {
113 0
114 }
115}
116pub struct InMemoryTranslationProvider {
117 name: String,
118 entries: RefCell<HashMap<(Culture, String), String>>,
119 revision: Cell<u64>,
120}
121impl Default for InMemoryTranslationProvider {
122 fn default() -> Self {
123 Self {
124 name: "InMemory".into(),
125 entries: RefCell::default(),
126 revision: Cell::new(0),
127 }
128 }
129}
130pub fn validate_translation_key(key: &str) -> Result<(), TranslationError> {
133 if key.trim().is_empty() {
134 Err(TranslationError::InvalidKey)
135 } else {
136 Ok(())
137 }
138}
139fn validate_provider_name(name: &str) -> Result<(), TranslationError> {
140 if name.trim().is_empty() {
141 Err(TranslationError::InvalidProviderName)
142 } else {
143 Ok(())
144 }
145}
146impl InMemoryTranslationProvider {
147 pub fn new(name: impl Into<String>) -> Self {
152 Self::try_new(name).expect("a translation provider must have a nonempty name")
153 }
154 pub fn try_new(name: impl Into<String>) -> Result<Self, TranslationError> {
155 let name = name.into();
156 validate_provider_name(&name)?;
157 Ok(Self {
158 name,
159 ..Self::default()
160 })
161 }
162 pub fn insert(
167 &self,
168 culture: Culture,
169 key: impl Into<String>,
170 value: impl Into<String>,
171 ) -> Option<String> {
172 self.try_insert(culture, key, value)
173 .expect("a translation key must not be empty or whitespace")
174 }
175 pub fn try_insert(
176 &self,
177 culture: Culture,
178 key: impl Into<String>,
179 value: impl Into<String>,
180 ) -> Result<Option<String>, TranslationError> {
181 let key = key.into();
182 validate_translation_key(&key)?;
183 let result = self
184 .entries
185 .borrow_mut()
186 .insert((culture, key), value.into());
187 self.revision.set(self.revision.get().wrapping_add(1));
188 Ok(result)
189 }
190 pub fn insert_many<K: Into<String>, V: Into<String>>(
193 &self,
194 culture: Culture,
195 translations: impl IntoIterator<Item = (K, V)>,
196 ) -> Result<(), TranslationError> {
197 let translations = translations
198 .into_iter()
199 .map(|(key, value)| {
200 let key = key.into();
201 validate_translation_key(&key)?;
202 Ok((key, value.into()))
203 })
204 .collect::<Result<Vec<_>, TranslationError>>()?;
205 if !translations.is_empty() {
206 self.entries.borrow_mut().extend(
207 translations
208 .into_iter()
209 .map(|(key, value)| ((culture.clone(), key), value)),
210 );
211 self.revision.set(self.revision.get().wrapping_add(1));
212 }
213 Ok(())
214 }
215 pub fn remove(&self, culture: &Culture, key: &str) -> Option<String> {
216 let result = self
217 .entries
218 .borrow_mut()
219 .remove(&(culture.clone(), key.into()));
220 if result.is_some() {
221 self.revision.set(self.revision.get().wrapping_add(1));
222 }
223 result
224 }
225 pub fn len(&self) -> usize {
226 self.entries.borrow().len()
227 }
228 pub fn is_empty(&self) -> bool {
229 self.entries.borrow().is_empty()
230 }
231 pub fn clear(&self) {
232 self.entries.borrow_mut().clear();
233 self.revision.set(self.revision.get().wrapping_add(1));
234 }
235}
236impl TranslationProvider for InMemoryTranslationProvider {
237 fn name(&self) -> &str {
238 &self.name
239 }
240 fn get_string(
241 &self,
242 key: &str,
243 culture: &Culture,
244 ) -> Result<LocalizedString, TranslationError> {
245 validate_translation_key(key)?;
246 Ok(
247 match self.entries.borrow().get(&(culture.clone(), key.into())) {
248 Some(value) => LocalizedString::found(key, value, culture.clone(), &self.name),
249 None => LocalizedString::missing(key, culture.clone(), &self.name),
250 },
251 )
252 }
253 fn revision(&self) -> u64 {
254 self.revision.get()
255 }
256}
257#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
258pub enum TranslationProviderFailureBehavior {
259 #[default]
260 ReportAndContinue,
261 ReportAndReturnError,
262}
263#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
264pub enum TranslationFormatFailureBehavior {
265 #[default]
266 ReportAndReturnUnformatted,
267 ReportAndReturnError,
268}
269pub struct CompositeTranslationProvider {
272 providers: Vec<Rc<dyn TranslationProvider>>,
273 failure_behavior: TranslationProviderFailureBehavior,
274 child_revisions: RefCell<Vec<u64>>,
275 revision: Cell<u64>,
276}
277impl CompositeTranslationProvider {
278 pub fn new(providers: Vec<Rc<dyn TranslationProvider>>) -> Self {
279 Self {
280 providers,
281 failure_behavior: Default::default(),
282 child_revisions: RefCell::default(),
283 revision: Cell::new(0),
284 }
285 }
286 pub fn with_failure_behavior(mut self, behavior: TranslationProviderFailureBehavior) -> Self {
287 self.failure_behavior = behavior;
288 self
289 }
290 pub fn providers(&self) -> &[Rc<dyn TranslationProvider>] {
291 &self.providers
292 }
293}
294impl TranslationProvider for CompositeTranslationProvider {
295 fn name(&self) -> &str {
296 "Composite"
297 }
298 fn get_string(
299 &self,
300 key: &str,
301 culture: &Culture,
302 ) -> Result<LocalizedString, TranslationError> {
303 validate_translation_key(key)?;
304 let mut diagnostics = Vec::new();
305 for provider in &self.providers {
306 match provider.get_string(key, culture) {
307 Ok(mut localized) => {
308 diagnostics.append(&mut localized.diagnostics);
309 if !localized.resource_not_found {
310 localized.diagnostics = diagnostics;
311 return Ok(localized);
312 }
313 }
314 Err(error) => {
315 if self.failure_behavior
316 == TranslationProviderFailureBehavior::ReportAndReturnError
317 {
318 return Err(error);
319 }
320 diagnostics.push(ProTranslateDiagnostic::provider(
321 key,
322 culture,
323 provider.name(),
324 error,
325 ));
326 }
327 }
328 }
329 let mut missing = LocalizedString::missing(key, culture.clone(), self.name());
330 missing.diagnostics = diagnostics;
331 Ok(missing)
332 }
333 fn revision(&self) -> u64 {
334 let revisions: Vec<_> = self.providers.iter().map(|p| p.revision()).collect();
337 let mut previous = self.child_revisions.borrow_mut();
338 if *previous != revisions {
339 *previous = revisions;
340 self.revision.set(self.revision.get().wrapping_add(1));
341 }
342 self.revision.get()
343 }
344}
345
346pub struct CallbackTranslationProvider<F> {
351 name: String,
352 callback: F,
353 revision: Cell<u64>,
354}
355impl<F> CallbackTranslationProvider<F>
356where
357 F: Fn(&str, &Culture) -> Result<Option<String>, TranslationError>,
358{
359 pub fn new(name: impl Into<String>, callback: F) -> Self {
360 Self::try_new(name, callback).expect("a translation provider must have a nonempty name")
361 }
362 pub fn try_new(name: impl Into<String>, callback: F) -> Result<Self, TranslationError> {
363 let name = name.into();
364 validate_provider_name(&name)?;
365 Ok(Self {
366 name,
367 callback,
368 revision: Cell::new(0),
369 })
370 }
371 pub fn invalidate(&self) {
372 self.revision.set(self.revision.get().wrapping_add(1));
373 }
374}
375impl<F> TranslationProvider for CallbackTranslationProvider<F>
376where
377 F: Fn(&str, &Culture) -> Result<Option<String>, TranslationError>,
378{
379 fn name(&self) -> &str {
380 &self.name
381 }
382 fn get_string(
383 &self,
384 key: &str,
385 culture: &Culture,
386 ) -> Result<LocalizedString, TranslationError> {
387 validate_translation_key(key)?;
388 Ok(match (self.callback)(key, culture)? {
389 Some(value) => LocalizedString::found(key, value, culture.clone(), &self.name),
390 None => LocalizedString::missing(key, culture.clone(), &self.name),
391 })
392 }
393 fn revision(&self) -> u64 {
394 self.revision.get()
395 }
396}