English (i18n/en/*.ftl) is compiled in and is the fallback; each other language's catalogue (i18n/<code>.ftl, only messages two readers agreed on) is served content-hashed under models/ui/, fetched on first use, checked against the size and SHA-256 the app's index (build.rs) gives it, and kept in IndexedDB (never Cache Storage). A message a catalogue lacks shows in English marked lang="en"; the page's lang and dir follow the language the app's words are in. Plurals by CLDR rules, $speaker from Words about myself. Settings → Language says how many of the app's words are in the language, checked by two AI readers and not yet by a native speaker. tests/ui_strings.rs: no English UI literal outside the catalogue, every id used is English and every English message used, and every catalogue has the English placeholders. Co-Authored-By: Claude Opus 5.5 <[email protected]>
89 lines
3.7 KiB
Rust
89 lines
3.7 KiB
Rust
//! The app's own words (src/i18n.rs): the English messages, compiled in,
|
|
//! and the index of the other languages' catalogues — each one's file name
|
|
//! (named by its content, as the site serves it under models/ui/), size,
|
|
//! SHA-256, and how many of the English messages it has.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::fmt::Write as _;
|
|
use std::path::Path;
|
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
/// The ids of the messages in Fluent source (`id = …` at the start of a
|
|
/// line; terms, `-id`, are not messages).
|
|
fn ids(source: &str) -> BTreeSet<String> {
|
|
source
|
|
.lines()
|
|
.filter_map(|line| {
|
|
let (id, _) = line.split_once('=')?;
|
|
let id = id.trim_end();
|
|
let mut chars = id.chars();
|
|
(chars.next().is_some_and(|c| c.is_ascii_alphabetic()) && chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'))
|
|
.then(|| id.to_string())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn main() {
|
|
let dir = Path::new("i18n");
|
|
println!("cargo:rerun-if-changed=i18n");
|
|
let out = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
|
|
|
|
let mut english_files: Vec<_> = std::fs::read_dir(dir.join("en"))
|
|
.expect("i18n/en")
|
|
.filter_map(Result::ok)
|
|
.map(|e| e.path())
|
|
.filter(|p| p.extension().is_some_and(|x| x == "ftl"))
|
|
.collect();
|
|
english_files.sort();
|
|
let mut english = String::new();
|
|
for path in &english_files {
|
|
println!("cargo:rerun-if-changed={}", path.display());
|
|
let text = std::fs::read_to_string(path).expect("an English catalogue file");
|
|
english.push_str(&text);
|
|
if !english.ends_with('\n') {
|
|
english.push('\n');
|
|
}
|
|
english.push('\n');
|
|
}
|
|
let english_ids = ids(&english);
|
|
std::fs::write(out.join("ui_en.ftl"), &english).expect("write ui_en.ftl");
|
|
|
|
let mut languages: Vec<_> = std::fs::read_dir(dir)
|
|
.expect("i18n")
|
|
.filter_map(Result::ok)
|
|
.map(|e| e.path())
|
|
.filter(|p| p.is_file() && p.extension().is_some_and(|x| x == "ftl"))
|
|
.collect();
|
|
languages.sort();
|
|
let mut index = String::from(
|
|
"/// A language's catalogue of the app's words.\n\
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n\
|
|
pub struct Catalogue {\n\
|
|
\x20 /// ISO 639-1 code.\n pub code: &'static str,\n\
|
|
\x20 /// Served as `models/ui/{file}`: `{code}.{first 12 hex of sha256}.ftl`.\n pub file: &'static str,\n\
|
|
\x20 pub sha256: &'static str,\n pub bytes: usize,\n\
|
|
\x20 /// How many of the English messages it has (agreed by two readers).\n pub messages: usize,\n}\n\n\
|
|
pub const CATALOGUES: &[Catalogue] = &[\n",
|
|
);
|
|
for path in &languages {
|
|
println!("cargo:rerun-if-changed={}", path.display());
|
|
let code = path.file_stem().and_then(|s| s.to_str()).expect("a catalogue's name is its language code");
|
|
let bytes = std::fs::read(path).expect("a catalogue");
|
|
let sha = Sha256::digest(&bytes).iter().fold(String::new(), |mut s, b| {
|
|
let _ = write!(s, "{b:02x}");
|
|
s
|
|
});
|
|
let text = String::from_utf8(bytes.clone()).expect("a catalogue is UTF-8");
|
|
let messages = ids(&text).intersection(&english_ids).count();
|
|
let _ = writeln!(
|
|
index,
|
|
" Catalogue {{ code: {code:?}, file: \"{code}.{}.ftl\", sha256: {sha:?}, bytes: {}, messages: {messages} }},",
|
|
&sha[..12],
|
|
bytes.len()
|
|
);
|
|
}
|
|
index.push_str("];\n\n/// How many messages the English has.\n");
|
|
let _ = writeln!(index, "pub const ENGLISH_MESSAGES: usize = {};", english_ids.len());
|
|
std::fs::write(out.join("ui_index.rs"), index).expect("write ui_index.rs");
|
|
}
|