diff --git a/AGENTS.md b/AGENTS.md index f6123cc5c..2ecfe99a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,13 +54,14 @@ Anything else is forbidden, including assets whose licence you cannot prove from - path or URL, title, author; - source (URL, with version or commit); - SPDX licence, and the licence file in the repo; - - SHA-256; + - SHA-256, except for translation catalogs (see below); - how it is used. +- **Translation catalogs** (`crates/ui-egui/src/i18n/*.tsv`, `kind = "translation"`) are project files under the project licence. Their entries carry no `sha256`: git history records every change and who made it. Every other kind (fonts, images, data) is hashed. - **`ATTRIBUTION.md`** is generated from it (`cargo xtask assets --write`). Never edit it by hand. - **`NOTICE`** keeps the licence notices that the licences require. - **`cargo xtask assets`** runs in `cargo xtask ci`. It fails if: - an asset file in the repository has no entry; - - an entry's hash does not match; + - an entry's hash is missing or does not match (translation catalogs carry none); - a licence is not on the allowlist; - a licence file is missing; - an image, icon or font names Adobe as author or source, except a font licensed under OFL-1.1 alone. diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 19c81bf00..deb37ceb9 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -2,7 +2,7 @@ -Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). +Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset but the translation catalogs, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). ## In this repository (288) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 106c1febf..c0bfada2d 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,12 +7,13 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1902f1c31698e6adfdaf65b35b1fe1628f5cd2bd0182dd3b99bcf5e21720f394" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). # # [[asset]] a file in this repository (committed or vendored); `sha256` must match the file. +# Translation catalogs (kind = "translation", crates/ui-egui/src/i18n/*.tsv) are project files +# under the project licence and carry no `sha256`: git history records their provenance. # [[bundled]] a file compiled into PdfCraft by a Cargo dependency (its crate version must match Cargo.lock). # [[fetched]] downloaded at build time by xtask (never committed), pinned by `sha256`. # [[build_input]] an optional build input outside this repository (e.g. craft-fonts via CRAFT_FONTS_DIR); @@ -30,7 +31,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "2e23c41fcd5b9118cfde03b3a45aa2f988f7eeba650677b6f1f6664e543c7a59" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "105703f2fbc07c0663308ebd6852199de12a5851a37507d7e51e477eb5e57218" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +51,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b08f8282923d2faaf1f658d2aff8f71fe5f2d3a33e792eeb50fb0e2b396ef24f" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +61,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "df17dada3df736e75bd9b03e5ecceec164cce63767e33a1efdb98c56a121c890" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +71,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8e8c0cfda0886c584e5286370ec00814206c3af3699c76665e636fa6fdb6310b" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +81,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3f31e5c9abdb0208d48d8c68aa81437d9d1e10aeb615dedf811fefa539d93279" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +91,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "815a3a672421cc3590fb1f267755316659cdb2046e30bbb3ad8b31e96f08b20a" [[asset]] path = "crates/ui-egui/src/i18n/lo.tsv" @@ -107,7 +101,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lao UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6bd67286795ed487eedf2ce549249c8a83286cb4273490c1349996050b540fed" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -118,7 +111,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9cd63425705fb9a3aa029ff3a5110d0877a34f9038f1d64a2102ccd6c3cb35a6" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -129,7 +121,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "76f138293fc12ea3584568a81f6f90a1cedfff6256e78b4f60cb21b1b66bbba4" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -140,7 +131,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0bd75a93f8d18deb832223bd0642cc4f4abde04a322b684a143a9d4b4687a344" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -151,7 +141,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3d539a28afcf8c2e36b7087ac23334b50bac7971e03da20a62b6e7e400763a87" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -162,7 +151,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "71b7e8180fc759aebad4660953ab8b55383def0572c1ace2288cd8bfeb1b82dc" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -173,7 +161,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e689fb24a8acfa7e85f0f60644f7e2da1225595e6cd6a94109fbe86082926244" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -184,7 +171,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c601ea117ec2a3cdba606fdc9a62220fcffbd0c20c1133bacb653a979cf046a5" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -195,7 +181,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "006867b891ae2f995c7a711e55b6401db29d20ab493889c6284dc3671f9faa2d" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -206,7 +191,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9e5b18a09c209525cf08ef53a242ed457cfa346295eef7facde482a75cf72f52" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -217,7 +201,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "2306dbbe6a73671fd20df116adb159a7f66c85fbbd1bae5ecef5f6e882d6b7f1" [[asset]] path = "crates/ui-egui/src/i18n/ckb.tsv" @@ -228,7 +211,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Central Kurdish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "864db8aa0a5e443c92e7e388f89349eb0f0e7beda35a0b81695d864dcec51b72" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -239,7 +221,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "463a2d28bd5c5ac8df31a5b5c68400d7a61ce51237a58d9337078c973e6f9e3d" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -250,7 +231,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bbef7f4e16f83e9e0116278e7a0a175bad79757f96a18dc639516572533f6d5b" [[asset]] path = "crates/ui-egui/src/i18n/hi.tsv" @@ -261,7 +241,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hindi UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "09d2840c02b40a6049b86c2cb68151d863913e8c91c22226f25d07496c6366aa" [[asset]] path = "crates/ui-egui/src/i18n/id.tsv" title = "Indonesian interface translation catalog" @@ -271,7 +250,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Indonesian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0dc1abdb262e4568f19ffb2233bfeb3686ae42df8a8d8bec2951351a50e6a886" [[asset]] path = "crates/ui-egui/src/i18n/ko.tsv" title = "Korean interface translation catalog" @@ -281,7 +259,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Korean UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a3e67cee4f3fff776e816523f810b2bb6353235dbdf85181c3f12896604a3339" [[asset]] path = "crates/ui-egui/src/i18n/ro.tsv" @@ -292,7 +269,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Romanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8e76922ebf2c707bf4a67a2df0940287b560ce012db367dd9c39fbe8e409cc1b" [[asset]] @@ -304,7 +280,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Finnish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "609efa92054aa76de4bb6062c0fcc0a8b450519f4b253b4c0f395b3cbb34d2ba" [[asset]] path = "assets/icons/accessibility.svg" @@ -3426,7 +3401,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "57f0dd007ad16518fbc87d8aeac840ef6d86412bb460aff9fb17b176408fb7a8" [[asset]] path = "crates/ui-egui/src/i18n/sl.tsv" @@ -3437,7 +3411,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a3b8351f0da80c2d394221dbe5d62e794bb019c74c2adfc5d933d999f7d3236f" [[asset]] @@ -3460,7 +3433,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Norwegian Bokmål UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "03477c4ab2d0d52543cb91b3aec6f27b2689e6b9bb7c95bbb8463664c75a9dc0" [[asset]] path = "assets/icons/superscript.svg" @@ -3515,7 +3487,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Albanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ebd177262d8fe20f960f06df084f8a03c9504bcd9990dded3a006e35bd8ef7c9" [[asset]] path = "crates/ui-egui/src/i18n/th.tsv" @@ -3526,7 +3497,6 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Thai UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1dbb66c05af0aec06d5a6fa154e89f1684203dde32c8e8aedd9aeec6c2d5ae02" [[asset]] path = "crates/ui-egui/src/i18n/ca.tsv" @@ -3537,4 +3507,3 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Catalan UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5879ee3540ffb630fc83d0f2cb293cc6cbf2e081ea4577161a770794e8cbd22c" diff --git a/docs/localization.md b/docs/localization.md index fc161e99a..880036237 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -74,7 +74,7 @@ PdfCraft uses the same system as PhotoCraft (`crates/ui-egui/src/i18n/`): - Placeholders (`{name}`) must appear in both columns. A trailing `…` must be kept. `i18n::fmt` fills placeholders in one pass, so a value such as a file name containing `{n}` is inserted as written. - A missing entry never fails a build or a test: that string shows in English. `cargo xtask i18n-report`, a step of `cargo xtask ci`, prints for each language how many strings it has no translation for and the first few. It counts every command, All tools label, literal passed to `tl!` or `i18n::t` in the UI source, engine wording (below) and key of the bundled catalogs, so a new English string shows up there until each catalog has it. - Catalogs are validated strictly: unknown escapes, unknown `@` contexts, placeholder or ellipsis mismatches, wrong plural form counts and duplicates are errors. The tests require the bundled catalogs to have none; at run time a bad line is skipped (and logged) and that string shows in English. -- Catalogs are assets: each `.tsv` has an `ATTRIBUTION.toml` entry with `kind = "translation"` and its SHA-256, checked by `cargo xtask assets`. After editing a catalog, put the new hash into its entry by hand (`sha256sum crates/ui-egui/src/i18n/.tsv`), because `cargo xtask assets --write` does not touch `ATTRIBUTION.toml`; the hash check is deliberate, so a changed asset gets its provenance re-checked. Without it `cargo xtask assets` and the test `assets::tests::the_real_manifest_passes` fail. +- Each `.tsv` has an `ATTRIBUTION.toml` entry with `kind = "translation"` (title, author, source, licence), checked by `cargo xtask assets`. Catalogs are project files under the project licence, so the entry has no `sha256` and editing a catalog doesn't touch `ATTRIBUTION.toml`; git history records their provenance. - Translations are clean-room: write them from the meaning of the English text, never from another product's string tables. ## Sentences the engine words @@ -93,6 +93,6 @@ Error details that come from the operating system, and faults rather than mistak 1. Copy the header of `ja.tsv` into `xx.tsv` and translate entries. 2. Add one row to `LANGUAGES` in `crates/ui-egui/src/i18n/mod.rs`: code, native name, catalog and plural rule. -3. Add an `ATTRIBUTION.toml` entry for the catalog (`kind = "translation"`), with the catalog's `sha256sum` as `sha256`, then run `cargo xtask assets --write` (it regenerates `ATTRIBUTION.md` only) and `cargo xtask assets`. +3. Add an `ATTRIBUTION.toml` entry for the catalog (`kind = "translation"`, no `sha256`), then run `cargo xtask assets --write` (it regenerates `ATTRIBUTION.md` only) and `cargo xtask assets`. The Preferences dropdown, the system-language match, the catalog tests (format, duplicates, placeholders, plural forms, command ids) and the translation gaps report then pick it up. diff --git a/xtask/src/assets.rs b/xtask/src/assets.rs index a34a9be4b..b3393d770 100644 --- a/xtask/src/assets.rs +++ b/xtask/src/assets.rs @@ -3,7 +3,8 @@ //! `ATTRIBUTION.toml` lists every asset: committed or vendored files (`[[asset]]`), files that //! Cargo dependencies compile into PdfCraft (`[[bundled]]`), and files xtask downloads at build //! time (`[[fetched]]`). This gate fails when: -//! - an asset-like file in the repository has no entry, or its SHA-256 differs; +//! - an asset-like file in the repository has no entry, or its SHA-256 is missing or differs +//! (translation catalogs are project files and carry none); //! - a licence is not on the allowlist, or a declared licence file is missing; //! - an icon, image or font names Adobe as author or source, or an `adobe_data` entry is not on //! the closed list approved in AGENTS.md §1.1; @@ -49,6 +50,11 @@ pub const ADOBE_DATA_ALLOWED: &[(&str, &str)] = &[("hayro-cmap", "assets/cmaps.b /// table has to be re-attested deliberately. pub const ADOBE_DATA_FILES: &[&str] = &["crates/fonts/src/std14_metrics.rs", "vendor/hayro-cmap/assets/cmaps.brotli"]; +/// Where the UI translation catalogs live. `kind = "translation"` entries for `.tsv` files here +/// are project files under the project licence: they carry no `sha256`, because git history +/// already records their provenance (AGENTS.md §1.3). +pub const TRANSLATION_DIR: &str = "crates/ui-egui/src/i18n/"; + /// File extensions that count as assets wherever they appear in the repository. const ASSET_EXTENSIONS: &[&str] = &[ "png", "jpg", "jpeg", "gif", "svg", "ico", "icns", "webp", "bmp", "tif", "tiff", "avif", "heic", "ttf", "otf", "ttc", "woff", "woff2", "pfb", @@ -97,11 +103,20 @@ pub struct Asset { pub licence_file: String, pub kind: String, pub usage: String, - pub sha256: String, + /// Required for every entry except a translation catalog ([`Asset::is_translation_catalog`]). + #[serde(default)] + pub sha256: Option, #[serde(default)] pub adobe_data: bool, } +impl Asset { + /// A UI translation catalog: a project file whose entry carries no hash. + pub fn is_translation_catalog(&self) -> bool { + self.kind == "translation" && self.path.starts_with(TRANSLATION_DIR) && self.path.ends_with(".tsv") + } +} + #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct Bundled { @@ -162,6 +177,9 @@ pub fn run(args: &[String]) -> Result<()> { std::fs::write(root.join("ATTRIBUTION.md"), &markdown)?; println!("assets: wrote ATTRIBUTION.md"); } + for a in manifest.asset.iter().filter(|a| a.is_translation_catalog() && a.sha256.is_some()) { + println!("assets: note: {}: translation catalogs aren't hashed; its sha256 line can be deleted", a.path); + } let mut problems = check(&root, &manifest, &repo_files(&root)?, &cargo_lock_versions(&root)?); match std::fs::read_to_string(root.join("ATTRIBUTION.md")) { Ok(existing) if existing == markdown => {} @@ -257,10 +275,16 @@ pub fn check(root: &Path, m: &Manifest, repo_files: &[String], lock: &BTreeSet<( if a.adobe_data && !(ADOBE_DATA_FILES.contains(&a.path.as_str()) && a.kind == "data") { problems.push(format!("{}: adobe_data is only allowed for the closed list in AGENTS.md §1.1", a.path)); } - match std::fs::read(root.join(&a.path)) { - Ok(bytes) if sha256_hex(&bytes) == a.sha256 => {} - Ok(_) => problems.push(format!("{}: SHA-256 does not match ATTRIBUTION.toml (re-verify provenance, then update the entry)", a.path)), - Err(_) => problems.push(format!("{}: listed but missing", a.path)), + let catalog = a.is_translation_catalog(); + match (std::fs::read(root.join(&a.path)), &a.sha256) { + (Err(_), _) => problems.push(format!("{}: listed but missing", a.path)), + // A leftover hash line (e.g. kept while resolving a rebase) is not checked; `run` notes it. + (Ok(_), _) if catalog => {} + (Ok(_), None) => problems.push(format!("{}: sha256 is missing (only translation catalogs go without one)", a.path)), + (Ok(bytes), Some(hash)) if sha256_hex(&bytes) == *hash => {} + (Ok(_), Some(_)) => { + problems.push(format!("{}: SHA-256 does not match ATTRIBUTION.toml (re-verify provenance, then update the entry)", a.path)) + } } } for f in repo_files { @@ -378,7 +402,7 @@ pub fn render_markdown(m: &Manifest) -> String { s.push_str("\n\n"); s.push_str("Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. "); s.push_str( - "The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). ", + "The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset but the translation catalogs, is [ATTRIBUTION.toml](ATTRIBUTION.toml). ", ); s.push_str("Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).\n\n"); let esc = |t: &str| t.replace('|', "\\|"); @@ -463,7 +487,7 @@ mod tests { licence_file: "Cargo.toml".into(), kind: kind.into(), usage: "u".into(), - sha256: sha256_hex(&std::fs::read(root().join(path)).unwrap_or_default()), + sha256: Some(sha256_hex(&std::fs::read(root().join(path)).unwrap_or_default())), adobe_data: false, } } @@ -530,6 +554,28 @@ mod tests { assert!(check(&root(), &m, &[], &lock(&[])).iter().any(|p| p.contains("from Adobe is forbidden"))); } + /// One `[[asset]]` entry as ATTRIBUTION.toml spells it, with `extra` lines appended. + fn parsed(path: &str, kind: &str, extra: &str) -> Manifest { + let text = format!( + "[[asset]]\npath = \"{path}\"\ntitle = \"t\"\nauthor = \"a\"\nsource = \"s\"\nlicence = \"MIT\"\nlicence_file = \"Cargo.toml\"\nkind = \"{kind}\"\nusage = \"u\"\n{extra}" + ); + toml::from_str(&text).unwrap() + } + + /// Catalogs change in most UI PRs; a pinned hash would make every pair of them conflict. + #[test] + fn translation_catalogs_carry_no_hash_and_everything_else_does() { + let check_one = |m: Manifest| check(&root(), &m, &[], &lock(&[])); + let ja = "crates/ui-egui/src/i18n/ja.tsv"; + assert_eq!(check_one(parsed(ja, "translation", "")), Vec::::new()); + let stale = format!("sha256 = \"{}\"\n", "0".repeat(64)); + assert_eq!(check_one(parsed(ja, "translation", &stale)), Vec::::new(), "a leftover hash is not checked"); + for (path, kind) in [("assets/fonts/Inter-Regular.ttf", "font"), (ja, "data"), ("Cargo.toml", "translation")] { + let p = check_one(parsed(path, kind, "")); + assert!(p.iter().any(|p| p.contains("sha256 is missing")), "{path} {kind}: {p:?}"); + } + } + #[test] fn adobe_ofl_fonts_are_allowed_and_other_adobe_fonts_rejected() { let l = lock(&[("noto", "1.0.0")]); @@ -562,7 +608,7 @@ mod tests { #[test] fn changed_file_fails_hash_check() { let mut a = asset("assets/icons/x.svg", "Lucide", "icon", "ISC"); - a.sha256 = "0".repeat(64); + a.sha256 = Some("0".repeat(64)); let p = check(&root(), &Manifest { asset: vec![a], ..Default::default() }, &[], &lock(&[])); assert!(p.iter().any(|p| p.contains("SHA-256 does not match")), "{p:?}"); }