Skip to main content

es_fluent/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[doc(hidden)]
4pub mod meta;
5
6#[doc(hidden)]
7pub mod registry;
8
9#[cfg(feature = "derive")]
10pub use es_fluent_derive::{EsFluent, EsFluentChoice, EsFluentThis, EsFluentVariants};
11
12#[doc(hidden)]
13pub use fluent_bundle::FluentValue;
14
15#[doc(hidden)]
16pub use inventory as __inventory;
17
18#[doc(hidden)]
19pub use rust_embed as __rust_embed;
20
21#[doc(hidden)]
22pub use es_fluent_manager_core as __manager_core;
23
24#[doc(hidden)]
25pub use unic_langid;
26
27use arc_swap::ArcSwap;
28use es_fluent_manager_core::FluentManager;
29use es_fluent_shared::EsFluentError;
30use parking_lot::RwLock;
31use std::sync::{Arc, OnceLock};
32
33#[cfg(feature = "build")]
34pub mod build {
35    pub use es_fluent_toml::build::*;
36}
37
38mod traits;
39pub use traits::{EsFluentChoice, FluentDisplay, ThisFtl, ToFluentString};
40
41#[doc(hidden)]
42static CONTEXT: OnceLock<ArcSwap<FluentManager>> = OnceLock::new();
43
44#[doc(hidden)]
45type DomainAwareCustomLocalizer = dyn for<'a> Fn(
46        Option<&str>,
47        &str,
48        Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
49    ) -> Option<String>
50    + Send
51    + Sync;
52
53#[doc(hidden)]
54static CUSTOM_LOCALIZER: OnceLock<RwLock<Option<Arc<DomainAwareCustomLocalizer>>>> =
55    OnceLock::new();
56
57fn custom_localizer_slot() -> &'static RwLock<Option<Arc<DomainAwareCustomLocalizer>>> {
58    CUSTOM_LOCALIZER.get_or_init(|| RwLock::new(None))
59}
60
61fn try_custom_localizer<'a>(
62    id: &str,
63    args: Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
64) -> Option<String> {
65    CUSTOM_LOCALIZER
66        .get()
67        .and_then(|slot| slot.read().clone())
68        .and_then(|custom_localizer| custom_localizer(None, id, args))
69}
70
71fn try_custom_localizer_in_domain<'a>(
72    domain: &str,
73    id: &str,
74    args: Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
75) -> Option<String> {
76    CUSTOM_LOCALIZER
77        .get()
78        .and_then(|slot| slot.read().clone())
79        .and_then(|custom_localizer| custom_localizer(Some(domain), id, args))
80}
81
82#[derive(Debug)]
83pub enum GlobalLocalizationError {
84    ContextAlreadyInitialized,
85    ContextNotInitialized,
86    CustomLocalizerAlreadyInitialized,
87    LanguageSelectionFailed(EsFluentError),
88}
89
90impl std::fmt::Display for GlobalLocalizationError {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Self::ContextAlreadyInitialized => {
94                f.write_str("the global FluentManager context is already initialized")
95            },
96            Self::ContextNotInitialized => {
97                f.write_str("the global FluentManager context is not initialized")
98            },
99            Self::CustomLocalizerAlreadyInitialized => {
100                f.write_str("the global custom localizer is already initialized")
101            },
102            Self::LanguageSelectionFailed(source) => {
103                write!(f, "failed to select the requested language: {source}")
104            },
105        }
106    }
107}
108
109impl std::error::Error for GlobalLocalizationError {
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        match self {
112            Self::LanguageSelectionFailed(source) => Some(source),
113            _ => None,
114        }
115    }
116}
117
118impl From<EsFluentError> for GlobalLocalizationError {
119    fn from(value: EsFluentError) -> Self {
120        Self::LanguageSelectionFailed(value)
121    }
122}
123
124/// Sets the global `FluentManager` context.
125///
126/// This function should be called once at the beginning of your application's
127/// lifecycle.
128///
129/// # Panics
130///
131/// This function will panic if the context has already been set.
132#[doc(hidden)]
133pub fn set_context(manager: FluentManager) {
134    try_set_context(manager).expect("Failed to set context");
135}
136
137/// Sets the global `FluentManager` context with a shared `ArcSwap<FluentManager>`.
138///
139/// This function is useful when you want to share the `FluentManager` between
140/// multiple threads.
141///
142/// # Panics
143///
144/// This function will panic if the context has already been set.
145#[doc(hidden)]
146pub fn set_shared_context(manager: Arc<FluentManager>) {
147    try_set_shared_context(manager).expect("Failed to set shared context");
148}
149
150#[doc(hidden)]
151pub fn try_set_context(manager: FluentManager) -> Result<(), GlobalLocalizationError> {
152    CONTEXT
153        .set(ArcSwap::from_pointee(manager))
154        .map_err(|_| GlobalLocalizationError::ContextAlreadyInitialized)
155}
156
157#[doc(hidden)]
158pub fn try_set_shared_context(manager: Arc<FluentManager>) -> Result<(), GlobalLocalizationError> {
159    CONTEXT
160        .set(ArcSwap::new(manager))
161        .map_err(|_| GlobalLocalizationError::ContextAlreadyInitialized)
162}
163
164/// Sets a domain-aware custom localizer function.
165///
166/// The callback receives `None` for plain [`localize`] requests and
167/// `Some(domain)` for [`localize_in_domain`] requests.
168#[doc(hidden)]
169pub fn set_custom_localizer_with_domain<F>(localizer: F)
170where
171    F: for<'a> Fn(
172            Option<&str>,
173            &str,
174            Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
175        ) -> Option<String>
176        + Send
177        + Sync
178        + 'static,
179{
180    try_set_custom_localizer_with_domain(localizer)
181        .expect("Failed to set domain-aware custom localizer");
182}
183
184#[doc(hidden)]
185pub fn try_set_custom_localizer_with_domain<F>(localizer: F) -> Result<(), GlobalLocalizationError>
186where
187    F: for<'a> Fn(
188            Option<&str>,
189            &str,
190            Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
191        ) -> Option<String>
192        + Send
193        + Sync
194        + 'static,
195{
196    let mut slot = custom_localizer_slot().write();
197    if slot.is_some() {
198        return Err(GlobalLocalizationError::CustomLocalizerAlreadyInitialized);
199    }
200    *slot = Some(Arc::new(localizer));
201    Ok(())
202}
203
204/// Replaces the custom localizer with a domain-aware callback.
205#[doc(hidden)]
206pub fn replace_custom_localizer_with_domain<F>(localizer: F)
207where
208    F: for<'a> Fn(
209            Option<&str>,
210            &str,
211            Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
212        ) -> Option<String>
213        + Send
214        + Sync
215        + 'static,
216{
217    *custom_localizer_slot().write() = Some(Arc::new(localizer));
218}
219
220/// Selects a language for all localizers in the global context.
221#[doc(hidden)]
222pub fn select_language(
223    lang: &unic_langid::LanguageIdentifier,
224) -> Result<(), GlobalLocalizationError> {
225    let context = CONTEXT
226        .get()
227        .ok_or(GlobalLocalizationError::ContextNotInitialized)?;
228    context.load().select_language(lang).map_err(Into::into)
229}
230
231/// Localizes a message by its ID.
232///
233/// This function will first try to use the custom localizer if it has been set.
234/// If the custom localizer returns `None`, it will then try to use the global
235/// context.
236///
237/// If the message is not found, a warning will be logged and the ID will be
238/// returned as the message.
239#[doc(hidden)]
240pub fn localize<'a>(
241    id: &str,
242    args: Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
243) -> String {
244    if let Some(message) = try_custom_localizer(id, args) {
245        return message;
246    }
247
248    if let Some(context) = CONTEXT.get()
249        && let Some(message) = context.load().localize(id, args)
250    {
251        return message;
252    }
253
254    tracing::warn!("Translation for '{}' not found or context not set.", id);
255    id.to_string()
256}
257
258/// Localizes a message by its ID within the given domain.
259///
260/// This first consults any installed custom localizer, then performs an
261/// explicit domain-scoped lookup against the shared context. Custom
262/// localizers receive `Some(domain)` for domain-scoped requests.
263#[doc(hidden)]
264pub fn localize_in_domain<'a>(
265    domain: &str,
266    id: &str,
267    args: Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
268) -> String {
269    if let Some(message) = try_custom_localizer_in_domain(domain, id, args) {
270        return message;
271    }
272
273    if let Some(context) = CONTEXT.get()
274        && let Some(message) = context.load().localize_in_domain(domain, id, args)
275    {
276        return message;
277    }
278
279    tracing::warn!(
280        "Translation for '{}' in domain '{}' not found or context not set.",
281        id,
282        domain
283    );
284    id.to_string()
285}