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 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/// Retrieves a localized message without arguments.
133///
134/// This function attempts to return a `Cow::Borrowed` referencing static binary data
135/// whenever possible to avoid allocation.
136///
137/// # Resolution Order
138///
139/// 1. **Current Language**: Checks if the key exists in the current language.
140/// 2. **Fallback Language**: If missing, checks the `FALLBACK_LANG_KEY` (en-US).
141/// 3. **Missing Key**: Returns the `key` itself wrapped in `Cow::Borrowed`.
142///
143/// # Arguments
144///
145/// * `bundles` - The collection of Fluent bundles (usually `crate::LOCALES`).
146/// * `cache` - The static cache map (usually `crate::CACHE`).
147/// * `key` - The message ID to look up.
148pub fn lookup_static<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
149 bundles: &'a B,
150 cache: &C,
151 key: &'a str,
152) -> Cow<'a, str> {
153 let current_key = &get_lang().key;
154 let is_fallback = current_key == FALLBACK_LANG_KEY;
155
156 // CURRENT LANGUAGE
157 if let Some(entry) = cache.get_entry(current_key, key) {
158 match entry {
159 CacheEntry::Static(s) => return Cow::Borrowed(s),
160 CacheEntry::Dynamic => {
161 if let Some(b) = bundles.get_bundle(current_key)
162 && let Some(val) = lookup_in_bundle(b, key)
163 {
164 return val;
165 }
166 }
167 }
168 }
169
170 // FALLBACK LANGUAGE
171 if !is_fallback && let Some(entry) = cache.get_entry(FALLBACK_LANG_KEY, key) {
172 match entry {
173 CacheEntry::Static(s) => return Cow::Borrowed(s),
174 CacheEntry::Dynamic => {
175 if let Some(b) = bundles.get_bundle(FALLBACK_LANG_KEY)
176 && let Some(val) = lookup_in_bundle(b, key)
177 {
178 return val;
179 }
180 }
181 }
182 }
183
184 Cow::Borrowed(key)
185}
186
187/// Retrieves a localized message with arguments.
188///
189/// Even when arguments are provided, this function checks if the underlying message
190/// is actually static. If so, it ignores the arguments and returns the static string
191/// to preserve performance.
192///
193/// # Arguments
194///
195/// * `bundles` - The collection of Fluent bundles.
196/// * `cache` - The static cache map.
197/// * `key` - The message ID to look up.
198/// * `args` - The arguments to interpolate into the message.
199pub fn lookup_dynamic<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
200 bundles: &'a B,
201 cache: &C,
202 key: &'a str,
203 args: &FluentArgs,
204) -> Cow<'a, str> {
205 let current_key = &get_lang().key;
206 let is_fallback = current_key == FALLBACK_LANG_KEY;
207
208 // CURRENT LANGUAGE
209 if let Some(entry) = cache.get_entry(current_key, key) {
210 match entry {
211 // Even if args are provided, if it's static, ignore args and return static string (Zero alloc)
212 CacheEntry::Static(s) => return Cow::Borrowed(s),
213 CacheEntry::Dynamic => {
214 if let Some(b) = bundles.get_bundle(current_key)
215 && let Some(val) = format_in_bundle(b, key, args)
216 {
217 return val;
218 }
219 }
220 }
221 }
222
223 // FALLBACK LANGUAGE
224 if !is_fallback && let Some(entry) = cache.get_entry(FALLBACK_LANG_KEY, key) {
225 match entry {
226 CacheEntry::Static(s) => return Cow::Borrowed(s),
227 CacheEntry::Dynamic => {
228 if let Some(b) = bundles.get_bundle(FALLBACK_LANG_KEY)
229 && let Some(val) = format_in_bundle(b, key, args)
230 {
231 return val;
232 }
233 }
234 }
235 }
236
237 Cow::Borrowed(key)
238}
239
240fn lookup_in_bundle<'a>(
241 bundle: &'a ConcurrentFluentBundle<FluentResource>,
242 key: &str,
243) -> Option<Cow<'a, str>> {
244 let msg = bundle.get_message(key)?;
245 let pattern = msg.value()?;
246 let mut errors = vec![];
247 Some(bundle.format_pattern(pattern, None, &mut errors))
248}
249
250fn format_in_bundle<'a>(
251 bundle: &'a ConcurrentFluentBundle<FluentResource>,
252 key: &str,
253 args: &FluentArgs,
254) -> Option<Cow<'a, str>> {
255 let msg = bundle.get_message(key)?;
256 let pattern = msg.value()?;
257 let mut errors = vec![];
258 Some(bundle.format_pattern(pattern, Some(args), &mut errors))
259}
260
261/// The primary accessor macro for localized strings.
262///
263/// It delegates to `lookup_static` or `lookup_dynamic` depending on whether arguments
264/// are provided.
265///
266/// # Examples
267///
268/// Basic usage:
269/// ```rust,ignore
270/// let title = t!("app-title");
271/// ```
272///
273/// With arguments:
274/// ```rust,ignore
275/// let welcome = t!("welcome-user", {
276/// "name" => "Alice",
277/// "unread_count" => 5
278/// });
279/// ```
280#[macro_export]
281macro_rules! t {
282 ($key:expr) => {
283 $crate::lookup_static(
284 &crate::LOCALES,
285 &crate::CACHE,
286 $key
287 )
288 };
289 ($key:expr, { $($k:expr => $v:expr),* $(,)? }) => {
290 {
291 let mut args = $crate::FluentArgs::new();
292 $( args.set($k, $v); )*
293 $crate::lookup_dynamic(
294 &crate::LOCALES,
295 &crate::CACHE,
296 $key,
297 &args
298 )
299 }
300 };
301}