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 crate::registry::take_owned(unsafe { (*api).free_str }, owned)
22}
23
24pub fn tr(key: &str) -> String {
29 match translation(key) {
30 Some(text) => format_translation(&text, &[]),
31 None => key.to_string(),
32 }
33}
34
35pub fn tag_name(kind: &str, id: &str) -> String {
40 let registry = match kind {
41 "entity" => "entity_type",
42 "biome" => "worldgen.biome",
43 other => other,
44 };
45 let key = format!("tag.{registry}.{}", id.replace([':', '/'], "."));
46 translation(&key).map_or_else(|| id.to_string(), |text| format_translation(&text, &[]))
47}
48
49pub fn tr_args(key: &str, args: &[&dyn Display]) -> String {
52 format_translation(&translation(key).unwrap_or_else(|| key.to_string()), args)
53}
54
55pub fn format_translation(pattern: &str, args: &[&dyn Display]) -> String {
60 let mut out = String::with_capacity(pattern.len());
61 let mut next = 0;
62 let mut rest = pattern;
63 while let Some(pos) = rest.find('%') {
64 out.push_str(&rest[..pos]);
65 let spec = &rest[pos + 1..];
66 if let Some(after) = spec.strip_prefix('%') {
67 out.push('%');
68 rest = after;
69 continue;
70 }
71 if let Some(after) = spec.strip_prefix(['s', 'd']) {
72 match args.get(next) {
73 Some(arg) => {
74 let _ = write!(out, "{arg}");
75 }
76 None => out.push_str(&rest[pos..pos + 2]),
77 }
78 next += 1;
79 rest = after;
80 continue;
81 }
82 let digits = spec.bytes().take_while(u8::is_ascii_digit).count();
83 let numbered = (digits > 0)
84 .then(|| spec[digits..].strip_prefix('$'))
85 .flatten()
86 .and_then(|s| s.strip_prefix(['s', 'd']));
87 match (numbered, spec[..digits].parse::<usize>()) {
88 (Some(after), Ok(n)) if n >= 1 => {
89 match args.get(n - 1) {
90 Some(arg) => {
91 let _ = write!(out, "{arg}");
92 }
93 None => out.push_str(&rest[pos..rest.len() - after.len()]),
94 }
95 rest = after;
96 }
97 _ => {
98 out.push('%');
99 rest = spec;
100 }
101 }
102 }
103 out.push_str(rest);
104 out
105}
106
107#[macro_export]
111macro_rules! tr {
112 ($key:expr_2021 $(,)?) => {
113 $crate::tr($key)
114 };
115 ($key:expr_2021, $($arg:expr_2021),+ $(,)?) => {
116 $crate::tr_args($key, &[$(&$arg as &dyn ::std::fmt::Display),+])
117 };
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn formats_like_minecraft() {
126 assert_eq!(
127 format_translation("%s of %s mods", &[&3, &"58"]),
128 "3 of 58 mods"
129 );
130 assert_eq!(format_translation("%2$s, %1$s", &[&"a", &"b"]), "b, a");
131 assert_eq!(format_translation("100%% by %s", &[&"X"]), "100% by X");
132 assert_eq!(format_translation("%d items", &[&5]), "5 items");
133 assert_eq!(
134 format_translation("missing %s and %3$s", &[]),
135 "missing %s and %3$s"
136 );
137 assert_eq!(format_translation("50% off %x", &[&1]), "50% off %x");
138 assert_eq!(format_translation("trailing %", &[]), "trailing %");
139 assert_eq!(format_translation("Привет, %s!", &[&"мир"]), "Привет, мир!");
140 }
141
142 #[test]
143 fn unknown_key_without_runtime_is_the_key() {
144 assert_eq!(tr("mymod.nothing"), "mymod.nothing");
145 assert_eq!(crate::tr!("count %s", 4), "count 4");
146 }
147}