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/// Extracts the file stem from a file path at compile time.
19/// Used by derive macros when `namespace = file` is specified.
20#[doc(hidden)]
21pub const fn __namespace_from_file_path(path: &str) -> &str {
22    let bytes = path.as_bytes();
23    let len = bytes.len();
24
25    // Find last '/' or '\\'
26    let mut last_slash = 0;
27    let mut i = 0;
28    while i < len {
29        if bytes[i] == b'/' || bytes[i] == b'\\' {
30            last_slash = i + 1;
31        }
32        i += 1;
33    }
34
35    // Find last '.'
36    let mut last_dot = len;
37    i = last_slash;
38    while i < len {
39        if bytes[i] == b'.' {
40            last_dot = i;
41        }
42        i += 1;
43    }
44
45    // If last_dot is at the start of filename (hidden file), use full name
46    if last_dot == last_slash {
47        last_dot = len;
48    }
49
50    // Extract substring [last_slash..last_dot]
51    // SAFETY: We're working with valid UTF-8 and only splitting at ASCII boundaries
52    let result_len = last_dot - last_slash;
53    if result_len == 0 {
54        return "unknown";
55    }
56
57    // Use unsafe to create the substring at compile time
58    unsafe {
59        core::str::from_utf8_unchecked(core::slice::from_raw_parts(
60            bytes.as_ptr().add(last_slash),
61            result_len,
62        ))
63    }
64}
65
66/// Extracts the relative path (without extension, stripping src/ prefix) from a file path at compile time.
67/// Used by derive macros when `namespace = file(relative)` is specified.
68#[doc(hidden)]
69pub const fn __namespace_from_file_path_relative(path: &str) -> &str {
70    let bytes = path.as_bytes();
71    let len = bytes.len();
72
73    // Find where the extension starts (last '.')
74    let mut last_dot = len;
75    let mut i = 0;
76    while i < len {
77        if bytes[i] == b'.' {
78            last_dot = i;
79        }
80        i += 1;
81    }
82
83    // Check for "src/" or "src\" prefix to skip
84    let start = if len >= 4
85        && bytes[0] == b's'
86        && bytes[1] == b'r'
87        && bytes[2] == b'c'
88        && (bytes[3] == b'/' || bytes[3] == b'\\')
89    {
90        4
91    } else {
92        0
93    };
94
95    let result_len = last_dot - start;
96    if result_len == 0 {
97        return "unknown";
98    }
99
100    // SAFETY: We're working with valid UTF-8 and only splitting at ASCII boundaries
101    unsafe {
102        core::str::from_utf8_unchecked(core::slice::from_raw_parts(
103            bytes.as_ptr().add(start),
104            result_len,
105        ))
106    }
107}
108
109#[doc(hidden)]
110pub use rust_embed as __rust_embed;
111
112#[doc(hidden)]
113pub use es_fluent_manager_core as __manager_core;
114
115#[doc(hidden)]
116pub use unic_langid;
117
118use arc_swap::ArcSwap;
119use es_fluent_manager_core::FluentManager;
120use std::sync::{Arc, OnceLock};
121
122mod traits;
123pub use traits::{EsFluentChoice, FluentDisplay, ThisFtl, ToFluentString};
124
125#[doc(hidden)]
126static CONTEXT: OnceLock<ArcSwap<FluentManager>> = OnceLock::new();
127
128#[doc(hidden)]
129static CUSTOM_LOCALIZER: OnceLock<
130    Box<
131        dyn Fn(&str, Option<&std::collections::HashMap<&str, FluentValue>>) -> Option<String>
132            + Send
133            + Sync,
134    >,
135> = OnceLock::new();
136
137/// Sets the global `FluentManager` context.
138///
139/// This function should be called once at the beginning of your application's
140/// lifecycle.
141///
142/// # Panics
143///
144/// This function will panic if the context has already been set.
145#[doc(hidden)]
146pub fn set_context(manager: FluentManager) {
147    CONTEXT
148        .set(ArcSwap::from_pointee(manager))
149        .map_err(|_| "Context already set")
150        .expect("Failed to set context");
151}
152
153/// Sets the global `FluentManager` context with a shared `ArcSwap<FluentManager>`.
154///
155/// This function is useful when you want to share the `FluentManager` between
156/// multiple threads.
157///
158/// # Panics
159///
160/// This function will panic if the context has already been set.
161#[doc(hidden)]
162pub fn set_shared_context(manager: Arc<FluentManager>) {
163    CONTEXT
164        .set(ArcSwap::new(manager))
165        .map_err(|_| "Context already set")
166        .expect("Failed to set shared context");
167}
168
169/// Sets a custom localizer function.
170///
171/// The custom localizer will be called before the global context's `localize`
172/// method. If the custom localizer returns `Some(message)`, the message will be
173/// returned. Otherwise, the global context will be used.
174///
175/// # Panics
176///
177/// This function will panic if the custom localizer has already been set.
178#[doc(hidden)]
179pub fn set_custom_localizer<F>(localizer: F)
180where
181    F: Fn(&str, Option<&std::collections::HashMap<&str, FluentValue>>) -> Option<String>
182        + Send
183        + Sync
184        + 'static,
185{
186    CUSTOM_LOCALIZER
187        .set(Box::new(localizer))
188        .map_err(|_| "Custom localizer already set")
189        .expect("Failed to set custom localizer");
190}
191
192/// Selects a language for all localizers in the global context.
193#[doc(hidden)]
194pub fn select_language(lang: &unic_langid::LanguageIdentifier) {
195    if let Some(context) = CONTEXT.get() {
196        context.load().select_language(lang);
197    }
198}
199
200/// Localizes a message by its ID.
201///
202/// This function will first try to use the custom localizer if it has been set.
203/// If the custom localizer returns `None`, it will then try to use the global
204/// context.
205///
206/// If the message is not found, a warning will be logged and the ID will be
207/// returned as the message.
208#[doc(hidden)]
209pub fn localize<'a>(
210    id: &str,
211    args: Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
212) -> String {
213    if let Some(custom_localizer) = CUSTOM_LOCALIZER.get()
214        && let Some(message) = custom_localizer(id, args)
215    {
216        return message;
217    }
218
219    if let Some(context) = CONTEXT.get()
220        && let Some(message) = context.load().localize(id, args)
221    {
222        return message;
223    }
224
225    tracing::warn!("Translation for '{}' not found or context not set.", id);
226    id.to_string()
227}