Skip to main content

yog_api/
lang.rs

1//! Translations — text in the player's language, from the lang files the
2//! game loaded (a mod's `.yres` texts become them at `yog build`).
3//!
4//! ```ignore
5//! let title = yog_api::tr("mymod.screen.title");
6//! let count = yog_api::tr!("mymod.screen.count", shown, total); // "%s of %s mods"
7//! ```
8
9use std::fmt::{Display, Write as _};
10
11/// The text of translation `key` in the current language, unformatted —
12/// `None` when no lang file defines it (or before the mod registered).
13pub fn translation(key: &str) -> Option<String> {
14    crate::call(&yog_event::game::Translate {
15        key: key.to_owned(),
16    })
17    .flatten()
18}
19
20/// The text of translation `key` in the current language, or `key` itself
21/// when it has none — what Minecraft shows for an unknown key, so a missing
22/// translation is easy to spot. Call it each time the text is shown (e.g.
23/// while building a UI tree): the player can switch language at any time.
24pub fn tr(key: &str) -> String {
25    match translation(key) {
26        Some(text) => format_translation(&text, &[]),
27        None => key.to_string(),
28    }
29}
30
31/// A tag's name in the player's language — `kind` the content's
32/// (`item`, `block`, `entity`, `fluid`, `biome`), `id` the tag's
33/// (`yog:ingots/iron`): the translation recipe viewers show
34/// (`tag.item.yog.ingots.iron`), else the id itself.
35pub 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
41/// [`tr`] with arguments for the text's format specifiers — see
42/// [`format_translation`]. The [`tr!`](crate::tr!) macro is shorter.
43pub fn tr_args(key: &str, args: &[&dyn Display]) -> String {
44    format_translation(&translation(key).unwrap_or_else(|| key.to_string()), args)
45}
46
47/// Fills Minecraft-style format specifiers in `pattern` (what lang files
48/// use): `%s`/`%d` take the next argument, `%1$s`/`%2$d` a numbered one
49/// (from 1), `%%` is a literal `%`. A specifier without an argument stays
50/// as written.
51pub 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/// `tr!("key")` = [`tr`]`("key")`; `tr!("key", a, b)` fills the text's
100/// `%s` specifiers with `a` and `b` (anything `Display`) — see
101/// [`format_translation`].
102#[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}