use std::time::{SystemTime, UNIX_EPOCH};
use crate::poly_translator::async_translator::{AsyncTranslator, Language, TranslationListOutput, TranslationOutput};
use crate::poly_translator::translator_error::TranslatorError;
use rand::Rng as _;
use reqwest::{Client, header::CONTENT_TYPE};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use uuid::{Context, Timestamp, Uuid};
pub struct YoudaoTranslator {
client: reqwest::Client,
app_key: String,
app_secret: String,
context: Context,
mac: [u8; 6],
}
fn generate_random_mac() -> [u8; 6] {
let mut rng = rand::rng();
let mut mac = [0u8; 6];
rng.fill(&mut mac);
mac[0] |= 0b00000010;
mac[0] &= 0b11111110;
mac
}
impl YoudaoTranslator {
pub fn new(app_key: &str, app_secret: &str) -> Self {
let seed: u16 = rand::rng().random();
Self {
mac: generate_random_mac(),
client: Client::new(),
app_key: app_key.to_string(),
app_secret: app_secret.to_string(),
context: Context::new(seed),
}
}
}
#[allow(dead_code)]
fn sha256_encode(sign_str: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(sign_str.as_bytes());
let result = hasher.finalize();
hex::encode(result)
}
#[async_trait::async_trait]
impl AsyncTranslator for YoudaoTranslator {
fn local(&self) -> bool {
false
}
async fn translate(
&self,
query: &str,
from: Option<Language>,
to: &Language,
) -> anyhow::Result<TranslationOutput> {
let mut t = self
.translate_vec(&[query.to_owned()], from, to)
.await?;
Ok(TranslationOutput {
text: t.text.remove(0),
lang: Some(*to),
})
}
async fn translate_vec(
&self,
query: &[String],
from: Option<Language>,
to: &Language,
) -> anyhow::Result<TranslationListOutput> {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let curtime = now.as_secs();
let nanos = now.subsec_nanos();
let ts = Timestamp::from_unix(&self.context, curtime, nanos);
let salt = Uuid::new_v1(ts, &self.mac).to_string();
let query = query.join("\n");
let sign_str = format!(
"{}{}{}{}{}",
self.app_key,
truncate(&query),
salt,
curtime,
self.app_secret
);
let from = match from {
Some(from) => from.to_youdao().ok_or(TranslatorError::UnknownLanguage(from))?,
None => "auto",
};
let data: Resp = self
.client
.post("https://openapi.youdao.com/api")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.form(&[
("from", from),
("to", to.to_youdao().ok_or(TranslatorError::UnknownLanguage(*to))?),
("signType", "v3"),
("curtime", &curtime.to_string()),
("appKey", self.app_key.as_str()),
("q", query.as_str()),
("salt", salt.as_str()),
("sign", &sha256_encode(&sign_str)),
])
.send()
.await?
.json()
.await?;
Ok(TranslationListOutput {
text: data
.translation
.into_iter()
.flat_map(|v| v.split("/n").map(|v| v.to_owned()).collect::<Vec<String>>())
.collect::<Vec<String>>(),
lang: None,
})
}
}
#[derive(Deserialize)]
#[allow(dead_code)]
pub struct Resp {
translation: Vec<String>,
}
#[allow(dead_code)]
fn truncate(s: &str) -> String {
let size = s.len();
if size <= 20 {
s.to_string()
} else {
let start = &s[..10];
let end = &s[size - 10..];
format!("{}{}{}", start, size, end)
}
}
#[cfg(test)]
mod tests {
use crate::poly_translator::async_translator::{AsyncTranslator as _, Language};
use crate::poly_translator::youdao_translator::{YoudaoTranslator, sha256_encode, truncate};
#[tokio::test]
async fn test_new_translator() {
let translator = YoudaoTranslator::new("test_app_key", "test_app_secret");
assert_eq!(translator.app_key, "test_app_key");
assert_eq!(translator.app_secret, "test_app_secret");
assert_eq!(translator.mac.len(), 6);
}
#[tokio::test]
async fn test_translator_fields() {
let translator = YoudaoTranslator::new("app_key_123", "app_secret_456");
assert_eq!(translator.app_key, "app_key_123");
assert_eq!(translator.app_secret, "app_secret_456");
}
#[tokio::test]
async fn test_translator_local() {
let translator = YoudaoTranslator::new("test_key", "test_secret");
assert!(!translator.local());
}
#[tokio::test]
async fn test_translator_mac() {
let translator = YoudaoTranslator::new("key", "secret");
assert_eq!(translator.mac.len(), 6);
}
#[test]
fn test_sha256_encode() {
let result = sha256_encode("test_string");
assert_eq!(result.len(), 64);
assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_sha256_consistency() {
let result1 = sha256_encode("hello");
let result2 = sha256_encode("hello");
assert_eq!(result1, result2);
}
#[test]
fn test_sha256_different() {
let result1 = sha256_encode("hello");
let result2 = sha256_encode("world");
assert_ne!(result1, result2);
}
#[test]
fn test_truncate_short() {
let result = truncate("hello world");
assert_eq!(result, "hello world");
}
#[test]
fn test_truncate_long() {
let long_text = "this is a very long text that exceeds twenty characters";
let result = truncate(long_text);
assert!(result.len() <= 23);
assert!(result.starts_with("this is a "));
assert!(result.ends_with("characters"));
}
#[test]
fn test_truncate_empty() {
let result = truncate("");
assert_eq!(result, "");
}
#[test]
fn test_truncate_exactly_20() {
let text = "12345678901234567890";
let result = truncate(text);
assert_eq!(result, text);
}
#[test]
fn test_truncate_contains_length() {
let long_text = "123456789012345678901";
let result = truncate(long_text);
assert!(result.contains("21"));
}
#[tokio::test]
async fn test_language_mapping() {
let langs = [
"ar", "de", "en", "es", "fr", "hi", "id", "it", "ja", "ko", "nl", "pt", "ru", "th",
"vi", "zh-CHS", "zh-CHT", "af", "am", "az", "be", "bg", "bn", "bs", "ca", "ceb", "co",
"cs", "cy", "da", "el", "eo", "et", "eu", "fa", "fi", "fj", "fy", "ga", "gd", "gl",
"gu", "ha", "haw", "he", "hi", "hr", "ht", "hu", "hy", "ig", "is", "jw", "ka", "kk",
"km", "kn", "ku", "ky", "la", "lb", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn",
"mr", "ms", "mt", "mww", "my", "ne", "nl", "no", "ny", "otq", "pa", "pl", "ps", "ro",
"sd", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr-Cyrl", "sr-Latn", "st", "su", "sv",
"sw", "ta", "te", "tg", "tl", "tlh", "to", "tr", "ty", "uk", "ur", "uz", "xh", "yi",
"yo", "yua", "yue", "zu",
];
assert!(langs.len() > 0);
for code in langs {
Language::from_youdao(code).expect(code);
}
}
#[tokio::test]
async fn test_all_languages_available() {
let langs = [
"ar", "de", "en", "es", "fr", "hi", "id", "it", "ja", "ko", "nl", "pt", "ru", "th",
"vi", "zh-CHS", "zh-CHT", "af", "am", "az", "be", "bg", "bn", "bs", "ca", "ceb", "co",
"cs", "cy", "da", "el", "eo", "et", "eu", "fa", "fi", "fj", "fy", "ga", "gd", "gl",
"gu", "ha", "haw", "he", "hi", "hr", "ht", "hu", "hy", "ig", "is", "jw", "ka", "kk",
"km", "kn", "ku", "ky", "la", "lb", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn",
"mr", "ms", "mt", "mww", "my", "ne", "nl", "no", "ny", "otq", "pa", "pl", "ps", "ro",
"sd", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr-Cyrl", "sr-Latn", "st", "su", "sv",
"sw", "ta", "te", "tg", "tl", "tlh", "to", "tr", "ty", "uk", "ur", "uz", "xh", "yi",
"yo", "yua", "yue", "zu",
];
assert!(langs.len() > 0);
for code in langs {
Language::from_youdao(code).expect(code);
}
}
#[cfg(test)]
#[tokio::test]
async fn test_translate_chinese_to_english() {
dotenv::dotenv().ok();
let app_key = std::env::var("YOUDAO_APP_KEY").expect("请设置 YOUDAO_APP_KEY 环境变量");
let app_secret = std::env::var("YOUDAO_APP_SECRET").expect("请设置 YOUDAO_APP_SECRET 环境变量");
let translator = YoudaoTranslator::new(&app_key, &app_secret);
let result = translator
.translate("你好世界", Some(Language::Chinese), &Language::English)
.await
.expect("翻译失败");
assert!(!result.text.is_empty());
println!("中译英结果: {}", result.text);
}
#[cfg(test)]
#[tokio::test]
async fn test_translate_english_to_chinese() {
dotenv::dotenv().ok();
let app_key = std::env::var("YOUDAO_APP_KEY").expect("请设置 YOUDAO_APP_KEY 环境变量");
let app_secret = std::env::var("YOUDAO_APP_SECRET").expect("请设置 YOUDAO_APP_SECRET 环境变量");
let translator = YoudaoTranslator::new(&app_key, &app_secret);
let result = translator
.translate("Hello World", Some(Language::English), &Language::Chinese)
.await
.expect("翻译失败");
assert!(!result.text.is_empty());
println!("英译中结果: {}", result.text);
}
}