open-standards/aac/aac-rs/crates/aac-web/build.rs
Transaction Science e48131a025 feat(aac-web): the app's own words through a Fluent catalogue, in the person's language
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]>
2026-10-08 12:08:27 -04:00

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");
}