Compare commits

...

7 Commits

Author SHA1 Message Date
Gabe
cc31a8004a fix: try fix workflows error 2025-11-12 23:14:31 +08:00
Gabe
fa14851596 fix: try fix workflows error 2025-11-12 23:08:08 +08:00
Gabe
d56c46e944 Update version number: 2.0.9 2025-11-12 22:25:59 +08:00
Gabe
9f8bcf1fe1 feat: Added Japanese and Korean language support 2025-11-12 21:41:29 +08:00
Gabe
e50387a796 fix: custom apis 2025-11-12 00:56:27 +08:00
Gabe
3d2eac8772 fix: save rule bug 2025-11-12 00:13:41 +08:00
Gabe
343f529cac fix: Optimize subtitle translation 2025-11-11 23:36:06 +08:00
22 changed files with 946 additions and 40 deletions

2
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=2.0.8
REACT_APP_VERSION=2.0.9
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -15,7 +15,7 @@ jobs:
version: latest
- uses: actions/setup-node@v4
with:
node-version: latest
node-version: "25.1.0"
cache: "pnpm"
- run: pnpm install
- run: pnpm build+zip

View File

@@ -5,6 +5,37 @@
如果接口的请求数据和返回数据符合以下规范,
则无需填写 `Request Hook``Response Hook`
### 非聚合翻译 (v2.0.9)
Request body
```json
{
"text": "hello", // 需要翻译的文本列表
"from":"auto", // 原文语言
"to": "zh-CN" // 目标语言
}
```
Response
```json
{
"text": "你好", // 译文
"src": "en" // 原文语言
}
// 或者
{
"text": "你好", // 译文
"from": "en" // 原文语言
}
```
### 聚合翻译
Request body
```json

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "2.0.8",
"version": "2.0.9",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "KISS Übersetzer"
},
"app_description": {
"message": "Eine einfache zweisprachige Übersetzungs-Erweiterung und Greasemonkey-Skript"
},
"toggle_translate": {
"message": "Übersetzung umschalten"
},
"toggle_style": {
"message": "Stile umschalten"
},
"open_options": {
"message": "Einstellungen öffnen"
},
"open_tranbox": {
"message": "Popup-Fenster öffnen"
}
}

View File

@@ -6,15 +6,15 @@
"message": "A simple bilingual translation extension & Greasemonkey script"
},
"toggle_translate": {
"message": "Toggle Translate"
"message": "Toggle Translation"
},
"toggle_style": {
"message": "Toggle Style"
"message": "Toggle Styles"
},
"open_options": {
"message": "Open Options"
"message": "Open Setting"
},
"open_tranbox": {
"message": "Translate Popup/Selected"
"message": "Open Popup Box"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "KISS Traductor"
},
"app_description": {
"message": "Una sencilla extensión y script de Greasemonkey para traducción bilingüe"
},
"toggle_translate": {
"message": "Alternar traducción"
},
"toggle_style": {
"message": "Cambiar estilo"
},
"open_options": {
"message": "Abrir configuración"
},
"open_tranbox": {
"message": "Abrir ventana emergente"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "KISS Traducteur"
},
"app_description": {
"message": "Une extension et un script Greasemonkey de traduction bilingue simple"
},
"toggle_translate": {
"message": "Activer/désactiver la traduction"
},
"toggle_style": {
"message": "Changer de style"
},
"open_options": {
"message": "Ouvrir les paramètres"
},
"open_tranbox": {
"message": "Ouvrir la fenêtre contextuelle"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "シンプル翻訳"
},
"app_description": {
"message": "シンプルなバイリンガル対訳翻訳拡張機能Tampermonkeyスクリプト"
},
"toggle_translate": {
"message": "翻訳の切り替え"
},
"toggle_style": {
"message": "スタイル切り替え"
},
"open_options": {
"message": "設定を開く"
},
"open_tranbox": {
"message": "ポップアップを開く"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "심플 번역"
},
"app_description": {
"message": "심플한 이중 언어 대조 번역 확장 프로그램 & Tampermonkey 스크립트"
},
"toggle_translate": {
"message": "번역 켜기"
},
"toggle_style": {
"message": "스타일 전환"
},
"open_options": {
"message": "설정 열기"
},
"open_tranbox": {
"message": "팝업 열기"
}
}

View File

@@ -15,6 +15,6 @@
"message": "打开设置"
},
"open_tranbox": {
"message": "翻译弹窗/选中文字"
"message": "打开弹窗"
}
}

View File

@@ -0,0 +1,20 @@
{
"app_name": {
"message": "簡約翻譯"
},
"app_description": {
"message": "一個簡約的雙語對照翻譯擴充功能與 Tampermonkey 腳本"
},
"toggle_translate": {
"message": "開啟翻譯"
},
"toggle_style": {
"message": "切換樣式"
},
"open_options": {
"message": "開啟設定"
},
"open_tranbox": {
"message": "開啟彈出視窗"
}
}

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.8",
"version": "2.0.9",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.8",
"version": "2.0.9",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.8",
"version": "2.0.9",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -589,8 +589,10 @@ const genCloudflareAI = ({ texts, from, to, url, key }) => {
return { url, body, headers };
};
const genCustom = ({ texts, fromLang, toLang, url, key }) => {
const body = { texts, from: fromLang, to: toLang };
const genCustom = ({ texts, fromLang, toLang, url, key, useBatchFetch }) => {
const body = useBatchFetch
? { texts, from: fromLang, to: toLang }
: { text: texts[0], from: fromLang, to: toLang };
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
@@ -810,6 +812,8 @@ export const parseTransRes = async (
history.add(userMsg, hookResult.modelMsg);
}
return hookResult.translations;
} else if (Array.isArray(hookResult)) {
return hookResult;
}
} catch (err) {
kissLog("run res hook", err);
@@ -912,7 +916,10 @@ export const parseTransRes = async (
}
return parseAIRes(modelMsg?.content, useBatchFetch);
case OPT_TRANS_CUSTOMIZE:
return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
if (useBatchFetch) {
return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
}
return [[res.text, res.src || res.from]];
default:
}

View File

@@ -559,7 +559,6 @@ const defaultApiOpts = {
},
[OPT_TRANS_CUSTOMIZE]: {
...defaultApi,
url: "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN",
reqHook: defaultRequestHook,
resHook: defaultResponseHook,
},

File diff suppressed because it is too large Load Diff

View File

@@ -226,9 +226,15 @@ export const saveRule = async (curRule) => {
}
const newRule = {};
Object.entries(GLOBLA_RULE).forEach(([key, val]) => {
const globalRule = {
...GLOBLA_RULE,
...(rules.find((r) => r.pattern === GLOBAL_KEY) || {}),
};
Object.keys(GLOBLA_RULE).forEach((key) => {
newRule[key] =
!curRule[key] || curRule[key] === val ? DEFAULT_RULE[key] : curRule[key];
!curRule[key] || curRule[key] === globalRule[key]
? DEFAULT_RULE[key]
: curRule[key];
});
rules.unshift(newRule);

View File

@@ -26,7 +26,7 @@ async function set(key, val) {
} else if (isGm) {
await (window.KISS_GM || GM).setValue(key, val);
} else {
window.localStorage.setItem(key, val);
window?.localStorage.setItem(key, val);
}
}
@@ -38,7 +38,7 @@ async function get(key) {
const val = await (window.KISS_GM || GM).getValue(key);
return val;
}
return window.localStorage.getItem(key);
return window?.localStorage.getItem(key);
}
async function del(key) {
@@ -47,7 +47,7 @@ async function del(key) {
} else if (isGm) {
await (window.KISS_GM || GM).deleteValue(key);
} else {
window.localStorage.removeItem(key);
window?.localStorage.removeItem(key);
}
}

View File

@@ -128,7 +128,7 @@ export function Menus({
return i18n("processing_subtitles");
}, [progressed, i18n]);
const { isAISegment, skipAd, isBilingual } = formData;
const { isAISegment, skipAd, isBilingual, showOrigin } = formData;
return (
<div
@@ -157,6 +157,12 @@ export function Menus({
value={isBilingual}
label={i18n("is_bilingual_view")}
/>
<Switch
onChange={handleChange}
name="showOrigin"
value={showOrigin}
label={i18n("show_origin_subtitle")}
/>
<Switch
onChange={handleChange}
name="skipAd"

View File

@@ -42,7 +42,7 @@ class YouTubeCaptionProvider {
#menuEventName = "kiss-event";
constructor(setting = {}) {
this.#setting = { ...setting, isAISegment: false };
this.#setting = { ...setting, isAISegment: false, showOrigin: false };
this.#i18n = newI18n(setting.uiLang || "zh");
this.#menuEventName = genEventName();
}
@@ -151,6 +151,10 @@ class YouTubeCaptionProvider {
if (node.matches(adLayoutSelector)) {
logger.debug("Youtube Provider: Ad ends!");
if (!this.#setting.showOrigin) {
this.#hideYtCaption();
}
if (videoEl && skipAd) {
videoEl.playbackRate = 1;
}
@@ -200,6 +204,16 @@ class YouTubeCaptionProvider {
this.#managerInstance?.updateSetting({ [name]: value });
} else if (name === "isAISegment") {
this.#reProcessEvents();
} else if (name === "showOrigin") {
this.#toggleShowOrigin();
}
}
#toggleShowOrigin() {
if (this.#setting.showOrigin) {
this.#destroyManager();
} else {
this.#startManager();
}
}
@@ -241,7 +255,8 @@ class YouTubeCaptionProvider {
toggleButton.appendChild(createLogoSVG());
kissControls.appendChild(toggleButton);
const { segApiSetting, isAISegment, skipAd, isBilingual } = this.#setting;
const { segApiSetting, isAISegment, skipAd, isBilingual, showOrigin } =
this.#setting;
const menu = new ShadowDomManager({
id: "kiss-subtitle-menus",
className: "notranslate",
@@ -254,9 +269,10 @@ class YouTubeCaptionProvider {
hasSegApi: !!segApiSetting,
eventName: this.#menuEventName,
initData: {
isAISegment,
skipAd,
isBilingual,
isAISegment, // AI智能断句
skipAd, // 快进广告
isBilingual, // 双语显示
showOrigin, // 显示原字幕
},
},
});
@@ -268,6 +284,10 @@ class YouTubeCaptionProvider {
createLogoSVG({ isSelected: true })
);
menu.show();
this.#sendMenusMsg({
action: MSG_MENUS_PROGRESSED,
data: this.#progressed,
});
} else {
this.#isMenuShow = false;
this.#toggleButton?.replaceChildren(createLogoSVG());
@@ -512,6 +532,9 @@ class YouTubeCaptionProvider {
}
#reProcessEvents() {
this.#progressed = 0;
this.#subtitles = [];
const videoId = this.#videoId;
const flatEvents = this.#flatEvents;
const fromLang = this.#fromLang;
@@ -557,19 +580,19 @@ class YouTubeCaptionProvider {
return subtitlesFallback();
}
const chunkCount = eventChunks.length;
if (chunkCount > 1) {
if (eventChunks.length > 1) {
const remainingChunks = eventChunks.slice(1);
this.#processRemainingChunksAsync({
chunks: remainingChunks,
chunkCount,
videoId,
fromLang,
toLang,
segApiSetting,
});
return [firstBatchSubtitles, 100 / eventChunks.length];
const processed = Math.floor(100 / eventChunks.length);
return [firstBatchSubtitles, processed];
} else {
return [firstBatchSubtitles, 100];
}
@@ -583,6 +606,10 @@ class YouTubeCaptionProvider {
return;
}
if (this.#setting.showOrigin) {
return;
}
if (!this.#subtitles.length) {
this.#showNotification(this.#i18n("waitting_for_subtitle"));
return;
@@ -605,8 +632,7 @@ class YouTubeCaptionProvider {
this.#showNotification(this.#i18n("subtitle_load_succeed"));
const ytCaption = document.querySelector(YT_CAPTION_SELECT);
ytCaption && (ytCaption.style.display = "none");
this.#hideYtCaption();
}
#destroyManager() {
@@ -619,6 +645,15 @@ class YouTubeCaptionProvider {
this.#managerInstance.destroy();
this.#managerInstance = null;
this.#showYtCaption();
}
#hideYtCaption() {
const ytCaption = document.querySelector(YT_CAPTION_SELECT);
ytCaption && (ytCaption.style.display = "none");
}
#showYtCaption() {
const ytCaption = document.querySelector(YT_CAPTION_SELECT);
ytCaption && (ytCaption.style.display = "block");
}
@@ -920,7 +955,6 @@ class YouTubeCaptionProvider {
async #processRemainingChunksAsync({
chunks,
chunkCount,
videoId,
fromLang,
toLang,
@@ -968,7 +1002,7 @@ class YouTubeCaptionProvider {
}
if (subtitlesForThisChunk.length > 0) {
const progressed = (chunkNum * 100) / chunkCount;
const progressed = Math.floor((chunkNum * 100) / (chunks.length + 1));
this.#subtitles.push(...subtitlesForThisChunk);
this.#progressed = progressed;