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