feat: support builtin AI

This commit is contained in:
Gabe
2025-10-04 21:25:54 +08:00
parent c353c88db8
commit 7b2b48f0d1
23 changed files with 558 additions and 243 deletions

View File

@@ -51,7 +51,9 @@
"GM": true,
"unsafeWindow": true,
"globalThis": true,
"messenger": true
"messenger": true,
"LanguageDetector": true,
"Translator": true
}
},
"browserslist": {

View File

@@ -10,12 +10,19 @@ import {
API_SPE_TYPES,
DEFAULT_API_SETTING,
OPT_TRANS_MICROSOFT,
MSG_BUILTINAI_DETECT,
MSG_BUILTINAI_TRANSLATE,
OPT_TRANS_BUILTINAI,
} from "../config";
import { sha256 } from "../libs/utils";
import { sha256, withTimeout } from "../libs/utils";
import { kissLog } from "../libs/log";
import { handleTranslate, handleMicrosoftLangdetect } from "./trans";
import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache";
import { getBatchQueue } from "../libs/batchQueue";
import { isBuiltinAIAvailable } from "../libs/browser";
import { chromeDetect, chromeTranslate } from "../libs/builtinAI";
import { fnPolyfill } from "../libs/fetch";
import { getFetchPool } from "../libs/pool";
/**
* 同步数据
@@ -332,6 +339,52 @@ export const apiTencentLangdetect = async (text) => {
return "";
};
/**
* 浏览器内置AI语言识别
* @param {*} text
* @returns
*/
export const apiBuiltinAIDetect = async (text) => {
if (!isBuiltinAIAvailable) {
return "";
}
const [lang, error] = await fnPolyfill({
fn: chromeDetect,
msg: MSG_BUILTINAI_DETECT,
text,
});
if (!error) {
return lang;
}
return "";
};
/**
* 浏览器内置AI翻译
* @param {*} param0
* @returns
*/
const apiBuiltinAITranslate = ({ text, from, to, apiSetting }) => {
if (!isBuiltinAIAvailable) {
return ["", true];
}
const { fetchInterval, fetchLimit, httpTimeout } = apiSetting;
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
return withTimeout(
fetchPool.push(fnPolyfill, {
fn: chromeTranslate,
msg: MSG_BUILTINAI_TRANSLATE,
text,
from,
to,
}),
httpTimeout
);
};
/**
* 统一翻译接口
* @param {*} param0
@@ -382,7 +435,14 @@ export const apiTranslate = async ({
// 请求接口数据
let trText = "";
let srLang = "";
if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) {
if (apiType === OPT_TRANS_BUILTINAI) {
[trText, srLang] = await apiBuiltinAITranslate({
text,
from,
to,
apiSetting,
});
} else if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) {
const { apiSlug, batchInterval, batchSize, batchLength } = apiSetting;
const key = `${apiSlug}_${fromLang}_${toLang}`;
const queue = getBatchQueue(key, handleTranslate, {
@@ -426,7 +486,7 @@ export const apiTranslate = async ({
}
}
const isSame = fromLang !== "auto" && srLang === to;
const isSame = fromLang === "auto" && srLang === to;
// 插入缓存
if (useCache && trText) {

View File

@@ -13,6 +13,8 @@ import {
MSG_INJECT_JS,
MSG_INJECT_CSS,
MSG_UPDATE_CSP,
MSG_BUILTINAI_DETECT,
MSG_BUILTINAI_TRANSLATE,
DEFAULT_CSPLIST,
DEFAULT_ORILIST,
CMD_TOGGLE_TRANSLATE,
@@ -31,6 +33,7 @@ import { saveRule } from "./libs/rules";
import { getCurTabId } from "./libs/msg";
import { injectInlineJs, injectInternalCss } from "./libs/injector";
import { kissLog } from "./libs/log";
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
globalThis.ContextType = "BACKGROUND";
@@ -234,43 +237,54 @@ browser.runtime.onStartup.addListener(async () => {
trySyncAllSubRules({ subrulesList });
});
/**
* 向当前活动标签页注入脚本或CSS
*/
const injectToCurrentTab = async (func, args) => {
const tabId = await getCurTabId();
return browser.scripting.executeScript({
target: { tabId, allFrames: true },
func: func,
args: [args],
world: "MAIN",
});
};
// 动作处理器映射表
const messageHandlers = {
[MSG_FETCH]: (args) => fetchHandle(args),
[MSG_GET_HTTPCACHE]: (args) => getHttpCache(args),
[MSG_PUT_HTTPCACHE]: (args) => putHttpCache(args),
[MSG_OPEN_OPTIONS]: () => browser.runtime.openOptionsPage(),
[MSG_SAVE_RULE]: (args) => saveRule(args),
[MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJs, args),
[MSG_INJECT_CSS]: (args) => injectToCurrentTab(injectInternalCss, args),
[MSG_UPDATE_CSP]: (args) => updateCspRules(args),
[MSG_CONTEXT_MENUS]: (args) => addContextMenus(args),
[MSG_COMMAND_SHORTCUTS]: () => browser.commands.getAll(),
[MSG_BUILTINAI_DETECT]: (args) => chromeDetect(args),
[MSG_BUILTINAI_TRANSLATE]: (args) => chromeTranslate(args),
};
/**
* 监听消息
* todo: 返回含错误的结构化信息
*/
browser.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_FETCH:
return await fetchHandle(args);
case MSG_GET_HTTPCACHE:
return await getHttpCache(args.input, args.init);
case MSG_PUT_HTTPCACHE:
return await putHttpCache(args.input, args.init, args.data);
case MSG_OPEN_OPTIONS:
return await browser.runtime.openOptionsPage();
case MSG_SAVE_RULE:
return await saveRule(args);
case MSG_INJECT_JS:
return await browser.scripting.executeScript({
target: { tabId: await getCurTabId(), allFrames: true },
func: injectInlineJs,
args: [args],
world: "MAIN",
});
case MSG_INJECT_CSS:
return await browser.scripting.executeScript({
target: { tabId: await getCurTabId(), allFrames: true },
func: injectInternalCss,
args: [args],
world: "MAIN",
});
case MSG_UPDATE_CSP:
return await updateCspRules(args);
case MSG_CONTEXT_MENUS:
return await addContextMenus(args);
case MSG_COMMAND_SHORTCUTS:
return await browser.commands.getAll();
default:
throw new Error(`message action is unavailable: ${action}`);
const handler = messageHandlers[action];
if (!handler) {
const errorMessage = `Message action is unavailable: ${action}`;
kissLog("runtime onMessage", action, new Error(errorMessage));
return null;
}
try {
const result = await handler(args);
return result;
} catch (err) {
kissLog("runtime onMessage", action, err);
return null;
}
});

View File

@@ -13,7 +13,7 @@ export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
export const OPT_DICT_BAIDU = "Baidu";
// export const OPT_DICT_BAIDU = "Baidu";
export const OPT_DICT_BING = "Bing";
export const OPT_DICT_YOUDAO = "Youdao";
export const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_YOUDAO];
@@ -24,6 +24,7 @@ export const OPT_SUG_YOUDAO = "Youdao";
export const OPT_SUG_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
export const OPT_SUG_MAP = new Set(OPT_SUG_ALL);
export const OPT_TRANS_BUILTINAI = "BuiltinAI";
export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_GOOGLE_2 = "Google2";
export const OPT_TRANS_MICROSOFT = "Microsoft";
@@ -45,10 +46,11 @@ export const OPT_TRANS_CUSTOMIZE = "Custom";
// 内置支持的翻译引擎
export const OPT_ALL_TYPES = [
OPT_TRANS_BUILTINAI,
OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
// OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_DEEPL,
@@ -66,12 +68,15 @@ export const OPT_ALL_TYPES = [
];
export const OPT_LANGDETECTOR_ALL = [
OPT_TRANS_BUILTINAI,
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
];
export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
// 翻译引擎特殊集合
export const API_SPE_TYPES = {
// 内置翻译
@@ -130,7 +135,6 @@ export const API_SPE_TYPES = {
OPT_TRANS_OPENROUTER,
OPT_TRANS_CUSTOMIZE,
]),
detector: new Set(OPT_LANGDETECTOR_ALL),
};
export const BUILTIN_STONES = [
@@ -205,6 +209,11 @@ export const OPT_LANGS_SPEC_DEFAULT_UC = new Map(
OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()])
);
export const OPT_LANGS_TO_SPEC = {
[OPT_TRANS_BUILTINAI]: new Map([
...OPT_LANGS_SPEC_DEFAULT,
["zh-CN", "zh"],
["zh-TW", "zh"],
]),
[OPT_TRANS_GOOGLE]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_MICROSOFT]: new Map([
@@ -392,6 +401,7 @@ const defaultApi = {
};
const defaultApiOpts = {
[OPT_TRANS_BUILTINAI]: defaultApi,
[OPT_TRANS_GOOGLE]: {
...defaultApi,
url: "https://translate.googleapis.com/translate_a/single",

View File

@@ -525,9 +525,14 @@ export const I18N = {
zh_TW: `自建 kiss-wroker 資料同步服務`,
},
about_api: {
zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`,
en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
zh_TW: `暫未列出的介面,理論上都可透過自訂介面的形式支援。`,
zh: `1、其中 BuiltinAI 为浏览器内置AI翻译目前仅 Chrome 138 及以上版本得到支持。`,
en: `1. BuiltinAI is the browser's built-in AI translation, which is currently only supported by Chrome 138 and above.`,
zh_TW: `1.其中 BuiltinAI 為瀏覽器內建AI翻譯目前僅 Chrome 138 以上版本支援。`,
},
about_api_2: {
zh: `2、暂未列出的接口理论上都可以通过自定义接口的形式支持。`,
en: `2. Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
zh_TW: `2、暫未列出的介面理論上都可透過自訂介面的形式支援。`,
},
about_api_proxy: {
zh: `查看自建一个翻译接口代理`,

View File

@@ -22,3 +22,5 @@ export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const MSG_INJECT_JS = "inject_js";
export const MSG_INJECT_CSS = "inject_css";
export const MSG_UPDATE_CSP = "update_csp";
export const MSG_BUILTINAI_DETECT = "builtinai_detect";
export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";

View File

@@ -25,8 +25,8 @@ export const OPT_STYLE_ALL = [
OPT_STYLE_LINE,
OPT_STYLE_DOTLINE,
OPT_STYLE_DASHLINE,
OPT_STYLE_DASHBOX,
OPT_STYLE_WAVYLINE,
OPT_STYLE_DASHBOX,
OPT_STYLE_FUZZY,
OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,

View File

@@ -1,6 +1,6 @@
import {
OPT_DICT_BAIDU,
OPT_SUG_BAIDU,
OPT_DICT_BING,
OPT_SUG_YOUDAO,
DEFAULT_HTTP_TIMEOUT,
OPT_TRANS_MICROSOFT,
DEFAULT_API_LIST,
@@ -91,8 +91,8 @@ export const DEFAULT_TRANBOX_SETTING = {
followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
// extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典
enSug: OPT_SUG_BAIDU, // 英文建议
enDict: OPT_DICT_BING, // 英文词典
enSug: OPT_SUG_YOUDAO, // 英文建议
};
// 订阅列表

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";
import { DEFAULT_API_LIST, API_SPE_TYPES } from "../config";
import { useSetting } from "./Setting";
@@ -12,6 +12,19 @@ function useApiState() {
export function useApiList() {
const { transApis, updateSetting } = useApiState();
useEffect(() => {
const curSlugs = new Set(transApis.map((api) => api.apiSlug));
const missApis = DEFAULT_API_LIST.filter(
(api) => !curSlugs.has(api.apiSlug)
);
if (missApis.length > 0) {
updateSetting((prev) => ({
...prev,
transApis: [...(prev?.transApis || []), ...missApis],
}));
}
}, [transApis, updateSetting]);
const userApis = useMemo(
() =>
transApis
@@ -55,7 +68,9 @@ export function useApiList() {
(apiSlug) => {
updateSetting((prev) => ({
...prev,
transApis: (prev?.transApis || []).filter((api) => api.apiSlug !== apiSlug),
transApis: (prev?.transApis || []).filter(
(api) => api.apiSlug !== apiSlug
),
}));
},
[updateSetting]

View File

@@ -15,3 +15,6 @@ function _browser() {
export const browser = _browser();
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
export const isBuiltinAIAvailable =
"LanguageDetector" in globalThis && "Translator" in globalThis;

168
src/libs/builtinAI.js Normal file
View File

@@ -0,0 +1,168 @@
import { kissLog, logger } from "./log";
/**
* Chrome 浏览器内置翻译
*/
class ChromeTranslator {
#translatorMap = new Map();
#detectorPromise = null;
constructor(options = {}) {
this.onProgress = options.onProgress || this.#defaultProgressHandler;
}
#defaultProgressHandler(type, progress) {
kissLog(`Downloading ${type} model: ${progress}%`);
}
#getDetectorPromise() {
if (!this.#detectorPromise) {
this.#detectorPromise = (async () => {
try {
const availability = await LanguageDetector.availability();
if (availability === "unavailable") {
throw new Error("LanguageDetector unavailable");
}
return await LanguageDetector.create({
monitor: (m) => this._monitorProgress(m, "detector"),
});
} catch (error) {
this.#detectorPromise = null;
throw error;
}
})();
}
return this.#detectorPromise;
}
#createTranslator(sourceLanguage, targetLanguage) {
const key = `${sourceLanguage}_${targetLanguage}`;
if (this.#translatorMap.has(key)) {
return this.#translatorMap.get(key);
}
const translatorPromise = (async () => {
try {
const avail = await Translator.availability({
sourceLanguage,
targetLanguage,
});
if (avail === "unavailable") {
throw new Error(
`Translator ${sourceLanguage}_${targetLanguage} unavailable`
);
}
const translator = await Translator.create({
sourceLanguage,
targetLanguage,
monitor: (m) => this._monitorProgress(m, `translator (${key})`),
});
this.#translatorMap.set(key, translator);
return translator;
} catch (error) {
this.#translatorMap.delete(key);
throw error;
}
})();
this.#translatorMap.set(key, translatorPromise);
return translatorPromise;
}
_monitorProgress(monitorable, type) {
monitorable.addEventListener("downloadprogress", (e) => {
const progress = e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0;
this.onProgress(type, progress);
});
}
async detectLanguage(text, confidenceThreshold = 0.4) {
if (!text) {
return ["", "Input text cannot be empty."];
}
try {
const detector = await this.#getDetectorPromise();
const results = await detector.detect(text);
if (!results || results.length === 0) {
return ["", "No language could be detected."];
}
const { detectedLanguage, confidence } = results[0];
if (confidence < confidenceThreshold) {
return [
"",
`Confidence of test results (${detectedLanguage} ${confidence.toFixed(
2
)}) below the set threshold ${confidenceThreshold}`,
];
}
return [detectedLanguage, ""];
} catch (error) {
kissLog("detectLanguage", error, `(${text})`);
return ["", error.message];
}
}
async translateText(text, targetLanguage, sourceLanguage = "auto") {
if (!text || !targetLanguage || typeof text !== "string") {
return ["", sourceLanguage, "Input text cannot be empty."];
}
try {
let finalSourceLanguage = sourceLanguage;
if (sourceLanguage === "auto") {
const [detectedLanguage, detectionError] =
await this.detectLanguage(text);
if (detectionError || !detectedLanguage) {
const reason =
detectionError || "Unable to determine source language.";
return [
"",
finalSourceLanguage,
`Automatic detection of source language failed: ${reason}`,
];
}
finalSourceLanguage = detectedLanguage;
}
if (finalSourceLanguage === targetLanguage) {
return ["", finalSourceLanguage, "Same lang"];
}
const translator = await this.#createTranslator(
finalSourceLanguage,
targetLanguage
);
const translatedText = await translator.translate(text);
return [translatedText, finalSourceLanguage, ""];
} catch (error) {
kissLog("translateText", error, `(${text})`);
if (
error &&
error.message &&
error.message.includes("Other generic failures occurred")
) {
logger.error("Generic failure detected, resetting translator cache.");
this.#translatorMap.clear();
}
return ["", sourceLanguage, error.message];
}
}
}
const chromeTranslator = new ChromeTranslator();
export const chromeDetect = (args) =>
chromeTranslator.detectLanguage(args.text);
export const chromeTranslate = (args) =>
chromeTranslator.translateText(args.text, args.to, args.from);

View File

@@ -45,7 +45,7 @@ const newCacheReq = async (input, init) => {
* @param {*} init
* @returns
*/
export const getHttpCache = async (input, init) => {
export const getHttpCache = async ({ input, init }) => {
try {
const req = await newCacheReq(input, init);
const cache = await caches.open(CACHE_NAME);
@@ -65,12 +65,12 @@ export const getHttpCache = async (input, init) => {
* @param {*} init
* @param {*} data
*/
export const putHttpCache = async (
export const putHttpCache = async ({
input,
init,
data,
maxAge = DEFAULT_CACHE_TIMEOUT // todo: 从设置里面读取最大缓存时间
) => {
maxAge = DEFAULT_CACHE_TIMEOUT, // todo: 从设置里面读取最大缓存时间
}) => {
try {
const req = await newCacheReq(input, init);
const cache = await caches.open(CACHE_NAME);
@@ -132,7 +132,7 @@ export const getHttpCachePolyfill = (input, init) => {
}
// 油猴/网页/BackgroundPage
return getHttpCache(input, init);
return getHttpCache({ input, init });
};
/**
@@ -149,5 +149,5 @@ export const putHttpCachePolyfill = (input, init, data) => {
}
// 油猴/网页/BackgroundPage
return putHttpCache(input, init, data);
return putHttpCache({ input, init, data });
};

View File

@@ -5,7 +5,8 @@ import {
OPT_TRANS_TENCENT,
OPT_LANGS_TO_CODE,
OPT_LANGS_MAP,
API_SPE_TYPES,
OPT_TRANS_BUILTINAI,
OPT_LANGDETECTOR_MAP,
} from "../config";
import { browser } from "./browser";
import {
@@ -13,6 +14,7 @@ import {
apiMicrosoftLangdetect,
apiBaiduLangdetect,
apiTencentLangdetect,
apiBuiltinAIDetect,
} from "../apis";
import { kissLog } from "./log";
@@ -21,6 +23,7 @@ const langdetectFns = {
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
[OPT_TRANS_BUILTINAI]: apiBuiltinAIDetect,
};
/**
@@ -31,8 +34,8 @@ const langdetectFns = {
export const tryDetectLang = async (text, langDetector = "-") => {
let deLang = "";
// 远程识别
if (API_SPE_TYPES.detector.has(langDetector)) {
// 内置AI/远程识别
if (OPT_LANGDETECTOR_MAP.has(langDetector)) {
try {
const lang = await langdetectFns[langDetector](text);
if (lang) {

View File

@@ -101,14 +101,14 @@ export const fetchHandle = async ({ input, init, opts }) => {
* @param {*} args
* @returns
*/
const fetchPolyfill = (args) => {
export const fnPolyfill = ({ fn, msg = MSG_FETCH, ...args }) => {
// 插件
if (isExt && !isBg()) {
return sendBgMsg(MSG_FETCH, args);
return sendBgMsg(msg, { ...args });
}
// 油猴/网页/BackgroundPage
return fetchHandle(args);
return fn({ ...args });
};
/**
@@ -138,9 +138,9 @@ export const fetchData = async (
// 通过任务池发送请求
if (usePool) {
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
return fetchPool.push(fetchPolyfill, { input, init, opts });
return fetchPool.push(fnPolyfill, { fn: fetchHandle, input, init, opts });
}
// 直接请求
return fetchPolyfill({ input, init, opts });
return fnPolyfill({ fn: fetchHandle, input, init, opts });
};

View File

@@ -274,12 +274,6 @@ export class Translator {
#glossary = {}; // AI词典
#textClass = {}; // 译文样式class
#textSheet = ""; // 译文样式字典
#apiSetting = null;
#placeholder = {
startDelimiter: "{",
endDelimiter: "}",
tagName: "i",
};
#isUserscript = false;
#transboxManager = null; // 划词翻译
@@ -309,20 +303,30 @@ export class Translator {
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
}
constructor(rule = {}, setting = {}, isUserscript = false) {
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
this.#apiSetting =
// 接口参数
// todo: 不用频繁查找计算
get #apiSetting() {
return (
this.#setting.transApis.find(
(api) => api.apiSlug === this.#rule.apiSlug
) || DEFAULT_API_SETTING;
) || DEFAULT_API_SETTING
);
}
// 占位符
get #placeholder() {
const [startDelimiter, endDelimiter] =
this.#apiSetting.placeholder.split(" ");
this.#placeholder = {
return {
startDelimiter,
endDelimiter,
tagName: this.#apiSetting.placetag,
};
}
constructor(rule = {}, setting = {}, isUserscript = false) {
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
this.#isUserscript = isUserscript;
this.#eventName = genEventName();

View File

@@ -333,3 +333,20 @@ export const parseUrlPattern = (href) => {
}
return href;
};
/**
* 带超时的任务
* @param {Promise|Function} task - 任务
* @param {number} timeout - 超时时间 (毫秒)
* @param {string} [timeoutMsg] - 超时错误提示
* @returns {Promise}
*/
export const withTimeout = (task, timeout, timeoutMsg = "Task timed out") => {
const promise = typeof task === "function" ? task() : task;
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(timeoutMsg)), timeout)
),
]);
};

View File

@@ -30,6 +30,7 @@ import {
OPT_TRANS_OLLAMA,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_NIUTRANS,
OPT_TRANS_BUILTINAI,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
DEFAULT_HTTP_TIMEOUT,
@@ -253,7 +254,8 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
onChange={handleChange}
/>
{!API_SPE_TYPES.machine.has(apiType) && (
{!API_SPE_TYPES.machine.has(apiType) &&
apiType !== OPT_TRANS_BUILTINAI && (
<>
<TextField
size="small"
@@ -606,6 +608,9 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
</Grid>
</Box>
{apiType !== OPT_TRANS_BUILTINAI && (
<>
{" "}
<TextField
size="small"
label={i18n("custom_header")}
@@ -626,8 +631,11 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
maxRows={10}
helperText={i18n("custom_body_help")}
/>
</>
)}
{apiType !== OPT_TRANS_CUSTOMIZE && (
{apiType !== OPT_TRANS_CUSTOMIZE &&
apiType !== OPT_TRANS_BUILTINAI && (
<>
<TextField
size="small"
@@ -782,7 +790,11 @@ export default function Apis() {
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">{i18n("about_api")}</Alert>
<Alert severity="info">
{i18n("about_api")}
<br />
{i18n("about_api_2")}
</Alert>
<Box>
<Button

View File

@@ -5,7 +5,6 @@ import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
// import CircularProgress from "@mui/material/CircularProgress";
import { useI18n } from "../../hooks/I18n";
import Box from "@mui/material/Box";
import { useFavWords } from "../../hooks/FavWords";
@@ -18,11 +17,9 @@ import ClearAllIcon from "@mui/icons-material/ClearAll";
import Alert from "@mui/material/Alert";
import { isValidWord } from "../../libs/utils";
import { kissLog } from "../../libs/log";
import { apiTranslate } from "../../apis";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
import { useConfirm } from "../../hooks/Confirm";
import { useSetting } from "../../hooks/Setting";
import { DICT_MAP } from "../Selection/DictMap";
import { dictHandlers } from "../Selection/DictHandler";
function FavAccordion({ word, index }) {
const [expanded, setExpanded] = useState(false);
@@ -83,17 +80,20 @@ export default function FavWords() {
const handleTranslation = async () => {
const { enDict } = setting?.tranboxSetting;
const dict = DICT_MAP[enDict];
const dict = dictHandlers[enDict];
if (!dict) return "";
const tranList = [];
for (const word of wordList) {
try {
const data = await dict.apiFn(word);
const tran = dict.toText(data);
tranList.push([word, tran].join("\n"));
const tran = dict
.toText(data)
.map((line) => `- ${line}`)
.join("\n");
tranList.push([`## ${word}`, tran].join("\n"));
} catch (err) {
// skip
kissLog("export translation", err);
}
}

View File

@@ -289,6 +289,64 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="apiSlug"
value={apiSlug}
label={i18n("translate_service")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{enabledApis.map((api) => (
<MenuItem key={api.apiSlug} value={api.apiSlug}>
{api.apiName}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="toLang"
value={toLang}
label={i18n("to_lang")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
@@ -354,22 +412,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="transTag"
value={transTag}
label={i18n("translation_element_tag")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"span"}>{`<span>`}</MenuItem>
<MenuItem value={"font"}>{`<font>`}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
@@ -386,64 +429,23 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="transTag"
value={transTag}
label={i18n("translation_element_tag")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"span"}>{`<span>`}</MenuItem>
<MenuItem value={"font"}>{`<font>`}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="apiSlug"
value={apiSlug}
label={i18n("translate_service")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{enabledApis.map((api) => (
<MenuItem key={api.apiSlug} value={api.apiSlug}>
{api.apiName}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="toLang"
value={toLang}
label={i18n("to_lang")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select

View File

@@ -340,7 +340,7 @@ export default function Settings() {
size="small"
name="langDetector"
value={langDetector}
label={i18n("detect_lang_remote")}
label={i18n("detected_lang")}
onChange={handleChange}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>

View File

@@ -9,10 +9,10 @@ import {
OPT_LANGS_TO,
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU,
OPT_DICT_BING,
OPT_DICT_ALL,
OPT_SUG_ALL,
OPT_SUG_BAIDU,
OPT_SUG_YOUDAO,
} from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
@@ -69,8 +69,8 @@ export default function Tranbox() {
followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "",
enDict = OPT_DICT_BAIDU,
enSug = OPT_SUG_BAIDU,
enDict = OPT_DICT_BING,
enSug = OPT_SUG_YOUDAO,
} = tranboxSetting;
return (

View File

@@ -7,7 +7,7 @@ import Divider from "@mui/material/Divider";
import Alert from "@mui/material/Alert";
import CopyBtn from "./CopyBtn";
import { useAsyncNow } from "../../hooks/Fetch";
import { DICT_MAP } from "./DictMap";
import { dictHandlers } from "./DictHandler";
function DictBody({ text, setCopyText, dict }) {
const { loading, error, data } = useAsyncNow(dict.apiFn, text);
@@ -17,7 +17,7 @@ function DictBody({ text, setCopyText, dict }) {
return;
}
const copyText = [text, dict.toText(data)].join("\n");
const copyText = [text, dict.toText(data).join("\n")].join("\n");
setCopyText(copyText);
}, [data, text, dict, setCopyText]);
@@ -46,7 +46,7 @@ function DictBody({ text, setCopyText, dict }) {
export default function DictCont({ text, enDict }) {
const [copyText, setCopyText] = useState(text);
const dict = DICT_MAP[enDict];
const dict = dictHandlers[enDict];
return (
<Stack spacing={1}>

View File

@@ -3,13 +3,11 @@ import AudioBtn from "./AudioBtn";
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
export const DICT_MAP = {
export const dictHandlers = {
[OPT_DICT_BING]: {
apiFn: apiMicrosoftDict,
toText: (data) =>
data.trs
?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`)
.join("\n"),
data.trs?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`) || [],
uiAudio: (data) => (
<Typography component="div">
{data?.aus.map(({ key, audio, phonetic }) => (
@@ -38,9 +36,9 @@ export const DICT_MAP = {
[OPT_DICT_YOUDAO]: {
apiFn: apiYoudaoDict,
toText: (data) =>
data?.ec?.word?.trs
?.map(({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`)
.join("\n"),
data?.ec?.word?.trs?.map(
({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`
) || [],
uiAudio: () => null,
uiTrans: (data) => (
<Typography component="ul">