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())
+ }
+ }
}
}