Skip to main content

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}