use crate::config::{Config, DirFile, API};
use anyhow::{anyhow, Result};
use argh::FromArgs;
use std::{
env::var,
path::{Path, PathBuf},
};
const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION"));
#[derive(FromArgs, Debug)]
#[argh(description = r#"
【bilingual】 作者:苦瓜小仔
针对 markdown 文件的命令行翻译。使用 `bilingual --help` 查看此帮助说明。
例子:
* `bilingual -a baidu multi queries -q single-query`
* `bilingual -a tencent -m xx.md`
* `bilingual -a niutrans -d ./dir-path`
* `bilingual -a tencent \#\ 标题 正文:模拟\ markdown\ 文件的内容。 -f zh -t en`
* `bilingual -a tencent -m xx.md -M xx-中文.md -d path -D path-中文`
* `bilingual -a tencent -m a.md -M a.md -m b.md -M b.md -d assets -D assets -d test -D test -r`
这个命令表明:将 a.md、b.md 文件以及 assets 目录、test 目录下的 md 文件的翻译结果直接替换掉源文件。
注意:本程序使用翻译云服务,因此需要自行申请翻译 API。命令行提供的 id 和 key 会覆盖掉配置文件的信息。
支持从环境变量或者配置文件 `bilingual.toml` 中获取信息,见 https://github.com/zjp-CN/bilingual/issues/27
"#)]
pub struct Bilingual {
#[argh(option, short = 'a', default = "API::default()")]
api: API,
#[argh(option, short = 'i', default = "String::new()")]
id: String,
#[argh(option, short = 'k', default = "String::new()")]
key: String,
#[argh(option, short = 'f', default = "String::from(\"en\")")]
from: String,
#[argh(option, short = 't', default = "String::from(\"zh\")")]
to: String,
#[argh(option, short = 'q', default = "String::new()")]
singlequery: String,
#[argh(option, short = 'm', long = "input-files")]
input_files: Vec<PathBuf>,
#[argh(option, short = 'd', long = "input-dirs")]
input_dirs: Vec<PathBuf>,
#[argh(option, short = 'M', long = "output-files")]
output_files: Vec<PathBuf>,
#[argh(option, short = 'D', long = "output-dirs")]
output_dirs: Vec<PathBuf>,
#[argh(switch, short = 'r', long = "replace-files")]
replace_file: bool,
#[argh(switch, long = "forbid-dir-creation")]
forbid_dir_creation: bool,
#[argh(switch, short = 'v')]
version: bool,
#[argh(option, default = "default_toml()")]
toml: PathBuf,
#[argh(positional)]
multiquery: Vec<String>,
}
fn default_toml() -> PathBuf {
const PWD_BILINGUAL_TOML: &str = "bilingual.toml";
if let Ok(s) = var("BILINGUAL_TOML") {
s.into()
} else if std::path::Path::new(PWD_BILINGUAL_TOML).exists() {
PWD_BILINGUAL_TOML.into()
} else if let Some(mut config_dir) = dirs::config_dir() {
debug!("配置目录为 {config_dir:?}");
config_dir.push(PWD_BILINGUAL_TOML); config_dir
} else {
PathBuf::new()
}
}
impl Bilingual {
pub fn run(mut self) -> Result<Config> {
debug!("{:#?}", self);
if self.version {
println!("{VERSION}");
std::process::exit(0);
}
let mut cf = Config::init(self.toml)?;
match self.api {
API::Baidu => baidu(self.id, self.key, &mut cf)?,
API::Tencent => tencent(self.id, self.key, &mut cf)?,
API::Niutrans => niutrans(self.key, &mut cf)?,
_ => anyhow::bail!("请输入 `-a` 参数来指定 baidu | tencent | niutrans 中的一个"),
}
if self.output_files.is_empty() {
cf.src.output_files =
self.input_files.iter().map(|f| new_filename(f, &self.to)).collect();
} else if self.input_files.len() == self.output_files.len() {
cf.src.output_files = self.output_files;
} else {
anyhow::bail!("-m 与 -M 的数量必须相等")
}
cf.src.input_files = self.input_files;
if self.output_dirs.is_empty() {
cf.src.output_dirs = self.input_dirs.iter().map(|f| new_dir(f, &self.to)).collect();
} else if self.input_dirs.len() == self.output_dirs.len() {
cf.src.output_dirs = self.output_dirs;
} else {
anyhow::bail!("-d 与 -D 的数量必须相等")
}
cf.src.input_dirs = self.input_dirs;
if !self.singlequery.is_empty() {
self.multiquery.push(self.singlequery);
}
cf.src.query = self.multiquery.join("\n\n");
cf.api = self.api;
cf.src.from = self.from;
cf.src.to = self.to;
cf.src.dir_file = DirFile::new(self.replace_file, self.forbid_dir_creation);
Ok(cf)
}
}
fn new_filename(f: &Path, to: &str) -> PathBuf {
let mut stem = f.file_stem().unwrap().to_os_string();
stem.reserve(6);
stem.push("-");
stem.push(to);
stem.push(".");
stem.push(f.extension().unwrap());
f.with_file_name(stem)
}
fn new_dir(f: &Path, to: &str) -> PathBuf {
let mut stem = f.file_stem().unwrap().to_os_string();
stem.reserve(3);
stem.push("-");
stem.push(to);
f.with_file_name(stem)
}
macro_rules! id_key {
(
$cf:ident, $api:ident, $name:literal, $key:ident = $keyl:literal $(, $id:ident = $idl:literal)?
) => {{
let cf = $cf;
if cf.$api.is_none() {
cf.$api.replace(::translation_api_cn::$api::User::default());
debug!("由于未找到配置文件,先使用默认配置(无 id 和 key)");
}
let c = cf.$api.as_mut().ok_or(anyhow!("覆盖 {} API 时出错", $name))?;
$(
if !$id.is_empty() {
c.$id = $id;
debug!("id 被命令行参数覆盖");
} else if let Ok(s) = var($idl) {
c.$id = s;
debug!("id 被 {} 环境变量覆盖", $idl);
}
::anyhow::ensure!(!c.$id.is_empty(), "id 不应该为空");
)?
if !$key.is_empty() {
c.key = $key;
debug!("key 被命令行参数覆盖");
} else if let Ok(s) = var($keyl) {
c.key = s;
debug!("id 被 {} 环境变量覆盖", $keyl);
}
::anyhow::ensure!(!c.key.is_empty(), "key 不应该为空");
Ok(())
}};
}
fn niutrans(key: String, cf: &mut Config) -> Result<()> {
id_key! {
cf, niutrans, "小牛翻译",
key = "BILINGUAL_NIUTRANS_KEY"
}
}
fn tencent(id: String, key: String, cf: &mut Config) -> Result<()> {
id_key! {
cf, tencent, "腾讯云",
key = "BILINGUAL_TENCENT_KEY",
id = "BILINGUAL_TENCENT_ID"
}
}
fn baidu(appid: String, key: String, cf: &mut Config) -> Result<()> {
id_key! {
cf, baidu, "百度翻译",
key = "BILINGUAL_BAIDU_KEY",
appid = "BILINGUAL_BAIDU_ID"
}
}
#[cfg(test)]
mod tests;