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:
1 parent
6629472811
commit
a13abf63f3
6 files changed
+3112
-7
No files matched your search
+5
-3
@@ -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
File diff suppressed because it is too large.
Load diff
+1013
File diff suppressed because it is too large.
Load diff
+1013
File diff suppressed because it is too large.
Load diff
+9
-1
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user