app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit f862d3b700e6bb1a99ef0bf68a3f911210dbb93e
parent 3e9b3055d4c3bfe671d8feda1fca635d26de69be
Author: triesap <tyson@radroots.org>
Date:   Sun, 19 Apr 2026 22:25:15 +0000

i18n: use facade build pipeline for app generation

- replace the mounted app i18n build script with mf2_i18n::build native module generation
- remove direct low-level mf2 build dependencies from the app workspace and i18n crate
- keep generated module and catalog artifacts sourced through the canonical facade path
- validate with cargo check -p radroots_app and git diff --check

Diffstat:
MCargo.lock | 7+------
MCargo.toml | 4----
Mcrates/shared/i18n/Cargo.toml | 7+------
Mcrates/shared/i18n/build.rs | 251++++---------------------------------------------------------------------------
4 files changed, 14 insertions(+), 255 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3759,6 +3759,7 @@ dependencies = [ name = "mf2_i18n" version = "0.1.0" dependencies = [ + "mf2_i18n_build", "mf2_i18n_core", "mf2_i18n_embedded", "mf2_i18n_native", @@ -5071,13 +5072,7 @@ dependencies = [ name = "radroots_app_i18n" version = "0.1.0" dependencies = [ - "hex", "mf2_i18n", - "mf2_i18n_build", - "mf2_i18n_core", - "serde", - "serde_json", - "toml 0.8.23", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -27,11 +27,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] } gpui = "0.2.2" gpui-component = "0.5.1" gpui-component-assets = "0.5.1" -hex = "0.4" mf2_i18n = { path = "../../../../vendor/triesap/mf2_i18n/crates/mf2_i18n" } -mf2_i18n_build = { path = "../../../../vendor/triesap/mf2_i18n/crates/mf2_i18n_build" } -mf2_i18n_core = { path = "../../../../vendor/triesap/mf2_i18n/crates/mf2_i18n_core" } -mf2_i18n_native = { path = "../../../../vendor/triesap/mf2_i18n/crates/mf2_i18n_native" } radroots_identity = { path = "../lib/crates/identity" } radroots_nostr = { path = "../lib/crates/nostr", features = ["client"] } radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts" } diff --git a/crates/shared/i18n/Cargo.toml b/crates/shared/i18n/Cargo.toml @@ -12,12 +12,7 @@ build = "build.rs" mf2_i18n = { workspace = true, features = ["native"] } [build-dependencies] -hex.workspace = true -mf2_i18n_build.workspace = true -mf2_i18n_core.workspace = true -serde.workspace = true -serde_json.workspace = true -toml.workspace = true +mf2_i18n = { workspace = true, features = ["build"] } [lints] workspace = true diff --git a/crates/shared/i18n/build.rs b/crates/shared/i18n/build.rs @@ -1,23 +1,7 @@ -use std::collections::BTreeMap; use std::env; -use std::fs; use std::path::{Path, PathBuf}; -use mf2_i18n_build::compiler::compile_message; -use mf2_i18n_build::id_map::{IdMap, build_id_map}; -use mf2_i18n_build::pack_encode::{PackBuildInput, encode_pack}; -use mf2_i18n_build::parser::parse_message; -use mf2_i18n_core::PackKind; -use serde::Deserialize; - -type Catalog = BTreeMap<String, String>; - -#[derive(Debug, Deserialize)] -struct ProjectConfig { - default_locale: String, - source_dirs: Vec<PathBuf>, - project_salt_path: PathBuf, -} +use mf2_i18n::build::{NativeModuleBuildOptions, build_native_module}; fn main() { let manifest_dir = @@ -27,233 +11,22 @@ fn main() { .and_then(Path::parent) .and_then(Path::parent) .expect("app root should be discoverable from shared i18n crate"); - let i18n_root = app_root.join("i18n"); - let config_path = i18n_root.join("mf2_i18n.toml"); - println!("cargo:rerun-if-changed={}", config_path.display()); - - let config = load_project_config(&config_path); - let salt_path = i18n_root.join(&config.project_salt_path); - println!("cargo:rerun-if-changed={}", salt_path.display()); - let id_salt = load_id_salt(&salt_path); - - let catalogs = load_catalogs(&i18n_root, &config.source_dirs); - let default_catalog = catalogs - .get(&config.default_locale) - .unwrap_or_else(|| { - panic!( - "default locale {} catalog should exist", - config.default_locale - ) - }) - .clone(); - ensure_catalog_keys_match(&catalogs); + let config_path = app_root.join("i18n").join("mf2_i18n.toml"); + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("out dir should exist")); - let id_map = build_id_map( - default_catalog.keys().cloned().collect::<Vec<_>>(), - &id_salt, - ) - .expect("id map should build"); - - let out_dir = - PathBuf::from(env::var("OUT_DIR").expect("out dir should exist")).join("app_i18n"); - fs::create_dir_all(&out_dir).expect("i18n out dir should be created"); - - write_id_map(&out_dir, &id_map); - for (locale, catalog) in &catalogs { - write_pack(&out_dir, locale, catalog, &id_map); - } - - let locale_ids = catalogs.keys().cloned().collect::<Vec<_>>(); - let default_catalog_keys = default_catalog.keys().cloned().collect::<Vec<_>>(); - write_generated_runtime( + let build_output = build_native_module(&NativeModuleBuildOptions::new( + &config_path, &out_dir, - &config.default_locale, - &locale_ids, - &default_catalog_keys, - ); -} - -fn load_project_config(path: &Path) -> ProjectConfig { - let raw = fs::read_to_string(path).unwrap_or_else(|error| { + "app_i18n", + )) + .unwrap_or_else(|error| { panic!( - "failed to read i18n project config {}: {error}", - path.display() + "failed to build app i18n native module from {}: {error}", + config_path.display() ) }); - toml::from_str(&raw).unwrap_or_else(|error| { - panic!( - "failed to parse i18n project config {}: {error}", - path.display() - ) - }) -} - -fn load_id_salt(path: &Path) -> Vec<u8> { - let raw = fs::read_to_string(path) - .unwrap_or_else(|error| panic!("failed to read id salt {}: {error}", path.display())); - let salt = raw.trim(); - assert!( - !salt.is_empty(), - "i18n id salt {} must not be empty", - path.display() - ); - salt.as_bytes().to_vec() -} - -fn load_catalogs(i18n_root: &Path, source_dirs: &[PathBuf]) -> BTreeMap<String, Catalog> { - assert!( - !source_dirs.is_empty(), - "i18n project config must declare at least one source dir" - ); - - let mut catalogs = BTreeMap::<String, Catalog>::new(); - for source_dir in source_dirs { - let source_root = i18n_root.join(source_dir); - println!("cargo:rerun-if-changed={}", source_root.display()); - let entries = fs::read_dir(&source_root).unwrap_or_else(|error| { - panic!( - "failed to read source dir {}: {error}", - source_root.display() - ) - }); - - for entry in entries { - let entry = entry.unwrap_or_else(|error| { - panic!( - "failed to read source dir entry under {}: {error}", - source_root.display() - ) - }); - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let locale = entry.file_name().to_string_lossy().into_owned(); - let messages_path = path.join("messages.json"); - if !messages_path.is_file() { - continue; - } - println!("cargo:rerun-if-changed={}", messages_path.display()); - let catalog = load_catalog(&messages_path); - let merged = catalogs.entry(locale.clone()).or_default(); - for (key, value) in catalog { - assert!( - merged.insert(key.clone(), value).is_none(), - "duplicate i18n message key {key} in locale {locale} from {}", - messages_path.display() - ); - } - } + for path in build_output.rerun_if_changed_paths() { + println!("cargo:rerun-if-changed={}", path.display()); } - - assert!( - !catalogs.is_empty(), - "at least one locale catalog is required" - ); - catalogs -} - -fn load_catalog(path: &Path) -> Catalog { - let raw = fs::read_to_string(path) - .unwrap_or_else(|error| panic!("failed to read i18n catalog {}: {error}", path.display())); - serde_json::from_str(&raw) - .unwrap_or_else(|error| panic!("failed to parse i18n catalog {}: {error}", path.display())) -} - -fn ensure_catalog_keys_match(catalogs: &BTreeMap<String, Catalog>) { - let Some((reference_locale, reference_catalog)) = catalogs.iter().next() else { - panic!("at least one i18n catalog is required"); - }; - - let reference_keys = reference_catalog.keys().cloned().collect::<Vec<_>>(); - for (locale, catalog) in catalogs.iter().skip(1) { - let keys = catalog.keys().cloned().collect::<Vec<_>>(); - assert_eq!( - keys, reference_keys, - "i18n catalog keys for locale {locale} do not match reference locale {reference_locale}" - ); - } -} - -fn write_id_map(out_dir: &Path, id_map: &IdMap) { - let entries = id_map - .entries() - .map(|(key, id)| (key.to_owned(), u32::from(id))) - .collect::<BTreeMap<_, _>>(); - let id_map_json = serde_json::to_vec_pretty(&entries).expect("id map json should serialize"); - fs::write(out_dir.join("id-map.json"), id_map_json).expect("id map json should write"); - - let hash = id_map.hash().expect("id map hash should build"); - let hash_text = format!("sha256:{}\n", hex::encode(hash)); - fs::write(out_dir.join("id-map.sha256"), hash_text).expect("id map hash should write"); -} - -fn write_pack(out_dir: &Path, locale: &str, catalog: &Catalog, id_map: &IdMap) { - let id_map_hash = id_map.hash().expect("id map hash should build"); - let mut messages = BTreeMap::new(); - - for (key, source) in catalog { - let parsed = parse_message(source).unwrap_or_else(|error| { - panic!("failed to parse i18n message for locale {locale} key {key}: {error:?}") - }); - let compiled = compile_message(&parsed).unwrap_or_else(|error| { - panic!("failed to compile i18n message for locale {locale} key {key}: {error}") - }); - let message_id = id_map - .get(key) - .unwrap_or_else(|| panic!("missing message id for locale {locale} key {key}")); - messages.insert(message_id, compiled.program); - } - - let pack_bytes = encode_pack(&PackBuildInput { - pack_kind: PackKind::Base, - id_map_hash, - locale_tag: locale.to_owned(), - parent_tag: None, - build_epoch_ms: 0, - messages, - }); - - fs::write(out_dir.join(format!("{locale}.mf2pack")), pack_bytes) - .unwrap_or_else(|error| panic!("failed to write i18n pack for locale {locale}: {error}")); -} - -fn write_generated_runtime( - out_dir: &Path, - default_locale: &str, - locale_ids: &[String], - default_catalog_keys: &[String], -) { - let packs_source = locale_ids - .iter() - .map(|locale| { - format!( - " ({locale:?}, include_bytes!(concat!(env!(\"OUT_DIR\"), \"/app_i18n/{locale}.mf2pack\")))," - ) - }) - .collect::<Vec<_>>() - .join("\n"); - let runtime_source = format!( - "mod generated {{\n mf2_i18n::define_i18n_module! {{\n init_policy: strict,\n default_locale: {default_locale:?},\n id_map_json: include_bytes!(concat!(env!(\"OUT_DIR\"), \"/app_i18n/id-map.json\")),\n id_map_hash: include_bytes!(concat!(env!(\"OUT_DIR\"), \"/app_i18n/id-map.sha256\")),\n packs: [\n{packs_source}\n ],\n }}\n}}\n" - ); - fs::write(out_dir.join("generated_module.rs"), runtime_source) - .expect("generated runtime module should write"); - - let supported_locale_values = locale_ids - .iter() - .map(|locale| format!("{locale:?}")) - .collect::<Vec<_>>() - .join(", "); - let key_values = default_catalog_keys - .iter() - .map(|key| format!("{key:?}")) - .collect::<Vec<_>>() - .join(", "); - let catalog_source = format!( - "const DEFAULT_LOCALE_ID: &str = {default_locale:?};\nconst SUPPORTED_LOCALE_IDS: &[&str] = &[{supported_locale_values}];\n#[cfg(test)] const DEFAULT_CATALOG_KEY_IDS: &[&str] = &[{key_values}];\n" - ); - fs::write(out_dir.join("generated_catalog.rs"), catalog_source) - .expect("generated catalog metadata should write"); }