1use std::fmt::{Display, Write as _};
10
11use yog_abi::YogStr;
12
13pub fn translation(key: &str) -> Option<String> {
16 let api = crate::registry::global_api();
17 if api.is_null() {
18 return None;
19 }
20 let owned = unsafe { ((*api).translate)((*api).ctx, YogStr::from_str(key)) };
21 if owned.is_none() {
22 return None;
23 }
24 let text = unsafe {
25 String::from_utf8_lossy(std::slice::from_raw_parts(owned.ptr, owned.len as usize))
26 .into_owned()
27 };
28 unsafe { ((*api).free_str)(owned.ptr, owned.len) };
29 Some(text)
30}
31
32pub fn tr(key: &str) -> String {
37 match translation(key) {
38 Some(text) => format_translation(&text, &[]),
39 None => key.to_string(),
40 }
41}
42
43pub fn tr_args(key: &str, args: &[&dyn Display]) -> String {
46 format_translation(&translation(key).unwrap_or_else(|| key.to_string()), args)
47}
48
49pub fn format_translation(pattern: &str, args: &[&dyn Display]) -> String {
54 let mut out = String::with_capacity(pattern.len());
55 let mut next = 0;
56 let mut rest = pattern;
57 while let Some(pos) = rest.find('%') {
58 out.push_str(&rest[..pos]);
59 let spec = &rest[pos + 1..];
60 if let Some(after) = spec.strip_prefix('%') {
61 out.push('%');
62 rest = after;
63 continue;
64 }
65 if let Some(after) = spec.strip_prefix(['s', 'd']) {
66 match args.get(next) {
67 Some(arg) => {
68 let _ = write!(out, "{arg}");
69 }
70 None => out.push_str(&rest[pos..pos + 2]),
71 }
72 next += 1;
73 rest = after;
74 continue;
75 }
76 let digits = spec.bytes().take_while(u8::is_ascii_digit).count();
77 let numbered = (digits > 0)
78 .then(|| spec[digits..].strip_prefix('$'))
79 .flatten()
80 .and_then(|s| s.strip_prefix(['s', 'd']));
81 match (numbered, spec[..digits].parse::<usize>()) {
82 (Some(after), Ok(n)) if n >= 1 => {
83 match args.get(n - 1) {
84 Some(arg) => {
85 let _ = write!(out, "{arg}");
86 }
87 None => out.push_str(&rest[pos..rest.len() - after.len()]),
88 }
89 rest = after;
90 }
91 _ => {
92 out.push('%');
93 rest = spec;
94 }
95 }
96 }
97 out.push_str(rest);
98 out
99}
100
101#[macro_export]
105macro_rules! tr {
106 ($key:expr $(,)?) => {
107 $crate::tr($key)
108 };
109 ($key:expr, $($arg:expr),+ $(,)?) => {
110 $crate::tr_args($key, &[$(&$arg as &dyn ::std::fmt::Display),+])
111 };
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn formats_like_minecraft() {
120 assert_eq!(
121 format_translation("%s of %s mods", &[&3, &"58"]),
122 "3 of 58 mods"
123 );
124 assert_eq!(format_translation("%2$s, %1$s", &[&"a", &"b"]), "b, a");
125 assert_eq!(format_translation("100%% by %s", &[&"X"]), "100% by X");
126 assert_eq!(format_translation("%d items", &[&5]), "5 items");
127 assert_eq!(
128 format_translation("missing %s and %3$s", &[]),
129 "missing %s and %3$s"
130 );
131 assert_eq!(format_translation("50% off %x", &[&1]), "50% off %x");
132 assert_eq!(format_translation("trailing %", &[]), "trailing %");
133 assert_eq!(format_translation("Привет, %s!", &[&"мир"]), "Привет, мир!");
134 }
135
136 #[test]
137 fn unknown_key_without_runtime_is_the_key() {
138 assert_eq!(tr("mymod.nothing"), "mymod.nothing");
139 assert_eq!(crate::tr!("count %s", 4), "count 4");
140 }
141}