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