Message catalogs sorted one message per line (cargo xtask lists [--fix], checked in cargo xtask ci and the Lists workflow); the language list one per line, English first then by id; THIRD-PARTY-LICENSES.txt and docs/features.md rewritten on main by the Generated files workflow instead of in PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
2.8 KiB
Rust
69 lines
2.8 KiB
Rust
//! `cargo xtask lists [--fix]`: files that many pull requests add to are kept in a canonical,
|
|
//! sorted form, so two changes that each add an item meet only when their items are neighbours
|
|
//! (CONTRIBUTING.md, "Lists many changes add to"). Checked by `cargo xtask ci` and the Lists
|
|
//! workflow; `--fix` rewrites them.
|
|
//!
|
|
//! - The message catalogs (`assets/locales/*.json`): one message per line, sorted by id, no
|
|
//! duplicate ids, as `serde_json` pretty-prints them.
|
|
|
|
use std::path::Path;
|
|
|
|
use serde_json::{Map, Value};
|
|
|
|
const LOCALES: &str = "assets/locales";
|
|
|
|
pub fn run(root: &Path, fix: bool) -> Result<(), String> {
|
|
let dir = root.join(LOCALES);
|
|
let entries = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
|
let mut files: Vec<_> = entries.filter_map(Result::ok).map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "json")).collect();
|
|
files.sort();
|
|
let mut stale = Vec::new();
|
|
for path in files {
|
|
let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
|
|
let canonical = canonical(&text).map_err(|e| format!("{}: {e}", path.display()))?;
|
|
if canonical == text.replace("\r\n", "\n") {
|
|
continue;
|
|
}
|
|
if fix {
|
|
std::fs::write(&path, &canonical).map_err(|e| format!("{}: {e}", path.display()))?;
|
|
println!("lists: sorted {}", path.display());
|
|
} else {
|
|
stale.push(path.strip_prefix(root).unwrap_or(&path).display().to_string());
|
|
}
|
|
}
|
|
if !stale.is_empty() {
|
|
return Err(format!(
|
|
"not sorted one message per line (or an id appears twice): {}\nrun `cargo xtask lists --fix` and commit the result",
|
|
stale.join(", ")
|
|
));
|
|
}
|
|
println!("lists: message catalogs are sorted");
|
|
Ok(())
|
|
}
|
|
|
|
/// A catalog as it should be written: its messages sorted by id, one per line. A repeated id
|
|
/// keeps its last message, the one the app reads.
|
|
pub fn canonical(text: &str) -> Result<String, String> {
|
|
let map: Map<String, Value> = match serde_json::from_str(text).map_err(|e| e.to_string())? {
|
|
Value::Object(map) => map,
|
|
_ => return Err("a catalog must be a JSON object".into()),
|
|
};
|
|
if let Some((id, _)) = map.iter().find(|(_, v)| !v.is_string()) {
|
|
return Err(format!("message `{id}` is not a string"));
|
|
}
|
|
Ok(serde_json::to_string_pretty(&Value::Object(map)).map_err(|e| e.to_string())? + "\n")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::canonical;
|
|
|
|
#[test]
|
|
fn catalogs_are_sorted_one_per_line_and_a_repeated_id_keeps_its_last_message() {
|
|
let text = "{\n \"b\": \"B\",\n \"a\": \"A\",\n \"b\": \"B2\"\n}";
|
|
assert_eq!(canonical(text).unwrap(), "{\n \"a\": \"A\",\n \"b\": \"B2\"\n}\n");
|
|
assert!(canonical("[]").is_err());
|
|
assert!(canonical("{\"a\": 1}").is_err());
|
|
}
|
|
}
|