Skip to main content

es_fluent/traits/
label.rs

1use super::FluentLocalizer;
2use crate::registry::{StaticFluentDomain, StaticFluentEntryId};
3
4/// A trait for types that have a Fluent label key representing the type itself.
5///
6/// This trait is automatically implemented by `#[derive(EsFluentLabel)]` for
7/// source types, and by `#[derive(EsFluentVariants)]` for each generated
8/// variant enum.
9pub trait FluentLabel {
10    /// Returns the validated static domain for this type-level label.
11    fn fluent_label_domain() -> StaticFluentDomain;
12
13    /// Returns the validated static message id for this type-level label.
14    fn fluent_label_id() -> StaticFluentEntryId;
15
16    /// Attempts to return the localized label for this type using an explicit
17    /// localization context.
18    fn try_localize_label<L: FluentLocalizer + ?Sized>(localizer: &L) -> Option<String> {
19        try_localize_label(
20            localizer,
21            Self::fluent_label_domain(),
22            Self::fluent_label_id(),
23        )
24    }
25
26    /// Returns the localized label for this type using an explicit localization
27    /// context.
28    fn localize_label<L: FluentLocalizer + ?Sized>(localizer: &L) -> String {
29        localize_label(
30            localizer,
31            Self::fluent_label_domain(),
32            Self::fluent_label_id(),
33        )
34    }
35
36    /// Returns deterministic fallback text for this type label without a
37    /// runtime localization context.
38    ///
39    /// Prefer [`Self::localize_label`] when UI code has a runtime manager.
40    /// This helper is intended for generated metadata, tests, and integration
41    /// scaffolding that cannot access app state.
42    fn fallback_label() -> String {
43        fallback_label::<Self>()
44    }
45}
46
47#[doc(hidden)]
48pub fn try_localize_label<L: FluentLocalizer + ?Sized>(
49    localizer: &L,
50    domain: StaticFluentDomain,
51    id: StaticFluentEntryId,
52) -> Option<String> {
53    localizer.localize_in_domain(domain, id, None)
54}
55
56#[doc(hidden)]
57pub fn localize_label<L: FluentLocalizer + ?Sized>(
58    localizer: &L,
59    domain: StaticFluentDomain,
60    id: StaticFluentEntryId,
61) -> String {
62    localizer
63        .localize_in_domain(domain, id, None)
64        .unwrap_or_else(|| {
65            tracing::warn!(
66                domain = domain.as_str(),
67                message_id = id.as_str(),
68                "missing Fluent label"
69            );
70            id.as_str().to_string()
71        })
72}
73
74/// Returns deterministic fallback text for a typed label without a runtime
75/// localization context.
76///
77/// The fallback is derived from the generated label id. Prefer
78/// [`FluentLabel::localize_label`] for user-facing UI that should follow the
79/// active locale.
80pub fn fallback_label<T: FluentLabel + ?Sized>() -> String {
81    humanize_fluent_entry_id(T::fluent_label_id())
82}
83
84/// Converts a validated static Fluent entry id into readable fallback text.
85///
86/// This strips a trailing `_label`, splits on `_` and `-`, drops empty
87/// segments, and uppercases the first character of each remaining segment.
88pub fn humanize_fluent_entry_id(id: StaticFluentEntryId) -> String {
89    humanize_fluent_entry_key(id.as_str())
90}
91
92fn humanize_fluent_entry_key(id: &str) -> String {
93    let id = id.strip_suffix("_label").unwrap_or(id);
94    id.split(['_', '-'])
95        .filter(|part| !part.is_empty())
96        .map(|part| {
97            let mut chars = part.chars();
98            match chars.next() {
99                Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
100                None => String::new(),
101            }
102        })
103        .collect::<Vec<_>>()
104        .join(" ")
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::FluentArgs;
111
112    fn static_domain(value: &'static str) -> StaticFluentDomain {
113        StaticFluentDomain::try_new(value).expect("valid test domain")
114    }
115
116    fn static_entry(value: &'static str) -> StaticFluentEntryId {
117        StaticFluentEntryId::try_new(value).expect("valid test message id")
118    }
119
120    struct LabelLocalizer;
121
122    impl FluentLocalizer for LabelLocalizer {
123        fn localize<'a>(
124            &self,
125            id: StaticFluentEntryId,
126            _args: Option<&FluentArgs<'a>>,
127        ) -> Option<String> {
128            (id == "label-id").then(|| "Label".to_string())
129        }
130
131        fn localize_in_domain<'a>(
132            &self,
133            domain: StaticFluentDomain,
134            id: StaticFluentEntryId,
135            args: Option<&FluentArgs<'a>>,
136        ) -> Option<String> {
137            (domain == "label-domain")
138                .then(|| self.localize(id, args))
139                .flatten()
140        }
141    }
142
143    struct TestLabel;
144
145    impl FluentLabel for TestLabel {
146        fn fluent_label_domain() -> StaticFluentDomain {
147            static_domain("label-domain")
148        }
149
150        fn fluent_label_id() -> StaticFluentEntryId {
151            static_entry("label-id")
152        }
153    }
154
155    #[test]
156    fn label_trait_exposes_typed_metadata_and_localizes_with_fallback() {
157        let localizer = LabelLocalizer;
158
159        assert_eq!(TestLabel::fluent_label_domain(), "label-domain");
160        assert_eq!(TestLabel::fluent_label_id(), "label-id");
161        assert_eq!(
162            TestLabel::try_localize_label(&localizer),
163            Some("Label".into())
164        );
165        assert_eq!(TestLabel::localize_label(&localizer), "Label");
166    }
167
168    #[test]
169    fn localize_label_helpers_return_localized_value_or_id_fallback() {
170        let localizer = LabelLocalizer;
171
172        assert_eq!(
173            try_localize_label(
174                &localizer,
175                static_domain("label-domain"),
176                static_entry("label-id")
177            ),
178            Some("Label".into())
179        );
180        assert_eq!(
181            try_localize_label(
182                &localizer,
183                static_domain("label-domain"),
184                static_entry("missing-id")
185            ),
186            None
187        );
188        assert_eq!(
189            localize_label(
190                &localizer,
191                static_domain("label-domain"),
192                static_entry("label-id")
193            ),
194            "Label"
195        );
196        assert_eq!(
197            localize_label(
198                &localizer,
199                static_domain("label-domain"),
200                static_entry("missing-id")
201            ),
202            "missing-id"
203        );
204    }
205
206    #[test]
207    fn fallback_label_helpers_humanize_typed_label_ids_without_a_localizer() {
208        assert_eq!(TestLabel::fallback_label(), "Label Id");
209        assert_eq!(fallback_label::<TestLabel>(), "Label Id");
210        assert_eq!(
211            humanize_fluent_entry_id(static_entry("sales-order_status_label")),
212            "Sales Order Status"
213        );
214    }
215}