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 `.ylang` files 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
11use yog_abi::YogStr;
12
13/// The text of translation `key` in the current language, unformatted —
14/// `None` when no lang file defines it (or before the mod registered).
15pub 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
32/// The text of translation `key` in the current language, or `key` itself
33/// when it has none — what Minecraft shows for an unknown key, so a missing
34/// translation is easy to spot. Call it each time the text is shown (e.g.
35/// while building a UI tree): the player can switch language at any time.
36pub fn tr(key: &str) -> String {
37    match translation(key) {
38        Some(text) => format_translation(&text, &[]),
39        None => key.to_string(),
40    }
41}
42
43/// [`tr`] with arguments for the text's format specifiers — see
44/// [`format_translation`]. The [`tr!`](crate::tr!) macro is shorter.
45pub fn tr_args(key: &str, args: &[&dyn Display]) -> String {
46    format_translation(&translation(key).unwrap_or_else(|| key.to_string()), args)
47}
48
49/// Fills Minecraft-style format specifiers in `pattern` (what lang files
50/// use): `%s`/`%d` take the next argument, `%1$s`/`%2$d` a numbered one
51/// (from 1), `%%` is a literal `%`. A specifier without an argument stays
52/// as written.
53pub 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/// `tr!("key")` = [`tr`]`("key")`; `tr!("key", a, b)` fills the text's
102/// `%s` specifiers with `a` and `b` (anything `Display`) — see
103/// [`format_translation`].
104#[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!(format_translation("%s of %s mods", &[&3, &"58"]), "3 of 58 mods");
121        assert_eq!(format_translation("%2$s, %1$s", &[&"a", &"b"]), "b, a");
122        assert_eq!(format_translation("100%% by %s", &[&"X"]), "100% by X");
123        assert_eq!(format_translation("%d items", &[&5]), "5 items");
124        assert_eq!(format_translation("missing %s and %3$s", &[]), "missing %s and %3$s");
125        assert_eq!(format_translation("50% off %x", &[&1]), "50% off %x");
126        assert_eq!(format_translation("trailing %", &[]), "trailing %");
127        assert_eq!(format_translation("Привет, %s!", &[&"мир"]), "Привет, мир!");
128    }
129
130    #[test]
131    fn unknown_key_without_runtime_is_the_key() {
132        assert_eq!(tr("mymod.nothing"), "mymod.nothing");
133        assert_eq!(crate::tr!("count %s", 4), "count 4");
134    }
135}