This commit is contained in:
zyronon
2025-08-17 17:03:50 +08:00
parent b94bd61263
commit a969fce5ac
23 changed files with 511 additions and 148 deletions

7
src/libs/qs.ts Normal file
View File

@@ -0,0 +1,7 @@
export default {
stringify: (params: Record<string, any>): string => {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
}

View File

@@ -0,0 +1 @@
复制这个库是因为他引入了franc-min这个包太大了50多k我用不到

145
src/libs/translate/baidu.ts Normal file
View File

@@ -0,0 +1,145 @@
import {
Language,
Translator,
TranslateError,
TranslateQueryResult
} from "./translator";
import md5 from "md5";
import qs from "../qs";
const langMap: [Language, string][] = [
["auto", "auto"],
["zh-CN", "zh"],
["en", "en"],
["yue", "yue"],
["wyw", "wyw"],
["ja", "jp"],
["ko", "kor"],
["fr", "fra"],
["es", "spa"],
["th", "th"],
["ar", "ara"],
["ru", "ru"],
["pt", "pt"],
["de", "de"],
["it", "it"],
["el", "el"],
["nl", "nl"],
["pl", "pl"],
["bg", "bul"],
["et", "est"],
["da", "dan"],
["fi", "fin"],
["cs", "cs"],
["ro", "rom"],
["sl", "slo"],
["sv", "swe"],
["hu", "hu"],
["zh-TW", "cht"],
["vi", "vie"]
];
export interface BaiduConfig {
placeholder?: string;
appid: string;
key: string;
}
export class Baidu extends Translator<BaiduConfig> {
readonly name = "baidu";
readonly endpoint = "https://api.fanyi.baidu.com/api/trans/vip/translate";
protected async query(
text: string,
from: Language,
to: Language,
config: BaiduConfig
): Promise<TranslateQueryResult> {
type BaiduTranslateError = {
error_code: "54001" | string;
error_msg: "Invalid Sign" | string;
};
type BaiduTranslateResult = {
from: string;
to: string;
trans_result: Array<{
dst: string;
src: string;
}>;
};
const salt = Date.now();
const {endpoint} = this;
const {appid, key} = config;
const res = await this.request<BaiduTranslateResult | BaiduTranslateError>(
endpoint,
{
params: {
from: Baidu.langMap.get(from),
to: Baidu.langMap.get(to),
q: text,
salt,
appid,
sign: md5(appid + text + salt + key)
}
}
).catch(() => {
throw new TranslateError("NETWORK_ERROR");
});
const {data} = res;
if ((data as BaiduTranslateError).error_code) {
console.error(
new Error("[Baidu service]" + (data as BaiduTranslateError).error_msg)
);
throw new TranslateError("API_SERVER_ERROR");
}
const {
trans_result: transResult,
from: langDetected
} = data as BaiduTranslateResult;
const transParagraphs = transResult.map(({dst}) => dst);
const detectedFrom = Baidu.langMapReverse.get(langDetected) as Language;
return {
text,
from: detectedFrom,
to,
origin: {
paragraphs: transResult.map(({src}) => src),
tts: await this.textToSpeech(text, detectedFrom)
},
trans: {
paragraphs: transParagraphs,
tts: await this.textToSpeech(transParagraphs.join(" "), to)
}
};
}
/** Translator lang to custom lang */
private static readonly langMap = new Map(langMap);
/** Custom lang to translator lang */
private static readonly langMapReverse = new Map(
langMap.map(([translatorLang, lang]) => [lang, translatorLang])
);
getSupportLanguages(): Language[] {
return [...Baidu.langMap.keys()];
}
async textToSpeech(text: string, lang: Language): Promise<string> {
return `https://fanyi.baidu.com/gettts?${qs.stringify({
lan: Baidu.langMap.get(lang !== "auto" ? lang : "zh-CN") || "zh",
text,
spd: 5,
})}`;
}
}
export default Baidu;

View File

@@ -0,0 +1,2 @@
export * from "./languages";
export * from "./locales";

View File

@@ -0,0 +1,123 @@
// eslint-disable-next-line @typescript-eslint/no-use-before-define
export type Language = (typeof languages)[number];
export const languages = [
"af",
"am",
"ar",
"auto",
"az",
"be",
"bg",
"bn",
"bs",
"ca",
"ceb",
"co",
"cs",
"cy",
"da",
"de",
"el",
"en",
"eo",
"es",
"et",
"eu",
"fa",
"fi",
"fil",
"fj",
"fr",
"fy",
"ga",
"gd",
"gl",
"gu",
"ha",
"haw",
"he",
"hi",
"hmn",
"hr",
"ht",
"hu",
"hy",
"id",
"ig",
"is",
"it",
"ja",
"jw",
"ka",
"kk",
"km",
"kn",
"ko",
"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",
"pt",
"ro",
"ru",
"sd",
"si",
"sk",
"sl",
"sm",
"sn",
"so",
"sq",
"sr",
"sr-Cyrl",
"sr-Latn",
"st",
"su",
"sv",
"sw",
"ta",
"te",
"tg",
"th",
"tlh",
"tlh-Qaak",
"to",
"tr",
"ty",
"ug",
"uk",
"ur",
"uz",
"vi",
"wyw",
"xh",
"yi",
"yo",
"yua",
"yue",
"zh-CN",
"zh-TW",
"zu"
] as const;

View File

@@ -0,0 +1,3 @@
import { Language } from "./languages";
export type Locale = { [key in Language]: string };

View File

@@ -0,0 +1,3 @@
export * from "../languages";
export * from "./type";
export * from "./translator";

View File

@@ -0,0 +1,100 @@
import {
Languages,
TranslatorEnv,
TranslatorInit,
TranslateResult,
TranslateQueryResult
} from "./type";
import {Language} from "../languages";
import Axios, {AxiosInstance, AxiosRequestConfig, AxiosPromise} from "axios";
export abstract class Translator<Config extends {} = {}> {
axios: AxiosInstance;
protected readonly env: TranslatorEnv;
/**
* 自定义选项
*/
config: Config;
/**
* 翻译源标识符
*/
abstract readonly name: string;
/**
* 可选的axios实例
*/
constructor(init: TranslatorInit<Config> = {}) {
this.env = init.env || "node";
this.axios = init.axios || Axios;
this.config = init.config || ({} as Config);
}
/**
* 获取翻译器所支持的语言列表: 语言标识符数组
*/
abstract getSupportLanguages(): Languages;
/**
* 下游应用调用的接口
*/
async translate(
text: string,
from: Language,
to: Language,
config = {} as Config
): Promise<TranslateResult> {
const queryResult = await this.query(text, from, to, {
...this.config,
...config
});
return {
...queryResult,
engine: this.name
};
}
/**
* 更新 token 的方法
*/
updateToken?(): Promise<void>;
/**
* 翻译源需要实现的方法
*/
protected abstract query(
text: string,
from: Language,
to: Language,
config: Config
): Promise<TranslateQueryResult>;
protected request<R = {}>(
url: string,
config?: AxiosRequestConfig
): AxiosPromise<R> {
return this.axios(url, config);
}
/**
* 如果翻译源提供了单独的检测语言的功能,请实现此接口
*/
async detect(text: string): Promise<Language> {
return
}
/**
* 文本转换为语音
* @returns {Promise<string|null>} 语言文件地址
*/
textToSpeech(
text: string,
lang: Language,
meta?: any // eslint-disable-line @typescript-eslint/no-explicit-any
): Promise<string | null> {
return Promise.resolve(null);
}
}

View File

@@ -0,0 +1,45 @@
import {Language} from "../languages";
import {AxiosInstance} from "axios";
export type Languages = Array<Language>;
export type TranslatorEnv = "node" | "ext";
export interface TranslatorInit<Config extends {}> {
env?: TranslatorEnv;
axios?: AxiosInstance;
config?: Config;
}
export type TranslateErrorType =
| "NETWORK_ERROR"
| "NETWORK_TIMEOUT"
| "API_SERVER_ERROR"
| "UNSUPPORTED_LANG"
| "UNKNOWN";
export class TranslateError extends Error {
constructor(message: TranslateErrorType) {
super(message);
}
}
/** 统一的查询结果的数据结构 */
export interface TranslateResult {
engine: string;
text: string;
from: Language;
to: Language;
/** 原文 */
origin: {
paragraphs: string[];
tts?: string;
};
/** 译文 */
trans: {
paragraphs: string[];
tts?: string;
};
}
export type TranslateQueryResult = Omit<TranslateResult, "engine">;