1use crate::event::Event;
2use crate::*;
3use std::{
4 cell::{Cell, RefCell},
5 collections::{HashMap, HashSet, VecDeque},
6 rc::Rc,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct TranslationFallbackOptions {
11 pub default_culture: Culture,
12 pub fallback_cultures: Vec<Culture>,
13 pub use_parent_cultures: bool,
14 pub use_default_culture: bool,
15 pub return_key_when_missing: bool,
16 pub provider_failure_behavior: TranslationProviderFailureBehavior,
17 pub format_failure_behavior: TranslationFormatFailureBehavior,
18}
19impl Default for TranslationFallbackOptions {
20 fn default() -> Self {
21 Self {
22 default_culture: Culture::invariant(),
23 fallback_cultures: Vec::new(),
24 use_parent_cultures: true,
25 use_default_culture: true,
26 return_key_when_missing: true,
27 provider_failure_behavior: Default::default(),
28 format_failure_behavior: Default::default(),
29 }
30 }
31}
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct TranslationCacheOptions {
34 pub enabled: bool,
35 pub cache_missing_translations: bool,
36 pub clear_on_culture_changed: bool,
37 pub maximum_entries: usize,
39}
40impl Default for TranslationCacheOptions {
41 fn default() -> Self {
42 Self {
43 enabled: true,
44 cache_missing_translations: true,
45 clear_on_culture_changed: true,
46 maximum_entries: 0,
47 }
48 }
49}
50type CacheKey = (Culture, String);
51#[derive(Default)]
52struct Cache {
53 values: HashMap<CacheKey, LocalizedString>,
54 order: VecDeque<CacheKey>,
55 provider_revision: u64,
56 epoch: u64,
57}
58impl Cache {
59 fn clear(&mut self) {
60 self.epoch = self.epoch.wrapping_add(1);
61 self.values.clear();
62 self.order.clear();
63 }
64}
65struct TranslationServiceInner {
66 provider: Rc<dyn TranslationProvider>,
67 cultures: CultureService,
68 generation: Cell<u64>,
69 options: RefCell<TranslationFallbackOptions>,
70 cache_options: RefCell<TranslationCacheOptions>,
71 cache: RefCell<Cache>,
72 changed: Event<CultureChangedEventArgs>,
73 diagnostics: Event<ProTranslateDiagnostic>,
74 diagnostic_sink: RefCell<Option<Rc<dyn ProTranslateDiagnosticSink>>>,
75 _culture_subscription: Subscription,
76}
77#[derive(Clone)]
78pub struct TranslationService(Rc<TranslationServiceInner>);
79impl TranslationService {
80 pub fn new(provider: Rc<dyn TranslationProvider>, cultures: CultureService) -> Self {
81 Self::with_options(provider, cultures, Default::default(), Default::default())
82 }
83 pub fn with_options(
84 provider: Rc<dyn TranslationProvider>,
85 cultures: CultureService,
86 options: TranslationFallbackOptions,
87 cache_options: TranslationCacheOptions,
88 ) -> Self {
89 Self(Rc::new_cyclic(
90 |weak: &std::rc::Weak<TranslationServiceInner>| {
91 let weak = weak.clone();
92 let subscription = cultures.subscribe(move |event| {
93 if let Some(service) = weak.upgrade() {
94 service
95 .generation
96 .set(service.generation.get().wrapping_add(1));
97 if service.cache_options.borrow().clear_on_culture_changed {
98 service.cache.borrow_mut().clear();
99 }
100 service.changed.emit(event.clone());
101 }
102 });
103 TranslationServiceInner {
104 provider,
105 cultures,
106 generation: Cell::new(0),
107 options: RefCell::new(options),
108 cache_options: RefCell::new(cache_options),
109 cache: RefCell::new(Cache::default()),
110 changed: Event::default(),
111 diagnostics: Event::default(),
112 diagnostic_sink: RefCell::new(None),
113 _culture_subscription: subscription,
114 }
115 },
116 ))
117 }
118 pub fn cultures(&self) -> CultureService {
119 self.0.cultures.clone()
120 }
121 pub fn current_culture(&self) -> Culture {
122 self.0.cultures.current_culture()
123 }
124 pub fn current_ui_culture(&self) -> Culture {
125 self.0.cultures.current_ui_culture()
126 }
127 pub fn options(&self) -> TranslationFallbackOptions {
128 self.0.options.borrow().clone()
129 }
130 pub fn set_options(&self, options: TranslationFallbackOptions) {
131 if *self.0.options.borrow() == options {
132 return;
133 }
134 *self.0.options.borrow_mut() = options;
135 self.refresh();
136 }
137 pub fn set_cache_options(&self, options: TranslationCacheOptions) {
138 *self.0.cache_options.borrow_mut() = options;
139 self.clear_cache();
140 }
141 pub fn set_diagnostic_sink(&self, sink: Option<Rc<dyn ProTranslateDiagnosticSink>>) {
142 *self.0.diagnostic_sink.borrow_mut() = sink;
143 }
144 pub fn subscribe(&self, callback: impl Fn(&CultureChangedEventArgs) + 'static) -> Subscription {
145 self.0.changed.subscribe(callback)
146 }
147 pub fn subscribe_diagnostics(
148 &self,
149 callback: impl Fn(&ProTranslateDiagnostic) + 'static,
150 ) -> Subscription {
151 self.0.diagnostics.subscribe(callback)
152 }
153 pub fn subscriber_count(&self) -> usize {
154 self.0.changed.listener_count()
155 }
156 pub fn clear_cache(&self) {
157 self.0.cache.borrow_mut().clear();
158 }
159 pub fn refresh(&self) {
162 self.0
163 .generation
164 .set(self.0.generation.get().wrapping_add(1));
165 self.clear_cache();
166 let snapshot = self.0.cultures.snapshot();
167 self.0.changed.emit(CultureChangedEventArgs {
168 old: snapshot.clone(),
169 new: snapshot,
170 });
171 }
172 pub fn cached_lookup_count(&self) -> usize {
173 self.0.cache.borrow().values.len()
174 }
175 pub fn remove_cached_string(&self, key: &str, culture: &Culture) -> bool {
176 let cache_key = (culture.clone(), key.into());
177 let mut cache = self.0.cache.borrow_mut();
178 cache.epoch = cache.epoch.wrapping_add(1);
181 let removed = cache.values.remove(&cache_key).is_some();
182 if removed {
183 cache.order.retain(|key| key != &cache_key);
184 }
185 removed
186 }
187 pub fn fallback_cultures(&self, culture: &Culture) -> Vec<Culture> {
188 let options = self.options();
189 Self::fallback_cultures_with(culture, &options)
190 }
191 fn fallback_cultures_with(
192 culture: &Culture,
193 options: &TranslationFallbackOptions,
194 ) -> Vec<Culture> {
195 let mut seen = HashSet::new();
196 let mut result = Vec::new();
197 let mut append = |culture: &Culture| {
198 let mut current = Some(culture.clone());
199 while let Some(culture) = current {
200 if seen.insert(culture.clone()) {
201 result.push(culture.clone());
202 }
203 current = if options.use_parent_cultures {
204 culture.parent()
205 } else {
206 None
207 };
208 }
209 };
210 append(culture);
211 for fallback in &options.fallback_cultures {
212 append(fallback);
213 }
214 if options.use_default_culture {
215 append(&options.default_culture);
216 }
217 result
218 }
219 pub fn get_string(&self, key: &str) -> Result<LocalizedString, TranslationError> {
220 self.get_string_for(key, &self.current_ui_culture())
221 }
222 pub fn get_string_for(
223 &self,
224 key: &str,
225 culture: &Culture,
226 ) -> Result<LocalizedString, TranslationError> {
227 validate_translation_key(key)?;
228 let cache_options = self.0.cache_options.borrow().clone();
229 let cache_key = (culture.clone(), key.into());
230 let provider_revision = self.0.provider.revision();
231 let generation = self.0.generation.get();
232 if cache_options.enabled {
233 let mut cache = self.0.cache.borrow_mut();
234 if cache.provider_revision != provider_revision {
235 cache.clear();
236 cache.provider_revision = provider_revision;
237 }
238 if let Some(value) = cache.values.get(&cache_key) {
239 return Ok(value.clone());
240 }
241 }
242 let cache_epoch = self.0.cache.borrow().epoch;
243 let options = self.options();
244 let mut all_diagnostics = Vec::new();
245 let mut result = None;
246 for candidate in Self::fallback_cultures_with(culture, &options) {
247 match self.0.provider.get_string(key, &candidate) {
248 Ok(mut localized) => {
249 for diagnostic in &localized.diagnostics {
250 self.report(diagnostic.clone());
251 }
252 all_diagnostics.append(&mut localized.diagnostics);
253 if !localized.resource_not_found {
254 localized.diagnostics = std::mem::take(&mut all_diagnostics);
255 result = Some(localized);
256 break;
257 }
258 }
259 Err(error) => {
260 let diagnostic = ProTranslateDiagnostic::provider(
261 key,
262 &candidate,
263 self.0.provider.name(),
264 error.clone(),
265 );
266 self.report(diagnostic.clone());
267 all_diagnostics.push(diagnostic);
268 if options.provider_failure_behavior
269 == TranslationProviderFailureBehavior::ReportAndReturnError
270 {
271 return Err(error);
272 }
273 }
274 }
275 }
276 let resolved = result.unwrap_or_else(|| {
277 let diagnostic = ProTranslateDiagnostic {
278 kind: ProTranslateDiagnosticKind::MissingTranslation,
279 severity: ProTranslateDiagnosticSeverity::Warning,
280 message: format!("Translation '{key}' was not found for culture '{culture}'"),
281 key: Some(key.into()),
282 culture: Some(culture.clone()),
283 provider_name: Some(self.0.provider.name().into()),
284 error: None,
285 };
286 self.report(diagnostic.clone());
287 all_diagnostics.push(diagnostic);
288 let mut localized =
289 LocalizedString::missing(key, culture.clone(), self.0.provider.name());
290 if !options.return_key_when_missing {
291 localized.value.clear();
292 }
293 localized.diagnostics = all_diagnostics;
294 localized
295 });
296 if cache_options.enabled
297 && (!resolved.resource_not_found || cache_options.cache_missing_translations)
298 {
299 if self.0.provider.revision() == provider_revision
302 && self.options() == options
303 && self.0.generation.get() == generation
304 && *self.0.cache_options.borrow() == cache_options
305 {
306 let mut cache = self.0.cache.borrow_mut();
307 if cache.epoch != cache_epoch {
308 return Ok(resolved);
309 }
310 let is_new = !cache.values.contains_key(&cache_key);
311 if is_new && cache_options.maximum_entries > 0 {
312 while cache.values.len() >= cache_options.maximum_entries {
313 if let Some(old) = cache.order.pop_front() {
314 cache.values.remove(&old);
315 } else {
316 break;
317 }
318 }
319 }
320 if is_new {
321 cache.order.push_back(cache_key.clone());
322 }
323 cache.values.insert(cache_key, resolved.clone());
324 }
325 }
326 Ok(resolved)
327 }
328 pub fn format(
329 &self,
330 key: &str,
331 arguments: &[FormatArgument],
332 ) -> Result<String, TranslationError> {
333 let snapshot = self.0.cultures.snapshot();
334 let localized = self.get_string_for(key, &snapshot.ui_culture)?;
335 if arguments.is_empty() {
336 return Ok(localized.value);
337 }
338 self.format_localized(localized, arguments, &snapshot.culture)
339 }
340 pub fn format_for(
341 &self,
342 key: &str,
343 arguments: &[FormatArgument],
344 culture: &Culture,
345 ) -> Result<String, TranslationError> {
346 let localized = self.get_string_for(key, culture)?;
347 if arguments.is_empty() {
348 return Ok(localized.value);
349 }
350 self.format_localized(localized, arguments, culture)
351 }
352 pub fn format_localized(
353 &self,
354 localized: LocalizedString,
355 arguments: &[FormatArgument],
356 culture: &Culture,
357 ) -> Result<String, TranslationError> {
358 let failure_behavior = self.options().format_failure_behavior;
359 match format_composite(&localized.value, arguments, culture) {
360 Ok(value) => Ok(value),
361 Err(error) => {
362 self.report(ProTranslateDiagnostic {
363 kind: ProTranslateDiagnosticKind::FormatFailure,
364 severity: ProTranslateDiagnosticSeverity::Error,
365 message: format!(
366 "Localized format '{}' failed for '{}': {error}",
367 localized.key, culture
368 ),
369 key: Some(localized.key),
370 culture: Some(culture.clone()),
371 provider_name: Some(localized.provider_name),
372 error: Some(error.clone()),
373 });
374 if failure_behavior == TranslationFormatFailureBehavior::ReportAndReturnError {
375 Err(error)
376 } else {
377 Ok(localized.value)
378 }
379 }
380 }
381 }
382 pub fn format_value(
385 &self,
386 key: &str,
387 template: &str,
388 arguments: &[FormatArgument],
389 culture: &Culture,
390 ) -> Result<String, TranslationError> {
391 self.format_localized(
392 LocalizedString::found(key, template, culture.clone(), self.0.provider.name()),
393 arguments,
394 culture,
395 )
396 }
397 pub fn observe(
398 &self,
399 key: impl Into<String>,
400 arguments: Vec<FormatArgument>,
401 ) -> ObservableLocalizedString {
402 self.try_observe(key, arguments)
403 .expect("a localized observation must have a nonempty key")
404 }
405 pub fn try_observe(
406 &self,
407 key: impl Into<String>,
408 arguments: Vec<FormatArgument>,
409 ) -> Result<ObservableLocalizedString, TranslationError> {
410 let key = key.into();
411 validate_translation_key(&key)?;
412 Ok(ObservableLocalizedString::new(self.clone(), key, arguments))
413 }
414 pub(crate) fn report(&self, diagnostic: ProTranslateDiagnostic) {
415 let sink = self.0.diagnostic_sink.borrow().clone();
416 if let Some(sink) = sink {
417 sink.report(&diagnostic);
418 }
419 self.0.diagnostics.emit(diagnostic);
420 }
421}
422#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct ObservableLocalizedSnapshot {
426 pub value: Result<String, TranslationError>,
427 pub localized_string: Result<LocalizedString, TranslationError>,
428 pub cultures: CultureSnapshot,
429}
430struct ObservableInner {
431 generation: Cell<u64>,
432 service: TranslationService,
433 key: String,
434 arguments: RefCell<Vec<FormatArgument>>,
435 snapshot: RefCell<ObservableLocalizedSnapshot>,
436 changed: Event<ObservableLocalizedSnapshot>,
437 subscription: RefCell<Option<Subscription>>,
438}
439#[derive(Clone)]
440pub struct ObservableLocalizedString(Rc<ObservableInner>);
441impl ObservableLocalizedString {
442 fn new(service: TranslationService, key: String, arguments: Vec<FormatArgument>) -> Self {
443 let snapshot = ObservableLocalizedSnapshot {
444 value: Err(TranslationError::InvalidKey),
445 localized_string: Err(TranslationError::InvalidKey),
446 cultures: service.cultures().snapshot(),
447 };
448 let observable = Self(Rc::new_cyclic(|weak: &std::rc::Weak<ObservableInner>| {
449 let weak = weak.clone();
450 let subscription = service.subscribe(move |_| {
451 if let Some(inner) = weak.upgrade() {
452 Self(inner).refresh();
453 }
454 });
455 ObservableInner {
456 generation: Cell::new(0),
457 service,
458 key,
459 arguments: RefCell::new(arguments),
460 snapshot: RefCell::new(snapshot),
461 changed: Event::default(),
462 subscription: RefCell::new(Some(subscription)),
463 }
464 }));
465 observable.refresh();
466 observable
467 }
468 pub fn key(&self) -> &str {
469 &self.0.key
470 }
471 pub fn value(&self) -> Result<String, TranslationError> {
472 self.0.snapshot.borrow().value.clone()
473 }
474 pub fn snapshot(&self) -> ObservableLocalizedSnapshot {
475 self.0.snapshot.borrow().clone()
476 }
477 pub fn localized_string(&self) -> Result<LocalizedString, TranslationError> {
478 self.0.snapshot.borrow().localized_string.clone()
479 }
480 pub fn culture(&self) -> Result<Culture, TranslationError> {
483 self.localized_string().map(|localized| localized.culture)
484 }
485 pub fn set_arguments(&self, arguments: Vec<FormatArgument>) {
491 self.update_arguments(arguments)
492 .expect("cannot update a disposed localized observation");
493 }
494 pub fn update_arguments(&self, arguments: Vec<FormatArgument>) -> Result<(), TranslationError> {
495 if self.is_disposed() {
496 return Err(TranslationError::Disposed);
497 }
498 *self.0.arguments.borrow_mut() = arguments;
499 self.refresh();
500 Ok(())
501 }
502 pub fn dispose(&self) {
505 self.0
506 .generation
507 .set(self.0.generation.get().wrapping_add(1));
508 self.0.subscription.borrow_mut().take();
509 }
510 pub fn is_disposed(&self) -> bool {
511 self.0.subscription.borrow().is_none()
512 }
513 pub fn refresh(&self) {
514 if self.is_disposed() {
515 return;
516 }
517 let observation_generation = self.0.generation.get().wrapping_add(1);
518 self.0.generation.set(observation_generation);
519 let arguments = self.0.arguments.borrow().clone();
520 let generation = self.0.service.0.generation.get();
521 let cultures = self.0.service.cultures().snapshot();
522 let localized_string = self
523 .0
524 .service
525 .get_string_for(&self.0.key, &cultures.ui_culture);
526 let value = localized_string.clone().and_then(|localized| {
527 if arguments.is_empty() {
528 Ok(localized.value)
529 } else {
530 self.0
531 .service
532 .format_localized(localized, &arguments, &cultures.culture)
533 }
534 });
535 if self.0.service.0.generation.get() != generation
538 || self.0.generation.get() != observation_generation
539 {
540 return;
541 }
542 let snapshot = ObservableLocalizedSnapshot {
543 value,
544 localized_string,
545 cultures,
546 };
547 *self.0.snapshot.borrow_mut() = snapshot.clone();
548 self.0.changed.emit(snapshot);
549 }
550 pub fn subscribe(
551 &self,
552 callback: impl Fn(&Result<String, TranslationError>) + 'static,
553 ) -> Subscription {
554 self.0
555 .changed
556 .subscribe(move |snapshot| callback(&snapshot.value))
557 }
558 pub fn subscribe_snapshot(
561 &self,
562 callback: impl Fn(&ObservableLocalizedSnapshot) + 'static,
563 ) -> Subscription {
564 self.0.changed.subscribe(callback)
565 }
566}