Skip to main content

es_fluent/traits/fluent_message/
localizer.rs

1use crate::registry::StaticFluentMessageKey;
2use es_fluent_manager_core::FluentManager;
3use std::sync::Arc;
4
5use super::{FluentArgs, FluentLocalizerLookup, FluentMessageLookup};
6
7const WITH_LOOKUP_CALLBACK_COUNT_ERROR: &str =
8    "FluentLocalizer::with_lookup must invoke its callback exactly once";
9
10/// A typed Fluent message that can be resolved by an explicit localization
11/// backend.
12///
13/// Derive macros implement this trait for `#[derive(EsFluent)]` and generated
14/// variant enums. Runtime managers use it to keep typed message call sites while
15/// routing lookup through a request, component, or application-scoped manager.
16pub trait FluentMessage {
17    /// Converts the message into a localized string using the supplied lookup
18    /// callback.
19    ///
20    /// Manual implementations should treat `localize` as the only lookup path
21    /// during rendering. Do not re-enter the same localizer to select a
22    /// language or perform other lock-taking lookups from this method; managers
23    /// may hold snapshot locks while invoking it.
24    fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String;
25}
26
27#[diagnostic::do_not_recommend]
28impl<T: FluentMessage + ?Sized> FluentMessage for &T {
29    fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String {
30        (**self).to_fluent_string_with(localize)
31    }
32}
33
34/// Runtime context that resolves Fluent message IDs for typed message values.
35///
36/// Managers and framework adapters implement this trait so callers can pass
37/// the active localization context explicitly.
38///
39/// # Implementing `FluentLocalizer`
40///
41/// Custom localizers should either use the default [`Self::with_lookup`]
42/// implementation or override it to provide one render-scoped snapshot. If
43/// `with_lookup(...)` is overridden, it must invoke the callback exactly once
44/// before returning. Failing to do so is a logic error and will panic in
45/// [`FluentLocalizerExt::localize_message`] and
46/// [`FluentLocalizerExt::try_localize_message`].
47pub trait FluentLocalizer {
48    /// Localizes a fully scoped static message key.
49    fn localize<'a>(
50        &self,
51        key: StaticFluentMessageKey,
52        args: Option<&'a FluentArgs<'a>>,
53    ) -> Option<String>;
54
55    /// Runs a group of lookups against one render-scoped localization view.
56    ///
57    /// Implementations must invoke the callback exactly once, must not call it
58    /// after `with_lookup(...)` returns, and should provide a stable lookup
59    /// snapshot for the duration of that callback. The extension methods rely
60    /// on this contract when rendering nested typed messages.
61    ///
62    /// The callback is the only supported lookup path inside a typed message
63    /// render. Custom `FluentMessage` implementations must not re-enter the
64    /// same localizer for language selection or other lock-taking operations
65    /// while this callback is active.
66    ///
67    /// The default implementation delegates each lookup independently. Managers
68    /// with mutable language selection should override this to hold the relevant
69    /// lock or snapshot for the whole callback.
70    ///
71    /// # Example
72    ///
73    /// ```
74    /// # use es_fluent::{FluentArgs, FluentLocalizer};
75    /// # use es_fluent::registry::StaticFluentMessageKey;
76    /// struct MyLocalizer;
77    ///
78    /// fn lookup(
79    ///     key: StaticFluentMessageKey,
80    ///     _args: Option<&FluentArgs<'_>>,
81    /// ) -> Option<String> {
82    ///     Some(format!("{}:{}:{}", key.owner(), key.domain(), key.id()))
83    /// }
84    ///
85    /// impl FluentLocalizer for MyLocalizer {
86    ///     fn localize<'a>(
87    ///         &self,
88    ///         key: StaticFluentMessageKey,
89    ///         args: Option<&FluentArgs<'a>>,
90    ///     ) -> Option<String> {
91    ///         lookup(key, args)
92    ///     }
93    ///
94    ///     fn with_lookup(
95    ///         &self,
96    ///         f: &mut dyn FnMut(&mut es_fluent::FluentLocalizerLookup<'_>),
97    ///     ) {
98    ///         let mut lookup = lookup;
99    ///         f(&mut lookup);
100    ///     }
101    /// }
102    /// ```
103    fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
104        let mut lookup =
105            |key: StaticFluentMessageKey, args: Option<&FluentArgs<'_>>| self.localize(key, args);
106        f(&mut lookup);
107    }
108}
109
110impl FluentLocalizer for FluentManager {
111    fn localize<'a>(
112        &self,
113        key: StaticFluentMessageKey,
114        args: Option<&FluentArgs<'a>>,
115    ) -> Option<String> {
116        FluentManager::localize(self, key, args.map(FluentArgs::as_raw))
117    }
118
119    fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
120        FluentManager::with_lookup(self, &mut |lookup| {
121            let mut typed_lookup = |key: StaticFluentMessageKey, args: Option<&FluentArgs<'_>>| {
122                lookup(key, args.map(FluentArgs::as_raw))
123            };
124            f(&mut typed_lookup);
125        });
126    }
127}
128
129impl<T: FluentLocalizer + ?Sized> FluentLocalizer for &T {
130    fn localize<'a>(
131        &self,
132        key: StaticFluentMessageKey,
133        args: Option<&FluentArgs<'a>>,
134    ) -> Option<String> {
135        (**self).localize(key, args)
136    }
137
138    fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
139        (**self).with_lookup(f);
140    }
141}
142
143impl<T: FluentLocalizer + ?Sized> FluentLocalizer for Arc<T> {
144    fn localize<'a>(
145        &self,
146        key: StaticFluentMessageKey,
147        args: Option<&FluentArgs<'a>>,
148    ) -> Option<String> {
149        (**self).localize(key, args)
150    }
151
152    fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
153        (**self).with_lookup(f);
154    }
155}
156
157/// Public extension methods for generic explicit localization contexts.
158///
159/// Concrete manager crates expose inherent `localize_message(...)` methods for
160/// application code. Import this trait when integration code works with a
161/// generic [`FluentLocalizer`] and still needs typed message rendering.
162pub trait FluentLocalizerExt: FluentLocalizer {
163    /// Attempts to render a derived typed message through this explicit
164    /// localizer.
165    ///
166    /// Returns `None` if any lookup in the message tree is missing. Use this
167    /// method when missing resources are an expected condition that the caller
168    /// handles explicitly.
169    fn try_localize_message<T>(&self, message: &T) -> Option<String>
170    where
171        T: FluentMessage + ?Sized,
172    {
173        let mut missing = false;
174        let mut value = None;
175        let mut callback_invocations = 0;
176
177        self.with_lookup(&mut |lookup| {
178            assert!(
179                callback_invocations == 0,
180                "{}",
181                WITH_LOOKUP_CALLBACK_COUNT_ERROR
182            );
183            callback_invocations = 1;
184
185            value = Some(message.to_fluent_string_with(&mut |key, args| {
186                lookup(key, args).unwrap_or_else(|| {
187                    missing = true;
188                    String::new()
189                })
190            }));
191        });
192
193        assert!(
194            callback_invocations == 1,
195            "{}",
196            WITH_LOOKUP_CALLBACK_COUNT_ERROR
197        );
198        let value = value.expect(WITH_LOOKUP_CALLBACK_COUNT_ERROR);
199        if missing { None } else { Some(value) }
200    }
201
202    /// Renders a derived typed message through this explicit localizer.
203    ///
204    /// A key generated under the package-local `fallback-str` policy returns
205    /// its snake_case Rust source name. Strict keys still panic.
206    fn localize_message<T>(&self, message: &T) -> String
207    where
208        T: FluentMessage + ?Sized,
209    {
210        let mut value = None;
211        let mut callback_invocations = 0;
212
213        self.with_lookup(&mut |lookup| {
214            assert!(
215                callback_invocations == 0,
216                "{}",
217                WITH_LOOKUP_CALLBACK_COUNT_ERROR
218            );
219            callback_invocations = 1;
220
221            value = Some(message.to_fluent_string_with(&mut |key, args| {
222                lookup(key, args)
223                    .unwrap_or_else(|| super::super::missing_fluent_value(key, "message"))
224            }));
225        });
226
227        assert!(
228            callback_invocations == 1,
229            "{}",
230            WITH_LOOKUP_CALLBACK_COUNT_ERROR
231        );
232        value.expect(WITH_LOOKUP_CALLBACK_COUNT_ERROR)
233    }
234}
235
236impl<T: FluentLocalizer + ?Sized> FluentLocalizerExt for T {}