1#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
20
21#[cfg(any(feature = "po-translator", feature = "mo-translator"))]
76mod rspolib_translator;
77#[cfg(any(feature = "po-translator", feature = "mo-translator"))]
78pub use rspolib_translator::MoPoTranslatorLoadError;
79
80#[cfg(feature = "mo-translator")]
81pub use rspolib_translator::MoTranslator;
82#[cfg(feature = "po-translator")]
83pub use rspolib_translator::PoTranslator;
84
85use std::borrow::Cow;
86
87#[doc(hidden)]
88pub mod runtime_format {
89 pub fn display_string(format_str: &str, args: &[(&str, &dyn ::std::fmt::Display)]) -> String {
100 use ::std::fmt::Write;
101 let fmt_len = format_str.len();
102 let mut res = String::with_capacity(2 * fmt_len);
103 let mut arg_idx = 0;
104 let mut pos = 0;
105 while let Some(mut p) = format_str[pos..].find(['{', '}']) {
106 if fmt_len - pos < p + 1 {
107 break;
108 }
109 p += pos;
110
111 if format_str.get(p..=p) == Some("}") {
113 res.push_str(&format_str[pos..=p]);
114 if format_str.get(p + 1..=p + 1) == Some("}") {
115 pos = p + 2;
116 } else {
117 pos = p + 1;
119 }
120 continue;
121 }
122
123 if format_str.get(p + 1..=p + 1) == Some("{") {
125 res.push_str(&format_str[pos..=p]);
126 pos = p + 2;
127 continue;
128 }
129
130 let end = if let Some(end) = format_str[p..].find('}') {
132 end + p
133 } else {
134 res.push_str(&format_str[pos..=p]);
136 pos = p + 1;
137 continue;
138 };
139 let argument = format_str[p + 1..end].trim();
140 let pa = if p == end - 1 {
141 arg_idx += 1;
142 arg_idx - 1
143 } else if let Ok(n) = argument.parse::<usize>() {
144 n
145 } else if let Some(p) = args.iter().position(|x| x.0 == argument) {
146 p
147 } else {
148 res.push_str(&format_str[pos..end]);
150 pos = end;
151 continue;
152 };
153
154 res.push_str(&format_str[pos..p]);
156 if let Some(a) = args.get(pa) {
157 write!(&mut res, "{}", a.1)
158 .expect("a Display implementation returned an error unexpectedly");
159 } else {
160 res.push_str(&format_str[p..=end]);
162 }
163 pos = end + 1;
164 }
165 res.push_str(&format_str[pos..]);
166 res
167 }
168
169 #[doc(hidden)]
170 #[macro_export]
172 macro_rules! runtime_format {
173 ($fmt:expr) => {{
174 String::from($fmt)
176 }};
177 ($fmt:expr, $($tail:tt)* ) => {{
178 $crate::runtime_format::display_string(
179 AsRef::as_ref(&$fmt),
180 $crate::runtime_format!(@parse_args [] $($tail)*),
181 )
182 }};
183
184 (@parse_args [$($args:tt)*]) => { &[ $( $args ),* ] };
185 (@parse_args [$($args:tt)*] $name:ident) => {
186 $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$name)])
187 };
188 (@parse_args [$($args:tt)*] $name:ident, $($tail:tt)*) => {
189 $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$name)] $($tail)*)
190 };
191 (@parse_args [$($args:tt)*] $name:ident = $e:expr) => {
192 $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$e)])
193 };
194 (@parse_args [$($args:tt)*] $name:ident = $e:expr, $($tail:tt)*) => {
195 $crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$e)] $($tail)*)
196 };
197 (@parse_args [$($args:tt)*] $e:expr) => {
198 $crate::runtime_format!(@parse_args [$($args)* ("" , &$e)])
199 };
200 (@parse_args [$($args:tt)*] $e:expr, $($tail:tt)*) => {
201 $crate::runtime_format!(@parse_args [$($args)* ("" , &$e)] $($tail)*)
202 };
203 }
204
205 #[cfg(test)]
206 mod tests {
207 #[test]
208 fn test_format() {
209 assert_eq!(runtime_format!("Hello"), "Hello");
210 assert_eq!(runtime_format!("Hello {}!", "world"), "Hello world!");
211 assert_eq!(runtime_format!("Hello {0}!", "world"), "Hello world!");
212 assert_eq!(
213 runtime_format!("Hello -{1}- -{0}-", 40 + 5, "World"),
214 "Hello -World- -45-"
215 );
216 assert_eq!(
217 runtime_format!(format!("Hello {{}}!"), format!("{}", "world")),
218 "Hello world!"
219 );
220 assert_eq!(
221 runtime_format!("Hello -{}- -{}-", 40 + 5, "World"),
222 "Hello -45- -World-"
223 );
224 assert_eq!(
225 runtime_format!("Hello {name}!", name = "world"),
226 "Hello world!"
227 );
228 let name = "world";
229 assert_eq!(runtime_format!("Hello {name}!", name), "Hello world!");
230 assert_eq!(runtime_format!("{} {}!", "Hello", name), "Hello world!");
231 assert_eq!(runtime_format!("{} {name}!", "Hello", name), "Hello world!");
232 assert_eq!(
233 runtime_format!("{0} {name}!", "Hello", name = "world"),
234 "Hello world!"
235 );
236
237 assert_eq!(
238 runtime_format!("Hello {{0}} {}", "world"),
239 "Hello {0} world"
240 );
241 }
242 }
243}
244
245pub trait Translator: Send + Sync {
252 fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str>;
253 fn ntranslate<'a>(
254 &'a self,
255 n: u64,
256 singular: &'a str,
257 plural: &'a str,
258 context: Option<&'a str>,
259 ) -> Cow<'a, str>;
260}
261
262impl<T: Translator> Translator for std::sync::Arc<T> {
263 fn translate<'a>(
264 &'a self,
265 string: &'a str,
266 context: Option<&'a str>,
267 ) -> std::borrow::Cow<'a, str> {
268 <T as Translator>::translate(self, string, context)
269 }
270
271 fn ntranslate<'a>(
272 &'a self,
273 n: u64,
274 singular: &'a str,
275 plural: &'a str,
276 context: Option<&'a str>,
277 ) -> std::borrow::Cow<'a, str> {
278 <T as Translator>::ntranslate(self, n, singular, plural, context)
279 }
280}
281
282#[doc(hidden)]
283pub mod internal {
284
285 use super::Translator;
286 use std::{borrow::Cow, collections::HashMap, sync::LazyLock, sync::RwLock};
287
288 static TRANSLATORS: LazyLock<RwLock<HashMap<&'static str, Box<dyn Translator>>>> =
289 LazyLock::new(Default::default);
290
291 pub fn with_translator<T>(module: &'static str, func: impl FnOnce(&dyn Translator) -> T) -> T {
292 let domain = domain_from_module(module);
293 let def = DefaultTranslator(domain);
294 func(
295 TRANSLATORS
296 .read()
297 .unwrap()
298 .get(domain)
299 .map(|x| &**x)
300 .unwrap_or(&def),
301 )
302 }
303
304 fn domain_from_module(module: &str) -> &str {
305 module.split("::").next().unwrap_or(module)
306 }
307
308 #[cfg(feature = "gettext-rs")]
309 fn mangle_context(ctx: &str, s: &str) -> String {
310 format!("{}\u{4}{}", ctx, s)
311 }
312 #[cfg(feature = "gettext-rs")]
313 fn demangle_context(r: String) -> String {
314 if let Some(x) = r.split('\u{4}').next_back() {
315 return x.to_owned();
316 }
317 r
318 }
319
320 struct DefaultTranslator(&'static str);
321
322 #[cfg(feature = "gettext-rs")]
323 impl Translator for DefaultTranslator {
324 fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str> {
325 Cow::Owned(if let Some(ctx) = context {
326 demangle_context(gettextrs::dgettext(self.0, mangle_context(ctx, string)))
327 } else {
328 gettextrs::dgettext(self.0, string)
329 })
330 }
331
332 fn ntranslate<'a>(
333 &'a self,
334 n: u64,
335 singular: &'a str,
336 plural: &'a str,
337 context: Option<&'a str>,
338 ) -> Cow<'a, str> {
339 let n = n as u32;
340 Cow::Owned(if let Some(ctx) = context {
341 demangle_context(gettextrs::dngettext(
342 self.0,
343 mangle_context(ctx, singular),
344 mangle_context(ctx, plural),
345 n,
346 ))
347 } else {
348 gettextrs::dngettext(self.0, singular, plural, n)
349 })
350 }
351 }
352
353 #[cfg(not(feature = "gettext-rs"))]
354 impl Translator for DefaultTranslator {
355 fn translate<'a>(&'a self, string: &'a str, _context: Option<&'a str>) -> Cow<'a, str> {
356 Cow::Borrowed(string)
357 }
358
359 fn ntranslate<'a>(
360 &'a self,
361 n: u64,
362 singular: &'a str,
363 plural: &'a str,
364 _context: Option<&'a str>,
365 ) -> Cow<'a, str> {
366 Cow::Borrowed(if n == 1 { singular } else { plural })
367 }
368 }
369
370 #[cfg(feature = "gettext-rs")]
371 pub fn init<T: Into<Vec<u8>>>(module: &'static str, dir: T) {
372 let dir = String::from_utf8(dir.into()).unwrap();
374 let _ = gettextrs::bindtextdomain(domain_from_module(module), dir);
376
377 static START: std::sync::Once = std::sync::Once::new();
378 START.call_once(|| {
379 gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
380 });
381 }
382
383 pub fn set_translator(module: &'static str, translator: impl Translator + 'static) {
384 let domain = domain_from_module(module);
385 TRANSLATORS
386 .write()
387 .unwrap()
388 .insert(domain, Box::new(translator));
389 }
390
391 pub fn unset_translator(module: &'static str) {
392 let domain = domain_from_module(module);
393 TRANSLATORS.write().unwrap().remove(domain);
394 }
395}
396
397#[macro_export]
451macro_rules! tr {
452 ($msgid:tt, $($tail:tt)* ) => {
453 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
454 t.translate($msgid, None), $($tail)*))
455 };
456 ($msgid:tt) => {
457 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
458 t.translate($msgid, None)))
459 };
460
461 ($msgctx:tt => $msgid:tt, $($tail:tt)* ) => {
462 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
463 t.translate($msgid, Some($msgctx)), $($tail)*))
464 };
465 ($msgctx:tt => $msgid:tt) => {
466 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
467 t.translate($msgid, Some($msgctx))))
468 };
469
470 ($msgid:tt | $plur:tt % $n:expr, $($tail:tt)* ) => {{
471 let n = $n;
472 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
473 t.ntranslate(n as u64, $msgid, $plur, None), $($tail)*, n=n))
474 }};
475 ($msgid:tt | $plur:tt % $n:expr) => {{
476 let n = $n;
477 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
478 t.ntranslate(n as u64, $msgid, $plur, None), n))
479
480 }};
481
482 ($msgctx:tt => $msgid:tt | $plur:tt % $n:expr, $($tail:tt)* ) => {{
483 let n = $n;
484 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
485 t.ntranslate(n as u64, $msgid, $plur, Some($msgctx)), $($tail)*, n=n))
486 }};
487 ($msgctx:tt => $msgid:tt | $plur:tt % $n:expr) => {{
488 let n = $n;
489 $crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
490 t.ntranslate(n as u64, $msgid, $plur, Some($msgctx)), n))
491 }};
492}
493
494#[cfg(feature = "gettext-rs")]
503#[macro_export]
504macro_rules! tr_init {
505 ($path:expr) => {
506 $crate::internal::init(module_path!(), $path)
507 };
508}
509
510#[macro_export]
521macro_rules! set_translator {
522 ($translator:expr) => {
523 $crate::internal::set_translator(module_path!(), $translator)
524 };
525}
526
527#[macro_export]
531macro_rules! unset_translator {
532 () => {
533 $crate::internal::unset_translator(module_path!())
534 };
535}
536
537#[cfg(feature = "gettext")]
538impl Translator for gettext::Catalog {
539 fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str> {
540 Cow::Borrowed(if let Some(ctx) = context {
541 self.pgettext(ctx, string)
542 } else {
543 self.gettext(string)
544 })
545 }
546 fn ntranslate<'a>(
547 &'a self,
548 n: u64,
549 singular: &'a str,
550 plural: &'a str,
551 context: Option<&'a str>,
552 ) -> Cow<'a, str> {
553 Cow::Borrowed(if let Some(ctx) = context {
554 self.npgettext(ctx, singular, plural, n)
555 } else {
556 self.ngettext(singular, plural, n)
557 })
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 #[test]
564 fn it_works() {
565 assert_eq!(tr!("Hello"), "Hello");
566 assert_eq!(tr!("ctx" => "Hello"), "Hello");
567 assert_eq!(tr!("Hello {}", "world"), "Hello world");
568 assert_eq!(tr!("ctx" => "Hello {}", "world"), "Hello world");
569
570 assert_eq!(
571 tr!("I have one item" | "I have {n} items" % 1),
572 "I have one item"
573 );
574 assert_eq!(
575 tr!("ctx" => "I have one item" | "I have {n} items" % 42),
576 "I have 42 items"
577 );
578 assert_eq!(
579 tr!("{} have one item" | "{} have {n} items" % 42, "I"),
580 "I have 42 items"
581 );
582 assert_eq!(
583 tr!("ctx" => "{0} have one item" | "{0} have {n} items" % 42, "I"),
584 "I have 42 items"
585 );
586
587 assert_eq!(
588 tr!("{} = {}", 255, format_args!("{:#x}", 255)),
589 "255 = 0xff"
590 );
591 }
592}