cranpose_localization/
message.rs1use std::{rc::Rc, sync::OnceLock};
2
3use fluent_bundle::{FluentArgs, FluentBundle, FluentError, FluentResource, FluentValue};
4
5use crate::LocalizationError;
6
7pub struct Message {
9 namespace: &'static str,
10 id: &'static str,
11 source: &'static SourceCatalog,
12}
13
14pub struct SourceCatalog {
16 source_locale: &'static str,
17 resource: &'static str,
18 parsed: OnceLock<Result<FluentResource, String>>,
19}
20
21impl Message {
22 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 pub fn namespace(&self) -> &'static str {
38 self.namespace
39 }
40
41 pub fn id(&self) -> &'static str {
43 self.id
44 }
45
46 pub fn format_source(&self, arguments: &[Argument<'_>]) -> Result<String, LocalizationError> {
48 self.source(&fluent_args(arguments))
49 }
50
51 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 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#[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 pub fn new(name: &'static str, value: impl Into<FluentValue<'a>>) -> Self {
157 Self {
158 name,
159 value: value.into(),
160 }
161 }
162
163 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 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}