Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
864e0651b1 | ||
|
|
65d328eb38 | ||
|
|
731d360323 | ||
|
|
c4ccdba268 | ||
|
|
4f00492e49 | ||
|
|
abcf2baad6 | ||
|
|
49a7698993 | ||
|
|
8d2548acaf | ||
|
|
251deb5886 | ||
|
|
7a15bdeadc | ||
|
|
1e59d57764 | ||
|
|
12b3768598 | ||
|
|
3abe5b98d0 | ||
|
|
ad004105c3 | ||
|
|
f70266197e | ||
|
|
cc31a8004a | ||
|
|
fa14851596 | ||
|
|
d56c46e944 | ||
|
|
9f8bcf1fe1 | ||
|
|
e50387a796 | ||
|
|
3d2eac8772 | ||
|
|
343f529cac | ||
|
|
3bfa12b61c | ||
|
|
79bd776ef9 | ||
|
|
222428ad47 | ||
|
|
4b3853dd22 | ||
|
|
9dd191902c | ||
|
|
3f524ad674 | ||
|
|
7e6376fcb7 | ||
|
|
6f35013faf | ||
|
|
e71acdaaa9 | ||
|
|
fd7c663282 | ||
|
|
89b2bbe9ac |
2
.env
2
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=2.0.7
|
||||
REACT_APP_VERSION=2.0.10
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -7,15 +7,15 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
version: 9.14.4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm build+zip
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
path: build
|
||||
deploy-web:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
folder: build/web
|
||||
create-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||
steps:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
|
||||
1
.pnpm-version
Normal file
1
.pnpm-version
Normal file
@@ -0,0 +1 @@
|
||||
9.14.4
|
||||
@@ -1,6 +1,6 @@
|
||||
# KISS Translator
|
||||
|
||||
English | [简体中文](README.md)
|
||||
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||
|
||||
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||
|
||||
|
||||
177
README.ja.md
Normal file
177
README.ja.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# KISS Translator シンプル翻訳
|
||||
|
||||
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||
|
||||
シンプルでオープンソースの [バイリンガル対照翻訳拡張機能&ユーザースクリプト](https://github.com/fishjar/kiss-translator)です。
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## 特徴
|
||||
|
||||
- [x] シンプルさを維持
|
||||
- [x] オープンソース
|
||||
- [x] 主要なブラウザに対応
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [x] Safari
|
||||
- [x] Thunderbird
|
||||
- [x] 複数の翻訳サービスをサポート
|
||||
- [x] Google/Microsoft
|
||||
- [x] Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] AzureAI/CloudflareAI
|
||||
- [x] Chromeブラウザ内蔵AI翻訳(BuiltinAI)
|
||||
- [x] 一般的な翻訳シナリオをカバー
|
||||
- [x] Webページのバイリンガル対照翻訳
|
||||
- [x] 入力ボックス翻訳
|
||||
- ショートカットキーで入力ボックス内のテキストを即座に他言語に翻訳
|
||||
- [x] テキスト選択翻訳
|
||||
- [x] 任意のページで翻訳ボックスを開き、複数の翻訳サービスで比較翻訳が可能
|
||||
- [x] 英語辞書翻訳
|
||||
- [x] 単語のブックマーク
|
||||
- [x] マウスオーバー翻訳
|
||||
- [x] YouTube 字幕翻訳
|
||||
- 任意の翻訳サービスを使用してビデオ字幕を翻訳し、バイリンガル表示をサポート
|
||||
- 基本的な字幕結合・改行アルゴリズムを内蔵し、翻訳品質を向上
|
||||
- AIによる改行機能をサポートし、翻訳品質をさらに向上
|
||||
- 字幕スタイルのカスタマイズ
|
||||
- [x] 多様な翻訳効果をサポート
|
||||
- [x] テキスト自動認識と手動ルールの2つのモードをサポート
|
||||
- テキスト自動認識モードにより、ほとんどのWebサイトでルールを記述しなくても完全な翻訳が可能
|
||||
- 手動ルールモードで、特定のWebサイトに合わせた最適な最適化が可能
|
||||
- [x] 翻訳テキストスタイルのカスタマイズ
|
||||
- [x] リッチテキストの翻訳と表示をサポートし、原文のリンクやその他のテキストスタイルを可能な限り保持
|
||||
- [x] 翻訳文のみの表示(原文を非表示)をサポート
|
||||
- [x] 翻訳APIの高度な機能
|
||||
- [x] カスタムAPIにより、理論上あらゆる翻訳インターフェースをサポート
|
||||
- [x] 翻訳テキストの統合バッチ送信
|
||||
- [x] AIコンテキスト(会話メモリ)機能をサポートし、翻訳品質を向上
|
||||
- [x] カスタムAI用語集
|
||||
- [x] すべてのインターフェースがフックやカスタムパラメータなどの高度な機能をサポート
|
||||
- [x] クライアント間のデータ同期
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [x] カスタム翻訳ルール
|
||||
- [x] ルールの購読/ルール共有
|
||||
- [x] カスタム専門用語
|
||||
- [x] カスタムショートカットキー
|
||||
- `Alt+Q` 翻訳をオン
|
||||
- `Alt+C` スタイル切り替え
|
||||
- `Alt+K` 設定ポップアップを開く
|
||||
- `Alt+S` 翻訳ポップアップを開く/選択テキストを翻訳
|
||||
- `Alt+O` 設定ページを開く
|
||||
- `Alt+I` 入力ボックス翻訳
|
||||
|
||||
## インストール
|
||||
|
||||
> 注:以下の理由により、ブラウザ拡張機能の使用を優先することをお勧めします
|
||||
>
|
||||
> - ブラウザ拡張機能の方が機能が完全です(ローカル言語認識、右クリックメニューなど)
|
||||
> - ユーザースクリプトはより多くの問題(クロスドメイン問題、スクリプトの競合など)に遭遇する可能性があります
|
||||
|
||||
- [x] ブラウザ拡張機能
|
||||
- [x] Chrome [インストール](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [x] Edge [インストール](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||
- [x] Firefox [インストール](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [ ] Safari (Mac)
|
||||
- [ ] Safari (iOS)
|
||||
- [x] Thunderbird [ダウンロード](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] ユーザースクリプト
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [インストールリンク](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [インストールリンク](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||
|
||||
## 関連プロジェクト
|
||||
|
||||
- データ同期サービス: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
- 本プロジェクトのデータ同期サービスとして使用できます。
|
||||
- 個人のプライベートなルールリストの共有にも使用できます。
|
||||
- セルフホスト、セルフマネジメント、データはプライベート。
|
||||
- コミュニティ購読ルール: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- コミュニティによってメンテナンスされた、最新かつ最も完全な購読ルールリストを提供します。
|
||||
- ルール関連の問題についての助けを求める。
|
||||
|
||||
## よくある質問(FAQ)
|
||||
|
||||
### ショートカットキーの設定方法
|
||||
|
||||
拡張機能の管理ページで設定します。例:
|
||||
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
### ルール設定の優先順位は?
|
||||
|
||||
個人ルール > 購読ルール > グローバルルール
|
||||
|
||||
グローバルルールの優先順位は最も低いですが、フォールバックルールとして非常に重要です。
|
||||
|
||||
### API(Ollamaなど)のテストに失敗する
|
||||
|
||||
APIテストの失敗には、一般的に以下の原因が考えられます:
|
||||
|
||||
- アドレスが間違っている:
|
||||
- 例えば `Ollama` にはネイティブAPIアドレスと `Openai` 互換のアドレスがありますが、本プラグインは現在、`Openai` 互換アドレスをサポートしており、`Ollama` ネイティブAPIアドレスはサポートしていません
|
||||
- 一部のAIモデルが統合翻訳をサポートしていない:
|
||||
- この場合、統合翻訳を無効にするか、カスタムAPIを使用して対応できます。
|
||||
- または、カスタムAPIを使用して対応します。詳細は[カスタムAPIサンプルドキュメント](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)を参照してください
|
||||
- 一部のAIモデルでパラメータが一致しない:
|
||||
- 例えば `Gemini` のネイティブAPIはパラメータの不一致が大きく、一部のバージョンのモデルが特定のパラメータをサポートしていないためエラーが返されることがあります。
|
||||
- この場合、`Hook` を使用してリクエスト `body` を変更するか、`Gemini2` (`Openai` 互換アドレス) に切り替えることができます
|
||||
- サーバーのクロスドメイン制限によりアクセスが拒否され、403エラーが返される:
|
||||
- 例えば `Ollama` を起動する際に、環境変数 `OLLAMA_ORIGINS=*` を追加する必要があります。参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### 入力したAPIがユーザースクリプトで使用できない
|
||||
|
||||
ユーザースクリプトは、リクエストを送信するためにドメインのホワイトリストを追加する必要があります。
|
||||
|
||||
### カスタムAPIのhook関数の設定方法
|
||||
|
||||
カスタムAPI機能は非常に強力で柔軟性があり、理論的にはどんな翻訳APIにも接続できます。
|
||||
|
||||
サンプル参照: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### ユーザースクリプトの設定ページに直接アクセスする方法
|
||||
|
||||
設定ページアドレス: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
### 字幕翻訳のヒント
|
||||
|
||||
KTボタンがオンの状態(青地に白文字)であれば、何度もクリックする必要はありません。Youtubeプレーヤーの字幕ボタンをクリックしてオンにするだけで、バイリンガル字幕が自動的に表示されるのを待つだけです。
|
||||
|
||||
## 今後の計画
|
||||
|
||||
本プロジェクトは余暇に開発しており、厳密なタイムスケジュールはありません。コミュニティの共同構築を歓迎します。以下は初期段階の機能の方向性です:
|
||||
|
||||
- [x] **テキストの統合送信**:リクエスト戦略を最適化し、翻訳APIの呼び出し回数を減らし、パフォーマンスを向上させます。
|
||||
- [x] **リッチテキスト翻訳の強化**:より複雑なページ構造やリッチテキストコンテンツの正確な翻訳をサポートします。
|
||||
- [x] **カスタム/AI APIの強化**:コンテキストメモリ、複数ラウンドの対話など、高度なAI機能をサポートします。
|
||||
- [x] **英語辞書のフォールバックメカニズム**:翻訳サービスが利用できない場合、他の辞書に切り替えるか、ローカル辞書での検索にフォールバックします。
|
||||
- [x] **YouTube字幕サポートの最適化**:ストリーミング字幕の結合と翻訳体験を改善し、途切れを減らします。
|
||||
- [ ] **ルール共同構築メカニズムのアップグレード**:より柔軟なルールの共有、バージョン管理、コミュニティレビュープロセスを導入します。
|
||||
|
||||
特定の方向に興味がある場合は、[Issues](https://github.com/fishjar/kiss-translator/issues) で議論したり、PRを送信したりすることを歓迎します!
|
||||
|
||||
## 開発ガイド
|
||||
|
||||
```sh
|
||||
git clone [https://github.com/fishjar/kiss-translator.git](https://github.com/fishjar/kiss-translator.git)
|
||||
cd kiss-translator
|
||||
git checkout dev # PRを送信する場合はdevブランチにプッシュすることをお勧めします
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## コミュニケーション
|
||||
|
||||
- [Telegram グループ](https://t.me/+RRCu_4oNwrM2NmFl)に参加
|
||||
|
||||
## 寄付
|
||||
|
||||

|
||||
178
README.ko.md
Normal file
178
README.ko.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# KISS Translator 심플 번역
|
||||
|
||||
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||
|
||||
심플하고 오픈 소스인 [이중 언어 대조 번역 확장 프로그램 & 유저 스크립트](https://github.com/fishjar/kiss-translator)입니다.
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## 특징
|
||||
|
||||
- [x] 심플함 유지
|
||||
- [x] 오픈 소스
|
||||
- [x] 주요 브라우저 지원
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [x] Safari
|
||||
- [x] Thunderbird
|
||||
- [x] 다양한 번역 서비스 지원
|
||||
- [x] Google/Microsoft
|
||||
- [x] Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] AzureAI/CloudflareAI
|
||||
- [x] Chrome 브라우저 내장 AI 번역(BuiltinAI)
|
||||
- [x] 일반적인 번역 시나리오 지원
|
||||
- [x] 웹페이지 이중 언어 대조 번역
|
||||
- [x] 입력창 번역
|
||||
- 단축키를 통해 입력창 내 텍스트를 즉시 다른 언어로 번역
|
||||
- [x] 텍스트 선택 번역
|
||||
- [x] 모든 페이지에서 번역창을 열어 여러 번역 서비스로 비교 번역 가능
|
||||
- [x] 영어 사전 번역
|
||||
- [x] 단어 즐겨찾기
|
||||
- [x] 마우스오버 번역
|
||||
- [x] YouTube 자막 번역
|
||||
- 모든 번역 서비스를 사용하여 비디오 자막을 번역하고 이중 언어로 표시 지원
|
||||
- 기본적인 자막 병합 및 줄 바꿈 알고리즘 내장으로 번역 품질 향상
|
||||
- AI 줄 바꿈 기능 지원으로 번역 품질 추가 향상
|
||||
- 사용자 정의 자막 스타일
|
||||
- [x] 다양한 번역 효과 지원
|
||||
- [x] 자동 텍스트 인식 및 수동 규칙 두 가지 모드 지원
|
||||
- 자동 텍스트 인식 모드는 대부분의 웹사이트에서 규칙 작성 없이도 완벽한 번역 가능
|
||||
- 수동 규칙 모드로 특정 웹사이트에 대한 최적의 최적화 가능
|
||||
- [x] 번역문 스타일 사용자 정의
|
||||
- [x] 리치 텍스트 번역 및 표시 지원, 원문의 링크 및 기타 텍스트 스타일 최대한 보존
|
||||
- [x] 번역문만 표시 (원문 숨기기) 지원
|
||||
- [x] 번역 인터페이스 고급 기능
|
||||
- [x] 사용자 정의 인터페이스를 통해 이론상 모든 번역 인터페이스 지원
|
||||
- [x] 번역 텍스트 일괄 통합 전송
|
||||
- [x] AI 컨텍스트 (대화 기억) 기능 지원으로 번역 품질 향상
|
||||
- [x] 사용자 정의 AI 용어 사전
|
||||
- [x] 모든 인터페이스는 후크 및 사용자 정의 파라미터 등 고급 기능 지원
|
||||
- [x] 클라이언트 간 데이터 동기화
|
||||
- [x] KISS-Worker (cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [x] 사용자 정의 번역 규칙
|
||||
- [x] 규칙 구독 / 규칙 공유
|
||||
- [x] 사용자 정의 전문 용어
|
||||
- [x] 사용자 정의 단축키
|
||||
- `Alt+Q` 번역 켜기
|
||||
- `Alt+C` 스타일 전환
|
||||
- `Alt+K` 설정 팝업 열기
|
||||
- `Alt+S` 번역 팝업 열기 / 선택한 텍스트 번역
|
||||
- `Alt+O` 설정 페이지 열기
|
||||
- `Alt+I` 입력창 번역
|
||||
|
||||
## 설치
|
||||
|
||||
> 참고: 다음과 같은 이유로 브라우저 확장 프로그램 사용을 우선적으로 권장합니다.
|
||||
>
|
||||
> - 브라우저 확장 프로그램의 기능이 더 완전합니다 (로컬 언어 인식, 우클릭 메뉴 등).
|
||||
> - 유저 스크립트는 사용상 더 많은 문제 (크로스 도메인 문제, 스크립트 충돌 등)를 겪을 수 있습니다.
|
||||
|
||||
- [x] 브라우저 확장 프로그램
|
||||
- [x] Chrome [설치 주소](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [x] Edge [설치 주소](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||
- [x] Firefox [설치 주소](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [ ] Safari (Mac)
|
||||
- [ ] Safari (iOS)
|
||||
- [x] Thunderbird [다운로드 주소](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] 유저 스크립트
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [설치 링크](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [설치 링크](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||
|
||||
## 관련 프로젝트
|
||||
|
||||
- 데이터 동기화 서비스: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
- 본 프로젝트의 데이터 동기화 서비스로 사용할 수 있습니다.
|
||||
- 개인의 비공개 규칙 목록을 공유하는 데에도 사용할 수 있습니다.
|
||||
- 직접 배포, 직접 관리, 데이터 비공개.
|
||||
- 커뮤니티 구독 규칙: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- 커뮤니티에서 유지 관리하는 최신의 가장 완벽한 구독 규칙 목록을 제공합니다.
|
||||
- 규칙 관련 문제에 대한 도움 요청.
|
||||
|
||||
## 자주 묻는 질문 (FAQ)
|
||||
|
||||
### 단축키는 어떻게 설정하나요?
|
||||
|
||||
플러그인 관리 페이지에서 설정합니다. 예:
|
||||
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
### 규칙 설정의 우선순위는 어떻게 되나요?
|
||||
|
||||
개인 규칙 > 구독 규칙 > 전역 규칙
|
||||
|
||||
그중 전역 규칙은 우선순위가 가장 낮지만, 예비 규칙으로서 매우 중요합니다.
|
||||
|
||||
### 인터페이스 (Ollama 등) 테스트 실패
|
||||
|
||||
일반적으로 인터페이스 테스트 실패는 다음과 같은 몇 가지 원인이 있습니다:
|
||||
|
||||
- 주소를 잘못 입력한 경우:
|
||||
- 예를 들어 `Ollama`는 네이티브 인터페이스 주소와 `Openai` 호환 주소가 있습니다. 본 플러그인은 현재 `Openai` 호환 주소를 통일되게 지원하며, `Ollama` 네이티브 인터페이스 주소는 지원하지 않습니다.
|
||||
- 일부 AI 모델이 통합 번역을 지원하지 않는 경우:
|
||||
- 이 경우 통합 번역을 비활성화하거나 사용자 정의 인터페이스 방식을 통해 사용할 수 있습니다.
|
||||
- 또는 사용자 정의 인터페이스 방식을 통해 사용합니다. 자세한 내용은 [사용자 정의 인터페이스 예시 문서](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)를 참조하세요.
|
||||
- 일부 AI 모델의 파라미터가 일치하지 않는 경우:
|
||||
- 예를 들어 `Gemini` 네이티브 인터페이스 파라미터는 매우 불일치하며, 일부 버전의 모델은 특정 파라미터를 지원하지 않아 오류를 반환할 수 있습니다.
|
||||
- 이 경우 `Hook`을 사용하여 요청 `body`를 수정하거나, `Gemini2` (`Openai` 호환 주소)로 변경할 수 있습니다.
|
||||
- 서버의 크로스 도메인 접근 제한으로 403 오류가 반환되는 경우:
|
||||
- 예를 들어 `Ollama` 시작 시 환경 변수 `OLLAMA_ORIGINS=*`를 추가해야 합니다. 참고: https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### 입력한 인터페이스를 유저 스크립트에서 사용할 수 없습니다
|
||||
|
||||
유저 스크립트는 도메인 화이트리스트를 추가해야 요청을 보낼 수 있습니다.
|
||||
|
||||
### 사용자 정의 인터페이스의 hook 함수는 어떻게 설정하나요?
|
||||
|
||||
사용자 정의 인터페이스 기능은 매우 강력하고 유연하며, 이론적으로 어떤 번역 인터페이스든 연결할 수 있습니다.
|
||||
|
||||
예시 참고: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### 유저 스크립트 설정 페이지로 바로 이동하는 방법
|
||||
|
||||
설정 페이지 주소: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
### 자막 번역 팁
|
||||
|
||||
KT 버튼이 켜진 상태(파란 바탕에 흰 글씨)이기만 하면, 여러 번 클릭할 필요 없이 Youtube 플레이어의 원래 자막 버튼을 클릭하여 켜기만 하면 이중 언어 자막이 자동으로 나타날 때까지 기다리면 됩니다.
|
||||
|
||||
## 향후 계획
|
||||
|
||||
본 프로젝트는 여가 시간에 개발되며, 엄격한 시간표는 없습니다. 커뮤니티의 공동 구축을 환영합니다. 다음은 초기 구상 중인 기능 방향입니다:
|
||||
|
||||
- [x] **텍스트 통합 전송**: 요청 전략을 최적화하여 번역 인터페이스 호출 횟수를 줄이고 성능을 향상시킵니다.
|
||||
- [x] **리치 텍스트 번역 강화**: 더 복잡한 페이지 구조와 리치 텍스트 콘텐츠의 정확한 번역을 지원합니다.
|
||||
- [x] **사용자 정의/AI 인터페이스 강화**: 컨텍스트 기억, 다중 턴 대화 등 고급 AI 기능을 지원합니다.
|
||||
- [x] **영어 사전 예비 메커니즘**: 번역 서비스가 실패할 경우 다른 사전으로 전환하거나 로컬 사전 조회로 대체합니다.
|
||||
- [x] **YouTube 자막 지원 최적화**: 스트리밍 자막의 병합 및 번역 경험을 개선하고, 끊김을 줄입니다.
|
||||
- [ ] **규칙 공동 구축 메커니즘 업그레이드**: 더 유연한 규칙 공유, 버전 관리 및 커뮤니티 검토 프로세스를 도입합니다.
|
||||
|
||||
특정 방향에 관심이 있다면, [Issues](https://github.com/fishjar/kiss-translator/issues)에서 토론하거나 PR을 제출해 주세요!
|
||||
|
||||
## 개발 가이드
|
||||
|
||||
```sh
|
||||
git clone [https://github.com/fishjar/kiss-translator.git](https://github.com/fishjar/kiss-translator.git)
|
||||
cd kiss-translator
|
||||
git checkout dev # PR 제출 시 dev 브랜치로 푸시하는 것을 권장합니다
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 커뮤니티
|
||||
|
||||
- [Telegram 그룹](https://t.me/+RRCu_4oNwrM2NmFl) 가입
|
||||
|
||||
## 후원
|
||||
|
||||

|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 简约翻译
|
||||
# KISS Translator 简约翻译
|
||||
|
||||
[English](README.en.md) | 简体中文
|
||||
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||
|
||||
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
## 谷歌翻译接口
|
||||
|
||||
> 此接口不支持聚合
|
||||
@@ -101,7 +156,10 @@ async (args) => {
|
||||
content: JSON.stringify({
|
||||
targetLanguage: args.toLang,
|
||||
segments: args.texts.map((text, id) => ({ id, text })),
|
||||
glossary: {},
|
||||
title: "", // 可省略
|
||||
description: "", // 可省略
|
||||
glossary: {}, // 可省略
|
||||
tone: "", // 可省略
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -134,7 +192,10 @@ async (args) => {
|
||||
content: JSON.stringify({
|
||||
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"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.10",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
310
pnpm-lock.yaml
generated
310
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
20
public/_locales/de/messages.json
Normal file
20
public/_locales/de/messages.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
20
public/_locales/es/messages.json
Normal file
20
public/_locales/es/messages.json
Normal 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"
|
||||
}
|
||||
}
|
||||
20
public/_locales/fr/messages.json
Normal file
20
public/_locales/fr/messages.json
Normal 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"
|
||||
}
|
||||
}
|
||||
20
public/_locales/ja/messages.json
Normal file
20
public/_locales/ja/messages.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"app_name": {
|
||||
"message": "シンプル翻訳"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "シンプルなバイリンガル対訳翻訳拡張機能&Tampermonkeyスクリプト"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "翻訳の切り替え"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "スタイル切り替え"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "設定を開く"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "ポップアップを開く"
|
||||
}
|
||||
}
|
||||
20
public/_locales/ko/messages.json
Normal file
20
public/_locales/ko/messages.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"app_name": {
|
||||
"message": "심플 번역"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "심플한 이중 언어 대조 번역 확장 프로그램 & Tampermonkey 스크립트"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "번역 켜기"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "스타일 전환"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "설정 열기"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "팝업 열기"
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,6 @@
|
||||
"message": "打开设置"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "翻译弹窗/选中文字"
|
||||
"message": "打开弹窗"
|
||||
}
|
||||
}
|
||||
|
||||
20
public/_locales/zh_TW/messages.json
Normal file
20
public/_locales/zh_TW/messages.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"app_name": {
|
||||
"message": "簡約翻譯"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "一個簡約的雙語對照翻譯擴充功能與 Tampermonkey 腳本"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "開啟翻譯"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "切換樣式"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "開啟設定"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "開啟彈出視窗"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.10",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.10",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.10",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
|
||||
@@ -419,7 +419,7 @@ export const apiTranslate = async ({
|
||||
toLang,
|
||||
apiSetting = DEFAULT_API_SETTING,
|
||||
docInfo = {},
|
||||
glossary = {},
|
||||
glossary,
|
||||
useCache = true,
|
||||
usePool = true,
|
||||
}) => {
|
||||
|
||||
@@ -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,36 +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({
|
||||
const promptObj = {
|
||||
targetLanguage: toLang,
|
||||
title: docInfo.title,
|
||||
description: docInfo.description,
|
||||
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]);
|
||||
};
|
||||
|
||||
@@ -558,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}`,
|
||||
@@ -647,6 +680,7 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
customHeader,
|
||||
customBody,
|
||||
events,
|
||||
tone,
|
||||
} = args;
|
||||
|
||||
if (API_SPE_TYPES.mulkeys.has(apiType)) {
|
||||
@@ -658,12 +692,19 @@ 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,
|
||||
@@ -674,6 +715,7 @@ export const genTransReq = async ({ reqHook, ...args }) => {
|
||||
toLang,
|
||||
texts,
|
||||
docInfo,
|
||||
tone,
|
||||
glossary,
|
||||
});
|
||||
}
|
||||
@@ -770,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);
|
||||
@@ -872,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:
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ export async function run(isUserscript = false) {
|
||||
// if (document?.documentElement?.tagName?.toUpperCase() !== "HTML") {
|
||||
// return;
|
||||
// }
|
||||
if (!document?.contentType?.includes("html")) {
|
||||
if (!document?.contentType?.includes("text")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}"; // 占位符
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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, // 是否双语显示
|
||||
|
||||
@@ -13,5 +13,11 @@ export function useDebouncedCallback(callback, delay) {
|
||||
[delay]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedCallback.cancel();
|
||||
};
|
||||
}, [debouncedCallback]);
|
||||
|
||||
return debouncedCallback;
|
||||
}
|
||||
|
||||
@@ -25,13 +25,17 @@ const SettingContext = createContext({
|
||||
reloadSetting: () => {},
|
||||
});
|
||||
|
||||
export function SettingProvider({ children }) {
|
||||
export function SettingProvider({ children, isSettingPage }) {
|
||||
const {
|
||||
data: setting,
|
||||
isLoading,
|
||||
update,
|
||||
reload,
|
||||
} = useStorage(STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY);
|
||||
} = useStorage(
|
||||
STOKEY_SETTING,
|
||||
DEFAULT_SETTING,
|
||||
isSettingPage ? KV_SETTING_KEY : ""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof setting?.darkMode === "boolean") {
|
||||
@@ -43,6 +47,8 @@ export function SettingProvider({ children }) {
|
||||
}, [setting?.darkMode, update]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSettingPage) return;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
logger.setLevel(setting?.logLevel);
|
||||
@@ -53,7 +59,7 @@ export function SettingProvider({ children }) {
|
||||
logger.error("Failed to fetch log level, using default.", error);
|
||||
}
|
||||
})();
|
||||
}, [setting]);
|
||||
}, [isSettingPage, setting?.logLevel]);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
(objOrFn) => {
|
||||
|
||||
@@ -62,10 +62,12 @@ class Logger {
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.level = newLevelObject;
|
||||
console.log(
|
||||
`[${this.config.prefix}] Log level dynamically set to ${this.config.level.name}`
|
||||
);
|
||||
if (this.config.level.value !== newLevelObject.value) {
|
||||
this.config.level = newLevelObject;
|
||||
console.log(
|
||||
`[${this.config.prefix}] Log level dynamically set to ${this.config.level.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,11 +100,11 @@ 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");
|
||||
appRoot.className = `${this._id}_wrapper`;
|
||||
appRoot.className = `${this._id}_wrapper notranslate`;
|
||||
shadowContainer.appendChild(appRoot);
|
||||
|
||||
const cache = createCache({
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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
|
||||
@@ -270,8 +274,7 @@ export class Translator {
|
||||
data, datalist, embed, head, iframe, input, noscript, map,
|
||||
object, option, param, picture, progress,
|
||||
select, script, style, track, textarea, template,
|
||||
video, wbr, .notranslate, [contenteditable], [translate='no'],
|
||||
${Translator.KISS_IGNORE_SELECTOR}`;
|
||||
video, wbr, .notranslate, [contenteditable='true'], [translate='no']`;
|
||||
|
||||
#setting; // 设置选项
|
||||
#rule; // 规则
|
||||
@@ -318,11 +321,15 @@ export class Translator {
|
||||
|
||||
// 忽略元素
|
||||
get #ignoreSelector() {
|
||||
if (this.#rule.isPlainText) {
|
||||
return Translator.KISS_IGNORE_SELECTOR;
|
||||
}
|
||||
|
||||
if (this.#rule.autoScan === "false") {
|
||||
return `${Translator.KISS_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
return `${Translator.KISS_IGNORE_SELECTOR}, ${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
// 接口参数
|
||||
@@ -349,7 +356,7 @@ export class Translator {
|
||||
|
||||
constructor({ rule = {}, setting = {}, favWords = [] }) {
|
||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule, isPlainText: false };
|
||||
this.#favWords = favWords;
|
||||
this.#apisMap = new Map(
|
||||
this.#setting.transApis.map((api) => [api.apiSlug, api])
|
||||
@@ -357,7 +364,7 @@ export class Translator {
|
||||
|
||||
this.#eventName = genEventName();
|
||||
this.#docInfo = {
|
||||
title: document.title,
|
||||
title: truncateWords(document.title),
|
||||
description: this.#getDocDescription(),
|
||||
};
|
||||
this.#combinedSkipsRegex = new RegExp(
|
||||
@@ -409,6 +416,19 @@ export class Translator {
|
||||
// 注入JS/CSS
|
||||
this.#initInjector();
|
||||
|
||||
// 纯文本预处理
|
||||
if (this.#rule.isPlainText) {
|
||||
document
|
||||
.querySelectorAll("pre")
|
||||
.forEach(
|
||||
(pre) =>
|
||||
(pre.innerHTML = pre.innerHTML?.replace(
|
||||
/(?:\r\n|\r|\n)/g,
|
||||
"<br />"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// 查找根节点并扫描
|
||||
document
|
||||
.querySelectorAll(this.#rule.rootsSelector || "body")
|
||||
@@ -1624,7 +1644,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 };
|
||||
@@ -1688,7 +1715,7 @@ export class Translator {
|
||||
// 翻译页面标题
|
||||
async #translateTitle() {
|
||||
const title = document.title;
|
||||
this.#docInfo.title = title;
|
||||
this.#docInfo.title = truncateWords(title);
|
||||
if (!title) return;
|
||||
|
||||
try {
|
||||
@@ -1775,7 +1802,11 @@ export class Translator {
|
||||
this.#rule[key] !== newRule[key]
|
||||
) {
|
||||
this.#rule[key] = newRule[key];
|
||||
if (key === "autoScan" || key === "hasShadowroot") {
|
||||
if (
|
||||
key === "autoScan" ||
|
||||
key === "hasShadowroot" ||
|
||||
key === "isPlainText"
|
||||
) {
|
||||
needsRescan = true;
|
||||
} else {
|
||||
hasChanged = true;
|
||||
|
||||
@@ -246,6 +246,8 @@ export default class TranslatorManager {
|
||||
}
|
||||
|
||||
#processActions({ action, args } = {}, fromExt = false) {
|
||||
if (!action) return;
|
||||
|
||||
if (!fromExt) {
|
||||
sendIframeMsg(action, args);
|
||||
}
|
||||
|
||||
@@ -59,14 +59,21 @@ export const sleep = (delay) =>
|
||||
*/
|
||||
export const debounce = (func, delay = 200) => {
|
||||
let timer = null;
|
||||
return (...args) => {
|
||||
|
||||
const debouncedFunc = (...args) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
debouncedFunc.cancel = () => {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
};
|
||||
|
||||
return debouncedFunc;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -409,3 +416,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);
|
||||
}
|
||||
|
||||
@@ -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
179
src/subtitle/Menus.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -6,34 +6,64 @@ import {
|
||||
APP_NAME,
|
||||
OPT_LANGS_TO_CODE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
MSG_MENUS_PROGRESSED,
|
||||
MSG_MENUS_UPDATEFORM,
|
||||
OPT_LANGS_SPEC_DEFAULT,
|
||||
} 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 +77,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 +152,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 +195,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 +385,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);
|
||||
@@ -325,37 +415,53 @@ class YouTubeCaptionProvider {
|
||||
return [];
|
||||
}
|
||||
|
||||
#getFromLang(lang) {
|
||||
if (lang === "zh") {
|
||||
return "zh-CN";
|
||||
}
|
||||
|
||||
return (
|
||||
OPT_LANGS_SPEC_DEFAULT.get(lang) ||
|
||||
OPT_LANGS_SPEC_DEFAULT.get(lang.slice(0, 2)) ||
|
||||
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang) ||
|
||||
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang.slice(0, 2)) ||
|
||||
"auto"
|
||||
);
|
||||
}
|
||||
|
||||
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,122 +472,146 @@ class YouTubeCaptionProvider {
|
||||
responseText
|
||||
);
|
||||
if (!events?.length) {
|
||||
logger.info("Youtube Provider: SubtitleEvents not got.");
|
||||
logger.debug("Youtube Provider: events not got:", videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = potUrl.searchParams.get("lang");
|
||||
const fromLang =
|
||||
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang) ||
|
||||
OPT_LANGS_TO_CODE[OPT_TRANS_MICROSOFT].get(lang.slice(0, 2)) ||
|
||||
"auto";
|
||||
const fromLang = this.#getFromLang(lang);
|
||||
|
||||
logger.debug(
|
||||
`Youtube Provider: fromLang: ${fromLang}, toLang: ${toLang}`
|
||||
`Youtube Provider: lang: ${lang}, 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);
|
||||
this.#showNotification(this.#i18n("subtitle_same_lang"));
|
||||
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 +619,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 +645,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 +658,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");
|
||||
}
|
||||
@@ -547,8 +686,13 @@ class YouTubeCaptionProvider {
|
||||
|
||||
if (noSpaceLanguages.some((l) => lang?.startsWith(l))) {
|
||||
const subtitles = [];
|
||||
|
||||
if (this.#isQualityPoor(flatEvents, 5, 0.5)) {
|
||||
return flatEvents;
|
||||
}
|
||||
|
||||
let currentLine = null;
|
||||
const MAX_LENGTH = 100;
|
||||
const MAX_LENGTH = 30;
|
||||
|
||||
for (const segment of flatEvents) {
|
||||
if (segment.text) {
|
||||
@@ -746,7 +890,7 @@ class YouTubeCaptionProvider {
|
||||
return sentences;
|
||||
}
|
||||
|
||||
#flatEvents(events = []) {
|
||||
#genFlatEvents(events = []) {
|
||||
const segments = [];
|
||||
let buffer = null;
|
||||
|
||||
@@ -839,7 +983,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 +1001,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 +1010,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));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function ReusableAutocomplete({
|
||||
name: name,
|
||||
value: newValue,
|
||||
},
|
||||
preventDefault: () => {},
|
||||
};
|
||||
onChange(syntheticEvent);
|
||||
}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function Options() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingProvider>
|
||||
<SettingProvider isSettingPage={true}>
|
||||
<ThemeProvider>
|
||||
<AlertProvider>
|
||||
<ConfirmProvider>
|
||||
|
||||
@@ -112,7 +112,10 @@ export default function PopupCont({
|
||||
|
||||
const handleChange = async (e) => {
|
||||
try {
|
||||
const { name, value } = e.target;
|
||||
let { name, value, checked } = e.target;
|
||||
if (name === "isPlainText") {
|
||||
value = checked;
|
||||
}
|
||||
setRule((pre) => ({ ...pre, [name]: value }));
|
||||
|
||||
if (!processActions) {
|
||||
@@ -204,6 +207,7 @@ export default function PopupCont({
|
||||
transOnly,
|
||||
hasRichText,
|
||||
hasShadowroot,
|
||||
isPlainText = false,
|
||||
} = rule;
|
||||
|
||||
return (
|
||||
@@ -322,75 +326,97 @@ export default function PopupCont({
|
||||
label={i18n("input_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="isPlainText"
|
||||
value={!isPlainText}
|
||||
checked={isPlainText}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label={i18n("plain_text_translate")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={apiSlug}
|
||||
name="apiSlug"
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{optApis.map(({ key, name }) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={fromLang}
|
||||
name="fromLang"
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={fromLang}
|
||||
name="fromLang"
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={toLang}
|
||||
name="toLang"
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={toLang}
|
||||
name="toLang"
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={apiSlug}
|
||||
name="apiSlug"
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
>
|
||||
{optApis.map(({ key, name }) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={
|
||||
commands["toggleStyle"]
|
||||
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
|
||||
: i18n("text_style_alt")
|
||||
}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{allTextStyles.map((item) => (
|
||||
<MenuItem key={item.styleSlug} value={item.styleSlug}>
|
||||
{item.styleName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||
size="small"
|
||||
value={textStyle}
|
||||
name="textStyle"
|
||||
label={
|
||||
commands["toggleStyle"]
|
||||
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
|
||||
: i18n("text_style_alt")
|
||||
}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
>
|
||||
{allTextStyles.map((item) => (
|
||||
<MenuItem key={item.styleSlug} value={item.styleSlug}>
|
||||
{item.styleName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
Reference in New Issue
Block a user