Skip to main content

shuyi_assets/lang/
places.rs

1//! What a key of English's screen text says about the place its text is
2//! drawn in ([`Place`]): how many cells wide it is (`max_cells`), and what
3//! a translator should know (`note`: where the text shows, what a
4//! placeholder holds). `en/ui.ron` writes them in place of the key's text:
5//!
6//! ```ron
7//! {
8//!     "title.start": (text: "Start", max_cells: 12, note: "The title menu's first item."),
9//!     "title.quit": "Quit",
10//! }
11//! ```
12//!
13//! A pack sets neither: they describe the place and the English, not the
14//! translation. [`Lang::overflow`] lists a pack's entries too wide for
15//! their place; the game never shows a note.
16use crate::prelude::*;
17
18use alloc::collections::BTreeMap;
19use core::fmt;
20
21use serde::Deserialize;
22use serde::de::value::MapAccessDeserializer;
23use serde::de::{Deserializer, MapAccess, Visitor};
24
25use super::{Lang, LangCode, text_width};
26use crate::font::CellWidths;
27
28/// What English's screen text says about where a key's text is drawn.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Place {
32    /// How many cells wide the place is; `None` for no limit.
33    pub max_cells: Option<usize>,
34    /// What a translator should know about the text, never shown in the
35    /// game.
36    pub note: Option<String>,
37}
38
39/// Where English's screen text is drawn, by key.
40pub type Places = BTreeMap<String, Place>;
41
42/// A value of `en/ui.ron`: the text, alone or with its place.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub(super) struct UiValue {
45    pub(super) text: String,
46    pub(super) place: Place,
47}
48
49/// The written form of a text with its place.
50#[derive(Deserialize)]
51#[serde(deny_unknown_fields)]
52struct Placed {
53    text: String,
54    #[serde(default)]
55    max_cells: Option<usize>,
56    #[serde(default)]
57    note: Option<String>,
58}
59
60impl<'de> Deserialize<'de> for UiValue {
61    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
62        struct ValueVisitor;
63
64        impl<'de> Visitor<'de> for ValueVisitor {
65            type Value = UiValue;
66
67            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68                f.write_str("a text, or (text: …, max_cells: …, note: …)")
69            }
70
71            fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<UiValue, E> {
72                Ok(UiValue {
73                    text: text.to_owned(),
74                    place: Place::default(),
75                })
76            }
77
78            fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<UiValue, A::Error> {
79                let placed = Placed::deserialize(MapAccessDeserializer::new(map))?;
80                Ok(UiValue {
81                    text: placed.text,
82                    place: Place {
83                        max_cells: placed.max_cells,
84                        note: placed.note,
85                    },
86                })
87            }
88        }
89
90        deserializer.deserialize_any(ValueVisitor)
91    }
92}
93
94/// A pack's entry wider than its key's place ([`Lang::overflow`]).
95#[derive(Debug, Clone, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct Overflow {
98    /// The entry's key.
99    pub key: String,
100    /// How many cells its place has ([`Place::max_cells`]).
101    pub max_cells: usize,
102    /// How many cells its text takes ([`text_width`]).
103    pub width: usize,
104    /// Its text.
105    pub text: String,
106}
107
108impl Lang {
109    /// These languages, English's screen text drawn in `places` (by key).
110    #[must_use]
111    pub fn with_places(mut self, places: Places) -> Self {
112        self.places = places;
113        self
114    }
115
116    /// What English says about where `key`'s text is drawn, if anything.
117    pub fn place(&self, key: &str) -> Option<&Place> {
118        self.places.get(key)
119    }
120
121    /// How many cells wide `key`'s place is, if English says.
122    pub fn max_cells(&self, key: &str) -> Option<usize> {
123        self.place(key).and_then(|p| p.max_cells)
124    }
125
126    /// What a translator should know about `key`'s text, if English says.
127    pub fn note(&self, key: &str) -> Option<&str> {
128        self.place(key).and_then(|p| p.note.as_deref())
129    }
130
131    /// The screen-text entries of `code`'s pack wider than their key's
132    /// [`max_cells`](Self::max_cells), measured with `widths` (the atlas's)
133    /// as the pack has them ([`LangInfo::widths`](super::LangInfo::widths)),
134    /// in key order. Every entry is measured, stale ones too, as its text
135    /// is written (placeholders unfilled). `None` for a code with no pack.
136    pub fn overflow(&self, code: &LangCode, widths: &CellWidths) -> Option<Vec<Overflow>> {
137        let pack = self.packs.get(code)?;
138        let widths = &pack.info.widths(widths);
139        let too_wide = |(key, entry): (&String, &super::Entry)| {
140            let max_cells = self.max_cells(key)?;
141            let width = text_width(&entry.text, widths);
142            (width > max_cells).then(|| Overflow {
143                key: key.clone(),
144                max_cells,
145                width,
146                text: entry.text.clone(),
147            })
148        };
149        Some(pack.ui.iter().filter_map(too_wide).collect())
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::super::{
156        LangFile, PackRules, english_and_places_from_source, english_from_source, from_files,
157        pack_from_sources,
158    };
159    use super::*;
160
161    const SOURCE_UI: &str = r#"{
162        "title.start": (text: "Start", max_cells: 8, note: "The title menu's first item."),
163        "title.quit": (text: "Quit", max_cells: 6),
164        "title.help": (text: "Press {Confirm}", note: "{Confirm} is a key's name."),
165        "title.name": "Demo",
166    }"#;
167
168    const RULES: PackRules<'static> = PackRules {
169        data_problem: |_, _| None,
170        tokens: &crate::dialogue::NoTokens,
171        limits: crate::dialogue::tests::LIMITS,
172        widths: &CellWidths::NARROW,
173        variants: &[],
174    };
175
176    fn errors(source: &str) -> Vec<String> {
177        english_and_places_from_source("en/ui.ron", source)
178            .unwrap_err()
179            .iter()
180            .map(ToString::to_string)
181            .collect()
182    }
183
184    #[test]
185    fn a_key_with_max_cells_and_a_note_loads() {
186        let (english, places) = english_and_places_from_source("en/ui.ron", SOURCE_UI).unwrap();
187        assert_eq!(english["title.start"], "Start");
188        assert_eq!(english["title.name"], "Demo");
189        assert_eq!(places.len(), 3, "{places:?}");
190        let lang = Lang::new(english, BTreeMap::new()).with_places(places);
191        assert_eq!(
192            lang.note("title.start"),
193            Some("The title menu's first item.")
194        );
195        assert_eq!(lang.max_cells("title.start"), Some(8));
196        assert_eq!(lang.note("title.quit"), None);
197        assert_eq!(lang.max_cells("title.quit"), Some(6));
198        assert_eq!(lang.max_cells("title.help"), None);
199        assert_eq!(lang.note("title.name"), None);
200        assert_eq!(lang.place("title.name"), None);
201        assert_eq!(lang.place("no.such"), None);
202        // Text alone reads the same as before.
203        assert_eq!(
204            english_from_source("en/ui.ron", SOURCE_UI).unwrap().len(),
205            4
206        );
207    }
208
209    #[test]
210    fn a_place_must_say_something_sensible() {
211        assert_eq!(
212            errors(r#"{"a": (text: "A", max_cells: 0), "b": (text: "B", note: " ")}"#),
213            [
214                "en/ui.ron: \"a\": max_cells must be at least 1",
215                "en/ui.ron: \"b\": a note must not be empty",
216            ]
217        );
218        let unknown = errors(r#"{"a": (text: "A", width: 3)}"#);
219        assert!(unknown[0].contains("width"), "{unknown:?}");
220        let no_text = errors(r#"{"a": (max_cells: 3)}"#);
221        assert!(no_text[0].contains("text"), "{no_text:?}");
222        let wrong = errors(r#"{"a": 3}"#);
223        assert!(wrong[0].contains("a text, or (text:"), "{wrong:?}");
224    }
225
226    #[test]
227    fn a_pack_sets_neither() {
228        let (english, _) = english_and_places_from_source("en/ui.ron", SOURCE_UI).unwrap();
229        let info = r#"(name: "X", made_by: Machine)"#;
230        let ui = r#"[(key: "title.quit", source: "Quit", text: "Q", max_cells: 9)]"#;
231        let errors = pack_from_sources(("xx/lang.ron", info), ("xx/ui.ron", ui), &english, &RULES)
232            .unwrap_err();
233        assert!(errors[0].to_string().contains("max_cells"), "{errors:?}");
234    }
235
236    #[test]
237    fn overflow_lists_the_entries_wider_than_their_place() {
238        let ui = r#"[
239            (key: "title.start", source: "Start", text: "はじめから"),
240            (key: "title.quit", source: "Quit", text: "Beenden!"),
241            (key: "title.help", source: "Press {Confirm}", text: "Drücke {Confirm} lange"),
242            (key: "title.name", source: "Demo", text: "Démonstration"),
243        ]"#;
244        let files = [
245            ("en/ui.ron", SOURCE_UI),
246            ("xx/lang.ron", r#"(name: "X", made_by: Machine)"#),
247            ("xx/ui.ron", ui),
248        ]
249        .map(|(path, source)| LangFile {
250            path,
251            source: Some(source),
252        });
253        let lang = from_files(&files, None, str::to_owned, &RULES).unwrap();
254        let xx = LangCode::new("xx").unwrap();
255        let overflow = |widths: &CellWidths| lang.overflow(&xx, widths).unwrap();
256        // Narrow, `はじめから` is 5 cells and fits 8; `Beenden!` is 8 > 6.
257        let narrow = overflow(&CellWidths::NARROW);
258        assert_eq!(
259            narrow,
260            [Overflow {
261                key: "title.quit".to_owned(),
262                max_cells: 6,
263                width: 8,
264                text: "Beenden!".to_owned(),
265            }]
266        );
267        // Wide, it is 10 cells: too wide too.
268        let wide = overflow(&CellWidths::wide("はじめから".chars()));
269        let keys: Vec<(&str, usize)> = wide.iter().map(|o| (o.key.as_str(), o.width)).collect();
270        assert_eq!(keys, [("title.quit", 8), ("title.start", 10)]);
271        // Exactly as wide as its place, it fits: 8 cells of 8.
272        let exact = overflow(&CellWidths::wide("はじめ".chars()));
273        let keys: Vec<&str> = exact.iter().map(|o| o.key.as_str()).collect();
274        assert_eq!(keys, ["title.quit"]);
275        assert_eq!(
276            lang.overflow(&LangCode::default(), &CellWidths::NARROW),
277            None
278        );
279    }
280}