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 {}