finance_query/translation/backend.rs
1//! Pluggable machine-translation backend.
2
3use std::sync::{Arc, OnceLock, RwLock};
4
5use async_trait::async_trait;
6
7use super::lang::Lang;
8use crate::error::Result;
9
10/// A machine-translation backend for free-form text.
11///
12/// Implement this to plug a custom engine (e.g. a hosted translation API)
13/// into the translation pipeline via [`set_backend`](super::set_backend).
14/// The built-in offline backend (feature `translation-offline`) is used by
15/// default when no custom backend is registered.
16///
17/// Inputs are English; implementations must return one translated string per
18/// input, preserving order. Inputs may contain multiple sentences.
19#[async_trait]
20pub trait TranslationBackend: Send + Sync {
21 /// Identifier used in logs and errors.
22 fn id(&self) -> &'static str {
23 "custom"
24 }
25
26 /// Whether this backend can translate into `target`.
27 ///
28 /// The pipeline calls this before [`translate_batch`](Self::translate_batch);
29 /// when it returns `false` the free-form text is left untranslated (English)
30 /// instead of failing the request. Defaults to `true` (assume full support).
31 fn supports(&self, target: &Lang) -> bool {
32 let _ = target;
33 true
34 }
35
36 /// Translate every English text into the target language, preserving order.
37 async fn translate_batch(&self, texts: &[String], target: &Lang) -> Result<Vec<String>>;
38}
39
40fn registry() -> &'static RwLock<Option<Arc<dyn TranslationBackend>>> {
41 static REGISTRY: OnceLock<RwLock<Option<Arc<dyn TranslationBackend>>>> = OnceLock::new();
42 REGISTRY.get_or_init(|| RwLock::new(None))
43}
44
45/// Register a custom translation backend for the whole process.
46///
47/// Takes precedence over the built-in offline backend. Free-form text fields
48/// are left untranslated when no backend is available (built-in dictionary
49/// terms are still translated).
50pub fn set_backend(backend: Arc<dyn TranslationBackend>) {
51 if let Ok(mut slot) = registry().write() {
52 *slot = Some(backend);
53 }
54}
55
56/// Resolve the active backend: custom first, then the built-in offline
57/// backend when the `translation-offline` feature is enabled.
58pub(crate) fn active_backend() -> Option<Arc<dyn TranslationBackend>> {
59 if let Ok(slot) = registry().read()
60 && let Some(backend) = slot.as_ref()
61 {
62 return Some(backend.clone());
63 }
64 #[cfg(feature = "translation-offline")]
65 {
66 Some(super::opusmt::shared())
67 }
68 #[cfg(not(feature = "translation-offline"))]
69 None
70}