feat: support builtin AI
This commit is contained in:
@@ -51,7 +51,9 @@
|
|||||||
"GM": true,
|
"GM": true,
|
||||||
"unsafeWindow": true,
|
"unsafeWindow": true,
|
||||||
"globalThis": true,
|
"globalThis": true,
|
||||||
"messenger": true
|
"messenger": true,
|
||||||
|
"LanguageDetector": true,
|
||||||
|
"Translator": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -10,12 +10,19 @@ import {
|
|||||||
API_SPE_TYPES,
|
API_SPE_TYPES,
|
||||||
DEFAULT_API_SETTING,
|
DEFAULT_API_SETTING,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
MSG_BUILTINAI_DETECT,
|
||||||
|
MSG_BUILTINAI_TRANSLATE,
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { sha256 } from "../libs/utils";
|
import { sha256, withTimeout } from "../libs/utils";
|
||||||
import { kissLog } from "../libs/log";
|
import { kissLog } from "../libs/log";
|
||||||
import { handleTranslate, handleMicrosoftLangdetect } from "./trans";
|
import { handleTranslate, handleMicrosoftLangdetect } from "./trans";
|
||||||
import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache";
|
import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache";
|
||||||
import { getBatchQueue } from "../libs/batchQueue";
|
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 "";
|
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
|
* @param {*} param0
|
||||||
@@ -382,7 +435,14 @@ export const apiTranslate = async ({
|
|||||||
// 请求接口数据
|
// 请求接口数据
|
||||||
let trText = "";
|
let trText = "";
|
||||||
let srLang = "";
|
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 { apiSlug, batchInterval, batchSize, batchLength } = apiSetting;
|
||||||
const key = `${apiSlug}_${fromLang}_${toLang}`;
|
const key = `${apiSlug}_${fromLang}_${toLang}`;
|
||||||
const queue = getBatchQueue(key, handleTranslate, {
|
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) {
|
if (useCache && trText) {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
MSG_INJECT_JS,
|
MSG_INJECT_JS,
|
||||||
MSG_INJECT_CSS,
|
MSG_INJECT_CSS,
|
||||||
MSG_UPDATE_CSP,
|
MSG_UPDATE_CSP,
|
||||||
|
MSG_BUILTINAI_DETECT,
|
||||||
|
MSG_BUILTINAI_TRANSLATE,
|
||||||
DEFAULT_CSPLIST,
|
DEFAULT_CSPLIST,
|
||||||
DEFAULT_ORILIST,
|
DEFAULT_ORILIST,
|
||||||
CMD_TOGGLE_TRANSLATE,
|
CMD_TOGGLE_TRANSLATE,
|
||||||
@@ -31,6 +33,7 @@ import { saveRule } from "./libs/rules";
|
|||||||
import { getCurTabId } from "./libs/msg";
|
import { getCurTabId } from "./libs/msg";
|
||||||
import { injectInlineJs, injectInternalCss } from "./libs/injector";
|
import { injectInlineJs, injectInternalCss } from "./libs/injector";
|
||||||
import { kissLog } from "./libs/log";
|
import { kissLog } from "./libs/log";
|
||||||
|
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
|
||||||
|
|
||||||
globalThis.ContextType = "BACKGROUND";
|
globalThis.ContextType = "BACKGROUND";
|
||||||
|
|
||||||
@@ -234,43 +237,54 @@ browser.runtime.onStartup.addListener(async () => {
|
|||||||
trySyncAllSubRules({ subrulesList });
|
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 }) => {
|
browser.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||||
switch (action) {
|
const handler = messageHandlers[action];
|
||||||
case MSG_FETCH:
|
|
||||||
return await fetchHandle(args);
|
if (!handler) {
|
||||||
case MSG_GET_HTTPCACHE:
|
const errorMessage = `Message action is unavailable: ${action}`;
|
||||||
return await getHttpCache(args.input, args.init);
|
kissLog("runtime onMessage", action, new Error(errorMessage));
|
||||||
case MSG_PUT_HTTPCACHE:
|
return null;
|
||||||
return await putHttpCache(args.input, args.init, args.data);
|
}
|
||||||
case MSG_OPEN_OPTIONS:
|
|
||||||
return await browser.runtime.openOptionsPage();
|
try {
|
||||||
case MSG_SAVE_RULE:
|
const result = await handler(args);
|
||||||
return await saveRule(args);
|
return result;
|
||||||
case MSG_INJECT_JS:
|
} catch (err) {
|
||||||
return await browser.scripting.executeScript({
|
kissLog("runtime onMessage", action, err);
|
||||||
target: { tabId: await getCurTabId(), allFrames: true },
|
return null;
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
|
|||||||
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
||||||
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
|
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_BING = "Bing";
|
||||||
export const OPT_DICT_YOUDAO = "Youdao";
|
export const OPT_DICT_YOUDAO = "Youdao";
|
||||||
export const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_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_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
|
||||||
export const OPT_SUG_MAP = new Set(OPT_SUG_ALL);
|
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 = "Google";
|
||||||
export const OPT_TRANS_GOOGLE_2 = "Google2";
|
export const OPT_TRANS_GOOGLE_2 = "Google2";
|
||||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||||
@@ -45,10 +46,11 @@ export const OPT_TRANS_CUSTOMIZE = "Custom";
|
|||||||
|
|
||||||
// 内置支持的翻译引擎
|
// 内置支持的翻译引擎
|
||||||
export const OPT_ALL_TYPES = [
|
export const OPT_ALL_TYPES = [
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
OPT_TRANS_GOOGLE_2,
|
OPT_TRANS_GOOGLE_2,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
OPT_TRANS_BAIDU,
|
// OPT_TRANS_BAIDU,
|
||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
OPT_TRANS_VOLCENGINE,
|
OPT_TRANS_VOLCENGINE,
|
||||||
OPT_TRANS_DEEPL,
|
OPT_TRANS_DEEPL,
|
||||||
@@ -66,12 +68,15 @@ export const OPT_ALL_TYPES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const OPT_LANGDETECTOR_ALL = [
|
export const OPT_LANGDETECTOR_ALL = [
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
OPT_TRANS_BAIDU,
|
OPT_TRANS_BAIDU,
|
||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
|
||||||
|
|
||||||
// 翻译引擎特殊集合
|
// 翻译引擎特殊集合
|
||||||
export const API_SPE_TYPES = {
|
export const API_SPE_TYPES = {
|
||||||
// 内置翻译
|
// 内置翻译
|
||||||
@@ -130,7 +135,6 @@ export const API_SPE_TYPES = {
|
|||||||
OPT_TRANS_OPENROUTER,
|
OPT_TRANS_OPENROUTER,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
]),
|
]),
|
||||||
detector: new Set(OPT_LANGDETECTOR_ALL),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BUILTIN_STONES = [
|
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()])
|
OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()])
|
||||||
);
|
);
|
||||||
export const OPT_LANGS_TO_SPEC = {
|
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]: OPT_LANGS_SPEC_DEFAULT,
|
||||||
[OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT,
|
[OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT,
|
||||||
[OPT_TRANS_MICROSOFT]: new Map([
|
[OPT_TRANS_MICROSOFT]: new Map([
|
||||||
@@ -392,6 +401,7 @@ const defaultApi = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultApiOpts = {
|
const defaultApiOpts = {
|
||||||
|
[OPT_TRANS_BUILTINAI]: defaultApi,
|
||||||
[OPT_TRANS_GOOGLE]: {
|
[OPT_TRANS_GOOGLE]: {
|
||||||
...defaultApi,
|
...defaultApi,
|
||||||
url: "https://translate.googleapis.com/translate_a/single",
|
url: "https://translate.googleapis.com/translate_a/single",
|
||||||
|
|||||||
@@ -525,9 +525,14 @@ export const I18N = {
|
|||||||
zh_TW: `自建 kiss-wroker 資料同步服務`,
|
zh_TW: `自建 kiss-wroker 資料同步服務`,
|
||||||
},
|
},
|
||||||
about_api: {
|
about_api: {
|
||||||
zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`,
|
zh: `1、其中 BuiltinAI 为浏览器内置AI翻译,目前仅 Chrome 138 及以上版本得到支持。`,
|
||||||
en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
en: `1. BuiltinAI is the browser's built-in AI translation, which is currently only supported by Chrome 138 and above.`,
|
||||||
zh_TW: `暫未列出的介面,理論上都可透過自訂介面的形式支援。`,
|
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: {
|
about_api_proxy: {
|
||||||
zh: `查看自建一个翻译接口代理`,
|
zh: `查看自建一个翻译接口代理`,
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
|
|||||||
export const MSG_INJECT_JS = "inject_js";
|
export const MSG_INJECT_JS = "inject_js";
|
||||||
export const MSG_INJECT_CSS = "inject_css";
|
export const MSG_INJECT_CSS = "inject_css";
|
||||||
export const MSG_UPDATE_CSP = "update_csp";
|
export const MSG_UPDATE_CSP = "update_csp";
|
||||||
|
export const MSG_BUILTINAI_DETECT = "builtinai_detect";
|
||||||
|
export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export const OPT_STYLE_ALL = [
|
|||||||
OPT_STYLE_LINE,
|
OPT_STYLE_LINE,
|
||||||
OPT_STYLE_DOTLINE,
|
OPT_STYLE_DOTLINE,
|
||||||
OPT_STYLE_DASHLINE,
|
OPT_STYLE_DASHLINE,
|
||||||
OPT_STYLE_DASHBOX,
|
|
||||||
OPT_STYLE_WAVYLINE,
|
OPT_STYLE_WAVYLINE,
|
||||||
|
OPT_STYLE_DASHBOX,
|
||||||
OPT_STYLE_FUZZY,
|
OPT_STYLE_FUZZY,
|
||||||
OPT_STYLE_HIGHLIGHT,
|
OPT_STYLE_HIGHLIGHT,
|
||||||
OPT_STYLE_BLOCKQUOTE,
|
OPT_STYLE_BLOCKQUOTE,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
OPT_DICT_BAIDU,
|
OPT_DICT_BING,
|
||||||
OPT_SUG_BAIDU,
|
OPT_SUG_YOUDAO,
|
||||||
DEFAULT_HTTP_TIMEOUT,
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
DEFAULT_API_LIST,
|
DEFAULT_API_LIST,
|
||||||
@@ -91,8 +91,8 @@ export const DEFAULT_TRANBOX_SETTING = {
|
|||||||
followSelection: false, // 翻译框是否跟随选中文本
|
followSelection: false, // 翻译框是否跟随选中文本
|
||||||
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||||
// extStyles: "", // 附加样式
|
// extStyles: "", // 附加样式
|
||||||
enDict: OPT_DICT_BAIDU, // 英文词典
|
enDict: OPT_DICT_BING, // 英文词典
|
||||||
enSug: OPT_SUG_BAIDU, // 英文建议
|
enSug: OPT_SUG_YOUDAO, // 英文建议
|
||||||
};
|
};
|
||||||
|
|
||||||
// 订阅列表
|
// 订阅列表
|
||||||
|
|||||||
@@ -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 { DEFAULT_API_LIST, API_SPE_TYPES } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
@@ -12,6 +12,19 @@ function useApiState() {
|
|||||||
export function useApiList() {
|
export function useApiList() {
|
||||||
const { transApis, updateSetting } = useApiState();
|
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(
|
const userApis = useMemo(
|
||||||
() =>
|
() =>
|
||||||
transApis
|
transApis
|
||||||
@@ -55,7 +68,9 @@ export function useApiList() {
|
|||||||
(apiSlug) => {
|
(apiSlug) => {
|
||||||
updateSetting((prev) => ({
|
updateSetting((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
transApis: (prev?.transApis || []).filter((api) => api.apiSlug !== apiSlug),
|
transApis: (prev?.transApis || []).filter(
|
||||||
|
(api) => api.apiSlug !== apiSlug
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
[updateSetting]
|
[updateSetting]
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ function _browser() {
|
|||||||
export const browser = _browser();
|
export const browser = _browser();
|
||||||
|
|
||||||
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
||||||
|
|
||||||
|
export const isBuiltinAIAvailable =
|
||||||
|
"LanguageDetector" in globalThis && "Translator" in globalThis;
|
||||||
|
|||||||
168
src/libs/builtinAI.js
Normal file
168
src/libs/builtinAI.js
Normal 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);
|
||||||
@@ -45,7 +45,7 @@ const newCacheReq = async (input, init) => {
|
|||||||
* @param {*} init
|
* @param {*} init
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getHttpCache = async (input, init) => {
|
export const getHttpCache = async ({ input, init }) => {
|
||||||
try {
|
try {
|
||||||
const req = await newCacheReq(input, init);
|
const req = await newCacheReq(input, init);
|
||||||
const cache = await caches.open(CACHE_NAME);
|
const cache = await caches.open(CACHE_NAME);
|
||||||
@@ -65,12 +65,12 @@ export const getHttpCache = async (input, init) => {
|
|||||||
* @param {*} init
|
* @param {*} init
|
||||||
* @param {*} data
|
* @param {*} data
|
||||||
*/
|
*/
|
||||||
export const putHttpCache = async (
|
export const putHttpCache = async ({
|
||||||
input,
|
input,
|
||||||
init,
|
init,
|
||||||
data,
|
data,
|
||||||
maxAge = DEFAULT_CACHE_TIMEOUT // todo: 从设置里面读取最大缓存时间
|
maxAge = DEFAULT_CACHE_TIMEOUT, // todo: 从设置里面读取最大缓存时间
|
||||||
) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const req = await newCacheReq(input, init);
|
const req = await newCacheReq(input, init);
|
||||||
const cache = await caches.open(CACHE_NAME);
|
const cache = await caches.open(CACHE_NAME);
|
||||||
@@ -132,7 +132,7 @@ export const getHttpCachePolyfill = (input, init) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 油猴/网页/BackgroundPage
|
// 油猴/网页/BackgroundPage
|
||||||
return getHttpCache(input, init);
|
return getHttpCache({ input, init });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,5 +149,5 @@ export const putHttpCachePolyfill = (input, init, data) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 油猴/网页/BackgroundPage
|
// 油猴/网页/BackgroundPage
|
||||||
return putHttpCache(input, init, data);
|
return putHttpCache({ input, init, data });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
OPT_LANGS_TO_CODE,
|
OPT_LANGS_TO_CODE,
|
||||||
OPT_LANGS_MAP,
|
OPT_LANGS_MAP,
|
||||||
API_SPE_TYPES,
|
OPT_TRANS_BUILTINAI,
|
||||||
|
OPT_LANGDETECTOR_MAP,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
apiMicrosoftLangdetect,
|
apiMicrosoftLangdetect,
|
||||||
apiBaiduLangdetect,
|
apiBaiduLangdetect,
|
||||||
apiTencentLangdetect,
|
apiTencentLangdetect,
|
||||||
|
apiBuiltinAIDetect,
|
||||||
} from "../apis";
|
} from "../apis";
|
||||||
import { kissLog } from "./log";
|
import { kissLog } from "./log";
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ const langdetectFns = {
|
|||||||
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
||||||
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
||||||
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
||||||
|
[OPT_TRANS_BUILTINAI]: apiBuiltinAIDetect,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,8 +34,8 @@ const langdetectFns = {
|
|||||||
export const tryDetectLang = async (text, langDetector = "-") => {
|
export const tryDetectLang = async (text, langDetector = "-") => {
|
||||||
let deLang = "";
|
let deLang = "";
|
||||||
|
|
||||||
// 远程识别
|
// 内置AI/远程识别
|
||||||
if (API_SPE_TYPES.detector.has(langDetector)) {
|
if (OPT_LANGDETECTOR_MAP.has(langDetector)) {
|
||||||
try {
|
try {
|
||||||
const lang = await langdetectFns[langDetector](text);
|
const lang = await langdetectFns[langDetector](text);
|
||||||
if (lang) {
|
if (lang) {
|
||||||
|
|||||||
@@ -101,14 +101,14 @@ export const fetchHandle = async ({ input, init, opts }) => {
|
|||||||
* @param {*} args
|
* @param {*} args
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const fetchPolyfill = (args) => {
|
export const fnPolyfill = ({ fn, msg = MSG_FETCH, ...args }) => {
|
||||||
// 插件
|
// 插件
|
||||||
if (isExt && !isBg()) {
|
if (isExt && !isBg()) {
|
||||||
return sendBgMsg(MSG_FETCH, args);
|
return sendBgMsg(msg, { ...args });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 油猴/网页/BackgroundPage
|
// 油猴/网页/BackgroundPage
|
||||||
return fetchHandle(args);
|
return fn({ ...args });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,9 +138,9 @@ export const fetchData = async (
|
|||||||
// 通过任务池发送请求
|
// 通过任务池发送请求
|
||||||
if (usePool) {
|
if (usePool) {
|
||||||
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
|
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 });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -274,12 +274,6 @@ export class Translator {
|
|||||||
#glossary = {}; // AI词典
|
#glossary = {}; // AI词典
|
||||||
#textClass = {}; // 译文样式class
|
#textClass = {}; // 译文样式class
|
||||||
#textSheet = ""; // 译文样式字典
|
#textSheet = ""; // 译文样式字典
|
||||||
#apiSetting = null;
|
|
||||||
#placeholder = {
|
|
||||||
startDelimiter: "{",
|
|
||||||
endDelimiter: "}",
|
|
||||||
tagName: "i",
|
|
||||||
};
|
|
||||||
|
|
||||||
#isUserscript = false;
|
#isUserscript = false;
|
||||||
#transboxManager = null; // 划词翻译
|
#transboxManager = null; // 划词翻译
|
||||||
@@ -309,20 +303,30 @@ export class Translator {
|
|||||||
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(rule = {}, setting = {}, isUserscript = false) {
|
// 接口参数
|
||||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
// todo: 不用频繁查找计算
|
||||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
get #apiSetting() {
|
||||||
this.#apiSetting =
|
return (
|
||||||
this.#setting.transApis.find(
|
this.#setting.transApis.find(
|
||||||
(api) => api.apiSlug === this.#rule.apiSlug
|
(api) => api.apiSlug === this.#rule.apiSlug
|
||||||
) || DEFAULT_API_SETTING;
|
) || DEFAULT_API_SETTING
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 占位符
|
||||||
|
get #placeholder() {
|
||||||
const [startDelimiter, endDelimiter] =
|
const [startDelimiter, endDelimiter] =
|
||||||
this.#apiSetting.placeholder.split(" ");
|
this.#apiSetting.placeholder.split(" ");
|
||||||
this.#placeholder = {
|
return {
|
||||||
startDelimiter,
|
startDelimiter,
|
||||||
endDelimiter,
|
endDelimiter,
|
||||||
tagName: this.#apiSetting.placetag,
|
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.#isUserscript = isUserscript;
|
||||||
this.#eventName = genEventName();
|
this.#eventName = genEventName();
|
||||||
|
|||||||
@@ -333,3 +333,20 @@ export const parseUrlPattern = (href) => {
|
|||||||
}
|
}
|
||||||
return 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)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
OPT_TRANS_OLLAMA,
|
OPT_TRANS_OLLAMA,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
OPT_TRANS_NIUTRANS,
|
OPT_TRANS_NIUTRANS,
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
DEFAULT_FETCH_LIMIT,
|
DEFAULT_FETCH_LIMIT,
|
||||||
DEFAULT_FETCH_INTERVAL,
|
DEFAULT_FETCH_INTERVAL,
|
||||||
DEFAULT_HTTP_TIMEOUT,
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
@@ -253,7 +254,8 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!API_SPE_TYPES.machine.has(apiType) && (
|
{!API_SPE_TYPES.machine.has(apiType) &&
|
||||||
|
apiType !== OPT_TRANS_BUILTINAI && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -606,6 +608,9 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{apiType !== OPT_TRANS_BUILTINAI && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("custom_header")}
|
label={i18n("custom_header")}
|
||||||
@@ -626,8 +631,11 @@ function ApiFields({ apiSlug, isUserApi, deleteApi }) {
|
|||||||
maxRows={10}
|
maxRows={10}
|
||||||
helperText={i18n("custom_body_help")}
|
helperText={i18n("custom_body_help")}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{apiType !== OPT_TRANS_CUSTOMIZE && (
|
{apiType !== OPT_TRANS_CUSTOMIZE &&
|
||||||
|
apiType !== OPT_TRANS_BUILTINAI && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -782,7 +790,11 @@ export default function Apis() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Alert severity="info">{i18n("about_api")}</Alert>
|
<Alert severity="info">
|
||||||
|
{i18n("about_api")}
|
||||||
|
<br />
|
||||||
|
{i18n("about_api_2")}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import Accordion from "@mui/material/Accordion";
|
|||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
// import CircularProgress from "@mui/material/CircularProgress";
|
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useFavWords } from "../../hooks/FavWords";
|
import { useFavWords } from "../../hooks/FavWords";
|
||||||
@@ -18,11 +17,9 @@ import ClearAllIcon from "@mui/icons-material/ClearAll";
|
|||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import { isValidWord } from "../../libs/utils";
|
import { isValidWord } from "../../libs/utils";
|
||||||
import { kissLog } from "../../libs/log";
|
import { kissLog } from "../../libs/log";
|
||||||
import { apiTranslate } from "../../apis";
|
|
||||||
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
|
|
||||||
import { useConfirm } from "../../hooks/Confirm";
|
import { useConfirm } from "../../hooks/Confirm";
|
||||||
import { useSetting } from "../../hooks/Setting";
|
import { useSetting } from "../../hooks/Setting";
|
||||||
import { DICT_MAP } from "../Selection/DictMap";
|
import { dictHandlers } from "../Selection/DictHandler";
|
||||||
|
|
||||||
function FavAccordion({ word, index }) {
|
function FavAccordion({ word, index }) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
@@ -83,17 +80,20 @@ export default function FavWords() {
|
|||||||
|
|
||||||
const handleTranslation = async () => {
|
const handleTranslation = async () => {
|
||||||
const { enDict } = setting?.tranboxSetting;
|
const { enDict } = setting?.tranboxSetting;
|
||||||
const dict = DICT_MAP[enDict];
|
const dict = dictHandlers[enDict];
|
||||||
if (!dict) return "";
|
if (!dict) return "";
|
||||||
|
|
||||||
const tranList = [];
|
const tranList = [];
|
||||||
for (const word of wordList) {
|
for (const word of wordList) {
|
||||||
try {
|
try {
|
||||||
const data = await dict.apiFn(word);
|
const data = await dict.apiFn(word);
|
||||||
const tran = dict.toText(data);
|
const tran = dict
|
||||||
tranList.push([word, tran].join("\n"));
|
.toText(data)
|
||||||
|
.map((line) => `- ${line}`)
|
||||||
|
.join("\n");
|
||||||
|
tranList.push([`## ${word}`, tran].join("\n"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// skip
|
kissLog("export translation", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,64 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</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}>
|
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
@@ -354,22 +412,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</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}>
|
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
@@ -386,64 +429,23 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</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}>
|
<Grid item xs={12} sm={12} md={6} lg={3}>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ export default function Settings() {
|
|||||||
size="small"
|
size="small"
|
||||||
name="langDetector"
|
name="langDetector"
|
||||||
value={langDetector}
|
value={langDetector}
|
||||||
label={i18n("detect_lang_remote")}
|
label={i18n("detected_lang")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
|
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
OPT_LANGS_TO,
|
OPT_LANGS_TO,
|
||||||
OPT_TRANBOX_TRIGGER_CLICK,
|
OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
OPT_TRANBOX_TRIGGER_ALL,
|
OPT_TRANBOX_TRIGGER_ALL,
|
||||||
OPT_DICT_BAIDU,
|
OPT_DICT_BING,
|
||||||
OPT_DICT_ALL,
|
OPT_DICT_ALL,
|
||||||
OPT_SUG_ALL,
|
OPT_SUG_ALL,
|
||||||
OPT_SUG_BAIDU,
|
OPT_SUG_YOUDAO,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import ShortcutInput from "./ShortcutInput";
|
import ShortcutInput from "./ShortcutInput";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
@@ -69,8 +69,8 @@ export default function Tranbox() {
|
|||||||
followSelection = false,
|
followSelection = false,
|
||||||
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
|
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
// extStyles = "",
|
// extStyles = "",
|
||||||
enDict = OPT_DICT_BAIDU,
|
enDict = OPT_DICT_BING,
|
||||||
enSug = OPT_SUG_BAIDU,
|
enSug = OPT_SUG_YOUDAO,
|
||||||
} = tranboxSetting;
|
} = tranboxSetting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Divider from "@mui/material/Divider";
|
|||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import CopyBtn from "./CopyBtn";
|
import CopyBtn from "./CopyBtn";
|
||||||
import { useAsyncNow } from "../../hooks/Fetch";
|
import { useAsyncNow } from "../../hooks/Fetch";
|
||||||
import { DICT_MAP } from "./DictMap";
|
import { dictHandlers } from "./DictHandler";
|
||||||
|
|
||||||
function DictBody({ text, setCopyText, dict }) {
|
function DictBody({ text, setCopyText, dict }) {
|
||||||
const { loading, error, data } = useAsyncNow(dict.apiFn, text);
|
const { loading, error, data } = useAsyncNow(dict.apiFn, text);
|
||||||
@@ -17,7 +17,7 @@ function DictBody({ text, setCopyText, dict }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyText = [text, dict.toText(data)].join("\n");
|
const copyText = [text, dict.toText(data).join("\n")].join("\n");
|
||||||
setCopyText(copyText);
|
setCopyText(copyText);
|
||||||
}, [data, text, dict, setCopyText]);
|
}, [data, text, dict, setCopyText]);
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ function DictBody({ text, setCopyText, dict }) {
|
|||||||
|
|
||||||
export default function DictCont({ text, enDict }) {
|
export default function DictCont({ text, enDict }) {
|
||||||
const [copyText, setCopyText] = useState(text);
|
const [copyText, setCopyText] = useState(text);
|
||||||
const dict = DICT_MAP[enDict];
|
const dict = dictHandlers[enDict];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import AudioBtn from "./AudioBtn";
|
|||||||
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
|
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
|
||||||
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
|
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
|
||||||
|
|
||||||
export const DICT_MAP = {
|
export const dictHandlers = {
|
||||||
[OPT_DICT_BING]: {
|
[OPT_DICT_BING]: {
|
||||||
apiFn: apiMicrosoftDict,
|
apiFn: apiMicrosoftDict,
|
||||||
toText: (data) =>
|
toText: (data) =>
|
||||||
data.trs
|
data.trs?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`) || [],
|
||||||
?.map(({ pos, def }) => `${pos ? `[${pos}] ` : ""}${def}`)
|
|
||||||
.join("\n"),
|
|
||||||
uiAudio: (data) => (
|
uiAudio: (data) => (
|
||||||
<Typography component="div">
|
<Typography component="div">
|
||||||
{data?.aus.map(({ key, audio, phonetic }) => (
|
{data?.aus.map(({ key, audio, phonetic }) => (
|
||||||
@@ -38,9 +36,9 @@ export const DICT_MAP = {
|
|||||||
[OPT_DICT_YOUDAO]: {
|
[OPT_DICT_YOUDAO]: {
|
||||||
apiFn: apiYoudaoDict,
|
apiFn: apiYoudaoDict,
|
||||||
toText: (data) =>
|
toText: (data) =>
|
||||||
data?.ec?.word?.trs
|
data?.ec?.word?.trs?.map(
|
||||||
?.map(({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`)
|
({ pos, tran }) => `${pos ? `[${pos}] ` : ""}${tran}`
|
||||||
.join("\n"),
|
) || [],
|
||||||
uiAudio: () => null,
|
uiAudio: () => null,
|
||||||
uiTrans: (data) => (
|
uiTrans: (data) => (
|
||||||
<Typography component="ul">
|
<Typography component="ul">
|
||||||
Reference in New Issue
Block a user