cranpose_localization/
message.rs1use std::{cell::RefCell, collections::BTreeMap, 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
103impl SourceCatalog {
104 pub const fn new(source_locale: &'static str, resource: &'static str) -> Self {
106 Self {
107 source_locale,
108 resource,
109 parsed: OnceLock::new(),
110 }
111 }
112
113 fn bundle(&'static self) -> Result<Rc<FluentBundle<&'static FluentResource>>, String> {
114 type SourceBundle = Result<Rc<FluentBundle<&'static FluentResource>>, String>;
115 thread_local! {
116 static SOURCES: RefCell<BTreeMap<usize, SourceBundle>> = const { RefCell::new(BTreeMap::new()) };
117 }
118 SOURCES.with(|sources| {
119 sources
120 .borrow_mut()
121 .entry(std::ptr::from_ref(self).addr())
122 .or_insert_with(|| self.prepare().map(Rc::new))
123 .clone()
124 })
125 }
126
127 fn prepare(&'static self) -> Result<FluentBundle<&'static FluentResource>, String> {
128 let language = self
129 .source_locale
130 .parse()
131 .map_err(|error| format!("{error}"))?;
132 let resource = self
133 .parsed
134 .get_or_init(|| {
135 FluentResource::try_new(self.resource.to_owned())
136 .map_err(|(_, errors)| format!("{errors:?}"))
137 })
138 .as_ref()
139 .map_err(Clone::clone)?;
140 let mut bundle = crate::new_bundle(language);
141 bundle
142 .add_resource(resource)
143 .map_err(|errors| format!("{errors:?}"))?;
144 Ok(bundle)
145 }
146}
147
148#[derive(Clone, Debug, PartialEq)]
150pub struct Argument<'a> {
151 pub(crate) name: &'static str,
152 pub(crate) value: FluentValue<'a>,
153}
154
155impl<'a> Argument<'a> {
156 pub fn new(name: &'static str, value: impl Into<FluentValue<'a>>) -> Self {
158 Self {
159 name,
160 value: value.into(),
161 }
162 }
163
164 pub fn take_owned(&mut self) -> Argument<'static> {
166 let value = match std::mem::replace(&mut self.value, FluentValue::None) {
167 FluentValue::String(text) => FluentValue::String(text.into_owned().into()),
168 FluentValue::Number(number) => FluentValue::Number(number),
169 FluentValue::Custom(custom) => FluentValue::Custom(custom),
170 FluentValue::None => FluentValue::None,
171 FluentValue::Error => FluentValue::Error,
172 };
173 Argument {
174 name: self.name,
175 value,
176 }
177 }
178
179 pub fn update_from(&mut self, source: &mut Argument<'_>) {
181 self.name = source.name;
182 if let FluentValue::String(target) = &mut self.value
183 && let FluentValue::String(std::borrow::Cow::Borrowed(value)) = &source.value
184 {
185 let target = target.to_mut();
186 target.clear();
187 target.push_str(value);
188 return;
189 }
190 self.value = source.take_owned().value;
191 }
192}