diff --git a/Cargo.lock b/Cargo.lock index 0501e07..985a758 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -200,6 +210,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -519,6 +554,30 @@ dependencies = [ "temp-dir", ] +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.4.1", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -543,6 +602,7 @@ version = "0.9.0" dependencies = [ "anyhow", "gettext", + "globwalk", "i18n-config", "i18n-embed", "lazy_static", @@ -551,7 +611,6 @@ dependencies = [ "subprocess", "thiserror", "tr", - "walkdir", ] [[package]] @@ -577,6 +636,7 @@ dependencies = [ "fluent-langneg", "fluent-syntax", "gettext", + "globwalk", "i18n-embed-impl", "intl-memoizer", "lazy_static", @@ -590,7 +650,6 @@ dependencies = [ "thiserror", "tr", "unic-langid", - "walkdir", "web-sys", ] @@ -629,6 +688,22 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -1268,9 +1343,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", diff --git a/Cargo.toml b/Cargo.toml index 9d0aafe..2c80290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ anyhow = { workspace = true } gettext = { workspace = true } tr = { workspace = true, default-features = false, features = ["gettext"] } clap = { version = "4.4.5", features = ["cargo"] } -rust-embed = { workspace = true } +rust-embed = { workspace = true } unic-langid = { workspace = true } env_logger = { workspace = true } log = { workspace = true } @@ -69,7 +69,7 @@ quote = "1.0" find-crate = "0.6" syn = "2.0" pretty_assertions = "1.4" -walkdir = "2.4" +globwalk = "0.9.1" serde = "1.0" serde_derive = "1.0" once_cell = "1.18" diff --git a/i18n-build/Cargo.toml b/i18n-build/Cargo.toml index b975bbf..4bf8cf1 100644 --- a/i18n-build/Cargo.toml +++ b/i18n-build/Cargo.toml @@ -21,9 +21,9 @@ maintenance = { status = "actively-developed" } [dependencies] subprocess = "0.2" anyhow = { workspace = true } -thiserror = { workspace = true } +thiserror = { workspace = true } tr = { workspace = true, default-features = false, features = ["gettext"] } -walkdir = { workspace = true } +globwalk = { workspace = true } i18n-embed = { workspace = true, features = ["gettext-system", "desktop-requester"], optional = true } i18n-config = { workspace = true } gettext = { workspace = true, optional = true } diff --git a/i18n-build/src/gettext_impl/mod.rs b/i18n-build/src/gettext_impl/mod.rs index d6b004f..d4b72f4 100644 --- a/i18n-build/src/gettext_impl/mod.rs +++ b/i18n-build/src/gettext_impl/mod.rs @@ -14,7 +14,7 @@ use anyhow::{anyhow, Context, Result}; use log::{debug, info}; use subprocess::Exec; use tr::tr; -use walkdir::WalkDir; +use globwalk::GlobWalkerBuilder; /// Run the `xtr` command () in order /// to extract the translateable strings from the crate. @@ -41,19 +41,19 @@ pub fn run_xtr( ); let mut rs_files: Vec> = Vec::new(); - for result in WalkDir::new(src_dir) { - match result { - Ok(entry) => { - let path = entry.path(); - - if let Some(extension) = path.extension() { - if extension.to_str() == Some("rs") { + match GlobWalkerBuilder::new(src_dir, "*.rs").build(){ + Ok(walker) => { + for result in walker { + match result { + Ok(entry) => { + let path = entry.path(); rs_files.push(Box::from(path)) } + Err(err) => return Err(anyhow!("error walking directory {}/src: {}", crt.name, err)), } } - Err(err) => return Err(anyhow!("error walking directory {}/src: {}", crt.name, err)), - } + }, + Err(err) => return Err(anyhow!("error walking directory {}/src: {}", crt.name, err)), } let mut pot_paths = Vec::new(); @@ -147,6 +147,134 @@ pub fn run_xtr( Ok(()) } +fn run_xgettext( + crt: &Crate, + gettext_config: &GettextConfig, + _src_dir: &Path, + pot_dir: &Path, + prepend_crate_path: bool, +) -> Result<()> { + info!( + "Performing string extraction with `xgettext` for crate \"{0}\"", + crt.path.display() + ); + let mut src_files: Vec> = Vec::new(); + + let patterns = &crt + .gettext_config_or_err()? + .xgettext; + + match GlobWalkerBuilder::from_patterns(&crt.path, patterns).build(){ + Ok(walker) => { + for result in walker { + match result { + Ok(entry) => { + let path = entry.path(); + src_files.push(Box::from(path)) + } + Err(err) => return Err(anyhow!("error walking directory {}/src: {}", crt.name, err)), + } + } + }, + Err(err) => return Err(anyhow!("error walking directory {}/src: {}", crt.name, err)), + } + + let mut pot_paths = Vec::new(); + + let pot_src_dir = if prepend_crate_path { + pot_dir.join(&crt.path) + } else { + pot_dir.to_path_buf() + }; + + // create pot and pot/tmp if they don't exist + util::create_dir_all_if_not_exists(&pot_src_dir)?; + + for src_file_path in src_files { + let parent_dir = src_file_path.parent().context(format!( + "the rs file {0} is not inside a directory", + src_file_path.to_string_lossy() + ))?; + + let src_dir_relative = parent_dir.strip_prefix(crt.path.to_path_buf()).map_err(|_| { + PathError::not_inside_dir(parent_dir, format!("crate {0}", crt.name), &crt.path) + })?; + let mut file_name = src_file_path.file_name().context(format!( + "expected src file path {0} would have a filename", + src_file_path.to_string_lossy() + ))?.to_owned(); + file_name.push(".pot"); + + let pot_file_path = pot_src_dir + .join(src_dir_relative) + .join(&PathBuf::from(file_name)); + + util::create_dir_all_if_not_exists(pot_file_path.parent().with_context(|| { + format!( + "Expected that pot file path \"{0}\" would be inside a directory (have a parent)", + &pot_file_path.to_string_lossy() + ) + })?)?; + + // ======= Run the `xgettext` command to extract translatable strings ======= + let xgettext_command_name = "xgettext"; + let mut xgettext = Command::new(xgettext_command_name); + + match &gettext_config.copyright_holder { + Some(copyright_holder) => { + xgettext.args(["--copyright-holder", copyright_holder.as_str()]); + } + None => {} + } + + match &gettext_config.msgid_bugs_address { + Some(msgid_bugs_address) => { + xgettext.args(["--msgid-bugs-address", msgid_bugs_address.as_str()]); + } + None => {} + } + + xgettext.args([ + "--package-name", + crt.name.as_str(), + "--package-version", + crt.version.as_str(), + "--default-domain", + crt.module_name().as_str(), + "--add-location", + "--from-code=UTF-8", + "--add-comments", + "-o", + pot_file_path.to_str().ok_or_else(|| { + PathError::not_valid_utf8(pot_file_path.clone(), "pot", PathType::File) + })?, + src_file_path.to_str().ok_or_else(|| { + PathError::not_valid_utf8(src_file_path.clone(), "src", PathType::File) + })?, + ]); + + util::run_command_and_check_success(xgettext_command_name, xgettext)?; + + // If there was nothing to translate, xgettext may not create a file + if pot_file_path.exists() { + pot_paths.push(pot_file_path.to_owned()); + } + } + + let mut msgcat_args: Vec> = Vec::new(); + + for path in &pot_paths { + msgcat_args.push(Box::from(path.as_os_str())); + } + + let combined_pot_file_path = crate_module_pot_file_path(crt, pot_dir); + + run_msgcat(&pot_paths, &combined_pot_file_path) + .context("There was a problem while trying to run the \"msgcat\" command.")?; + + Ok(()) +} + fn crate_module_pot_file_path>(crt: &Crate<'_>, pot_dir: P) -> PathBuf { pot_dir .as_ref() @@ -403,6 +531,9 @@ pub fn run(crt: &Crate) -> Result<()> { .expect("expected gettext config to be present"); let do_xtr = config_crate.gettext_config_or_err()?.xtr.unwrap_or(true); + let do_xgettext = !config_crate + .gettext_config_or_err()? + .xgettext.is_empty(); // We don't use the i18n_config (which potentially comes from the // parent crate )to get the subcrates, because this would result @@ -455,6 +586,18 @@ pub fn run(crt: &Crate) -> Result<()> { )?; } + if do_xgettext { + let prepend_crate_path = + crt.path.canonicalize().unwrap() != config_crate.path.canonicalize().unwrap(); + run_xgettext( + crt, + gettext_config, + src_dir.as_path(), + pot_dir.as_path(), + prepend_crate_path, + )?; + } + // figure out where there are any subcrates which need their output // pot files concatinated with this crate's pot file let mut concatinate_crates = vec![]; diff --git a/i18n-build/src/watch.rs b/i18n-build/src/watch.rs index 1dafbb4..192f543 100644 --- a/i18n-build/src/watch.rs +++ b/i18n-build/src/watch.rs @@ -6,7 +6,7 @@ use std::path::Path; use anyhow::{anyhow, Result}; -use walkdir::WalkDir; +use globwalk::GlobWalkerBuilder; /// Tell `Cargo` to rerun the build script that calls this function /// (upon rebuild) if the specified file/directory changes. @@ -28,14 +28,19 @@ pub fn cargo_rerun_if_changed(path: &Path) -> Result<(), PathError> { pub fn cargo_rerun_if_dir_changed(path: &Path) -> Result<()> { cargo_rerun_if_changed(path)?; - for result in WalkDir::new(path) { - match result { - Ok(entry) => { - cargo_rerun_if_changed(entry.path())?; + match GlobWalkerBuilder::new(path, "*").build(){ + Ok(walker) => { + for result in walker { + match result { + Ok(entry) => { + cargo_rerun_if_changed(entry.path())?; + } + Err(err) => return Err(anyhow!("error walking directory gui/: {}", err)), + } } - Err(err) => return Err(anyhow!("error walking directory gui/: {}", err)), - } - } + }, + Err(err) => return Err(anyhow!("error walking directory gui/: {}", err)), + }; Ok(()) } diff --git a/i18n-config/src/gettext.rs b/i18n-config/src/gettext.rs index 6f8675a..069b224 100644 --- a/i18n-config/src/gettext.rs +++ b/i18n-config/src/gettext.rs @@ -33,6 +33,13 @@ pub struct GettextConfig { pub msgid_bugs_address: Option, /// Whether or not to perform string extraction using the `xtr` command. pub xtr: Option, + /// List of files (or file glob patterns) the `xgettext` command needs to + /// run for. + /// Patterns are in gitignore style, so exclude rules are also possible + /// When no patterns are given, xgettext will not run. + /// By default this is **[]**. + #[serde(default)] + pub xgettext: Vec, /// Generate ‘#: filename:line’ lines (default) in the pot files when /// running the `xtr` command. If the type is ‘full’ (the default), /// it generates the lines with both file name and line number. diff --git a/i18n-config/src/lib.rs b/i18n-config/src/lib.rs index d1ffbae..e306281 100644 --- a/i18n-config/src/lib.rs +++ b/i18n-config/src/lib.rs @@ -83,6 +83,15 @@ impl<'a> Crate<'a> { config_file_path: P2, ) -> Result, I18nConfigError> { let path_into = path.into(); + let path_into = match path_into.canonicalize() { + Ok(p) => p, + Err(_) => { + return Err(I18nConfigError::NotACrate( + path_into, + WhyNotCrate::NoCargoToml, + )); + } + }; let config_file_path_into = config_file_path.into(); diff --git a/i18n-embed/Cargo.toml b/i18n-embed/Cargo.toml index 484d414..971e3e3 100644 --- a/i18n-embed/Cargo.toml +++ b/i18n-embed/Cargo.toml @@ -33,11 +33,11 @@ rust-embed = { workspace = true, optional = true } thiserror = { workspace = true } tr = { version = "0.1", default-features = false, optional = true } unic-langid = { workspace = true } -walkdir = { workspace = true, optional = true } +globwalk = { workspace = true, optional = true } web-sys = { version = "0.3", features = ["Window", "Navigator"], optional = true } [dev-dependencies] -doc-comment = { workspace = true } +doc-comment = { workspace = true } env_logger = { workspace = true } maplit = "1.0" pretty_assertions = { workspace = true } @@ -52,4 +52,4 @@ fluent-system = ["fluent", "fluent-syntax", "parking_lot", "i18n-embed-impl", "i desktop-requester = ["locale_config"] web-sys-requester = ["web-sys"] -filesystem-assets = ["walkdir"] +filesystem-assets = ["globwalk"] diff --git a/i18n-embed/src/assets.rs b/i18n-embed/src/assets.rs index ad4cbb0..f6ae913 100644 --- a/i18n-embed/src/assets.rs +++ b/i18n-embed/src/assets.rs @@ -66,8 +66,8 @@ impl I18nAssets for FileSystemAssets { Ok(contents) => Some(Cow::from(contents)), Err(e) => { log::error!( - target: "i18n_embed::assets", - "Unexpected error while reading localization asset file: {}", + target: "i18n_embed::assets", + "Unexpected error while reading localization asset file: {}", e); None } @@ -75,34 +75,39 @@ impl I18nAssets for FileSystemAssets { } fn filenames_iter(&self) -> Box> { - Box::new( - walkdir::WalkDir::new(&self.base_dir) - .into_iter() - .filter_map(|f| match f { - Ok(f) => { - if f.file_type().is_file() { - match f.file_name().to_str() { - Some(filename) => Some(filename.to_string()), - None => { - log::error!( - target: "i18n_embed::assets", - "Filename {:?} is not valid UTF-8.", + match globwalk::GlobWalkerBuilder::new(&self.base_dir, "*").build() { + Ok(walker) => Box::new(walker.into_iter().filter_map(|f| match f { + Ok(f) => { + if f.file_type().is_file() { + match f.file_name().to_str() { + Some(filename) => Some(filename.to_string()), + None => { + log::error!( + target: "i18n_embed::assets", + "Filename {:?} is not valid UTF-8.", f.file_name()); - None - } + None } - } else { - None } - } - Err(err) => { - log::error!( - target: "i18n_embed::assets", - "Unexpected error while gathering localization asset filenames: {}", - err); + } else { None } - }), - ) + } + Err(err) => { + log::error!( + target: "i18n_embed::assets", + "Unexpected error while gathering localization asset filenames: {}", + err); + None + } + })), + Err(err) => { + log::error!( + target: "i18n_embed::assets", + "Unexpected error while gathering localization asset filenames: {}", + err); + Box::new(vec![].into_iter()) + } + } } }