Skip to main content

cranpose_localization/
message.rs

1use std::{rc::Rc, sync::OnceLock};
2
3use fluent_bundle::{FluentArgs, FluentBundle, FluentError, FluentResource, FluentValue};
4
5use crate::LocalizationError;
6
7/// A static message definition emitted by `tr!` or generated catalog bindings.
8pub struct Message {
9    namespace: &'static str,
10    id: &'static str,
11    source: &'static SourceCatalog,
12}
13
14/// Source-language messages shared by generated accessors and parsed on first use.
15pub struct SourceCatalog {
16    source_locale: &'static str,
17    resource: &'static str,
18    parsed: OnceLock<Result<FluentResource, String>>,
19}
20
21impl Message {
22    /// Defines a message using a source catalog containing `id`.
23    /// Prefer the macros, which validate the source at compile time.
24    pub const fn new(
25        namespace: &'static str,
26        id: &'static str,
27        source: &'static SourceCatalog,
28    ) -> Self {
29        Self {
30            namespace,
31            id,
32            source,
33        }
34    }
35
36    /// The package namespace used for catalog lookup and app overrides.
37    pub fn namespace(&self) -> &'static str {
38        self.namespace
39    }
40
41    /// The stable catalog identifier.
42    pub fn id(&self) -> &'static str {
43        self.id
44    }
45
46    /// Formats the source-language message without any application catalog.
47    pub fn format_source(&self, arguments: &[Argument<'_>]) -> Result<String, LocalizationError> {
48        self.source(&fluent_args(arguments))
49    }
50
51    /// Returns best-effort source text after a formatting error, preserving readable
52    /// text and Fluent's unresolved-variable markers instead of showing a catalog key.
53    pub fn fallback_text(&self, arguments: &[Argument<'_>]) -> String {
54        self.source_output(&fluent_args(arguments))
55            .map_or_else(|_| "…".to_owned(), |(text, _)| text)
56    }
57
58    pub(crate) fn source(&self, args: &FluentArgs<'_>) -> Result<String, LocalizationError> {
59        let (text, errors) = self.source_output(args)?;
60        if errors.is_empty() {
61            Ok(text)
62        } else {
63            Err(self.error(format!("{errors:?}")))
64        }
65    }
66
67    fn source_output(
68        &self,
69        args: &FluentArgs<'_>,
70    ) -> Result<(String, Vec<FluentError>), LocalizationError> {
71        let bundle = self.source.bundle().map_err(|detail| self.error(detail))?;
72        let pattern = bundle
73            .get_message(self.id)
74            .and_then(|message| message.value())
75            .ok_or_else(|| self.error("source message has no value".to_owned()))?;
76        let mut errors = Vec::new();
77        let text = bundle.format_pattern(pattern, Some(args), &mut errors);
78        Ok((text.into_owned(), errors))
79    }
80
81    pub(crate) fn error(&self, detail: String) -> LocalizationError {
82        LocalizationError::Format {
83            namespace: self.namespace.to_owned(),
84            id: self.id.to_owned(),
85            detail,
86        }
87    }
88}
89
90pub(crate) fn fluent_args<'a>(arguments: &'a [Argument<'_>]) -> FluentArgs<'a> {
91    arguments
92        .iter()
93        .map(|argument| {
94            let value = match &argument.value {
95                FluentValue::String(text) => FluentValue::from(text.as_ref()),
96                value => value.clone(),
97            };
98            (argument.name, value)
99        })
100        .collect()
101}
102
103pub(crate) type SourceBundle = Result<Rc<FluentBundle<&'static FluentResource>>, String>;
104
105impl SourceCatalog {
106    /// Defines source-language Fluent data. Macros validate it before compilation.
107    pub const fn new(source_locale: &'static str, resource: &'static str) -> Self {
108        Self {
109            source_locale,
110            resource,
111            parsed: OnceLock::new(),
112        }
113    }
114
115    fn bundle(&'static self) -> SourceBundle {
116        crate::thread_state::with(|state| {
117            state
118                .source_bundles
119                .borrow_mut()
120                .entry(std::ptr::from_ref(self).addr())
121                .or_insert_with(|| self.prepare().map(Rc::new))
122                .clone()
123        })
124    }
125
126    fn prepare(&'static self) -> Result<FluentBundle<&'static FluentResource>, String> {
127        let language = self
128            .source_locale
129            .parse()
130            .map_err(|error| format!("{error}"))?;
131        let resource = self
132            .parsed
133            .get_or_init(|| {
134                FluentResource::try_new(self.resource.to_owned())
135                    .map_err(|(_, errors)| format!("{errors:?}"))
136            })
137            .as_ref()
138            .map_err(Clone::clone)?;
139        let mut bundle = crate::new_bundle(language);
140        bundle
141            .add_resource(resource)
142            .map_err(|errors| format!("{errors:?}"))?;
143        Ok(bundle)
144    }
145}
146
147/// A named value passed to a message without allocating a string for numbers.
148#[derive(Clone, Debug, PartialEq)]
149pub struct Argument<'a> {
150    pub(crate) name: &'static str,
151    pub(crate) value: FluentValue<'a>,
152}
153
154impl<'a> Argument<'a> {
155    /// Borrows strings and preserves numeric values for Fluent plural selection.
156    pub fn new(name: &'static str, value: impl Into<FluentValue<'a>>) -> Self {
157        Self {
158            name,
159            value: value.into(),
160        }
161    }
162
163    /// Transfers owned values into a cache, copying only borrowed text that must outlive the call.
164    pub fn take_owned(&mut self) -> Argument<'static> {
165        let value = match std::mem::replace(&mut self.value, FluentValue::None) {
166            FluentValue::String(text) => FluentValue::String(text.into_owned().into()),
167            FluentValue::Number(number) => FluentValue::Number(number),
168            FluentValue::Custom(custom) => FluentValue::Custom(custom),
169            FluentValue::None => FluentValue::None,
170            FluentValue::Error => FluentValue::Error,
171        };
172        Argument {
173            name: self.name,
174            value,
175        }
176    }
177
178    /// Updates an owned cache argument, reusing string storage when possible.
179    pub fn update_from(&mut self, source: &mut Argument<'_>) {
180        self.name = source.name;
181        if let FluentValue::String(target) = &mut self.value
182            && let FluentValue::String(std::borrow::Cow::Borrowed(value)) = &source.value
183        {
184            let target = target.to_mut();
185            target.clear();
186            target.push_str(value);
187            return;
188        }
189        self.value = source.take_owned().value;
190    }
191}