use reqwest::Client;
use serde::{Deserialize, Serialize};
use crate::poly_translator::async_translator::{AsyncTranslator, Language, TranslationListOutput, TranslationOutput};
use crate::poly_translator::translator_error::{ApiError, TranslatorError};
pub struct BaiduTranslator {
url: String,
app_id: String,
key: String,
client: Client,
}
#[async_trait::async_trait]
impl AsyncTranslator for BaiduTranslator {
fn local(&self) -> bool {
false
}
async fn translate(
&self,
query: &str,
from: Option<Language>,
to: &Language,
) -> anyhow::Result<TranslationOutput> {
let to = to.to_baidu().ok_or(TranslatorError::UnknownLanguage(*to))?;
let from = match from {
Some(item) => item.to_baidu().ok_or(TranslatorError::UnknownLanguage(item))?,
None => "auto",
};
let form = Form::new(&self.app_id, query, "0", &self.key, from, to);
let resp: Response = self
.client
.post(&self.url)
.form(&form)
.send()
.await?
.json()
.await?;
let resp = match resp {
Response::Ok(v) => v,
Response::Err(v) => {
Err(TranslatorError::ApiError(ApiError::Baidu {
message: v.solution().to_owned(),
code: v.code,
}))?;
unreachable!()
}
};
Ok(TranslationOutput {
text: resp
.trans_result
.iter()
.map(|v| v.dst.to_string())
.collect::<Vec<_>>()
.join("\n"),
lang: Some(
Language::from_baidu(&resp.to).ok_or(TranslatorError::CouldNotMapLanguage(Some(resp.to)))?,
),
})
}
async fn translate_vec(
&self,
query: &[String],
from: Option<Language>,
to: &Language,
) -> anyhow::Result<TranslationListOutput> {
let v = self.translate(&query.join("\n"), from, to).await?;
Ok(TranslationListOutput {
text: v.text.split('\n').map(|v| v.to_string()).collect(),
lang: v.lang,
})
}
}
impl BaiduTranslator {
#[allow(dead_code)]
pub fn new(app_id: &str, key: &str) -> Self {
Self {
url: "https://fanyi-api.baidu.com/api/trans/vip/translate".to_string(),
app_id: app_id.to_string(),
key: key.to_string(),
client: Client::new(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Form {
pub q: String,
pub from: String,
pub to: String,
pub appid: String,
pub salt: String,
pub sign: String,
}
impl Form {
#[allow(dead_code)]
fn new(appid: &str, q: &str, salt: &str, key: &str, from: &str, to: &str) -> Self {
let data = format!("{}{}{}{}", &appid, q, salt, &key);
let sign = format!("{:x}", md5::compute(data));
Self {
q: q.to_string(),
from: from.to_string(),
to: to.to_string(),
appid: appid.to_string(),
salt: salt.to_string(),
sign,
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
#[allow(dead_code)]
enum Response {
Ok(TranslationResponse),
Err(BaiduApiError),
}
#[derive(Debug, Clone, Deserialize)]
pub struct BaiduApiError {
#[serde(rename = "error_code")]
pub code: String,
#[serde(rename = "error_msg")]
pub msg: String,
}
impl std::error::Error for BaiduApiError {}
impl std::fmt::Display for BaiduApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Error code: `{}`\nError message: `{}`\nError meaning: {}\nThe above content is returned by Baidu translation API",
self.code,
self.msg,
self.solution()
)
}
}
impl BaiduApiError {
pub fn solution(&self) -> &str {
match self.code.as_bytes() {
b"52000" => "成功",
b"52001" => "请求超时。\n解决方案:请重试。",
b"52002" => "系统错误。\n解决方案:请重试。",
b"52003" => {
"未授权用户。\n解决方案:请检查appid是否正确或服务是否已开通。"
}
b"54000" => {
"必填参数为空。\n解决方案:请检查是否传递了所有必要参数。"
}
b"54001" => {
"签名错误。\n解决方案:请检查签名生成方式。"
}
b"54003" => {
"访问频率受限。\n解决方案:请降低调用频率,或通过认证后切换到高级版本。"
}
b"54004" => {
"账户余额不足。\n解决方案:请前往管理控制台充值。"
}
b"54005" => {
"长查询请求过于频繁。\n解决方案:请降低长查询的发送频率,3秒后重试。"
}
b"58000" => {
"客户端IP非法。\n解决方案:检查个人信息中填写的IP地址是否正确,可前往开发者信息-基本信息进行修改。"
}
b"58001" => {
"目标语言方向不支持。\n解决方案:检查目标语言是否在语言列表中。"
}
b"58002" => {
"服务目前已下线。\n解决方案:请前往管理控制台开启服务。"
}
b"58003" => {
"如果同一IP在同一天使用多个APPID发送翻译请求,该IP将在当日剩余时间内被禁止请求,次日解封。请勿将APPID和密钥输入第三方软件。"
}
b"90107" => {
"认证未通过或已失效。\n解决方案:请前往我的认证查看认证进度。"
}
b"20003" => {
"请检查请求文本是否涉及颠覆、暴力或类似主题相关内容。"
}
_ => "未知错误",
}
}
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct Sentence {
pub dst: String,
}
#[derive(Deserialize)]
struct TranslationResponse {
pub to: String,
pub trans_result: Vec<Sentence>,
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::poly_translator::async_translator::{AsyncTranslator, Language};
use crate::poly_translator::baidu_translator::{BaiduTranslator, Form};
#[tokio::test]
async fn test_new_translator() {
let translator = BaiduTranslator::new("test_app_id", "test_key");
assert_eq!(translator.app_id, "test_app_id");
assert_eq!(translator.key, "test_key");
assert!(translator.url.contains("baidu.com"));
}
#[test]
fn test_form_creation() {
let form = Form::new("appid", "hello", "salt", "key", "en", "zh");
assert_eq!(form.q, "hello");
assert_eq!(form.from, "en");
assert_eq!(form.to, "zh");
assert_eq!(form.appid, "appid");
assert_eq!(form.salt, "salt");
assert!(!form.sign.is_empty());
}
#[test]
fn test_form_empty_query() {
let form = Form::new("appid", "", "salt", "key", "auto", "zh");
assert!(form.sign.len() == 32);
}
#[tokio::test]
async fn test_translator_fields() {
let translator = BaiduTranslator::new("app_id_123", "key_456");
assert_eq!(translator.app_id, "app_id_123");
assert_eq!(translator.key, "key_456");
assert!(translator.url.starts_with("https://"));
}
#[tokio::test]
async fn test_translator_local() {
let translator = BaiduTranslator::new("test_id", "test_key");
assert!(!translator.local());
}
#[test]
fn test_language_mapping() {
let langs = [
"zh", "en", "yue", "wyw", "jp", "kor", "fra", "spa", "th", "ara", "ru", "pt", "de",
"it", "el", "nl", "pl", "bul", "est", "dan", "fin", "cs", "rom", "slo", "swe", "hu",
"cht", "vie", "ara", "gle", "oci", "alb", "arq", "aka", "arg", "amh", "asm", "aym",
"aze", "ast", "oss", "est", "oji", "ori", "orm", "pl", "per", "bre", "bak", "baq",
"pot", "bel", "ber", "pam", "bul", "sme", "ped", "bem", "bli", "bis", "bal", "ice",
"bos", "bho", "chv", "tso", "dan", "de", "tat", "sha", "tet", "div", "log", "ru",
"fra", "fil", "fin", "san", "fri", "ful", "fao", "gla", "kon", "ups", "hkm", "kal",
"geo", "guj", "gra", "eno", "grn", "kor", "nl", "hup", "hak", "ht", "mot", "hau",
"kir", "glg", "frn", "cat", "cs", "kab", "kan", "kau", "kah", "cor", "xho", "cos",
"cre", "cri", "kli", "hrv", "que", "kas", "kok", "kur", "lat", "lao", "rom", "lag",
"lav", "lim", "lin", "lug", "ltz", "ruy", "kin", "lit", "roh", "ro", "loj", "may",
"bur", "mar", "mg", "mal", "mac", "mah", "mai", "glv", "mau", "mao", "ben", "mlt",
"hmn", "nor", "nea", "nbl", "afr", "sot", "nep", "pt", "pan", "pap", "pus", "nya",
"twi", "chr", "jp", "swe", "srd", "sm", "sec", "srp", "sol", "sin", "epo", "nob", "sk",
"slo", "swa", "src", "som", "sco", "th", "tr", "tgk", "tam", "tgl", "tir", "tel",
"tua", "tuk", "ukr", "wln", "wel", "ven", "wol", "urd", "spa", "heb", "el", "hu",
"fry", "sil", "hil", "los", "haw", "nno", "nqo", "snd", "sna", "ceb", "syr", "sun",
"en", "hi", "id", "it", "vie", "yid", "ina", "ach", "ing", "ibo", "ido", "yor", "arm",
"iku", "zh", "cht", "wyw", "yue", "zaz", "frm", "zul", "jav",
];
for lang_str in langs.into_iter().collect::<HashSet<_>>() {
if lang_str == "slo" {
continue;
}
Language::from_baidu(lang_str).expect(lang_str);
}
}
#[test]
fn test_duplicate_language_codes() {
let langs = vec!["zh", "en", "zh", "en", "jp"];
let unique_langs: HashSet<&str> = langs.into_iter().collect();
assert_eq!(unique_langs.len(), 3);
}
#[test]
fn test_form_signature_consistency() {
let form1 = Form::new("appid", "hello", "salt", "key", "en", "zh");
let form2 = Form::new("appid", "hello", "salt", "key", "en", "zh");
assert_eq!(form1.sign, form2.sign);
}
#[test]
fn test_different_queries_different_signatures() {
let form1 = Form::new("appid", "hello", "salt", "key", "en", "zh");
let form2 = Form::new("appid", "world", "salt", "key", "en", "zh");
assert_ne!(form1.sign, form2.sign);
}
#[cfg(test)]
#[tokio::test]
async fn test_translate_chinese_to_english() {
dotenv::dotenv().ok();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
let app_id = std::env::var("BAIDU_APP_ID").expect("请设置 BAIDU_APP_ID 环境变量");
let key = std::env::var("BAIDU_KEY").expect("请设置 BAIDU_KEY 环境变量");
let translator = BaiduTranslator::new(&app_id, &key);
let result = translator
.translate("你好世界", Some(Language::Chinese), &Language::English)
.await;
match result {
Ok(output) => {
assert!(!output.text.is_empty());
println!("中译英结果: {}", output.text);
}
Err(e) => {
let err_msg = e.to_string();
println!("中译英错误: {}", err_msg);
if let Some(api_err) = e.downcast_ref::<crate::poly_translator::translator_error::TranslatorError>() {
println!("错误类型: {:?}", api_err);
if let crate::poly_translator::translator_error::TranslatorError::ApiError(api_details) = api_err {
println!("API错误详情: {:?}", api_details);
}
}
panic!("翻译失败: {}", err_msg);
}
}
}
#[cfg(test)]
#[tokio::test]
async fn test_translate_english_to_chinese() {
dotenv::dotenv().ok();
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
let app_id = std::env::var("BAIDU_APP_ID").expect("请设置 BAIDU_APP_ID 环境变量");
let key = std::env::var("BAIDU_KEY").expect("请设置 BAIDU_KEY 环境变量");
let translator = BaiduTranslator::new(&app_id, &key);
let result = translator
.translate("Hello World", Some(Language::English), &Language::Chinese)
.await;
match result {
Ok(output) => {
assert!(!output.text.is_empty());
println!("英译中结果: {}", output.text);
}
Err(e) => {
let err_msg = e.to_string();
println!("英译中错误: {}", err_msg);
if let Some(api_err) = e.downcast_ref::<crate::poly_translator::translator_error::TranslatorError>() {
println!("错误类型: {:?}", api_err);
if let crate::poly_translator::translator_error::TranslatorError::ApiError(api_details) = api_err {
println!("API错误详情: {:?}", api_details);
}
}
panic!("翻译失败: {}", err_msg);
}
}
}
}