Skip to main content

fluent_zero/
lib.rs

1// fluent-zero/src/lib.rs
2//! # fluent-zero
3//!
4//! A zero-allocation, high-performance Fluent localization loader designed for
5//! GUIs and games.
6//!
7//! This crate works in tandem with the `fluent-zero-build` crate. The build script
8//! generates static Perfect Hash Maps (PHF) that allow `O(1)` lookups for localized
9//! strings. When a string is static (contains no variables), it returns a `&'static str`
10//! reference to the binary's read-only data, avoiding all heap allocations.
11
12extern crate self as fluent_zero;
13
14use std::{
15    borrow::Cow,
16    collections::HashMap,
17    hash::BuildHasher,
18    sync::{Arc, LazyLock},
19};
20
21use arc_swap::ArcSwap;
22pub use fluent_bundle::{
23    FluentArgs, FluentResource, concurrent::FluentBundle as ConcurrentFluentBundle,
24};
25pub use fluent_syntax;
26pub use phf;
27pub use unic_langid::LanguageIdentifier;
28
29// =========================================================================
30// 1. UNIFIED CACHE TYPES
31// =========================================================================
32
33/// Represents the result of a cache lookup from the generated PHF map.
34///
35/// This enum allows the system to distinguish between zero-cost static strings
36/// and those that require the heavier `FluentBundle` machinery.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum CacheEntry {
39    /// The message is static and contains no variables.
40    ///
41    /// The payload is a direct reference to the string in the binary's data section.
42    Static(&'static str),
43    /// The message is dynamic (contains variables/selectors).
44    ///
45    /// This indicates that the system must load the `ConcurrentFluentBundle` to
46    /// resolve the final string.
47    Dynamic,
48}
49
50// =========================================================================
51// 2. GLOBAL STATE
52// =========================================================================
53
54/// Internal state holding the currently active language configuration.
55pub struct LocaleState {
56    /// The parsed identifier (e.g., `en-US`).
57    id: LanguageIdentifier,
58    /// The string representation used for cache keys (e.g., "en-US").
59    key: String,
60}
61
62/// The global thread-safe storage for the current language.
63///
64/// Uses `ArcSwap` to allow lock-free reads, which is critical for high-performance
65/// hot paths in GUI rendering loops.
66static CURRENT_LANG: LazyLock<ArcSwap<LocaleState>> = LazyLock::new(|| {
67    let id: LanguageIdentifier = "en-US".parse().unwrap();
68    ArcSwap::from_pointee(LocaleState {
69        key: id.to_string(),
70        id,
71    })
72});
73
74/// Constant fallback key allows for pointer-sized `&str` checks instead of parsing.
75static FALLBACK_LANG_KEY: &str = "en-US";
76
77/// Updates the runtime language for the application.
78///
79/// This operation is atomic. Subsequent calls to `t!` will immediately reflect
80/// the new language.
81///
82/// # Arguments
83///
84/// * `lang` - The new `LanguageIdentifier` to set (e.g., parsed from "fr-FR").
85pub fn set_lang(lang: LanguageIdentifier) {
86    let key = lang.to_string();
87    let new_state = LocaleState { id: lang, key };
88    CURRENT_LANG.store(Arc::new(new_state));
89}
90
91/// Retrieves the current language state.
92///
93/// Returns a guard containing the `Arc<LocaleState>`. This is primarily used
94/// internally by the lookup functions but is exposed for diagnostics.
95pub fn get_lang() -> arc_swap::Guard<std::sync::Arc<LocaleState>> {
96    CURRENT_LANG.load()
97}
98
99// =========================================================================
100// 3. TRAIT ABSTRACTIONS
101// =========================================================================
102
103/// A store that maps `(Locale, Key)` to a `CacheEntry`.
104///
105/// This trait exists to abstract over the generated `phf::Map` and standard `HashMap`s
106/// used in testing.
107pub trait CacheStore: Sync + Send {
108    /// Retrieves a cache entry for a specific language and message key.
109    fn get_entry(&self, lang: &str, key: &str) -> Option<CacheEntry>;
110}
111
112// Impl for Generated PHF Map
113impl CacheStore for phf::Map<&'static str, &'static phf::Map<&'static str, CacheEntry>> {
114    fn get_entry(&self, lang: &str, key: &str) -> Option<CacheEntry> {
115        // Single hash on `lang` (usually very small map), then Single hash on `key`.
116        self.get(lang).and_then(|m| m.get(key)).copied()
117    }
118}
119
120/// A collection capable of retrieving a `ConcurrentFluentBundle` by language key.
121pub trait BundleCollection: Sync + Send {
122    /// Retrieves the bundle for the specified language.
123    fn get_bundle(&self, lang: &str) -> Option<&ConcurrentFluentBundle<FluentResource>>;
124}
125
126// Impl for Generated PHF Map
127impl BundleCollection
128    for phf::Map<&'static str, &'static LazyLock<ConcurrentFluentBundle<FluentResource>>>
129{
130    fn get_bundle(&self, lang: &str) -> Option<&ConcurrentFluentBundle<FluentResource>> {
131        self.get(lang).map(|lazy| &***lazy)
132    }
133}
134
135// Impl for HashMap (For Tests)
136impl<S: BuildHasher + Sync + Send> BundleCollection
137    for HashMap<String, ConcurrentFluentBundle<FluentResource>, S>
138{
139    fn get_bundle(&self, lang: &str) -> Option<&ConcurrentFluentBundle<FluentResource>> {
140        self.get(lang)
141    }
142}
143
144// =========================================================================
145// 4. LOOKUP HELPER (STATIC)
146// =========================================================================
147
148/// Retrieves a localized message without arguments.
149///
150/// This function attempts to return a `Cow::Borrowed` referencing static binary data
151/// whenever possible to avoid allocation.
152///
153/// # Resolution Order
154///
155/// 1. **Current Language**: Checks if the key exists in the current language.
156/// 2. **Fallback Language**: If missing, checks the `FALLBACK_LANG_KEY` (en-US).
157/// 3. **Missing Key**: Returns the `key` itself wrapped in `Cow::Borrowed`.
158///
159/// # Arguments
160///
161/// * `bundles` - The collection of Fluent bundles (usually `crate::LOCALES`).
162/// * `cache` - The static cache map (usually `crate::CACHE`).
163/// * `key` - The message ID to look up.
164pub fn lookup_static<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
165    bundles: &'a B,
166    cache: &C,
167    key: &'a str,
168) -> Cow<'a, str> {
169    let current_key = &get_lang().key;
170    let is_fallback = current_key == FALLBACK_LANG_KEY;
171
172    // --- STEP 1: CURRENT LANGUAGE ---
173    if let Some(entry) = cache.get_entry(current_key, key) {
174        match entry {
175            CacheEntry::Static(s) => return Cow::Borrowed(s),
176            CacheEntry::Dynamic => {
177                if let Some(b) = bundles.get_bundle(current_key)
178                    && let Some(val) = lookup_in_bundle(b, key)
179                {
180                    return val;
181                }
182            }
183        }
184    }
185
186    // --- STEP 2: FALLBACK LANGUAGE ---
187    if !is_fallback && let Some(entry) = cache.get_entry(FALLBACK_LANG_KEY, key) {
188        match entry {
189            CacheEntry::Static(s) => return Cow::Borrowed(s),
190            CacheEntry::Dynamic => {
191                if let Some(b) = bundles.get_bundle(FALLBACK_LANG_KEY)
192                    && let Some(val) = lookup_in_bundle(b, key)
193                {
194                    return val;
195                }
196            }
197        }
198    }
199
200    Cow::Borrowed(key)
201}
202
203// =========================================================================
204// 5. LOOKUP DYNAMIC
205// =========================================================================
206
207/// Retrieves a localized message with arguments.
208///
209/// Even when arguments are provided, this function checks if the underlying message
210/// is actually static. If so, it ignores the arguments and returns the static string
211/// to preserve performance.
212///
213/// # Arguments
214///
215/// * `bundles` - The collection of Fluent bundles.
216/// * `cache` - The static cache map.
217/// * `key` - The message ID to look up.
218/// * `args` - The arguments to interpolate into the message.
219pub fn lookup_dynamic<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
220    bundles: &'a B,
221    cache: &C,
222    key: &'a str,
223    args: &FluentArgs,
224) -> Cow<'a, str> {
225    let current_key = &get_lang().key;
226    let is_fallback = current_key == FALLBACK_LANG_KEY;
227
228    // --- STEP 1: CURRENT LANGUAGE ---
229    if let Some(entry) = cache.get_entry(current_key, key) {
230        match entry {
231            // Even if args are provided, if it's static, ignore args and return static string (Zero alloc)
232            CacheEntry::Static(s) => return Cow::Borrowed(s),
233            CacheEntry::Dynamic => {
234                if let Some(b) = bundles.get_bundle(current_key)
235                    && let Some(val) = format_in_bundle(b, key, args)
236                {
237                    return val;
238                }
239            }
240        }
241    }
242
243    // --- STEP 2: FALLBACK LANGUAGE ---
244    if !is_fallback && let Some(entry) = cache.get_entry(FALLBACK_LANG_KEY, key) {
245        match entry {
246            CacheEntry::Static(s) => return Cow::Borrowed(s),
247            CacheEntry::Dynamic => {
248                if let Some(b) = bundles.get_bundle(FALLBACK_LANG_KEY)
249                    && let Some(val) = format_in_bundle(b, key, args)
250                {
251                    return val;
252                }
253            }
254        }
255    }
256
257    Cow::Borrowed(key)
258}
259
260fn lookup_in_bundle<'a>(
261    bundle: &'a ConcurrentFluentBundle<FluentResource>,
262    key: &str,
263) -> Option<Cow<'a, str>> {
264    let msg = bundle.get_message(key)?;
265    let pattern = msg.value()?;
266    let mut errors = vec![];
267    Some(bundle.format_pattern(pattern, None, &mut errors))
268}
269
270fn format_in_bundle<'a>(
271    bundle: &'a ConcurrentFluentBundle<FluentResource>,
272    key: &str,
273    args: &FluentArgs,
274) -> Option<Cow<'a, str>> {
275    let msg = bundle.get_message(key)?;
276    let pattern = msg.value()?;
277    let mut errors = vec![];
278    Some(bundle.format_pattern(pattern, Some(args), &mut errors))
279}
280
281/// The primary accessor macro for localized strings.
282///
283/// It delegates to `lookup_static` or `lookup_dynamic` depending on whether arguments
284/// are provided.
285///
286/// # Examples
287///
288/// Basic usage:
289/// ```rust,ignore
290/// let title = t!("app-title");
291/// ```
292///
293/// With arguments:
294/// ```rust,ignore
295/// let welcome = t!("welcome-user", {
296///     "name" => "Alice",
297///     "unread_count" => 5
298/// });
299/// ```
300#[macro_export]
301macro_rules! t {
302    ($key:expr) => {
303        $crate::lookup_static(
304            &crate::LOCALES,
305            &crate::CACHE,
306            $key
307        )
308    };
309    ($key:expr, { $($k:expr => $v:expr),* $(,)? }) => {
310        {
311            let mut args = $crate::FluentArgs::new();
312            $( args.set($k, $v); )*
313            $crate::lookup_dynamic(
314                &crate::LOCALES,
315                &crate::CACHE,
316                $key,
317                &args
318            )
319        }
320    };
321}