1use std::fmt::{Display, Write as _};
10
11pub fn translation(key: &str) -> Option<String> {
14 crate::call(&yog_event::game::Translate {
15 key: key.to_owned(),
16 })
17 .flatten()
18}
19
20pub fn tr(key: &str) -> String {
25 match translation(key) {
26 Some(text) => format_translation(&text, &[]),
27 None => key.to_string(),
28 }
29}
30
31pub fn tag_name(kind: &str, id: &str) -> String {
36 let registry = yog_ids::registry_of(kind).replace(yog_ids::sep::PATH, ".");
37 let key = format!("tag.{registry}.{}", id.replace([':', '/'], "."));
38 translation(&key).map_or_else(|| id.to_string(), |text| format_translation(&text, &[]))
39}
40
41pub fn tr_args(key: &str, args: &[&dyn Display]) -> String {
44 format_translation(&translation(key).unwrap_or_else(|| key.to_string()), args)
45}
46
47pub fn format_translation(pattern: &str, args: &[&dyn Display]) -> String {
52 let mut out = String::with_capacity(pattern.len());
53 let mut next = 0;
54 let mut rest = pattern;
55 while let Some(pos) = rest.find('%') {
56 out.push_str(&rest[..pos]);
57 let spec = &rest[pos + 1..];
58 if let Some(after) = spec.strip_prefix('%') {
59 out.push('%');
60 rest = after;
61 continue;
62 }
63 if let Some(after) = spec.strip_prefix(['s', 'd']) {
64 match args.get(next) {
65 Some(arg) => {
66 let _ = write!(out, "{arg}");
67 }
68 None => out.push_str(&rest[pos..pos + 2]),
69 }
70 next += 1;
71 rest = after;
72 continue;
73 }
74 let digits = spec.bytes().take_while(u8::is_ascii_digit).count();
75 let numbered = (digits > 0)
76 .then(|| spec[digits..].strip_prefix('$'))
77 .flatten()
78 .and_then(|s| s.strip_prefix(['s', 'd']));
79 match (numbered, spec[..digits].parse::<usize>()) {
80 (Some(after), Ok(n)) if n >= 1 => {
81 match args.get(n - 1) {
82 Some(arg) => {
83 let _ = write!(out, "{arg}");
84 }
85 None => out.push_str(&rest[pos..rest.len() - after.len()]),
86 }
87 rest = after;
88 }
89 _ => {
90 out.push('%');
91 rest = spec;
92 }
93 }
94 }
95 out.push_str(rest);
96 out
97}
98
99#[macro_export]
103macro_rules! tr {
104 ($key:expr_2021 $(,)?) => {
105 $crate::tr($key)
106 };
107 ($key:expr_2021, $($arg:expr_2021),+ $(,)?) => {
108 $crate::tr_args($key, &[$(&$arg as &dyn ::std::fmt::Display),+])
109 };
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn formats_like_minecraft() {
118 assert_eq!(
119 format_translation("%s of %s mods", &[&3, &"58"]),
120 "3 of 58 mods"
121 );
122 assert_eq!(format_translation("%2$s, %1$s", &[&"a", &"b"]), "b, a");
123 assert_eq!(format_translation("100%% by %s", &[&"X"]), "100% by X");
124 assert_eq!(format_translation("%d items", &[&5]), "5 items");
125 assert_eq!(
126 format_translation("missing %s and %3$s", &[]),
127 "missing %s and %3$s"
128 );
129 assert_eq!(format_translation("50% off %x", &[&1]), "50% off %x");
130 assert_eq!(format_translation("trailing %", &[]), "trailing %");
131 assert_eq!(format_translation("Привет, %s!", &[&"мир"]), "Привет, мир!");
132 }
133
134 #[test]
135 fn unknown_key_without_runtime_is_the_key() {
136 assert_eq!(tr("mymod.nothing"), "mymod.nothing");
137 assert_eq!(crate::tr!("count %s", 4), "count 4");
138 }
139}