Compare commits

..

24 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
Gabe
3bfa12b61c Update version number: 2.0.8 2025-11-10 01:17:20 +08:00
Gabe
79bd776ef9 fix: format 2025-11-10 01:15:54 +08:00
Gabe
222428ad47 feat: pre translate time 2025-11-10 01:08:09 +08:00
Gabe
4b3853dd22 fix: download subtitle 2025-11-10 00:35:30 +08:00
Gabe
9dd191902c fix: update dependencies 2025-11-10 00:30:37 +08:00
Gabe
3f524ad674 feat: supports download subtitle 2025-11-10 00:21:07 +08:00
Gabe
7e6376fcb7 fix: rules 2025-11-07 23:40:20 +08:00
Gabe
6f35013faf fix: split pattern (#384) 2025-11-07 21:31:11 +08:00
Gabe
e71acdaaa9 fix: truncate doc title 2025-11-07 00:25:25 +08:00
Gabe
fd7c663282 feat: Restore CSS injection functionality 2025-11-06 23:33:49 +08:00
Gabe
89b2bbe9ac fix: tone error (#382) 2025-11-06 20:15:15 +08:00
Gabe
7eb64a463b Update version number: 2.0.7 2025-11-05 23:26:12 +08:00
Gabe
8971a28abc fix: html font size (#378) 2025-11-05 23:15:40 +08:00
Gabe
2ff989429f doc: i18n 2025-11-05 22:08:44 +08:00
Gabe
24369e2581 fix: Some element tagnames are lowercase. (#377) 2025-11-05 21:41:18 +08:00
Gabe
2bb8a5182c fix: Some element tagnames are lowercase. (#377) 2025-11-05 20:48:12 +08:00
Gabe
629bf9461a fix: AI language code 2025-11-05 01:03:44 +08:00
40 changed files with 2034 additions and 348 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.6
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

@@ -1,10 +1,41 @@
# 自定义接口示例
# 自定义接口说明及示例
## 默认接口规范
如果接口的请求数据和返回数据符合以下规范,
则无需填写 `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
@@ -21,7 +52,7 @@ Response
[
{
"text": "你好", // 译文
"src": "en" // 原文语言
"src": "en" // 原文语言
}
]
```
@@ -33,12 +64,36 @@ v2.0.4版后亦支持以下 Response 格式
"translations": [ // 译文列表
{
"text": "你好", // 译文
"src": "en" // 原文语言
"src": "en" // 原文语言
}
]
}
```
## Prompt 相关
`Prompt` 可替换占位符:
```js
`{{from}}` // 原文语言名称
`{{to}}` // 目标语言名称
`{{fromLang}}` // 原文语言代码
`{{toLang}}` // 目标语言代码
`{{text}}` // 原文
`{{tone}}` // 风格
`{{title}}` // 页面标题
`{{description}}` // 页面描述
```
Hook 中 `Prompt` 类型说明:
```js
`systemPrompt` // 聚合翻译 System Prompt
`nobatchPrompt` // 非聚合翻译 System Prompt
`nobatchUserPrompt` // 非聚合翻译 User Prompt
`subtitlePrompt` // 字幕翻译 System Prompt
```
## 谷歌翻译接口
> 此接口不支持聚合
@@ -99,9 +154,12 @@ async (args) => {
{
role: "user",
content: JSON.stringify({
targetLanguage: args.to,
targetLanguage: args.toLang,
segments: args.texts.map((text, id) => ({ id, text })),
glossary: {},
title: "", // 可省略
description: "", // 可省略
glossary: {}, // 可省略
tone: "", // 可省略
}),
},
],
@@ -132,9 +190,12 @@ async (args) => {
{
role: "user",
content: JSON.stringify({
targetLanguage: args.to,
targetLanguage: args.toLang,
segments: args.texts.map((text, id) => ({ id, text })),
glossary: {},
title: "", // 可省略
description: "", // 可省略
glossary: {}, // 可省略
tone: "", // 可省略
}),
},
],
@@ -295,6 +356,7 @@ Hook参数里面的语言含义说明
["cs", "Czech - Čeština"],
["da", "Danish - Dansk"],
["nl", "Dutch - Nederlands"],
["fa", "Persian - فارسی"],
["fi", "Finnish - Suomi"],
["fr", "French - Français"],
["de", "German - Deutsch"],

View File

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

310
pnpm-lock.yaml generated
View File

@@ -87,10 +87,6 @@ importers:
packages:
'@aashutoshrathi/word-wrap@1.2.6':
resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
engines: {node: '>=0.10.0'}
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@@ -109,6 +105,10 @@ packages:
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
engines: {node: '>=6.9.0'}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.22.20':
resolution: {integrity: sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==}
engines: {node: '>=6.9.0'}
@@ -128,10 +128,18 @@ packages:
resolution: {integrity: sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.28.5':
resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-annotate-as-pure@7.22.5':
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
engines: {node: '>=6.9.0'}
'@babel/helper-annotate-as-pure@7.27.3':
resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
engines: {node: '>=6.9.0'}
'@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==}
engines: {node: '>=6.9.0'}
@@ -165,6 +173,10 @@ packages:
resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
'@babel/helper-hoist-variables@7.22.5':
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'}
@@ -181,6 +193,10 @@ packages:
resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.27.1':
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.22.20':
resolution: {integrity: sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==}
engines: {node: '>=6.9.0'}
@@ -195,8 +211,8 @@ packages:
resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
engines: {node: '>=6.9.0'}
'@babel/helper-plugin-utils@7.24.0':
resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==}
'@babel/helper-plugin-utils@7.27.1':
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
engines: {node: '>=6.9.0'}
'@babel/helper-remap-async-to-generator@7.22.20':
@@ -231,10 +247,18 @@ packages:
resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.22.20':
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.22.15':
resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==}
engines: {node: '>=6.9.0'}
@@ -263,6 +287,11 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.28.5':
resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15':
resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==}
engines: {node: '>=6.9.0'}
@@ -406,8 +435,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-syntax-jsx@7.24.1':
resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==}
'@babel/plugin-syntax-jsx@7.27.1':
resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -852,10 +881,18 @@ packages:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.22.20':
resolution: {integrity: sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.28.5':
resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
engines: {node: '>=6.9.0'}
'@babel/types@7.22.19':
resolution: {integrity: sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==}
engines: {node: '>=6.9.0'}
@@ -864,6 +901,10 @@ packages:
resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.5':
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
@@ -1053,8 +1094,14 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.10.0':
resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint-community/regexpp@4.8.1':
@@ -1175,6 +1222,9 @@ packages:
resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/gen-mapping@0.3.3':
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'}
@@ -1183,6 +1233,10 @@ packages:
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.1.2':
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
@@ -1193,9 +1247,15 @@ packages:
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.19':
resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@leichtgewicht/ip-codec@2.0.4':
resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==}
@@ -1697,8 +1757,8 @@ packages:
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@webassemblyjs/ast@1.11.6':
resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==}
@@ -1790,6 +1850,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
address@1.2.2:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'}
@@ -2113,8 +2178,8 @@ packages:
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
caniuse-lite@1.0.30001599:
resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==}
caniuse-lite@1.0.30001754:
resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
case-sensitive-paths-webpack-plugin@2.4.0:
resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==}
@@ -2297,6 +2362,10 @@ packages:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
@@ -2463,6 +2532,15 @@ packages:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
@@ -2848,8 +2926,8 @@ packages:
engines: {node: '>=4'}
hasBin: true
esquery@1.5.0:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
esquery@1.6.0:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
engines: {node: '>=0.10'}
esrecurse@4.3.0:
@@ -2992,8 +3070,8 @@ packages:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
flatted@3.3.1:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.3:
resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
@@ -3297,8 +3375,8 @@ packages:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
ignore@5.3.1:
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
immer@9.0.21:
@@ -3308,6 +3386,10 @@ packages:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
import-local@3.1.0:
resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==}
engines: {node: '>=8'}
@@ -3744,6 +3826,11 @@ packages:
engines: {node: '>=4'}
hasBin: true
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
hasBin: true
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -4224,8 +4311,8 @@ packages:
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
engines: {node: '>= 0.8.0'}
optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
p-limit@2.3.0:
@@ -4331,6 +4418,9 @@ packages:
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
@@ -6021,8 +6111,6 @@ packages:
snapshots:
'@aashutoshrathi/word-wrap@1.2.6': {}
'@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.2.1':
@@ -6042,6 +6130,12 @@ snapshots:
'@babel/highlight': 7.22.20
chalk: 2.4.2
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.22.20': {}
'@babel/core@7.22.20':
@@ -6079,10 +6173,22 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.19
jsesc: 2.5.2
'@babel/generator@7.28.5':
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.5
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.22.5':
dependencies:
'@babel/types': 7.22.19
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
'@babel/types': 7.28.5
'@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
dependencies:
'@babel/types': 7.22.19
@@ -6133,6 +6239,8 @@ snapshots:
'@babel/template': 7.22.15
'@babel/types': 7.22.19
'@babel/helper-globals@7.28.0': {}
'@babel/helper-hoist-variables@7.22.5':
dependencies:
'@babel/types': 7.22.19
@@ -6149,6 +6257,13 @@ snapshots:
dependencies:
'@babel/types': 7.24.0
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.28.5
'@babel/types': 7.28.5
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.22.20(@babel/core@7.22.20)':
dependencies:
'@babel/core': 7.22.20
@@ -6164,7 +6279,7 @@ snapshots:
'@babel/helper-plugin-utils@7.22.5': {}
'@babel/helper-plugin-utils@7.24.0': {}
'@babel/helper-plugin-utils@7.27.1': {}
'@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.22.20)':
dependencies:
@@ -6196,8 +6311,12 @@ snapshots:
'@babel/helper-string-parser@7.24.1': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.22.20': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-option@7.22.15': {}
'@babel/helper-wrap-function@7.22.20':
@@ -6234,6 +6353,10 @@ snapshots:
dependencies:
'@babel/types': 7.22.19
'@babel/parser@7.28.5':
dependencies:
'@babel/types': 7.28.5
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.22.20)':
dependencies:
'@babel/core': 7.22.20
@@ -6341,7 +6464,7 @@ snapshots:
'@babel/plugin-syntax-flow@7.24.1(@babel/core@7.22.20)':
dependencies:
'@babel/core': 7.22.20
'@babel/helper-plugin-utils': 7.24.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.22.20)':
dependencies:
@@ -6368,10 +6491,10 @@ snapshots:
'@babel/core': 7.22.20
'@babel/helper-plugin-utils': 7.22.5
'@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.22.20)':
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.22.20)':
dependencies:
'@babel/core': 7.22.20
'@babel/helper-plugin-utils': 7.24.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.20)':
dependencies:
@@ -6689,11 +6812,13 @@ snapshots:
'@babel/plugin-transform-react-jsx@7.23.4(@babel/core@7.22.20)':
dependencies:
'@babel/core': 7.22.20
'@babel/helper-annotate-as-pure': 7.22.5
'@babel/helper-module-imports': 7.24.3
'@babel/helper-plugin-utils': 7.24.0
'@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.22.20)
'@babel/types': 7.24.0
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-module-imports': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.22.20)
'@babel/types': 7.28.5
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-react-pure-annotations@7.22.5(@babel/core@7.22.20)':
dependencies:
@@ -6918,6 +7043,12 @@ snapshots:
'@babel/parser': 7.22.16
'@babel/types': 7.22.19
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.28.5
'@babel/types': 7.28.5
'@babel/traverse@7.22.20':
dependencies:
'@babel/code-frame': 7.22.13
@@ -6933,6 +7064,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/traverse@7.28.5':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.28.5
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.28.5
'@babel/template': 7.27.2
'@babel/types': 7.28.5
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@babel/types@7.22.19':
dependencies:
'@babel/helper-string-parser': 7.22.5
@@ -6945,6 +7088,11 @@ snapshots:
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
'@babel/types@7.28.5':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@0.2.3': {}
'@buttercup/fetch@0.1.2':
@@ -7163,18 +7311,23 @@ snapshots:
eslint: 8.57.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.10.0': {}
'@eslint-community/eslint-utils@4.9.0(eslint@8.57.0)':
dependencies:
eslint: 8.57.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint-community/regexpp@4.8.1': {}
'@eslint/eslintrc@2.1.4':
dependencies:
ajv: 6.12.6
debug: 4.3.4
debug: 4.4.3
espree: 9.6.1
globals: 13.24.0
ignore: 5.3.1
import-fresh: 3.3.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.0
minimatch: 3.1.2
strip-json-comments: 3.1.1
@@ -7203,7 +7356,7 @@ snapshots:
'@humanwhocodes/config-array@0.11.14':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
debug: 4.3.4
debug: 4.4.3
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -7399,6 +7552,11 @@ snapshots:
'@types/yargs': 17.0.24
chalk: 4.1.2
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/gen-mapping@0.3.3':
dependencies:
'@jridgewell/set-array': 1.1.2
@@ -7407,6 +7565,8 @@ snapshots:
'@jridgewell/resolve-uri@3.1.1': {}
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.1.2': {}
'@jridgewell/source-map@0.3.5':
@@ -7416,11 +7576,18 @@ snapshots:
'@jridgewell/sourcemap-codec@1.4.15': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.19':
dependencies:
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@leichtgewicht/ip-codec@2.0.4': {}
'@mui/base@5.0.0-beta.40(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
@@ -7971,7 +8138,7 @@ snapshots:
'@typescript-eslint/types': 5.62.0
eslint-visitor-keys: 3.4.3
'@ungap/structured-clone@1.2.0': {}
'@ungap/structured-clone@1.3.0': {}
'@webassemblyjs/ast@1.11.6':
dependencies:
@@ -8069,9 +8236,9 @@ snapshots:
dependencies:
acorn: 8.10.0
acorn-jsx@5.3.2(acorn@8.11.3):
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.11.3
acorn: 8.15.0
acorn-walk@7.2.0: {}
@@ -8081,6 +8248,8 @@ snapshots:
acorn@8.11.3: {}
acorn@8.15.0: {}
address@1.2.2: {}
adjust-sourcemap-loader@4.0.0:
@@ -8244,7 +8413,7 @@ snapshots:
autoprefixer@10.4.16(postcss@8.4.30):
dependencies:
browserslist: 4.23.0
caniuse-lite: 1.0.30001599
caniuse-lite: 1.0.30001754
fraction.js: 4.3.6
normalize-range: 0.1.2
picocolors: 1.0.0
@@ -8444,7 +8613,7 @@ snapshots:
browserslist@4.23.0:
dependencies:
caniuse-lite: 1.0.30001599
caniuse-lite: 1.0.30001754
electron-to-chromium: 1.4.713
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0)
@@ -8484,11 +8653,11 @@ snapshots:
caniuse-api@3.0.0:
dependencies:
browserslist: 4.23.0
caniuse-lite: 1.0.30001599
caniuse-lite: 1.0.30001754
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
caniuse-lite@1.0.30001599: {}
caniuse-lite@1.0.30001754: {}
case-sensitive-paths-webpack-plugin@2.4.0: {}
@@ -8661,6 +8830,12 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
crypt@0.0.2: {}
crypto-random-string@2.0.0: {}
@@ -8823,6 +8998,10 @@ snapshots:
dependencies:
ms: 2.1.2
debug@4.4.3:
dependencies:
ms: 2.1.3
decimal.js@10.4.3: {}
decode-named-character-reference@1.0.2:
@@ -9270,24 +9449,24 @@ snapshots:
eslint@8.57.0:
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
'@eslint-community/regexpp': 4.10.0
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.0)
'@eslint-community/regexpp': 4.12.2
'@eslint/eslintrc': 2.1.4
'@eslint/js': 8.57.0
'@humanwhocodes/config-array': 0.11.14
'@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8
'@ungap/structured-clone': 1.2.0
'@ungap/structured-clone': 1.3.0
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.4
cross-spawn: 7.0.6
debug: 4.4.3
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1
esquery: 1.5.0
esquery: 1.6.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 6.0.1
@@ -9295,7 +9474,7 @@ snapshots:
glob-parent: 6.0.2
globals: 13.24.0
graphemer: 1.4.0
ignore: 5.3.1
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
@@ -9305,7 +9484,7 @@ snapshots:
lodash.merge: 4.6.2
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.3
optionator: 0.9.4
strip-ansi: 6.0.1
text-table: 0.2.0
transitivePeerDependencies:
@@ -9313,15 +9492,15 @@ snapshots:
espree@9.6.1:
dependencies:
acorn: 8.11.3
acorn-jsx: 5.3.2(acorn@8.11.3)
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 3.4.3
esprima@1.2.2: {}
esprima@4.0.1: {}
esquery@1.5.0:
esquery@1.6.0:
dependencies:
estraverse: 5.3.0
@@ -9508,11 +9687,11 @@ snapshots:
flat-cache@3.2.0:
dependencies:
flatted: 3.3.1
flatted: 3.3.3
keyv: 4.5.4
rimraf: 3.0.2
flatted@3.3.1: {}
flatted@3.3.3: {}
follow-redirects@1.15.3: {}
@@ -9838,7 +10017,7 @@ snapshots:
ignore@5.2.4: {}
ignore@5.3.1: {}
ignore@5.3.2: {}
immer@9.0.21: {}
@@ -9847,6 +10026,11 @@ snapshots:
parent-module: 1.0.1
resolve-from: 4.0.0
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
import-local@3.1.0:
dependencies:
pkg-dir: 4.2.0
@@ -10527,6 +10711,8 @@ snapshots:
jsesc@2.5.2: {}
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1: {}
@@ -11079,14 +11265,14 @@ snapshots:
type-check: 0.3.2
word-wrap: 1.2.5
optionator@0.9.3:
optionator@0.9.4:
dependencies:
'@aashutoshrathi/word-wrap': 1.2.6
deep-is: 0.1.4
fast-levenshtein: 2.0.6
levn: 0.4.1
prelude-ls: 1.2.1
type-check: 0.4.0
word-wrap: 1.2.5
p-limit@2.3.0:
dependencies:
@@ -11174,6 +11360,8 @@ snapshots:
picocolors@1.0.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
pify@2.3.0: {}

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.6",
"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.6",
"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.6",
"version": "2.0.9",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -419,7 +419,7 @@ export const apiTranslate = async ({
toLang,
apiSetting = DEFAULT_API_SETTING,
docInfo = {},
glossary = {},
glossary,
useCache = true,
usePool = true,
}) => {

View File

@@ -30,6 +30,11 @@ import {
defaultSubtitlePrompt,
defaultNobatchPrompt,
defaultNobatchUserPrompt,
INPUT_PLACE_TONE,
INPUT_PLACE_TITLE,
INPUT_PLACE_DESCRIPTION,
INPUT_PLACE_TO_LANG,
INPUT_PLACE_FROM_LANG,
} from "../config";
import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl";
@@ -62,35 +67,62 @@ const keyPick = (apiSlug, key = "", cacheMap) => {
return keys[curIndex];
};
const genSystemPrompt = ({ systemPrompt, from, to }) =>
const genSystemPrompt = ({
systemPrompt,
tone,
from,
to,
fromLang,
toLang,
texts,
docInfo: { title = "", description = "" } = {},
}) =>
systemPrompt
.replaceAll(INPUT_PLACE_TITLE, title)
.replaceAll(INPUT_PLACE_DESCRIPTION, description)
.replaceAll(INPUT_PLACE_TONE, tone)
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to);
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_FROM_LANG, fromLang)
.replaceAll(INPUT_PLACE_TO_LANG, toLang)
.replaceAll(INPUT_PLACE_TEXT, texts[0]);
const genUserPrompt = ({
nobatchUserPrompt,
useBatchFetch,
tone,
glossary = {},
glossary,
from,
to,
fromLang,
toLang,
texts,
docInfo,
docInfo: { title = "", description = "" } = {},
}) => {
if (useBatchFetch) {
return JSON.stringify({
targetLanguage: to,
title: docInfo.title,
description: docInfo.description,
const promptObj = {
targetLanguage: toLang,
segments: texts.map((text, i) => ({ id: i, text })),
glossary,
tone,
});
};
title && (promptObj.title = title);
description && (promptObj.description = description);
glossary &&
Object.keys(glossary).length !== 0 &&
(promptObj.glossary = glossary);
tone && (promptObj.tone = tone);
return JSON.stringify(promptObj);
}
return nobatchUserPrompt
.replaceAll(INPUT_PLACE_TITLE, title)
.replaceAll(INPUT_PLACE_DESCRIPTION, description)
.replaceAll(INPUT_PLACE_TONE, tone)
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_FROM_LANG, fromLang)
.replaceAll(INPUT_PLACE_TO_LANG, toLang)
.replaceAll(INPUT_PLACE_TEXT, texts[0]);
};
@@ -557,8 +589,10 @@ const genCloudflareAI = ({ texts, from, to, url, key }) => {
return { url, body, headers };
};
const genCustom = ({ texts, from, to, url, key }) => {
const body = { texts, from, to };
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}`,
@@ -638,12 +672,15 @@ export const genTransReq = async ({ reqHook, ...args }) => {
useBatchFetch,
from,
to,
fromLang,
toLang,
texts,
docInfo,
glossary,
customHeader,
customBody,
events,
tone,
} = args;
if (API_SPE_TYPES.mulkeys.has(apiType)) {
@@ -655,20 +692,30 @@ export const genTransReq = async ({ reqHook, ...args }) => {
}
if (API_SPE_TYPES.ai.has(apiType)) {
args.systemPrompt = genSystemPrompt({
systemPrompt: useBatchFetch ? systemPrompt : nobatchPrompt,
from,
to,
});
args.userPrompt = !!events
args.systemPrompt = events
? systemPrompt
: genSystemPrompt({
systemPrompt: useBatchFetch ? systemPrompt : nobatchPrompt,
from,
to,
fromLang,
toLang,
texts,
docInfo,
tone,
});
args.userPrompt = events
? JSON.stringify(events)
: genUserPrompt({
nobatchUserPrompt,
useBatchFetch,
from,
to,
fromLang,
toLang,
texts,
docInfo,
tone,
glossary,
});
}
@@ -765,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);
@@ -867,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

@@ -118,6 +118,13 @@ async function getFavWords(rule) {
*/
export async function run(isUserscript = false) {
try {
// if (document?.documentElement?.tagName?.toUpperCase() !== "HTML") {
// return;
// }
if (!document?.contentType?.includes("html")) {
return;
}
// 读取设置信息
const setting = await getSettingWithDefault();

View File

@@ -9,7 +9,12 @@ export const DEFAULT_CONTEXT_SIZE = 3; // 上下文会话数量
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
export const INPUT_PLACE_FROM_LANG = "{{fromLang}}"; // 占位符
export const INPUT_PLACE_TO_LANG = "{{toLang}}"; // 占位符
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_TONE = "{{tone}}"; // 占位符
export const INPUT_PLACE_TITLE = "{{title}}"; // 占位符
export const INPUT_PLACE_DESCRIPTION = "{{description}}"; // 占位符
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
@@ -312,14 +317,14 @@ export const OPT_LANGS_TO_SPEC = {
["id", "id"],
["vi", "vi"],
]),
[OPT_TRANS_OPENAI]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_GEMINI]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_GEMINI_2]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_CLAUDE]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_OLLAMA]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_OPENROUTER]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_CLOUDFLAREAI]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_CUSTOMIZE]: OPT_LANGS_SPEC_DEFAULT,
[OPT_TRANS_OPENAI]: OPT_LANGS_SPEC_NAME,
[OPT_TRANS_GEMINI]: OPT_LANGS_SPEC_NAME,
[OPT_TRANS_GEMINI_2]: OPT_LANGS_SPEC_NAME,
[OPT_TRANS_CLAUDE]: OPT_LANGS_SPEC_NAME,
[OPT_TRANS_OLLAMA]: OPT_LANGS_SPEC_NAME,
[OPT_TRANS_OPENROUTER]: OPT_LANGS_SPEC_NAME,
[OPT_TRANS_CLOUDFLAREAI]: OPT_LANGS_SPEC_NAME,
[OPT_TRANS_CUSTOMIZE]: OPT_LANGS_SPEC_NAME,
};
const specToCode = (m) =>
@@ -342,7 +347,7 @@ Object.entries(OPT_LANGS_TO_SPEC).forEach(([t, m]) => {
});
export const defaultNobatchPrompt = `You are a professional, authentic machine translation engine.`;
export const defaultNobatchUserPrompt = `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`;
export const defaultNobatchUserPrompt = `Translate the following source text to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`;
export const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
@@ -554,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

@@ -33,3 +33,6 @@ export const EVENT_KISS = "event_kiss_translate";
export const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
// export const MSG_GLOBAL_VAR_FETCH = "KISS_GLOBAL_VAR_FETCH";
// export const MSG_GLOBAL_VAR_BACK = "KISS_GLOBAL_VAR_BACK";
export const MSG_MENUS_PROGRESSED = "progressed";
export const MSG_MENUS_UPDATEFORM = "updateFormData";

View File

@@ -8,8 +8,8 @@ export const SHADOW_KEY = ">>>";
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const DEFAULT_TRANS_TAG = "font";
export const DEFAULT_SELECT_STYLE =
"-webkit-line-clamp: unset; max-height: none; height: auto;";
// export const DEFAULT_SELECT_STYLE =
// "-webkit-line-clamp: unset; max-height: none; height: auto;";
export const OPT_TIMING_PAGESCROLL = "mk_pagescroll"; // 滚动加载翻译
export const OPT_TIMING_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
@@ -108,11 +108,11 @@ export const GLOBLA_RULE = {
textExtStyle: "", // 译文附加样式
termsStyle: "font-weight: bold;", // 专业术语样式
highlightStyle: "color: red;", // 高亮词汇样式
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
selectStyle: "", // 选择器节点样式
parentStyle: "", // 选择器父节点样式
grandStyle: "", // 选择器祖节点样式
injectJs: "", // 注入JS
// injectCss: "", // 注入CSS(作废)
injectCss: "", // 注入CSS
transOnly: "false", // 是否仅显示译文
// transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废)
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
@@ -165,6 +165,9 @@ const RULES_MAP = {
"www.youtube.com": {
rootsSelector: `ytd-page-manager`,
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
grandStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
},
"web.telegram.org": {
autoScan: `false`,

View File

@@ -112,6 +112,8 @@ export const DEFAULT_SUBTITLE_SETTING = {
apiSlug: OPT_TRANS_MICROSOFT,
segSlug: "-", // AI智能断句
chunkLength: 1000, // AI处理切割长度
preTrans: 90, // 提前翻译时长
throttleTrans: 30, // 节流翻译间隔
// fromLang: "en",
toLang: "zh-CN",
isBilingual: true, // 是否双语显示

View File

@@ -1,5 +1,3 @@
import { run } from "./common";
if (document.documentElement && document.documentElement.tagName === "HTML") {
run();
}
run();

View File

@@ -27,6 +27,14 @@ export default function Theme({ children, options = {}, styles = {} }) {
}, []);
const theme = useMemo(() => {
let htmlFontSize = 16;
try {
const s = window.getComputedStyle(document.documentElement).fontSize;
htmlFontSize = parseInt(s.replace("px", ""));
} catch (err) {
//
}
const isDarkMode =
darkMode === "dark" || (darkMode === "auto" && systemMode === THEME_DARK);
@@ -35,7 +43,7 @@ export default function Theme({ children, options = {}, styles = {} }) {
mode: isDarkMode ? THEME_DARK : THEME_LIGHT,
},
typography: {
htmlFontSize: document.documentElement.style.fontSize ? "16px" : 16,
htmlFontSize,
},
...options,
});

View File

@@ -34,7 +34,7 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
}
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
r.pattern.split(/\n|,/).some((p) => isMatch(href, p.trim()))
);
const globalRule = {
...GLOBLA_RULE,
@@ -58,7 +58,7 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
"parentStyle",
"grandStyle",
"injectJs",
// "injectCss",
"injectCss",
"transStartHook",
"transEndHook",
// "transRemoveHook",
@@ -138,7 +138,7 @@ export const checkRules = (rules) => {
parentStyle,
grandStyle,
injectJs,
// injectCss,
injectCss,
apiSlug,
fromLang,
toLang,
@@ -171,7 +171,7 @@ export const checkRules = (rules) => {
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
grandStyle: type(grandStyle) === "string" ? grandStyle : "",
injectJs: type(injectJs) === "string" ? injectJs : "",
// injectCss: type(injectCss) === "string" ? injectCss : "",
injectCss: type(injectCss) === "string" ? injectCss : "",
apiSlug:
type(apiSlug) === "string" && apiSlug.trim() !== ""
? apiSlug.trim()
@@ -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

@@ -15,7 +15,13 @@ export default class ShadowDomManager {
_ReactComponent;
_props;
constructor({ id, className = "", reactComponent, props = {} }) {
constructor({
id,
className = "",
reactComponent,
props = {},
rootElement = document.body,
}) {
if (!id || !reactComponent) {
throw new Error("ID and a React Component must be provided.");
}
@@ -23,6 +29,7 @@ export default class ShadowDomManager {
this._className = className;
this._ReactComponent = reactComponent;
this._props = props;
this._rootElement = rootElement;
}
get isVisible() {
@@ -93,7 +100,7 @@ export default class ShadowDomManager {
host.className = this._className;
}
document.body.appendChild(host);
this._rootElement.appendChild(host);
this.#hostElement = host;
const shadowContainer = host.attachShadow({ mode: "open" });
const appRoot = document.createElement("div");

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

@@ -83,8 +83,8 @@ export function createLogoSVG({
const primaryColor = "#209CEE";
const secondaryColor = "#E9F5FD";
const path1Fill = isSelected ? primaryColor : secondaryColor;
const path2Fill = isSelected ? secondaryColor : primaryColor;
const path1Fill = isSelected ? secondaryColor : primaryColor;
const path2Fill = isSelected ? primaryColor : secondaryColor;
const path1 = createSVGElement("path", {
d: "M0 0 C10.56 0 21.12 0 32 0 C32 10.56 32 21.12 32 32 C21.44 32 10.88 32 0 32 C0 21.44 0 10.88 0 0 Z ",

View File

@@ -13,6 +13,7 @@ import {
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
OPT_SPLIT_PARAGRAPH_DISABLE,
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
MSG_INJECT_CSS,
} from "../config";
import { interpreter } from "./interpreter";
import { clearFetchPool } from "./pool";
@@ -26,6 +27,9 @@ import { shortcutRegister } from "./shortcut";
import { tryDetectLang } from "./detect";
import { trustedTypesHelper } from "./trustedTypes";
import { injectJs, INJECTOR } from "../injectors";
import { injectInternalCss } from "./injector";
import { isExt } from "./client";
import { sendBgMsg } from "./msg";
/**
* @class Translator
@@ -357,7 +361,7 @@ export class Translator {
this.#eventName = genEventName();
this.#docInfo = {
title: document.title,
title: truncateWords(document.title),
description: this.#getDocDescription(),
};
this.#combinedSkipsRegex = new RegExp(
@@ -1322,7 +1326,10 @@ export class Translator {
// node.matches(this.#ignoreSelector) ||
!node.textContent.trim()
) {
if (node.tagName === "IMG" || node.tagName === "SVG") {
if (
node.tagName?.toUpperCase() === "IMG" ||
node.tagName?.toUpperCase() === "SVG"
) {
node.style.width = `${node.offsetWidth}px`;
node.style.height = `${node.offsetHeight}px`;
}
@@ -1336,7 +1343,7 @@ export class Translator {
if (
this.#rule.hasRichText === "true" &&
Translator.TAGS.WARP.has(node.tagName)
Translator.TAGS.WARP.has(node.tagName?.toUpperCase())
) {
wrapCounter++;
const startPlaceholder = `<${this.#placeholder.tagName}${wrapCounter}>`;
@@ -1621,7 +1628,14 @@ export class Translator {
// injectCss && injectInternalCss(injectCss);
// }
const { injectJs, toLang } = this.#rule;
const { injectJs, injectCss, toLang } = this.#rule;
if (isExt) {
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
} else {
injectCss && injectInternalCss(injectCss);
}
if (injectJs?.trim()) {
const apiSetting = { ...this.#apiSetting };
const docInfo = { ...this.#docInfo };
@@ -1685,7 +1699,7 @@ export class Translator {
// 翻译页面标题
async #translateTitle() {
const title = document.title;
this.#docInfo.title = title;
this.#docInfo.title = truncateWords(title);
if (!title) return;
try {

View File

@@ -409,3 +409,76 @@ export const randomBetween = (min, max, integer = true) => {
const value = Math.random() * (max - min) + min;
return integer ? Math.floor(value) : value;
};
/**
* 根据文件名自动获取 MIME 类型
* @param {*} filename
* @returns
*/
function getMimeTypeFromFilename(filename) {
const defaultType = "application/octet-stream";
if (!filename || filename.indexOf(".") === -1) {
return defaultType;
}
const extension = filename.split(".").pop().toLowerCase();
const mimeMap = {
// 文本
txt: "text/plain;charset=utf-8",
html: "text/html;charset=utf-8",
css: "text/css;charset=utf-8",
js: "text/javascript;charset=utf-8",
json: "application/json;charset=utf-8",
xml: "application/xml;charset=utf-8",
md: "text/markdown;charset=utf-8",
vtt: "text/vtt;charset=utf-8",
// 图像
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
svg: "image/svg+xml",
webp: "image/webp",
ico: "image/x-icon",
// 音频/视频
mp3: "audio/mpeg",
mp4: "video/mp4",
webm: "video/webm",
wav: "audio/wav",
// 应用程序/文档
pdf: "application/pdf",
zip: "application/zip",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
};
// 默认值
return mimeMap[extension] || defaultType;
}
/**
* 下载文件
* @param {*} str
* @param {*} filename
*/
export function downloadBlobFile(str, filename = "kiss-file.txt") {
const mimeType = getMimeTypeFromFilename(filename);
const blob = new Blob([str], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename || `kiss-file.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

View File

@@ -12,8 +12,8 @@ export class BilingualSubtitleManager {
#captionWindowEl = null;
#paperEl = null;
#currentSubtitleIndex = -1;
#preTranslateSeconds = 90;
#throttleSeconds = 30;
// #preTranslateSeconds = 90;
// #throttleSeconds = 30;
#setting = {};
#isAdPlaying = false;
#throttledTriggerTranslations;
@@ -34,7 +34,7 @@ export class BilingualSubtitleManager {
this.#throttledTriggerTranslations = throttle(
this.#triggerTranslations.bind(this),
this.#throttleSeconds * 1000
(setting.throttleTrans ?? 30) * 1000
);
}
@@ -294,7 +294,8 @@ export class BilingualSubtitleManager {
* @param {number} currentTimeMs
*/
#triggerTranslations(currentTimeMs) {
const lookAheadMs = this.#preTranslateSeconds * 1000;
const { preTrans = 90 } = this.#setting;
const lookAheadMs = preTrans * 1000;
for (const sub of this.#formattedSubtitles) {
const isCurrent = sub.start <= currentTimeMs && sub.end >= currentTimeMs;
@@ -356,4 +357,8 @@ export class BilingualSubtitleManager {
this.#currentSubtitleIndex = -1;
this.onTimeUpdate();
}
updateSetting(obj) {
this.#setting = { ...this.#setting, ...obj };
}
}

179
src/subtitle/Menus.js Normal file
View File

@@ -0,0 +1,179 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { MSG_MENUS_PROGRESSED, MSG_MENUS_UPDATEFORM } from "../config";
function Label({ children }) {
return (
<div
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{children}
</div>
);
}
function MenuItem({ children, onClick, disabled = false }) {
const [hover, setHover] = useState(false);
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0px 8px",
opacity: hover ? 1 : 0.8,
background: `rgba(255, 255, 255, ${hover ? 0.1 : 0})`,
cursor: disabled ? "default" : "pointer",
transition: "background 0.2s, opacity 0.2s",
borderRadius: 5,
}}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={onClick}
>
{children}
</div>
);
}
function Switch({ label, name, value, onChange, disabled }) {
const handleClick = useCallback(() => {
if (disabled) return;
onChange({ name, value: !value });
}, [disabled, onChange, name, value]);
return (
<MenuItem onClick={handleClick} disabled={disabled}>
<Label>{label}</Label>
<div
style={{
width: 40,
height: 24,
borderRadius: 12,
background: value ? "rgba(32,156,238,.8)" : "rgba(255,255,255,.3)",
position: "relative",
}}
>
<div
style={{
width: 20,
height: 20,
borderRadius: 10,
position: "absolute",
left: 2,
top: 2,
background: "rgba(255,255,255,.9)",
transform: `translateX(${value ? 16 : 0}px)`,
}}
></div>
</div>
</MenuItem>
);
}
function Button({ label, onClick, disabled }) {
const handleClick = useCallback(() => {
if (disabled) return;
onClick();
}, [disabled, onClick]);
return (
<MenuItem onClick={handleClick} disabled={disabled}>
<Label>{label}</Label>
</MenuItem>
);
}
export function Menus({
i18n,
initData,
updateSetting,
downloadSubtitle,
hasSegApi,
eventName,
}) {
const [formData, setFormData] = useState(initData);
const [progressed, setProgressed] = useState(0);
const handleChange = useCallback(
({ name, value }) => {
setFormData((pre) => ({ ...pre, [name]: value }));
updateSetting({ name, value });
},
[updateSetting]
);
useEffect(() => {
const handler = (e) => {
const { action, data } = e.detail || {};
if (action === MSG_MENUS_PROGRESSED) {
setProgressed(data);
} else if (action === MSG_MENUS_UPDATEFORM) {
setFormData((pre) => ({ ...pre, ...data }));
}
};
window.addEventListener(eventName, handler);
return () => window.removeEventListener(eventName, handler);
}, [eventName]);
const status = useMemo(() => {
if (progressed === 0) return i18n("waiting_subtitles");
if (progressed === 100) return i18n("download_subtitles");
return i18n("processing_subtitles");
}, [progressed, i18n]);
const { isAISegment, skipAd, isBilingual, showOrigin } = formData;
return (
<div
style={{
position: "absolute",
left: 0,
bottom: 100,
background: "rgba(0,0,0,.6)",
width: 200,
lineHeight: "40px",
fontSize: 16,
padding: 8,
borderRadius: 5,
}}
>
<Switch
onChange={handleChange}
name="isAISegment"
value={isAISegment}
label={i18n("ai_segmentation")}
disabled={!hasSegApi}
/>
<Switch
onChange={handleChange}
name="isBilingual"
value={isBilingual}
label={i18n("is_bilingual_view")}
/>
<Switch
onChange={handleChange}
name="showOrigin"
value={showOrigin}
label={i18n("show_origin_subtitle")}
/>
<Switch
onChange={handleChange}
name="skipAd"
value={skipAd}
label={i18n("is_skip_ad")}
/>
<Button
label={`${status} [${progressed}%] `}
onClick={downloadSubtitle}
disabled={progressed !== 100}
/>
</div>
);
}

View File

@@ -6,34 +6,63 @@ import {
APP_NAME,
OPT_LANGS_TO_CODE,
OPT_TRANS_MICROSOFT,
MSG_MENUS_PROGRESSED,
MSG_MENUS_UPDATEFORM,
} from "../config";
import { sleep } from "../libs/utils.js";
import { sleep, genEventName, downloadBlobFile } from "../libs/utils.js";
import { createLogoSVG } from "../libs/svg.js";
import { randomBetween } from "../libs/utils.js";
import { newI18n } from "../config";
import ShadowDomManager from "../libs/shadowDomManager.js";
import { Menus } from "./Menus.js";
import { buildBilingualVtt } from "./vtt.js";
const VIDEO_SELECT = "#container video";
const CONTORLS_SELECT = ".ytp-right-controls";
const YT_CAPTION_SELECT = "#ytp-caption-window-container";
const YT_AD_SELECT = ".video-ads";
const YT_SUBTITLE_BTN_SELECT = "button.ytp-subtitles-button";
class YouTubeCaptionProvider {
#setting = {};
#videoId = "";
#subtitles = [];
#flatEvents = [];
#progressedNum = 0;
#fromLang = "auto";
#processingId = null;
#managerInstance = null;
#toggleButton = null;
#enabled = false;
#ytControls = null;
#isBusy = false;
#fromLang = "auto";
#isMenuShow = false;
#notificationEl = null;
#notificationTimeout = null;
#i18n = () => "";
#menuEventName = "kiss-event";
constructor(setting = {}) {
this.#setting = setting;
this.#setting = { ...setting, isAISegment: false, showOrigin: false };
this.#i18n = newI18n(setting.uiLang || "zh");
this.#menuEventName = genEventName();
}
get #videoId() {
const docUrl = new URL(document.location.href);
return docUrl.searchParams.get("v");
}
get #videoEl() {
return document.querySelector(VIDEO_SELECT);
}
set #progressed(num) {
this.#progressedNum = num;
this.#sendMenusMsg({ action: MSG_MENUS_PROGRESSED, data: num });
}
get #progressed() {
return this.#progressedNum;
}
initialize() {
@@ -47,35 +76,47 @@ class YouTubeCaptionProvider {
});
window.addEventListener("yt-navigate-finish", () => {
setTimeout(() => {
if (this.#toggleButton) {
this.#toggleButton.style.opacity = "0.5";
}
this.#destroyManager();
this.#doubleClick();
}, 1000);
logger.debug("Youtube Provider: yt-navigate-finish", this.#videoId);
this.#destroyManager();
this.#subtitles = [];
this.#flatEvents = [];
this.#progressed = 0;
this.#fromLang = "auto";
this.#setting.isAISegment = false;
this.#sendMenusMsg({
action: MSG_MENUS_UPDATEFORM,
data: { isAISegment: false },
});
});
this.#waitForElement(CONTORLS_SELECT, (ytControls) =>
this.#injectToggleButton(ytControls)
);
this.#waitForElement(CONTORLS_SELECT, (ytControls) => {
const ytSubtitleBtn = ytControls.querySelector(YT_SUBTITLE_BTN_SELECT);
if (ytSubtitleBtn) {
ytSubtitleBtn.addEventListener("click", () => {
if (ytSubtitleBtn.getAttribute("aria-pressed") === "true") {
this.#startManager();
} else {
this.#destroyManager();
}
});
}
this.#injectToggleButton(ytControls);
});
this.#waitForElement(YT_AD_SELECT, (adContainer) => {
this.#moAds(adContainer);
});
}
get #videoEl() {
return document.querySelector(VIDEO_SELECT);
}
#moAds(adContainer) {
const { skipAd = false } = this.#setting;
const adLayoutSelector = ".ytp-ad-player-overlay-layout";
const skipBtnSelector =
".ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern";
const observer = new MutationObserver((mutations) => {
const { skipAd = false } = this.#setting;
for (const mutation of mutations) {
if (mutation.type === "childList") {
const videoEl = this.#videoEl;
@@ -110,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;
}
@@ -149,60 +194,109 @@ class YouTubeCaptionProvider {
});
}
async #doubleClick() {
const button = this.#ytControls?.querySelector(
"button.ytp-subtitles-button"
);
if (button) {
await sleep(randomBetween(50, 100));
button.click();
await sleep(randomBetween(500, 1000));
button.click();
updateSetting({ name, value }) {
if (this.#setting[name] === value) return;
logger.debug("Youtube Provider: update setting", name, value);
this.#setting[name] = value;
if (name === "isBilingual") {
this.#managerInstance?.updateSetting({ [name]: value });
} else if (name === "isAISegment") {
this.#reProcessEvents();
} else if (name === "showOrigin") {
this.#toggleShowOrigin();
}
}
#injectToggleButton(ytControls) {
this.#ytControls = ytControls;
#toggleShowOrigin() {
if (this.#setting.showOrigin) {
this.#destroyManager();
} else {
this.#startManager();
}
}
downloadSubtitle() {
if (!this.#subtitles.length || this.#progressed !== 100) {
logger.debug("Youtube Provider: The subtitle is not yet ready.");
return;
}
try {
const vtt = buildBilingualVtt(this.#subtitles);
downloadBlobFile(
vtt,
`kiss-subtitles-${this.#videoId}_${Date.now()}.vtt`
);
} catch (error) {
logger.info("Youtube Provider: download subtitles:", error);
}
}
#sendMenusMsg({ action, data }) {
window.dispatchEvent(
new CustomEvent(this.#menuEventName, { detail: { action, data } })
);
}
#injectToggleButton(ytControls) {
const kissControls = document.createElement("div");
kissControls.className = "notranslate kiss-subtitle-controls";
Object.assign(kissControls.style, {
height: "100%",
position: "relative",
});
const toggleButton = document.createElement("button");
toggleButton.className = "ytp-button kiss-subtitle-button";
toggleButton.title = APP_NAME;
Object.assign(toggleButton.style, {
color: "white",
opacity: "0.5",
});
toggleButton.appendChild(createLogoSVG());
kissControls.appendChild(toggleButton);
toggleButton.onclick = () => {
if (this.#isBusy) {
logger.info(`Youtube Provider: It's budy now...`);
this.#showNotification(this.#i18n("subtitle_data_processing"));
}
const { segApiSetting, isAISegment, skipAd, isBilingual, showOrigin } =
this.#setting;
const menu = new ShadowDomManager({
id: "kiss-subtitle-menus",
className: "notranslate",
reactComponent: Menus,
rootElement: kissControls,
props: {
i18n: this.#i18n,
updateSetting: this.updateSetting.bind(this),
downloadSubtitle: this.downloadSubtitle.bind(this),
hasSegApi: !!segApiSetting,
eventName: this.#menuEventName,
initData: {
isAISegment, // AI智能断句
skipAd, // 快进广告
isBilingual, // 双语显示
showOrigin, // 显示原字幕
},
},
});
if (!this.#enabled) {
logger.info(`Youtube Provider: Feature toggled ON.`);
this.#enabled = true;
toggleButton.onclick = () => {
if (!this.#isMenuShow) {
this.#isMenuShow = true;
this.#toggleButton?.replaceChildren(
createLogoSVG({ isSelected: true })
);
this.#startManager();
menu.show();
this.#sendMenusMsg({
action: MSG_MENUS_PROGRESSED,
data: this.#progressed,
});
} else {
logger.info(`Youtube Provider: Feature toggled OFF.`);
this.#enabled = false;
this.#isMenuShow = false;
this.#toggleButton?.replaceChildren(createLogoSVG());
this.#destroyManager();
menu.hide();
}
};
this.#toggleButton = toggleButton;
this.#ytControls?.prepend(kissControls);
ytControls?.prepend(kissControls);
}
#isSameLang(lang1, lang2) {
@@ -290,11 +384,6 @@ class YouTubeCaptionProvider {
}
}
#getVideoId() {
const docUrl = new URL(document.location.href);
return docUrl.searchParams.get("v");
}
async #aiSegment({ videoId, fromLang, toLang, chunkEvents, segApiSetting }) {
try {
const events = chunkEvents.filter((item) => item.text);
@@ -326,36 +415,38 @@ class YouTubeCaptionProvider {
}
async #handleInterceptedRequest(url, responseText) {
if (this.#isBusy) {
logger.info("Youtube Provider is busy...");
const videoId = this.#videoId;
if (!videoId) {
logger.debug("Youtube Provider: videoId not found.");
return;
}
this.#isBusy = true;
const potUrl = new URL(url);
if (videoId !== potUrl.searchParams.get("v")) {
logger.debug("Youtube Provider: skip other timedtext:", videoId);
return;
}
if (this.#flatEvents.length) {
logger.debug("Youtube Provider: video was processed:", videoId);
return;
}
if (videoId === this.#processingId) {
logger.debug("Youtube Provider: video is processing:", videoId);
return;
}
this.#processingId = videoId;
try {
const videoId = this.#getVideoId();
if (!videoId) {
logger.info("Youtube Provider: videoId not found.");
return;
}
if (videoId === this.#videoId) {
logger.info("Youtube Provider: videoId already processed.");
return;
}
const potUrl = new URL(url);
if (videoId !== potUrl.searchParams.get("v")) {
logger.info("Youtube Provider: skip other timedtext.");
return;
}
const { segApiSetting, toLang } = this.#setting;
this.#showNotification(this.#i18n("starting_to_process_subtitle"));
const { toLang } = this.#setting;
const captionTracks = await this.#getCaptionTracks(videoId);
const captionTrack = this.#findCaptionTrack(captionTracks);
if (!captionTrack) {
logger.info("Youtube Provider: CaptionTrack not found.");
logger.debug("Youtube Provider: CaptionTrack not found:", videoId);
return;
}
@@ -366,7 +457,7 @@ class YouTubeCaptionProvider {
responseText
);
if (!events?.length) {
logger.info("Youtube Provider: SubtitleEvents not got.");
logger.debug("Youtube Provider: events not got:", videoId);
return;
}
@@ -380,108 +471,134 @@ class YouTubeCaptionProvider {
`Youtube Provider: fromLang: ${fromLang}, toLang: ${toLang}`
);
if (this.#isSameLang(fromLang, toLang)) {
logger.info("Youtube Provider: skip same lang", fromLang, toLang);
logger.debug("Youtube Provider: skip same lang", fromLang, toLang);
return;
}
this.#showNotification(this.#i18n("starting_to_process_subtitle"));
const flatEvents = this.#genFlatEvents(events);
if (!flatEvents?.length) {
logger.debug("Youtube Provider: flatEvents not got:", videoId);
return;
}
const flatEvents = this.#flatEvents(events);
if (!flatEvents.length) return;
this.#flatEvents = flatEvents;
this.#fromLang = fromLang;
if (potUrl.searchParams.get("kind") === "asr" && segApiSetting) {
logger.info("Youtube Provider: Starting AI ...");
this.#processEvents({
videoId,
flatEvents,
fromLang,
});
} catch (error) {
logger.warn("Youtube Provider: handle subtitle", error);
this.#showNotification(this.#i18n("subtitle_load_failed"));
} finally {
this.#processingId = null;
}
}
const eventChunks = this.#splitEventsIntoChunks(
flatEvents,
segApiSetting.chunkLength
async #processEvents({ videoId, flatEvents, fromLang }) {
try {
const [subtitles, progressed] = await this.#eventsToSubtitles({
videoId,
flatEvents,
fromLang,
});
if (!subtitles?.length) {
logger.debug(
"Youtube Provider: events to subtitles got empty",
videoId
);
const subtitlesFallback = () =>
this.#formatSubtitles(flatEvents, fromLang);
return;
}
if (eventChunks.length === 0) {
this.#onCaptionsReady({
videoId,
subtitles: subtitlesFallback(),
fromLang,
isInitialLoad: true,
});
return;
}
const firstChunkEvents = eventChunks[0];
const firstBatchSubtitles = await this.#aiSegment({
if (videoId !== this.#videoId) {
logger.debug(
"Youtube Provider: videoId changed!",
videoId,
this.#videoId
);
return;
}
this.#subtitles = subtitles;
this.#progressed = progressed;
this.#startManager();
} catch (error) {
logger.info("Youtube Provider: process events", error);
this.#showNotification(this.#i18n("subtitle_load_failed"));
}
}
#reProcessEvents() {
this.#progressed = 0;
this.#subtitles = [];
const videoId = this.#videoId;
const flatEvents = this.#flatEvents;
const fromLang = this.#fromLang;
if (!videoId || !flatEvents.length) {
return;
}
this.#showNotification(this.#i18n("starting_reprocess_events"));
this.#destroyManager();
this.#processEvents({ videoId, flatEvents, fromLang });
}
async #eventsToSubtitles({ videoId, flatEvents, fromLang }) {
const { isAISegment, segApiSetting, chunkLength, toLang } = this.#setting;
const subtitlesFallback = () => [
this.#formatSubtitles(flatEvents, fromLang),
100,
];
// potUrl.searchParams.get("kind") === "asr"
if (isAISegment && segApiSetting) {
logger.info("Youtube Provider: Starting AI ...");
this.#showNotification(this.#i18n("ai_processing_pls_wait"));
const eventChunks = this.#splitEventsIntoChunks(flatEvents, chunkLength);
if (eventChunks.length === 0) {
return subtitlesFallback();
}
const firstChunkEvents = eventChunks[0];
const firstBatchSubtitles = await this.#aiSegment({
videoId,
chunkEvents: firstChunkEvents,
fromLang,
toLang,
segApiSetting,
});
if (!firstBatchSubtitles?.length) {
return subtitlesFallback();
}
if (eventChunks.length > 1) {
const remainingChunks = eventChunks.slice(1);
this.#processRemainingChunksAsync({
chunks: remainingChunks,
videoId,
chunkEvents: firstChunkEvents,
fromLang,
toLang,
segApiSetting,
});
if (!firstBatchSubtitles?.length) {
this.#onCaptionsReady({
videoId,
subtitles: subtitlesFallback(),
fromLang,
isInitialLoad: true,
});
return;
}
const processed = Math.floor(100 / eventChunks.length);
this.#onCaptionsReady({
videoId,
subtitles: firstBatchSubtitles,
fromLang,
isInitialLoad: true,
});
if (eventChunks.length > 1) {
const remainingChunks = eventChunks.slice(1);
this.#processRemainingChunksAsync({
chunks: remainingChunks,
videoId,
fromLang,
toLang,
segApiSetting,
});
}
return [firstBatchSubtitles, processed];
} else {
const subtitles = this.#formatSubtitles(flatEvents, fromLang);
if (!subtitles?.length) {
logger.info("Youtube Provider: No subtitles after format.");
return;
}
this.#onCaptionsReady({
videoId,
subtitles,
fromLang,
isInitialLoad: true,
});
return [firstBatchSubtitles, 100];
}
} catch (error) {
logger.warn("Youtube Provider: unknow error", error);
this.#showNotification(this.#i18n("subtitle_load_failed"));
} finally {
this.#isBusy = false;
}
}
#onCaptionsReady({ videoId, subtitles, fromLang }) {
this.#subtitles = subtitles;
this.#videoId = videoId;
this.#fromLang = fromLang;
if (this.#toggleButton) {
this.#toggleButton.style.opacity = subtitles.length ? "1" : "0.5";
}
this.#destroyManager();
if (this.#enabled) {
this.#startManager();
} else {
this.#showNotification(this.#i18n("subtitle_data_is_ready"));
}
return subtitlesFallback();
}
#startManager() {
@@ -489,11 +606,12 @@ class YouTubeCaptionProvider {
return;
}
const videoId = this.#getVideoId();
if (!this.#subtitles?.length || this.#videoId !== videoId) {
logger.info("Youtube Provider: No subtitles");
this.#showNotification(this.#i18n("try_get_subtitle_data"));
this.#doubleClick();
if (this.#setting.showOrigin) {
return;
}
if (!this.#subtitles.length) {
this.#showNotification(this.#i18n("waitting_for_subtitle"));
return;
}
@@ -514,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() {
@@ -528,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");
}
@@ -746,7 +872,7 @@ class YouTubeCaptionProvider {
return sentences;
}
#flatEvents(events = []) {
#genFlatEvents(events = []) {
const segments = [];
let buffer = null;
@@ -839,7 +965,7 @@ class YouTubeCaptionProvider {
for (let i = 0; i < chunks.length; i++) {
const chunkEvents = chunks[i];
const chunkNum = i + 2;
logger.info(
logger.debug(
`Youtube Provider: Processing subtitle chunk ${chunkNum}/${chunks.length + 1}: ${chunkEvents[0]?.start} --> ${chunkEvents[chunkEvents.length - 1]?.start}`
);
@@ -857,7 +983,7 @@ class YouTubeCaptionProvider {
if (aiSubtitles?.length > 0) {
subtitlesForThisChunk = aiSubtitles;
} else {
logger.info(
logger.debug(
`Youtube Provider: AI segmentation for chunk ${chunkNum} returned no data.`
);
subtitlesForThisChunk = this.#formatSubtitles(chunkEvents, fromLang);
@@ -866,19 +992,29 @@ class YouTubeCaptionProvider {
subtitlesForThisChunk = this.#formatSubtitles(chunkEvents, fromLang);
}
if (this.#getVideoId() !== videoId) {
logger.info("Youtube Provider: videoId changed!");
if (videoId !== this.#videoId) {
logger.info(
"Youtube Provider: videoId changed!!",
videoId,
this.#videoId
);
break;
}
if (subtitlesForThisChunk.length > 0 && this.#managerInstance) {
logger.info(
`Youtube Provider: Appending ${subtitlesForThisChunk.length} subtitles from chunk ${chunkNum}.`
if (subtitlesForThisChunk.length > 0) {
const progressed = Math.floor((chunkNum * 100) / (chunks.length + 1));
this.#subtitles.push(...subtitlesForThisChunk);
this.#progressed = progressed;
logger.debug(
`Youtube Provider: Appending ${subtitlesForThisChunk.length} subtitles from chunk ${chunkNum} (${this.#progressed}%).`
);
this.#subtitles.push(subtitlesForThisChunk);
this.#managerInstance.appendSubtitles(subtitlesForThisChunk);
if (this.#managerInstance) {
this.#managerInstance.appendSubtitles(subtitlesForThisChunk);
}
} else {
logger.info(`Youtube Provider: Chunk ${chunkNum} no subtitles.`);
logger.debug(`Youtube Provider: Chunk ${chunkNum} no subtitles.`);
}
await sleep(randomBetween(500, 1000));

View File

@@ -54,6 +54,25 @@ function parseTimestampToMilliseconds(timestamp) {
return (hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds;
}
/**
* 将毫秒数转换为VTT时间戳字符串 (HH:MM:SS.mmm).
*
* @param {number} ms - 总毫秒数.
* @returns {string} - 格式化的VTT时间戳 (HH:MM:SS.mmm).
*/
function formatMillisecondsToTimestamp(ms) {
const totalSeconds = Math.floor(ms / 1000);
const milliseconds = String(ms % 1000).padStart(3, "0");
const totalMinutes = Math.floor(totalSeconds / 60);
const seconds = String(totalSeconds % 60).padStart(2, "0");
const hours = String(Math.floor(totalMinutes / 60)).padStart(2, "0");
const minutes = String(totalMinutes % 60).padStart(2, "0");
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
/**
* 解析包含双语字幕的VTT文件内容。
* @param {string} vttText - VTT文件的文本内容。
@@ -97,3 +116,31 @@ export function parseBilingualVtt(vttText) {
return result;
}
/**
* 将 parseBilingualVtt 生成的JSON数据转换回标准的VTT字幕字符串。
* @param {Array<Object>} cues - 字幕对象数组,
* @returns {string} - 格式化的VTT文件内容字符串。
*/
export function buildBilingualVtt(cues) {
if (!Array.isArray(cues)) {
return "WEBVTT";
}
const header = "WEBVTT";
const cueBlocks = cues.map((cue, index) => {
const startTime = formatMillisecondsToTimestamp(cue.start);
const endTime = formatMillisecondsToTimestamp(cue.end);
const cueIndex = index + 1;
const timestampLine = `${startTime} --> ${endTime}`;
const textLine = cue.text || "";
const translationLine = cue.translation || "";
return `${cueIndex}\n${timestampLine}\n${textLine}\n${translationLine}`;
});
return [header, ...cueBlocks].join("\n\n");
}

View File

@@ -2,6 +2,7 @@ import FileDownloadIcon from "@mui/icons-material/FileDownload";
import LoadingButton from "@mui/lab/LoadingButton";
import { useState } from "react";
import { kissLog } from "../../libs/log";
import { downloadBlobFile } from "../../libs/utils";
export default function DownloadButton({ handleData, text, fileName }) {
const [loading, setLoading] = useState(false);
@@ -10,13 +11,7 @@ export default function DownloadButton({ handleData, text, fileName }) {
try {
setLoading(true);
const data = await handleData();
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName || `${Date.now()}.json`);
document.body.appendChild(link);
link.click();
link.remove();
downloadBlobFile(data, fileName);
} catch (err) {
kissLog("download", err);
} finally {

View File

@@ -32,6 +32,7 @@ export default function ReusableAutocomplete({
name: name,
value: newValue,
},
preventDefault: () => {},
};
onChange(syntheticEvent);
}

View File

@@ -106,7 +106,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
parentStyle = "",
grandStyle = "",
injectJs = "",
// injectCss = "",
injectCss = "",
apiSlug,
fromLang,
toLang,
@@ -651,7 +651,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
maxRows={10}
/> */}
{/* <TextField
<TextField
size="small"
label={i18n("inject_css")}
helperText={i18n("inject_css_helper")}
@@ -661,7 +661,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
onChange={handleChange}
maxRows={10}
multiline
/> */}
/>
<TextField
size="small"
label={i18n("inject_js")}

View File

@@ -30,6 +30,8 @@ export default function SubtitleSetting() {
apiSlug,
segSlug,
chunkLength,
preTrans = 90,
throttleTrans = 30,
toLang,
isBilingual,
skipAd = false,
@@ -114,6 +116,32 @@ export default function SubtitleSetting() {
max={20000}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ValidationInput
fullWidth
size="small"
label={i18n("pre_trans_seconds")}
type="number"
name="preTrans"
value={preTrans}
onChange={handleChange}
min={10}
max={36000}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ValidationInput
fullWidth
size="small"
label={i18n("throttle_trans_interval")}
type="number"
name="throttleTrans"
value={throttleTrans}
onChange={handleChange}
min={1}
max={3600}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth