use crate::error::{PathError, PathType};
use crate::util;
use i18n_config::{Crate, GettextConfig, I18nConfigError};
use std::ffi::OsStr;
use std::fs::{create_dir_all, File};
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use log::{debug, info};
use subprocess::Exec;
use tr::tr;
use walkdir::WalkDir;
pub fn run_xtr(
crt: &Crate,
gettext_config: &GettextConfig,
src_dir: &Path,
pot_dir: &Path,
prepend_crate_path: bool,
) -> Result<()> {
info!(
"Performing string extraction with `xtr` for crate \"{0}\"",
crt.path.to_string_lossy()
);
let mut rs_files: Vec<Box<Path>> = 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") {
rs_files.push(Box::from(path))
}
}
}
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).join("src")
} else {
pot_dir.join("src")
};
util::create_dir_all_if_not_exists(&pot_src_dir)?;
for rs_file_path in rs_files {
let parent_dir = rs_file_path.parent().context(format!(
"the rs file {0} is not inside a directory",
rs_file_path.to_string_lossy()
))?;
let src_dir_relative = parent_dir.strip_prefix(src_dir).map_err(|_| {
PathError::not_inside_dir(parent_dir, format!("crate {0}/src", crt.name), src_dir)
})?;
let file_stem = rs_file_path.file_stem().context(format!(
"expected rs file path {0} would have a filename",
rs_file_path.to_string_lossy()
))?;
let pot_file_path = pot_src_dir
.join(src_dir_relative)
.join(file_stem)
.with_extension("pot");
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()
)
})?)?;
let xtr_command_name = "xtr";
let mut xtr = Command::new(xtr_command_name);
match &gettext_config.copyright_holder {
Some(copyright_holder) => {
xtr.args(&["--copyright-holder", copyright_holder.as_str()]);
}
None => {}
}
match &gettext_config.msgid_bugs_address {
Some(msgid_bugs_address) => {
xtr.args(&["--msgid-bugs-address", msgid_bugs_address.as_str()]);
}
None => {}
}
xtr.args(&[
"--package-name",
crt.name.as_str(),
"--package-version",
crt.version.as_str(),
"--default-domain",
crt.module_name().as_str(),
"--add-location",
gettext_config.add_location.to_str(),
"-o",
pot_file_path.to_str().ok_or_else(|| {
PathError::not_valid_utf8(pot_file_path.clone(), "pot", PathType::File)
})?,
rs_file_path.to_str().ok_or_else(|| {
PathError::not_valid_utf8(rs_file_path.clone(), "rs", PathType::File)
})?,
]);
util::run_command_and_check_success(xtr_command_name, xtr)?;
pot_paths.push(pot_file_path.to_owned());
}
let mut msgcat_args: Vec<Box<OsStr>> = 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<'a, P: AsRef<Path>>(crt: &Crate<'a>, pot_dir: P) -> Result<PathBuf> {
Ok(pot_dir
.as_ref()
.join(crt.module_name())
.with_extension("pot"))
}
pub fn run_msgcat<P: AsRef<Path>, I: IntoIterator<Item = P>>(
input_pot_paths: I,
output_pot_path: P,
) -> Result<()> {
let input_pot_paths_iter = input_pot_paths.into_iter();
let mut input_pot_paths_strings: Vec<String> = Vec::new();
let mut msgcat_args: Vec<Box<OsStr>> = Vec::new();
let mut output_in_input = false;
for input_path in input_pot_paths_iter {
let input_path_ref = input_path.as_ref();
input_pot_paths_strings.push(input_path_ref.to_string_lossy().to_string());
msgcat_args.push(Box::from(input_path_ref.as_os_str()));
output_in_input |= input_path_ref == output_pot_path.as_ref();
}
info!(
"Concatinating pot files {0:?} with `msgcat` into \"{1}\"",
input_pot_paths_strings,
output_pot_path.as_ref().to_string_lossy()
);
let interim_output_pot_path = if output_in_input {
output_pot_path.as_ref().with_extension("pot.tmp")
} else {
output_pot_path.as_ref().to_path_buf()
};
util::create_dir_all_if_not_exists(
&interim_output_pot_path
.parent()
.expect("expected there to be a parent to the interim output pot path"),
)?;
util::remove_file_if_exists(&interim_output_pot_path)?;
let output_pot_file = File::create(&interim_output_pot_path)
.map_err(|e| PathError::cannot_create_file(&interim_output_pot_path, e))?;
let msgcat_command_name = "msgcat";
let msgcat = Exec::cmd(msgcat_command_name)
.args(msgcat_args.as_slice())
.stdout(output_pot_file);
debug!("Running command: {0:?}", msgcat);
msgcat.join().with_context(|| {
tr!(
"There was a problem executing the \"{0}\" command",
msgcat_command_name
)
})?;
if output_in_input {
util::remove_file_if_exists(&output_pot_path)?;
util::rename_file(&interim_output_pot_path, &output_pot_path)?;
}
Ok(())
}
pub fn run_msginit(crt: &Crate, pot_dir: &Path, po_dir: &Path) -> Result<()> {
info!(
"Initializing new po files with `msginit` for crate \"{0}\"",
crt.path.to_string_lossy()
);
let pot_file_path = pot_dir.join(crt.module_name()).with_extension("pot");
util::check_path_exists(&pot_file_path)?;
util::create_dir_all_if_not_exists(po_dir)?;
let msginit_command_name = "msginit";
let gettext_config = crt.gettext_config_or_err()?;
let target_locales = &gettext_config.target_languages;
for locale in target_locales {
let po_locale_dir = po_dir.join(locale.clone());
let po_path = po_locale_dir.join(crt.module_name()).with_extension("po");
if !po_path.exists() {
create_dir_all(po_locale_dir.clone())
.map_err(|e| PathError::cannot_create_dir(po_locale_dir, e))?;
let mut msginit = Command::new(msginit_command_name);
msginit.args(&[
format!(
"--input={}",
pot_file_path
.to_str()
.ok_or_else(|| PathError::not_valid_utf8(
pot_file_path.clone(),
"pot",
PathType::File,
))?
),
format!("--locale={}.UTF-8", locale),
format!(
"--output={}",
po_path.to_str().ok_or_else(|| PathError::not_valid_utf8(
po_path.clone(),
"po",
PathType::File,
))?
),
]);
util::run_command_and_check_success(msginit_command_name, msginit)?;
}
}
Ok(())
}
pub fn run_msgmerge(crt: &Crate, pot_dir: &Path, po_dir: &Path) -> Result<()> {
info!(
"Merging message changes in pot files to po files with `msgmerge` for crate \"{0}\"",
crt.path.to_string_lossy()
);
let pot_file_path = pot_dir.join(crt.module_name()).with_extension("pot");
util::check_path_exists(&pot_file_path)?;
let msgmerge_command_name = "msgmerge";
let gettext_config = crt.gettext_config_or_err()?;
let target_locales = &gettext_config.target_languages;
for locale in target_locales {
let po_file_path = po_dir
.join(locale)
.join(crt.module_name())
.with_extension("po");
util::check_path_exists(&po_file_path)?;
let mut msgmerge = Command::new(msgmerge_command_name);
msgmerge.args(&[
"--silent",
"--backup=none",
"--update",
po_file_path.to_str().ok_or_else(|| {
PathError::not_valid_utf8(po_file_path.clone(), "pot", PathType::File)
})?,
pot_file_path.to_str().ok_or_else(|| {
PathError::not_valid_utf8(pot_file_path.clone(), "pot", PathType::File)
})?,
]);
util::run_command_and_check_success(msgmerge_command_name, msgmerge)?;
}
Ok(())
}
pub fn run_msgfmt(crt: &Crate, po_dir: &Path, mo_dir: &Path) -> Result<()> {
info!(
"Compiling po files to mo files with `msgfmt` for crate \"{0}\"",
crt.path.to_string_lossy()
);
let msgfmt_command_name = "msgfmt";
let gettext_config = crt.gettext_config_or_err()?;
let target_locales = &gettext_config.target_languages;
for locale in target_locales {
let po_file_path = po_dir
.join(locale.clone())
.join(crt.module_name())
.with_extension("po");
util::check_path_exists(&po_file_path)?;
let mo_locale_dir = mo_dir.join(locale);
if !mo_locale_dir.exists() {
create_dir_all(mo_locale_dir.clone()).context("trouble creating mo directory")?;
}
let mo_file_path = mo_locale_dir.join(crt.module_name()).with_extension("mo");
let mut msgfmt = Command::new(msgfmt_command_name);
msgfmt.args(&[
format!(
"--output-file={}",
mo_file_path
.to_str()
.expect("mo file path is not valid utf-8")
)
.as_str(),
po_file_path
.to_str()
.expect("po file path is not valid utf-8"),
]);
util::run_command_and_check_success(msgfmt_command_name, msgfmt)?;
}
Ok(())
}
pub fn run<'a>(crt: &'a Crate) -> Result<()> {
info!(
"Localizing crate \"{0}\" using the gettext system",
crt.path.to_string_lossy()
);
let (config_crate, _i18n_config) = crt.active_config()?.unwrap_or_else(|| {
panic!(
"expected that there would be an active config for the crate: \"{0}\" at \"{1}\"",
crt.name,
crt.path.to_string_lossy()
)
});
let gettext_config = config_crate
.gettext_config_or_err()
.expect("expected gettext config to be present");
let do_xtr = match config_crate.gettext_config_or_err()?.xtr {
Some(xtr_value) => xtr_value,
None => true,
};
let subcrates: Vec<Crate> = match &crt.i18n_config {
Some(config) => {
let subcrates: Result<Vec<Crate>, I18nConfigError> = config
.subcrates
.iter()
.map(|subcrate_path| {
Crate::from(
subcrate_path.clone(),
Some(crt),
crt.config_file_path.clone(),
)
})
.collect();
subcrates.with_context(|| {
let subcrate_path_strings: Vec<String> = config
.subcrates
.iter()
.map(|path| path.to_string_lossy().to_string())
.collect();
tr!(
"There was a problem parsing one of the subcrates: \"{0}\".",
subcrate_path_strings.join(", ")
)
})?
}
None => vec![],
};
let src_dir = crt.path.join("src");
let pot_dir = config_crate.path.join(gettext_config.pot_dir());
let po_dir = config_crate.path.join(gettext_config.po_dir());
let mo_dir = config_crate.path.join(gettext_config.mo_dir());
if do_xtr {
let prepend_crate_path =
crt.path.canonicalize().unwrap() != config_crate.path.canonicalize().unwrap();
run_xtr(
crt,
&gettext_config,
src_dir.as_path(),
pot_dir.as_path(),
prepend_crate_path,
)?;
}
let mut concatinate_crates = vec![];
for subcrate in &subcrates {
run(subcrate)?;
if subcrate.collated_subcrate() {
concatinate_crates.push(subcrate);
}
}
if !concatinate_crates.is_empty() {
assert!(crt.gettext_config_or_err()?.collate_extracted_subcrates);
concatinate_crates.insert(0, crt);
let concatinate_crate_paths_result: Result<Vec<PathBuf>, _> = concatinate_crates
.iter()
.map(|concat_crt: &&Crate| crate_module_pot_file_path(concat_crt, &pot_dir))
.collect();
let concatinate_crate_paths = concatinate_crate_paths_result?;
let output_pot_path = crate_module_pot_file_path(crt, &pot_dir)?;
run_msgcat(concatinate_crate_paths, output_pot_path)?;
concatinate_crates.remove(0);
for subcrate in concatinate_crates {
let subcrate_output_pot_path = crate_module_pot_file_path(subcrate, &pot_dir)?;
util::remove_file_or_error(subcrate_output_pot_path)?;
}
}
if !(crt.collated_subcrate()) {
run_msginit(crt, pot_dir.as_path(), po_dir.as_path())?;
run_msgmerge(crt, pot_dir.as_path(), po_dir.as_path())?;
run_msgfmt(crt, po_dir.as_path(), mo_dir.as_path())?;
}
Ok(())
}