Add Spanish, Italian and Russian; dropdown order and language file tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 16:05:56 +01:00
1 parent 6629472811
commit a13abf63f3
6 files changed
+3112 -7

No files matched your search

+5 -3
View File
@@ -130,9 +130,11 @@ number by one (a new month starts again at `00001`) and prints the old and the n
## Adding a language ## Adding a language
1. Copy `lang/en.lang` to `lang/<code>.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are). 1. Copy `lang/en.lang` to `lang/<code>.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are).
2. Add the file to `SOURCES` in `src/i18n.rs`. 2. Add the file to `SOURCES` in `src/i18n.rs` and its code to `ORDER` (its place in the language dropdown).
3. The Dinowyde font covers Latin (all European accents), Greek and Cyrillic; the test 3. Give it a 12x9 pixel flag in `src/menu/flags.rs` (18 European languages have one already).
`every_text_of_the_game_has_its_glyphs` in `src/gfx.rs` lists characters it cannot draw (add the new file there). 4. `cargo test` checks that the file has every key and placeholder of `en.lang`
(`every_language_has_all_keys_and_placeholders`) and that the fonts can draw every character in it
(`every_text_of_the_game_has_its_glyphs`; Dinowyde covers Latin with all European accents, Greek and Cyrillic).
## Refreshing the generated files ## Refreshing the generated files
+1013
View File
File diff suppressed because it is too large. Load diff
+1013
View File
File diff suppressed because it is too large. Load diff
+1013
View File
File diff suppressed because it is too large. Load diff
+9 -1
View File
@@ -2189,7 +2189,15 @@ mod font_tests {
let (_, map) = load_font(); let (_, map) = load_font();
let question = font_src(&map, '?'); let question = font_src(&map, '?');
let mut missing = std::collections::BTreeSet::new(); let mut missing = std::collections::BTreeSet::new();
for file in [include_str!("../lang/en.lang"), include_str!("../lang/de.lang")] { // every language file in lang/
let files: Vec<String> = std::fs::read_dir("lang")
.expect("lang directory")
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |x| x == "lang"))
.map(|e| std::fs::read_to_string(e.path()).expect("language file"))
.collect();
assert!(files.len() >= 2);
for file in &files {
for ch in file.chars().filter(|c| !c.is_control() && *c != ' ') { for ch in file.chars().filter(|c| !c.is_control() && *c != ' ') {
if ch != '?' && font_src(&map, ch) == question { if ch != '?' && font_src(&map, ch) == question {
missing.insert(ch); missing.insert(ch);
+59 -3
View File
@@ -5,7 +5,17 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
const SOURCES: &[(&str, &str)] = &[("en", include_str!("../lang/en.lang")), ("de", include_str!("../lang/de.lang"))]; // English first: it is the fallback for missing keys
const SOURCES: &[(&str, &str)] = &[
("en", include_str!("../lang/en.lang")),
("de", include_str!("../lang/de.lang")),
("es", include_str!("../lang/es.lang")),
("ru", include_str!("../lang/ru.lang")),
("it", include_str!("../lang/it.lang")),
];
// order of the languages in the language dropdown
const ORDER: &[&str] = &["de", "en", "es", "pt", "nl", "da", "no", "sv", "fi", "ru", "hr", "hu", "sr", "ro", "sk", "cs", "it", "el"];
struct I18n { struct I18n {
langs: Vec<(String, HashMap<String, String>)>, langs: Vec<(String, HashMap<String, String>)>,
@@ -35,9 +45,11 @@ thread_local! {
// Available languages: (code, display name in that language) // Available languages: (code, display name in that language)
pub fn languages() -> Vec<(String, String)> { pub fn languages() -> Vec<(String, String)> {
I18N.with(|i| { let mut l: Vec<(String, String)> = I18N.with(|i| {
i.borrow().langs.iter().map(|(c, m)| (c.clone(), m.get("lang.name").cloned().unwrap_or_else(|| c.clone()))).collect() i.borrow().langs.iter().map(|(c, m)| (c.clone(), m.get("lang.name").cloned().unwrap_or_else(|| c.clone()))).collect()
}) });
l.sort_by_key(|(c, _)| ORDER.iter().position(|o| o == c).unwrap_or(ORDER.len()));
l
} }
pub fn set_language(code: &str) -> bool { pub fn set_language(code: &str) -> bool {
@@ -85,3 +97,47 @@ pub fn tr_in(code: &str, key: &str) -> String {
i.langs.iter().find(|(c, _)| c == code).and_then(|(_, m)| m.get(key)).cloned().unwrap_or_else(|| key.to_string()) i.langs.iter().find(|(c, _)| c == code).and_then(|(_, m)| m.get(key)).cloned().unwrap_or_else(|| key.to_string())
}) })
} }
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn placeholders(v: &str) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let mut rest = v;
while let Some(a) = rest.find('{') {
match rest[a..].find('}') {
Some(b) => {
out.insert(rest[a..a + b + 1].to_string());
rest = &rest[a + b + 1..];
}
None => break,
}
}
out
}
#[test]
fn every_language_has_all_keys_and_placeholders() {
let en = parse(SOURCES[0].1);
for (code, src) in SOURCES.iter().skip(1) {
let m = parse(src);
let missing: Vec<&String> = en.keys().filter(|k| !m.contains_key(*k)).collect();
let extra: Vec<&String> = m.keys().filter(|k| !en.contains_key(*k)).collect();
assert!(missing.is_empty() && extra.is_empty(), "{}: missing {:?}, extra {:?}", code, missing, extra);
for (k, v) in &en {
assert_eq!(placeholders(v), placeholders(&m[k]), "{}: placeholders of {}", code, k);
}
assert!(m.get("lang.name").map_or(false, |n| !n.is_empty()), "{} has a name", code);
assert!(ORDER.contains(code), "{} has a place in the dropdown", code);
}
}
#[test]
fn the_dropdown_follows_the_chosen_order() {
let codes: Vec<String> = languages().into_iter().map(|(c, _)| c).collect();
let pos = |c: &str| ORDER.iter().position(|o| *o == c).unwrap();
assert!(codes.windows(2).all(|w| pos(&w[0]) < pos(&w[1])), "{:?}", codes);
}
}