Skip to main content

fluent_zero/
lib.rs

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