Full Code of fishjar/kiss-translator for AI

dev b7202b8adbf7 cached
185 files
903.6 KB
273.5k tokens
671 symbols
1 requests
Download .txt
Showing preview only (954K chars total). Download the full file or copy to clipboard to get everything.
Repository: fishjar/kiss-translator
Branch: dev
Commit: b7202b8adbf7
Files: 185
Total size: 903.6 KB

Directory structure:
gitextract_hbvdxx0r/

├── .babelrc
├── .github/
│   └── workflows/
│       └── release.yml
├── .gitignore
├── .pnpm-version
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.en.md
├── README.ja.md
├── README.ko.md
├── README.md
├── VERSION_MANAGEMENT.md
├── config-overrides.js
├── custom-api.md
├── custom-api_v2.md
├── kiss-translator.webm
├── package.json
├── public/
│   ├── .nojekyll
│   ├── _locales/
│   │   ├── de/
│   │   │   └── messages.json
│   │   ├── en/
│   │   │   └── messages.json
│   │   ├── es/
│   │   │   └── messages.json
│   │   ├── fr/
│   │   │   └── messages.json
│   │   ├── ja/
│   │   │   └── messages.json
│   │   ├── ko/
│   │   │   └── messages.json
│   │   ├── zh_CN/
│   │   │   └── messages.json
│   │   └── zh_TW/
│   │       └── messages.json
│   ├── content.html
│   ├── index.html
│   ├── manifest.firefox.json
│   ├── manifest.json
│   └── manifest.thunderbird.json
└── src/
    ├── apis/
    │   ├── baidu.js
    │   ├── deepl.js
    │   ├── history.js
    │   ├── index.js
    │   └── trans.js
    ├── background.js
    ├── common.js
    ├── components/
    │   └── Logo/
    │       ├── icon.base64.js
    │       └── index.js
    ├── config/
    │   ├── api.js
    │   ├── app.js
    │   ├── client.js
    │   ├── i18n.js
    │   ├── index.js
    │   ├── msg.js
    │   ├── quotes.js
    │   ├── rules.js
    │   ├── setting.js
    │   ├── storage.js
    │   ├── styles.js
    │   └── url.js
    ├── content.js
    ├── hooks/
    │   ├── Alert.js
    │   ├── Api.js
    │   ├── Audio.js
    │   ├── ColorMode.js
    │   ├── Confirm.js
    │   ├── CustomStyles.js
    │   ├── DebouncedCallback.js
    │   ├── Fab.js
    │   ├── FavWords.js
    │   ├── Fetch.js
    │   ├── I18n.js
    │   ├── InputRule.js
    │   ├── Loading.js
    │   ├── MouseHover.js
    │   ├── Rules.js
    │   ├── Setting.js
    │   ├── Shortcut.js
    │   ├── Storage.js
    │   ├── SubRules.js
    │   ├── Subtitle.js
    │   ├── Sync.js
    │   ├── Theme.js
    │   ├── Tranbox.js
    │   ├── ValidationInput.js
    │   ├── WindowSize.js
    │   ├── useAutoHideTranBtn.js
    │   ├── useSelectionController.js
    │   ├── useTranBoxState.js
    │   └── useTranboxShortcuts.js
    ├── index.js
    ├── injector-shadowroot.js
    ├── injector-subtitle.js
    ├── injectors/
    │   ├── index.js
    │   ├── shadowroot.js
    │   └── xmlhttp.js
    ├── libs/
    │   ├── auth.js
    │   ├── batchQueue.js
    │   ├── blacklist.js
    │   ├── browser.js
    │   ├── builtinAI.js
    │   ├── cache.js
    │   ├── client.js
    │   ├── detect.js
    │   ├── docInfo.js
    │   ├── domManager.js
    │   ├── fabManager.js
    │   ├── fetch.js
    │   ├── gm.js
    │   ├── iframe.js
    │   ├── injector.js
    │   ├── inputTranslate.js
    │   ├── interpreter.js
    │   ├── log.js
    │   ├── mobile.js
    │   ├── msg.js
    │   ├── pool.js
    │   ├── popupManager.js
    │   ├── rules.js
    │   ├── shadowDomManager.js
    │   ├── shortcut.js
    │   ├── storage.js
    │   ├── stream.js
    │   ├── style.js
    │   ├── subRules.js
    │   ├── svg.js
    │   ├── sync.js
    │   ├── touch.js
    │   ├── tranbox.js
    │   ├── translator.js
    │   ├── translatorManager.js
    │   ├── trustedTypes.js
    │   ├── url.js
    │   └── utils.js
    ├── options.js
    ├── popup.js
    ├── rules.js
    ├── scripts/
    │   ├── archive.mjs
    │   ├── build-ios.mjs
    │   ├── build-safari.js
    │   ├── build-safari.mjs
    │   ├── build-task.mjs
    │   ├── sync-version.mjs
    │   └── update-version.mjs
    ├── subtitle/
    │   ├── BilingualSubtitleManager.js
    │   ├── Menus.js
    │   ├── YouTubeCaptionProvider.js
    │   ├── YouTubeSubtitleList.js
    │   ├── subtitle.js
    │   └── vtt.js
    ├── userscript.js
    └── views/
        ├── Action/
        │   ├── ContentFab.js
        │   ├── Draggable.js
        │   └── index.js
        ├── Options/
        │   ├── About.js
        │   ├── Apis.js
        │   ├── DarkModeButton.js
        │   ├── DownloadButton.js
        │   ├── FavWords.js
        │   ├── Header.js
        │   ├── HelpButton.js
        │   ├── InputSetting.js
        │   ├── Layout.js
        │   ├── MouseHover.js
        │   ├── Navigator.js
        │   ├── Playground.js
        │   ├── ReusableAutocomplete.js
        │   ├── Rules.js
        │   ├── Setting.js
        │   ├── ShortcutInput.js
        │   ├── ShowMoreButton.js
        │   ├── StylesSetting.js
        │   ├── Subtitle.js
        │   ├── SyncSetting.js
        │   ├── Tranbox.js
        │   ├── UploadButton.js
        │   └── index.js
        ├── Popup/
        │   ├── Header.js
        │   ├── PopupCont.js
        │   └── index.js
        └── Selection/
            ├── AudioBtn.js
            ├── CopyBtn.js
            ├── DictCont.js
            ├── DictHandler.js
            ├── DraggableResizable.js
            ├── FavBtn.js
            ├── SugCont.js
            ├── TranBox.js
            ├── TranBtn.js
            ├── TranCont.js
            ├── TranForm.js
            └── index.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
    "presets": [
        "@babel/preset-env"
    ]
}


================================================
FILE: .github/workflows/release.yml
================================================
name: publish release version

on:
  push:
    tags:
      - "v*"

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9.14.4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: "pnpm"
      - run: pnpm install
      - run: pnpm build+zip
      - uses: actions/upload-artifact@v4
        with:
          name: build-artifacts
          path: build
  deploy-web:
    needs: build
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-artifacts
          path: build
      - name: Deploy to GitHub Pages
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          folder: build/web
  create-release:
    needs: build
    runs-on: ubuntu-24.04
    outputs:
      upload_url: ${{ steps.create-release.outputs.upload_url }}
    steps:
      - uses: actions/checkout@v4
      - name: Extract Release Notes
        id: extract_notes
        shell: bash
        run: |
          LOG_FILE="CHANGELOG.md"
          
          if [ -f "$LOG_FILE" ]; then
            # 使用 awk 提取:从第一个 "## " 后面开始,直到遇到下一个 "## " 停止
            # 这里的逻辑假设最新日志在文件最上方
            NOTES=$(awk '/^## / {if (p) exit; p=1; next} p' "$LOG_FILE")
          else
            NOTES="Release ${{ github.ref }}"
          fi
          
          # 处理多行文本并写入 GITHUB_OUTPUT
          {
            echo "body<<EOF"
            echo "$NOTES"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - uses: actions/create-release@v1
        id: create-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          body: ${{ steps.extract_notes.outputs.body }}
          draft: false
          prerelease: false
  upload-release:
    needs: [build, create-release]
    strategy:
      matrix:
        client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-artifacts
          path: build
      - uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ needs.create-release.outputs.upload_url }}
          asset_path: ./build/${{ matrix.client }}.zip
          asset_name: kiss-translator_${{ github.ref_name }}_${{ matrix.client }}.zip
          asset_content_type: application/zip


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
/.obsidian
.pnp.js
.yarn

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json

*.crx
*.pem
*.zip


================================================
FILE: .pnpm-version
================================================
9.14.4


================================================
FILE: .prettierignore
================================================
node_modules
build
public
package.json


================================================
FILE: .prettierrc
================================================
{
  "arrowParens": "always",
  "bracketSpacing": true,
  "endOfLine": "lf",
  "htmlWhitespaceSensitivity": "css",
  "insertPragma": false,
  "singleAttributePerLine": false,
  "bracketSameLine": false,
  "jsxSingleQuote": false,
  "printWidth": 80,
  "proseWrap": "preserve",
  "quoteProps": "as-needed",
  "requirePragma": false,
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "es5",
  "useTabs": false,
  "embeddedLanguageFormatting": "auto",
  "vueIndentScriptAndStyle": false,
  "experimentalTernaries": false,
  "parser": "babel"
}


================================================
FILE: CHANGELOG.md
================================================
## v2.0.20

- 优化本地语言识别不准确时的处理逻辑
- 修复 MacOS 编译 Safari 插件脚本错误
- 优化调整划词翻译逻辑
  - 修复移动端不显示header的bug
  - 仅对翻译框的大小和位置持久化,其他设置不持久化,仅在当前页面生效
  - 修复翻译框有时会突然变成0,0位置,和0,0大小的bug
- 修复移动端输入框翻译圆点按钮无效的bug
- youtube视频右边的字幕滚动列表可以在设置中选择关闭
- 接口设置中增加`复制接口`功能
- 修复接口权重排序在popup无效的bug
- 规则设置增加`扫描全部节点`的功能,并替换popup中`shadowroot`的位置
  - `扫描全部节点`相当于忽略所有`忽略元素`,翻译全部内容,并同时扫描`shadowroot`
- 更新内置部分规则,新安装时会尝试同步订阅规则
- 其他一下小优化

## v2.0.19

- 修复油猴脚本切换翻译时的脚本错误
- 添加自动更新版本号脚本

## v2.0.18

- 支持翻译closed模式的 shadowroot 中的内容
- 保存规则时,可选子域名
- AI聚合翻译,支持流式传输,优化翻译体验
- 优化划词翻译窗口显示效果
  - 划词翻译按钮增加自动隐藏逻辑(5秒 / 移动100px / 右键)
- 增加翻译状态的图标显示,已翻译会有一个绿色小勾
- 优化输入框翻译,解决一些兼容性问题
  - 可设置在输入框上方显示一个圆形翻译按钮,使得移动端也可以使用输入框翻译功能
- 优化字幕翻译,增加一些新功能
  - 字幕翻译设置样式时,支持可视化编辑
  - 优化字幕翻译逻辑,优先使用用户选择的字幕轨
  - 字幕翻译右侧的字幕滚动列表可以手动点击按钮关闭
  - 字幕翻译的菜单去掉shadowroot
  - 增加下拉选项,可以随时切换AI断句的模型
  - 重构字幕增强功能设置,新增“移动端禁用”选项(默认)
- 优化移动端检测逻辑,修复触屏笔记本被误判为移动端的问题
- 在所有场景下均给AI接口添加上下文信息,包括网页标题、描述、摘要
- 翻译接口增加排序权重
- 新增占位标签格式,优化google2的翻译效果
- bing词典增加显示单词时态信息


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.


================================================
FILE: README.en.md
================================================
# KISS Translator

[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).

[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)

## Features

- [x] Keep it simple, smart
- [x] Open source
- [x] Adapt to common browsers
  - [x] Chrome/Edge
  - [x] Firefox
  - [x] Kiwi (Android)
  - [x] Orion (iOS)
  - [x] Safari
  - [x] Thunderbird
- [x] Supports multiple translation services
  - [x] Google/Microsoft
  - [x] Tencent/Volcengine
  - [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
  - [x] DeepL/DeepLX/NiuTrans
  - [x] AzureAI / CloudflareAI
  - [x] Chrome built-in AI translation (BuiltinAI)
- [x] Covers common translation scenarios
  - [x] Webpage bilingual translation
  - [x] Input-box translation
    - Instantly translate text in input fields into other languages via shortcut keys
  - [x] Text selection translation
    - [x] Open translation popup on any page, support multiple translation services for comparison
    - [x] English dictionary lookup
    - [x] Save vocabulary
  - [x] Hover translation
  - [x] YouTube subtitle translation
    - Support translating video subtitles with any translation service and display bilingually
    - Built-in basic subtitle merging and sentence-splitting algorithm to improve translation quality
    - Supports AI-powered sentence segmentation for even better translation
    - Custom subtitle style
- [x] Supports diverse translation modes
  - [x] Supports both automatic text recognition and manual rule modes
    - Automatic text recognition mode allows most sites to be translated fully without writing rules
    - Manual rule mode enables extreme optimization for specific sites
  - [x] Custom translation styling
  - [x] Supports rich-text translation and rendering, preserving links and other text styles where possible
  - [x] Option to show only translation (hide original text)
- [x] Advanced translation API features
  - [x] With custom API support, theoretically works with any translation service
  - [x] Batch aggregation of translation requests
  - [x] Supports streaming for real-time translation results
  - [x] Supports AI conversation context memory to improve translation quality
  - [x] Custom AI terminology dictionary
  - [x] All APIs support hooks and custom parameters for advanced usage
- [x] Cross-client data synchronization
  - [x] KISS-Worker(cloudflare/docker)
  - [x] WebDAV
- [x] Custom translation rules
  - [x] Rule subscription/rule sharing
  - [x] Customized terminology
- [x] Custom shortcut keys
  - `Alt+Q` Toggle Translation
  - `Alt+C` Toggle Styles
  - `Alt+K` Open Setting Popup
  - `Alt+S` Open Translate Popup / Translate Selected Text
  - `Alt+O` Open Options Page
  - `Alt+I` Input Box Translation

## Install

> Note: For the following reasons, it is recommended to use browser extensions first
>
> - Browser extensions have more complete functions (local language recognition, context menu, etc.)
> - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)

- [x] Browser extension
  - [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=en)
    - [x] Kiwi (Android)
    - [x] Orion (iOS)
  - [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=en)
  - [x] Firefox [Installation address](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
  - [ ] Safari
    - [ ] Safari (Mac)
    - [ ] Safari (iOS)
  - [x] Thunderbird [Download address](https://github.com/fishjar/kiss-translator/releases)
- [x] GreaseMonkey Script
  - [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](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)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)

## Associated Projects

- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
  - Data synchronization service available for this project.
  - Can also be used to share personal private rule lists.
  - Deploy by yourself, manage by yourself, data is private.
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
  - Provides the latest and most complete list of subscription rules maintained by the community.
  - Help with rules-related issues.

## Frequently Asked Questions

### How to Set Keyboard Shortcuts

Set this in the extension management page, for example:

- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
- firefox [about:addons](about:addons)

### What is the priority order of rule settings?

Personal Rules > Subscription Rules > Global Rules

Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.

### API (Ollama, etc.) Test Failure

Common reasons for API test failures include:

- Incorrect address:
  - For example, `Ollama` has a native API address and an `Openai`-compatible address. This plugin currently supports the `Openai`-compatible address and does not support the `Ollama` native API address.
- Some AI models do not support batch translation:
  - In this case, you can choose to disable batch translation or use a custom API.
  - Alternatively, you can use a custom API. For details, please refer to: [Custom API Example Documentation](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
- Some AI models have inconsistent parameters:
  - For example, the parameters of the `Gemini` native API are highly inconsistent. Some model versions do not support certain parameters, leading to errors.
  - In this case, you can modify the request body using a `Hook`, or replace it with `Gemini2` (an OpenAI-compatible address).
- The server restricts cross-origin access, returning a 403 error:
  - For example, `Ollama` requires adding the environment variable `OLLAMA_ORIGINS=*` when starting. See: https://github.com/fishjar/kiss-translator/issues/174

### Custom API doesn't work in Tampermonkey scripts

Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.

### How to set up a hook function for a custom API

Custom APIs are very powerful and flexible, and can theoretically connect to any translation API.

Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)

### How to directly access the Tampermonkey script settings page

Settings page address: https://fishjar.github.io/kiss-translator/options.html

## Future Plans 

 This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:

- [x] **Batch Text Requests**: Optimize request strategy to reduce translation API calls and improve performance.
- [x] **Enhanced Rich Text Translation**: Support accurate translation of complex page structures and rich text content.
- [x] **Advanced Custom/AI Interfaces**: Add support for streaming, context memory, multi-turn conversations, and other advanced AI features.
- [x] **Fallback English Dictionary**: When translation services fail, fall back to a local dictionary lookup.
- [x] **Improved YouTube Subtitle Support**: Enhance merging and translation experience for streaming subtitles, reducing sentence fragmentation.
- [ ] **Upgraded Rule Collaboration System**: Introduce more flexible rule sharing, version management, and community review processes.

 If you're interested in any of these directions, feel free to discuss in [Issues](https://github.com/fishjar/kiss-translator/issues) or submit a PR!

## Development Guidelines

```sh
git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator
git checkout dev # Submit a PR suggestion to push to the dev branch
pnpm install
pnpm build
```

### External Trigger Example

```js
// `toggle_translate`   Toggle translation
// `toggle_styles`      Toggle styles
// `toggle_popup`       Open/close control panel
// `toggle_transbox`    Open/close translation popup
// `toggle_hover_node`  Translate hovered paragraph
// `input_translate`    Translate input box
window.dispatchEvent(new CustomEvent("kiss_translator", {detail: { action: "toggle_translate" }}));
```

## Discussion

- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)

## Appreciate

![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)


================================================
FILE: README.ja.md
================================================
# 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] ストリーミング伝送をサポートし、翻訳結果をリアルタイムで表示
  - [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=ja)
    - [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=ja)
  - [x] Firefox [インストール](https://addons.mozilla.org/ja/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

## 今後の計画 

 本プロジェクトは余暇に開発しており、厳密なタイムスケジュールはありません。コミュニティの共同構築を歓迎します。以下は初期段階の機能の方向性です:

- [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
```

### 外部トリガーの例

```js
// `toggle_translate`   翻訳を切り替え
// `toggle_styles`      スタイルを切り替え
// `toggle_popup`       コントロールパネルを開く/閉じる
// `toggle_transbox`    翻訳ポップアップを開く/閉じる
// `toggle_hover_node`  マウスオーバー中の段落を翻訳
// `input_translate`    入力欄を翻訳
window.dispatchEvent(new CustomEvent("kiss_translator", {detail: { action: "toggle_translate" }}));
```

## コミュニケーション

- [Telegram グループ](https://t.me/+RRCu_4oNwrM2NmFl)に参加

## 寄付

![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)


================================================
FILE: README.ko.md
================================================
# 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] 스트리밍 전송 지원, 번역 결과 실시간 표시
  - [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=ko)
    - [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=ko)
  - [x] Firefox [설치 주소](https://addons.mozilla.org/ko/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

## 향후 계획 

 본 프로젝트는 여가 시간에 개발되며, 엄격한 시간표는 없습니다. 커뮤니티의 공동 구축을 환영합니다. 다음은 초기 구상 중인 기능 방향입니다:

- [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
```

### 외부 트리거 예시

```js
// `toggle_translate`   번역 전환
// `toggle_styles`      스타일 전환
// `toggle_popup`       제어 패널 열기/닫기
// `toggle_transbox`    번역 팝업 열기/닫기
// `toggle_hover_node`  마우스를 올린 문단 번역
// `input_translate`    입력창 번역
window.dispatchEvent(new CustomEvent("kiss_translator", {detail: { action: "toggle_translate" }}));
```

## 커뮤니티

- [Telegram 그룹](https://t.me/+RRCu_4oNwrM2NmFl) 가입

## 후원

![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)



================================================
FILE: README.md
================================================
# 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] 支持流式传输,实时显示翻译结果
  - [x] 支持AI上下文会话记忆功能,提升翻译效果
  - [x] 自定义AI术语词典
  - [x] 所有接口均支持Hook和自定义参数等高级功能
- [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)
  - 提供社区维护的,最新最全的订阅规则列表。
  - 求助规则相关的问题。

## 常见问题

### 如何设置快捷键

在插件管理那里设置,例如: 

- 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

## 未来规划 

 本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:

- [x] **聚合发送文本**:优化请求策略,减少翻译接口调用次数,提升性能。
- [x] **增强富文本翻译**:支持更复杂的页面结构和富文本内容的准确翻译。
- [x] **强化自定义/AI 接口**:支持流式传输、上下文记忆、多轮对话等高级 AI 功能。
- [x] **英文词典备灾机制**:当翻译服务失效时,可切换其他词典或 fallback 到本地词典查询。
- [x] **优化 YouTube 字幕支持**:改进流式字幕的合并与翻译体验,减少断句。
- [ ] **规则共建机制升级**:引入更灵活的规则分享、版本管理与社区评审流程。
 
 如果你对某个方向感兴趣,欢迎在 [Issues](https://github.com/fishjar/kiss-translator/issues) 中讨论或提交 PR!

## 开发指引

```sh
git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator
git checkout dev # 提交PR建议推送到dev分支
pnpm install
pnpm build
```

### 外部触发示例

```js
// `toggle_translate`   切换翻译
// `toggle_styles`      切换样式
// `toggle_popup`       打开/关闭控制面板
// `toggle_transbox`    打开/关闭翻译弹窗
// `toggle_hover_node`  翻译鼠标悬停段落
// `input_translate`    翻译输入框
window.dispatchEvent(new CustomEvent("kiss_translator", {detail: { action: "toggle_translate" }}));
```

## 交流

- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)

## 赞赏

![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)


================================================
FILE: VERSION_MANAGEMENT.md
================================================
# 版本号管理说明

## 📌 背景

项目的版本号分散在多个文件中:
- `package.json`
- `.env` (REACT_APP_VERSION)
- `public/manifest.json`
- `public/manifest.firefox.json`
- `public/manifest.thunderbird.json`

为了避免手动更新多个文件的麻烦,现已实现自动化版本号管理方案。

## ✨ 解决方案

### 单一版本源

**`package.json` 是唯一的版本号来源**,其他文件的版本号会自动从 `package.json` 同步。

### 自动同步机制

构建时会自动同步版本号:

```bash
pnpm build        # 构建前自动同步版本号
pnpm build+zip    # 打包前自动同步版本号
```

手动同步版本号:

```bash
pnpm sync-version  # 手动触发版本号同步
```

## 🚀 版本号更新方法

### 方法一:使用快捷命令(推荐)

```bash
# 补丁版本更新: 2.0.19 -> 2.0.20
pnpm version:patch

# 次版本更新: 2.0.19 -> 2.1.0
pnpm version:minor

# 主版本更新: 2.0.19 -> 3.0.0
pnpm version:major

# 手动指定版本号: 设置为 2.1.0
pnpm version:set -- 2.1.0
```

这些命令会自动完成:
1. ✅ 更新 `package.json` 中的版本号
2. ✅ 自动同步到其他所有文件

### 方法二:手动更新(不推荐)

如果你手动修改了 `package.json` 的版本号,记得运行:

```bash
pnpm sync-version
```

## 📝 完整的版本发布流程

```bash
# 0. 格式化
pnpm format

# 1. 更新版本号(自动完成所有文件的同步)
pnpm version:patch

# 2. 更新 CHANGELOG.md(手动编辑)
# 添加新版本的更新内容

# 3. 提交更改
git add .
git commit -m "chore: bump version to 2.0.20"

# 4. 构建和打包(构建前会再次确保版本号同步)
pnpm build+zip

# 5. 推送代码
git push origin dev

# 6. 合并到 master
git checkout master
git merge dev

# 7. 打 tag
git tag -a v2.0.20 -m "Release version 2.0.20"
git push origin master v2.0.20

# 8. 切换回 dev 分支
git checkout dev
```

## 🛠️ 相关脚本文件

- `src/scripts/sync-version.mjs` - 版本号同步脚本
- `src/scripts/update-version.mjs` - 版本号更新脚本

## ⚠️ 注意事项

1. **不要手动修改** `.env`、`manifest.json` 等文件中的版本号
2. **只需修改** `package.json` 中的版本号,或者使用 `pnpm version:*` 命令
3. 每次构建前会自动同步版本号,确保所有文件版本一致
4. 更新版本后记得同步更新 `CHANGELOG.md`


================================================
FILE: config-overrides.js
================================================
const paths = require("react-scripts/config/paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TerserPlugin = require("terser-webpack-plugin");
// const webpack = require("webpack");

console.log("process.env.REACT_APP_CLIENT", process.env.REACT_APP_CLIENT);

// 扩展
const extWebpack = (config, env) => {
  const isEnvProduction = env === "production";
  const minify = isEnvProduction && {
    removeComments: true,
    collapseWhitespace: true,
    removeRedundantAttributes: true,
    useShortDoctype: true,
    removeEmptyAttributes: true,
    removeStyleLinkTypeAttributes: true,
    keepClosingSlash: true,
    minifyJS: true,
    minifyCSS: true,
    minifyURLs: true,
  };
  const names = [
    "HtmlWebpackPlugin",
    "WebpackManifestPlugin",
    "MiniCssExtractPlugin",
  ];

  config.entry = {
    popup: paths.appSrc + "/popup.js",
    options: paths.appSrc + "/options.js",
    background: paths.appSrc + "/background.js",
    content: paths.appSrc + "/content.js",
    "injector-subtitle": paths.appSrc + "/injector-subtitle.js",
    "injector-shadowroot": paths.appSrc + "/injector-shadowroot.js",
  };

  config.output.filename = "[name].js";
  config.output.assetModuleFilename = "media/[name][ext]";
  config.optimization.splitChunks = { cacheGroups: { default: false } };
  config.optimization.runtimeChunk = false;

  config.plugins = config.plugins.filter(
    (plugin) => !names.includes(plugin.constructor.name)
  );

  config.plugins.push(
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ["options"],
      template: paths.appHtml,
      filename: "options.html",
      minify,
    }),
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ["popup"],
      template: paths.appHtml,
      filename: "popup.html",
      minify,
    }),
    new WebpackManifestPlugin({
      fileName: "asset-manifest.json",
    }),
    new MiniCssExtractPlugin({
      filename: "css/[name].css",
    })
  );

  return config;
};

// 油猴
const userscriptWebpack = (config, env) => {
  const banner = `// ==UserScript==
// @name          ${process.env.REACT_APP_NAME}
// @namespace     ${process.env.REACT_APP_HOMEPAGE}
// @version       ${process.env.REACT_APP_VERSION}
// @description   A simple bilingual translation extension & Greasemonkey script (一个简约的双语对照翻译扩展 & 油猴脚本)
// @author        Gabe<yugang2002@gmail.com>
// @homepageURL   ${process.env.REACT_APP_HOMEPAGE}
// @license       GPL-3.0
// @match         *://*/*
// @icon          ${process.env.REACT_APP_LOGOURL}
// @downloadURL   ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @updateURL     ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @grant         GM.xmlHttpRequest
// @grant         GM_xmlhttpRequest
// @grant         GM.registerMenuCommand
// @grant         GM_registerMenuCommand
// @grant         GM.unregisterMenuCommand
// @grant         GM_unregisterMenuCommand
// @grant         GM.setValue
// @grant         GM_setValue
// @grant         GM.getValue
// @grant         GM_getValue
// @grant         GM.deleteValue
// @grant         GM_deleteValue
// @grant         GM.info
// @grant         GM_info
// @grant         unsafeWindow
// @connect       translate.googleapis.com
// @connect       translate-pa.googleapis.com
// @connect       generativelanguage.googleapis.com
// @connect       api-edge.cognitive.microsofttranslator.com
// @connect       edge.microsoft.com
// @connect       bing.com
// @connect       api-free.deepl.com
// @connect       api.deepl.com
// @connect       www2.deepl.com
// @connect       api.openai.com
// @connect       generativelanguage.googleapis.com
// @connect       openai.azure.com
// @connect       workers.dev
// @connect       github.io
// @connect       github.com
// @connect       githubusercontent.com
// @connect       kiss-translator.rayjar.com
// @connect       ghproxy.com
// @connect       dav.jianguoyun.com
// @connect       fanyi.baidu.com
// @connect       transmart.qq.com
// @connect       niutrans.com
// @connect       translate.volcengine.com
// @connect       dict.youdao.com
// @connect       api.anthropic.com
// @connect       api.siliconflow.cn
// @connect       api.cloudflare.com
// @connect       openrouter.ai
// @connect       localhost
// @connect       127.0.0.1
// @run-at        document-end
// ==/UserScript==

`;

  const names = ["HtmlWebpackPlugin"];

  config.entry = {
    main: paths.appIndexJs,
    options: paths.appSrc + "/options.js",
    "kiss-translator.user": paths.appSrc + "/userscript.js",
  };

  config.output.filename = "[name].js";
  config.output.publicPath = env === "production" ? "./" : "/";
  config.optimization.splitChunks = { cacheGroups: { default: false } };
  config.optimization.runtimeChunk = false;
  config.optimization.minimize = env === "production";

  if (config.optimization.minimize) {
    config.optimization.minimizer = [
      new TerserPlugin({
        extractComments: false,
        terserOptions: {
          format: {
            comments: false,
            preamble: banner,
          },
        },
      }),
    ];
  }

  if (env === "production") config.devtool = false;

  config.plugins = config.plugins.filter(
    (plugin) => !names.includes(plugin.constructor.name)
  );

  config.plugins.push(
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ["main"],
      template: paths.appHtml,
      filename: "index.html",
    }),
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ["options"],
      template: paths.appHtml,
      filename: "options.html",
    })
    // new webpack.BannerPlugin({
    //   banner,
    //   raw: true,
    //   entryOnly: true,
    //   include: "kiss-translator.user",
    // })
  );

  return config;
};

// 开发
const webWebpack = (config, env) => {
  const names = ["HtmlWebpackPlugin"];

  config.entry = {
    main: paths.appIndexJs,
    options: paths.appSrc + "/options.js",
    content: paths.appSrc + "/userscript.js",
  };

  config.output.filename = "[name].js";
  config.output.publicPath = "/";

  config.plugins = config.plugins.filter(
    (plugin) => !names.includes(plugin.constructor.name)
  );

  config.plugins.push(
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ["main"],
      template: paths.appHtml,
      filename: "index.html",
    }),
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ["options"],
      template: paths.appHtml,
      filename: "options.html",
    }),
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ["content"],
      template: paths.appPublic + "/content.html",
      filename: "content.html",
    })
  );

  return config;
};

let webpackConfig;
switch (process.env.REACT_APP_CLIENT) {
  case "userscript":
    webpackConfig = userscriptWebpack;
    break;
  case "web":
    webpackConfig = webWebpack;
    break;
  default:
    webpackConfig = extWebpack;
}

module.exports = {
  webpack: webpackConfig,
};


================================================
FILE: custom-api.md
================================================
# 自定义接口示例(本文档已过期,新版不再适用)

V2版的示例请查看这里:[custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)

以下示例为网友提供,仅供学习参考。

## 本地运行 Seed-X-PPO-7B 量化模型

> 由网友 emptyghost6 提供,来源:https://linux.do/t/topic/828257

URL

```sh
http://localhost:8000/v1/completions
```

Request Hook

```js
(text, from, to, url, key) => {
  // 模型支持的语言代码到完整名称的映射
  const langFullNameMap = {
    ar: 'Arabic', fr: 'French', ms: 'Malay', ru: 'Russian',
    cs: 'Czech', hr: 'Croatian', nb: 'Norwegian Bokmal', sv: 'Swedish',
    da: 'Danish', hu: 'Hungarian', nl: 'Dutch', th: 'Thai',
    de: 'German', id: 'Indonesian', no: 'Norwegian', tr: 'Turkish',
    en: 'English', it: 'Italian', pl: 'Polish', uk: 'Ukrainian',
    es: 'Spanish', ja: 'Japanese', pt: 'Portuguese', vi: 'Vietnamese',
    fi: 'Finnish', ko: 'Korean', ro: 'Romanian', zh: 'Chinese'
  };

  // 将 Hook 系统的语言代码转换为模型 API 支持的代码
  const getModelLangCode = (lang) => {
    if (lang === 'zh-CN' || lang === 'zh-TW') return 'zh';
    return lang;
  };

  const sourceLangCode = getModelLangCode(from);
  const targetLangCode = getModelLangCode(to);

  const sourceLangName = langFullNameMap[sourceLangCode] || from;
  const targetLangName = langFullNameMap[targetLangCode] || to;

  const prompt = `Translate it to ${targetLangName}:\n${text} <${targetLangCode}>`;

  // 构建请求体对象
  const bodyObject = {
    model: "./ByteDance-Seed/Seed-X-PPO-7B-AWQ-Int4",
    prompt: prompt,
    max_tokens: 2048,
    temperature: 0.0,
  };

  // 返回最终的请求配置
  return [url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    // 关键改动:将 JavaScript 对象转换为 JSON 字符串
    body: JSON.stringify(bodyObject),
  }];
}
```

Response Hook

```js
(res, text, from, to) => {
  // 检查返回是否有效
  if (res && res.choices && res.choices.length > 0 && res.choices[0].text) {

    // 提取译文并去除可能存在的前后空格
    const translatedText = res.choices[0].text.trim();

    // 比较原文与译文,相同为 true,否则为 false。
    const areTextsIdentical = text.trim() === translatedText;

    // 返回数组:[翻译后的文本, 是否与原文相同]
    return [translatedText, areTextsIdentical];
  }
  // 如果响应格式不正确或没有结果,则抛出错误
  throw new Error("Invalid API response format or no translation found.");
}
```

## 接入 openrouter

> 由网友 Rick Sanchez 提供

URL

```sh
https://openrouter.ai/api/v1/chat/completions
```

Request Hook

```js
(text, from, to, url, key) => [url, {
  method: "POST",
  headers: {
      "Authorization": `Bearer ${key}`,
      "Content-type": "application/json",
  },
  body: JSON.stringify({
    "model": "deepseek/deepseek-chat-v3-0324:free", //可自定义你的模型
    "messages": [
      {
        "role": "user",
        "content":  //可自定义你的提示词
`You are a professional ${to} native translator. Your task is to produce a fluent, natural, and culturally appropriate translation of the following text from ${from} to ${to}, fully conveying the meaning, tone, and nuance of the original.

## Translation Rules
1. Output only the final polished translation — no explanations, intermediate drafts, or notes.
2. Translate in a way that reads naturally to a native ${to} audience, adapting idioms, cultural references, and tone when necessary.
3. Preserve proper nouns, technical terms, brand names, and URLs exactly as in the original text unless a widely accepted ${to} equivalent exists.
4. Keep any formatting (Markdown, HTML tags, bullet points, numbering) intact and positioned naturally within the translation.
5. Adapt humor, metaphors, and figurative language to culturally relevant forms in ${to} while keeping the original intent.
6. Maintain the same level of formality or informality as the original.

Source Text: ${text}

Translated Text:`
      }
    ]
  })
}]
```

Response Hook

```js
(res, text, from, to) => [
  res.choices?.[0]?.message?.content ?? "", 
  false
]
```

## 接入 gemini-2.5-flash, 关闭思考模式, 去审查

> 由网友 Rick Sanchez 提供

URL

```sh
https://generativelanguage.googleapis.com/v1beta/models
```

Request Hook

```js
(text, from, to, url, key) => [`${url}/gemini-2.5-flash:generateContent?key=${key}`, {
    headers: {
        "Content-Type": "application/json",
    },
    method: "POST",
    body: JSON.stringify({
        "generationConfig": {
            "temperature": 0.8,
            "thinkingConfig": {
                "thinkingBudget": 0, //gemini-2.5-flash设为0关闭思考模式
            },
        },
        "safetySettings": [
            {
                "category": "HARM_CATEGORY_HARASSMENT",
                "threshold": "BLOCK_NONE",
            },
            {
                "category": "HARM_CATEGORY_HATE_SPEECH",
                "threshold": "BLOCK_NONE",
            },
            {
                "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
                "threshold": "BLOCK_NONE",
            },
            {
                "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
                "threshold": "BLOCK_NONE",
            }
        ],
        "contents": [{
            "parts": [{
                "text": `自定义提示词`
            }]
        }],
    }),
}]
```

Response Hook

```js
(res, text, from, to) => [
  res.candidates?.[0]?.content?.parts?.[0]?.text ?? "",
  false
]
```

## 接入 Qwen-MT

> 由网友 atom 提供

URL

```sh
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
```

Request Hook

```js
(text, from, to, url, key) => {
  const mapLanguageCode = (lang) => ({
    'zh-CN': 'zh',
    'zh-TW': 'zh_tw',
  })[lang] || lang;

  const targetLang = mapLanguageCode(to);

  return [
    url,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${key}`
      },
      body: JSON.stringify({
        "model": "qwen-mt-turbo",
        "messages": [
          {
            "role": "user",
            "content": text
          }
        ],
        "translation_options": {
          "source_lang": "auto",
          "target_lang": targetLang
        }
      })
    }
  ];
}
```

Response Hook

```js
(res, text, from, to) => [res.choices?.[0]?.message?.content ?? "", false]
```


## 接入 deepl 接口

> 来源: https://github.com/fishjar/kiss-translator/issues/101#issuecomment-2123786236

Request Hook

```js
(text, from, to, url, key) => [
  url,
  {
    headers: {
      "Content-type": "application/json",
    },
    method: "POST",
    body: JSON.stringify({
      text,
      target_lang: "ZH",
      source_lang: "auto",
    }),
  },
]
```

Response Hook

```js
(res, text, from, to) => [res.data, "ZH" === res.source_lang]
```

## 接入智谱AI大模型

> 来源: https://github.com/fishjar/kiss-translator/issues/205#issuecomment-2642422679

Request Hook

```js
(text, from, to, url, key) => [url, {
  "method": "POST",
  "headers": {
    "Content-type": "application/json",
    "Authorization": key
  },
  "body": JSON.stringify({
  	"model": "glm-4-flash",
  	"messages": [
  		{
  			"role":"system",
  			"content": "You are a professional, authentic machine translation engine. You only return the translated text, without any explanations."
  		},
  		{
  			"role": "user",
  			"content": `Translate the following text into ${to}. If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes:\n\n ${text} `
  		}
  	]
  })
}]
```

## 接入谷歌新接口

> 由网友 Bush2021 提供,来源:https://github.com/fishjar/kiss-translator/issues/225#issuecomment-2810950717

URL

```sh
https://translate-pa.googleapis.com/v1/translateHtml
```

KEY

```sh
AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520
```

Request Hook

```js
(text, from, to, url, key) => [url, {
    method: "POST", 
    headers: { 
        "Content-Type": "application/json+protobuf", 
        "X-Goog-API-Key": key
    }, 
    body: JSON.stringify([[[text], from || "auto", to], "wt_lib"])
}]
```

Response Hook

```js
(res, text, from, to) => [res?.[0]?.join(" ") || "Translation unavailable", to === res?.[1]?.[0]]
```




================================================
FILE: custom-api_v2.md
================================================
# 自定义接口说明及示例

## 默认接口规范

如果接口的请求数据和返回数据符合以下规范,
则无需填写 `Request Hook` 或 `Response Hook`。


### 非聚合翻译

Request body

```json
{
  "text": "hello",    // 需要翻译的文本列表
  "from":"auto",      // 原文语言
  "to": "zh-CN"       // 目标语言
}
```

Response

```json
{
  "text": "你好",    // 译文
  "src": "en"       // 原文语言
}

// 或者
{
  "text": "你好",    // 译文
  "from": "en"       // 原文语言
}
```


### 聚合翻译

Request body

```json
{
  "texts": ["hello"], // 需要翻译的文本列表
  "from":"auto",      // 原文语言
  "to": "zh-CN"       // 目标语言
}
```

Response

```json
[
  {
    "text": "你好",    // 译文
    "src": "en"       // 原文语言
  }
]
```

v2.0.4版后亦支持以下 Response 格式

```json
{
  "translations": [   // 译文列表
    {
      "text": "你好",  // 译文
      "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
```

## 谷歌翻译接口

> 此接口不支持聚合

URL

```
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
```

Request Hook

```js
async (args) => {
  const url = args.url.replace("{{text}}", args.texts[0]);
  const method = "GET";
  return { url, method };
};
```

Response Hook

```js
async ({ res }) => {
  return { translations: [[res?.sentences?.[0]?.trans || "", res?.src]] };
};
```


## Ollama

> 此示例为开启聚合翻译的写法

* 注意 ollama 启动参数需要添加环境变量 `OLLAMA_ORIGINS=*`
* 检查环境变量生效命令:`systemctl show ollama | grep OLLAMA_ORIGINS`

URL

```
http://localhost:11434/v1/chat/completions
```

Request Hook

```js
async (args) => {
  const url = args.url;
  const method = "POST";
  const headers = { "Content-type": "application/json" };
  const body = {
    model: "gemma3", // 或 args.model
    messages: [
      {
        role: "system",
        content: args.systemPrompt,
      },
      {
        role: "user",
        content: JSON.stringify({
          targetLanguage: args.toLang,
          segments: args.texts.map((text, id) => ({ id, text })),
          title: "", // 可省略
          description: "", // 可省略
          glossary: {}, // 可省略
          tone: "", // 可省略
        }),
      },
    ],
    temperature: 0,
    max_tokens: 20480,
    think: false,
    stream: false,
  };

  return { url, body, headers, method };
};
```

Response Hook

```js
async ({ res, parseAIRes }) => {
  const translations = parseAIRes(res?.choices?.[0]?.message?.content);
  return { translations };
};
```


## 硅基流动

> 此示例为禁用聚合翻译的写法

URL

```
https://api.siliconflow.cn/v1/chat/completions
```

Request Hook

```js
async (args) => {
  const url = args.url;
  const method = "POST";
  const headers = {
    "Content-type": "application/json",
    Authorization: `Bearer ${args.key}`,
  };
  const body = {
    model: "tencent/Hunyuan-MT-7B", // 或 args.model,
    messages: [
      {
        role: "system",
        content: args.systemPrompt,
      },
      {
        role: "user",
        content: args.userPrompt,
      },
    ],
    temperature: 0,
    max_tokens: 20480,
  };

  return { url, body, headers, method };
};
```

Response Hook

```js
async ({ res }) => {
  return { translations: [[res?.choices?.[0]?.message?.content || ""]] };
};
```


## 语言代码表及说明

Hook参数里面的语言含义说明:

- `toLang`, `fromLang` 是本插件支持的标准语言代码
- `to`, `from` 是转换后的适用于特定接口的语言代码

如果你的自定义接口与下面的标准语言代码不匹配,需要自行映射转换。

```
["en", "English - English"],
["zh-CN", "Simplified Chinese - 简体中文"],
["zh-TW", "Traditional Chinese - 繁體中文"],
["ar", "Arabic - العربية"],
["bg", "Bulgarian - Български"],
["ca", "Catalan - Català"],
["hr", "Croatian - Hrvatski"],
["cs", "Czech - Čeština"],
["da", "Danish - Dansk"],
["nl", "Dutch - Nederlands"],
["fa", "Persian - فارسی"],
["fi", "Finnish - Suomi"],
["fr", "French - Français"],
["de", "German - Deutsch"],
["el", "Greek - Ελληνικά"],
["hi", "Hindi - हिन्दी"],
["hu", "Hungarian - Magyar"],
["id", "Indonesian - Indonesia"],
["it", "Italian - Italiano"],
["ja", "Japanese - 日本語"],
["ko", "Korean - 한국어"],
["ms", "Malay - Melayu"],
["mt", "Maltese - Malti"],
["nb", "Norwegian - Norsk Bokmål"],
["pl", "Polish - Polski"],
["pt", "Portuguese - Português"],
["ro", "Romanian - Română"],
["ru", "Russian - Русский"],
["sk", "Slovak - Slovenčina"],
["sl", "Slovenian - Slovenščina"],
["es", "Spanish - Español"],
["sv", "Swedish - Svenska"],
["ta", "Tamil - தமிழ்"],
["te", "Telugu - తెలుగు"],
["th", "Thai - ไทย"],
["tr", "Turkish - Türkçe"],
["uk", "Ukrainian - Українська"],
["vi", "Vietnamese - Tiếng Việt"],
```


================================================
FILE: package.json
================================================
{
  "name": "kiss-translator",
  "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
  "version": "2.0.20",
  "author": "Gabe<yugang2002@gmail.com>",
  "private": true,
  "dependencies": {
    "@emotion/cache": "^11.11.0",
    "@emotion/css": "^11.13.5",
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
    "@mui/icons-material": "^5.15.15",
    "@mui/lab": "5.0.0-alpha.170",
    "@mui/material": "^5.15.15",
    "@streamparser/json": "^0.0.22",
    "query-string": "^8.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-markdown": "^8.0.7",
    "react-router-dom": "^6.16.0",
    "react-scripts": "5.0.1",
    "sval": "^0.5.2",
    "webdav": "^5.3.0",
    "webextension-polyfill": "^0.10.0"
  },
  "scripts": {
    "start": "cross-env REACT_APP_CLIENT=web react-app-rewired start",
    "start:userscript": "cross-env REACT_APP_CLIENT=userscript react-app-rewired start",
    "sync-version": "zx src/scripts/sync-version.mjs",
    "version:patch": "zx src/scripts/update-version.mjs patch",
    "version:minor": "zx src/scripts/update-version.mjs minor",
    "version:major": "zx src/scripts/update-version.mjs major",
    "version:set": "zx src/scripts/update-version.mjs set",
    "build:chrome": "zx src/scripts/build-task.mjs --target=chrome",
    "build:edge": "zx src/scripts/build-task.mjs --target=edge",
    "build:safari-output": "zx src/scripts/build-task.mjs --target=safari",
    "build:thunderbird": "zx src/scripts/build-task.mjs --target=thunderbird",
    "build:firefox": "zx src/scripts/build-task.mjs --target=firefox",
    "build:web": "zx src/scripts/build-task.mjs --target=web",
    "build:safari": "node src/scripts/build-safari.mjs",
    "build:userscript-ios": "zx src/scripts/build-ios.mjs",
    "build:rules": "babel-node src/rules.js",
    "format": "prettier --write \"**/*.{js,json,html}\"",
    "build": "pnpm sync-version && pnpm format && pnpm build:chrome && pnpm build:edge && pnpm build:thunderbird && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:rules",
    "zip": "zx src/scripts/archive.mjs",
    "build+zip": "cross-env CI=true pnpm build && pnpm zip",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "globals": {
      "GM": true,
      "unsafeWindow": true,
      "globalThis": true,
      "messenger": true,
      "LanguageDetector": true,
      "Translator": true
    }
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@babel/core": "^7.22.20",
    "@babel/node": "^7.22.19",
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    "@babel/preset-env": "^7.22.20",
    "bestzip": "^2.2.1",
    "cross-env": "^7.0.3",
    "dotenv": "^17.2.1",
    "find-up": "^7.0.0",
    "prettier": "3.6.2",
    "react-app-rewired": "^2.2.1",
    "zx": "^8.8.1"
  }
}


================================================
FILE: public/.nojekyll
================================================


================================================
FILE: public/_locales/de/messages.json
================================================
{
  "app_name": {
    "message": "KISS Übersetzer"
  },
  "app_description": {
    "message": "Eine minimalistische zweisprachige Übersetzungserweiterung, die Webseiten-, Textauswahl- und Videountertitel-Übersetzung unterstützt."
  },
  "toggle_translate": {
    "message": "Übersetzung umschalten"
  },
  "toggle_style": {
    "message": "Stile umschalten"
  },
  "open_options": {
    "message": "Einstellungen öffnen"
  },
  "open_tranbox": {
    "message": "Popup-Fenster öffnen"
  },
  "open_separate_window": {
    "message": "In eigenem Fenster öffnen"
  }
}


================================================
FILE: public/_locales/en/messages.json
================================================
{
  "app_name": {
    "message": "KISS Translator"
  },
  "app_description": {
    "message": "A minimalist bilingual translation extension that supports webpage, text selection, and video subtitle translation."
  },
  "toggle_translate": {
    "message": "Toggle Translation"
  },
  "toggle_style": {
    "message": "Toggle Styles"
  },
  "open_options": {
    "message": "Open Setting"
  },
  "open_tranbox": {
    "message": "Open Popup Box"
  },
  "open_separate_window": {
    "message": "Open Independent Window"
  }
}


================================================
FILE: public/_locales/es/messages.json
================================================
{
  "app_name": {
    "message": "KISS Traductor"
  },
  "app_description": {
    "message": "Una extensión de traducción bilingüe minimalista que admite la traducción de páginas web, selección de texto y subtítulos de video."
  },
  "toggle_translate": {
    "message": "Alternar traducción"
  },
  "toggle_style": {
    "message": "Cambiar estilo"
  },
  "open_options": {
    "message": "Abrir configuración"
  },
  "open_tranbox": {
    "message": "Abrir ventana emergente"
  },
  "open_separate_window": {
    "message": "Abrir en ventana independiente"
  }
}


================================================
FILE: public/_locales/fr/messages.json
================================================
{
  "app_name": {
    "message": "KISS Traducteur"
  },
  "app_description": {
    "message": "Une extension de traduction bilingue minimaliste prenant en charge la traduction de pages web, de texte sélectionné et de sous-titres vidéo."
  },
  "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"
  },
  "open_separate_window": {
    "message": "Ouvrir dans une fenêtre indépendante"
  }
}


================================================
FILE: public/_locales/ja/messages.json
================================================
{
  "app_name": {
    "message": "シンプル翻訳"
  },
  "app_description": {
    "message": "Webサイト、テキスト選択、動画字幕などの翻訳に対応した、シンプルで直感的な二言語対応翻訳拡張機能。複数の翻訳サービスやAI翻訳APIをサポートし、豊富で柔軟なカスタマイズオプションを備えています。"
  },
  "toggle_translate": {
    "message": "翻訳の切り替え"
  },
  "toggle_style": {
    "message": "スタイル切り替え"
  },
  "open_options": {
    "message": "設定を開く"
  },
  "open_tranbox": {
    "message": "ポップアップを開く"
  },
  "open_separate_window": {
    "message": "独立ウィンドウで開く"
  }
}


================================================
FILE: public/_locales/ko/messages.json
================================================
{
  "app_name": {
    "message": "심플 번역"
  },
  "app_description": {
    "message": "웹페이지, 단어 선택, 동영상 자막 번역 등을 지원하는 심플한 이중 언어 대조 번역 확장 프로그램. 다양한 번역 서비스 및 AI 번역 인터페이스를 지원하며, 풍부하고 유연한 사용자 설정 옵션을 제공합니다."
  },
  "toggle_translate": {
    "message": "번역 켜기"
  },
  "toggle_style": {
    "message": "스타일 전환"
  },
  "open_options": {
    "message": "설정 열기"
  },
  "open_tranbox": {
    "message": "팝업 열기"
  },
  "open_separate_window": {
    "message": "독립 창으로 열기"
  }
}


================================================
FILE: public/_locales/zh_CN/messages.json
================================================
{
  "app_name": {
    "message": "简约翻译"
  },
  "app_description": {
    "message": "一个简约的双语对照翻译扩展,支持网页、划词、视频字幕翻译等功能,支持多种翻译服务及AI翻译接口,拥有丰富灵活的自定义选项。"
  },
  "toggle_translate": {
    "message": "开启翻译"
  },
  "toggle_style": {
    "message": "切换样式"
  },
  "open_options": {
    "message": "打开设置"
  },
  "open_tranbox": {
    "message": "打开弹窗"
  },
  "open_separate_window": {
    "message": "打开独立窗口"
  }
}


================================================
FILE: public/_locales/zh_TW/messages.json
================================================
{
  "app_name": {
    "message": "簡約翻譯"
  },
  "app_description": {
    "message": "一個簡約的雙語對照翻譯擴充功能,支持網頁、劃詞、影片字幕翻譯等功能,支持多種翻譯服務及 AI 翻譯接口,擁有豐富靈活的自定義選項。"
  },
  "toggle_translate": {
    "message": "開啟翻譯"
  },
  "toggle_style": {
    "message": "切換樣式"
  },
  "open_options": {
    "message": "開啟設定"
  },
  "open_tranbox": {
    "message": "開啟彈出視窗"
  },
  "open_separate_window": {
    "message": "打開獨立視窗"
  }
}


================================================
FILE: public/content.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>%REACT_APP_NAME%</title>
  </head>

  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root">
    </div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>


================================================
FILE: public/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>%REACT_APP_NAME% v%REACT_APP_VERSION%</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>


================================================
FILE: public/manifest.firefox.json
================================================
{
  "manifest_version": 2,
  "name": "__MSG_app_name__",
  "description": "__MSG_app_description__",
  "version": "2.0.20",
  "default_locale": "en",
  "author": "Gabe<yugang2002@gmail.com>",
  "homepage_url": "https://github.com/fishjar/kiss-translator",
  "background": {
    "scripts": [
      "background.js"
    ]
  },
  "content_scripts": [
    {
      "js": [
        "content.js"
      ],
      "matches": [
        "<all_urls>",
        "file://*/*"
      ],
      "all_frames": true
    }
  ],
  "web_accessible_resources": [
    "injector-subtitle.js",
    "injector-shadowroot.js"
  ],
  "commands": {
    "_execute_browser_action": {
      "suggested_key": {
        "default": "Alt+K"
      }
    },
    "toggleTranslate": {
      "suggested_key": {
        "default": "Alt+Q"
      },
      "description": "__MSG_toggle_translate__"
    },
    "openTranbox": {
      "suggested_key": {
        "default": "Alt+S"
      },
      "description": "__MSG_open_tranbox__"
    },
    "openSeparateWindow": {
      "description": "__MSG_open_separate_window__"
    },
    "toggleStyle": {
      "suggested_key": {
        "default": "Alt+C"
      },
      "description": "__MSG_toggle_style__"
    },
    "openOptions": {
      "description": "__MSG_open_options__"
    }
  },
  "permissions": [
    "<all_urls>",
    "storage",
    "contextMenus",
    "scripting",
    "declarativeNetRequest"
  ],
  "icons": {
    "16": "images/logo16.png",
    "32": "images/logo32.png",
    "48": "images/logo48.png",
    "128": "images/logo128.png"
  },
  "browser_action": {
    "default_icon": {
      "128": "images/logo128.png"
    },
    "default_title": "__MSG_app_name__",
    "default_popup": "popup.html"
  },
  "options_ui": {
    "page": "options.html",
    "open_in_tab": true
  }
}


================================================
FILE: public/manifest.json
================================================
{
  "manifest_version": 3,
  "name": "__MSG_app_name__",
  "description": "__MSG_app_description__",
  "version": "2.0.20",
  "default_locale": "en",
  "author": "Gabe<yugang2002@gmail.com>",
  "homepage_url": "https://github.com/fishjar/kiss-translator",
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "js": [
        "content.js"
      ],
      "matches": [
        "<all_urls>",
        "file://*/*"
      ],
      "all_frames": true
    }
  ],
  "web_accessible_resources": [
    {
      "resources": [
        "injector-subtitle.js"
      ],
      "matches": [
        "https://www.youtube.com/*"
      ]
    },
    {
      "resources": [
        "injector-shadowroot.js"
      ],
      "matches": [
        "<all_urls>"
      ]
    }
  ],
  "commands": {
    "_execute_action": {
      "suggested_key": {
        "default": "Alt+K"
      }
    },
    "toggleTranslate": {
      "suggested_key": {
        "default": "Alt+Q"
      },
      "description": "__MSG_toggle_translate__"
    },
    "openTranbox": {
      "suggested_key": {
        "default": "Alt+S"
      },
      "description": "__MSG_open_tranbox__"
    },
    "openSeparateWindow": {
      "description": "__MSG_open_separate_window__"
    },
    "toggleStyle": {
      "suggested_key": {
        "default": "Alt+C"
      },
      "description": "__MSG_toggle_style__"
    },
    "openOptions": {
      "description": "__MSG_open_options__"
    }
  },
  "permissions": [
    "storage",
    "contextMenus",
    "scripting",
    "declarativeNetRequest",
    "declarativeNetRequestWithHostAccess"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "icons": {
    "16": "images/logo16.png",
    "32": "images/logo32.png",
    "48": "images/logo48.png",
    "128": "images/logo128.png"
  },
  "action": {
    "default_icon": {
      "128": "images/logo128.png"
    },
    "default_title": "__MSG_app_name__",
    "default_popup": "popup.html"
  },
  "options_ui": {
    "page": "options.html",
    "open_in_tab": true
  }
}


================================================
FILE: public/manifest.thunderbird.json
================================================
{
  "manifest_version": 2,
  "name": "__MSG_app_name__",
  "description": "__MSG_app_description__",
  "version": "2.0.20",
  "default_locale": "en",
  "author": "Gabe<yugang2002@gmail.com>",
  "homepage_url": "https://github.com/fishjar/kiss-translator",
  "browser_specific_settings": {
    "gecko": {
      "id": "yugang2002@gmail.com",
      "strict_min_version": "78.0"
    }
  },
  "background": {
    "scripts": [
      "background.js"
    ]
  },
  "content_scripts": [
    {
      "js": [
        "content.js"
      ],
      "matches": [
        "<all_urls>",
        "file://*/*"
      ],
      "all_frames": true
    }
  ],
  "web_accessible_resources": [
    "injector-subtitle.js",
    "injector-shadowroot.js"
  ],
  "commands": {
    "_execute_browser_action": {
      "suggested_key": {
        "default": "Alt+K"
      }
    },
    "toggleTranslate": {
      "suggested_key": {
        "default": "Alt+Q"
      },
      "description": "__MSG_toggle_translate__"
    },
    "openTranbox": {
      "suggested_key": {
        "default": "Alt+S"
      },
      "description": "__MSG_open_tranbox__"
    },
    "openSeparateWindow": {
      "description": "__MSG_open_separate_window__"
    },
    "toggleStyle": {
      "suggested_key": {
        "default": "Alt+C"
      },
      "description": "__MSG_toggle_style__"
    },
    "openOptions": {
      "description": "__MSG_open_options__"
    }
  },
  "permissions": [
    "<all_urls>",
    "storage",
    "menus",
    "messagesModify",
    "scripting",
    "declarativeNetRequest"
  ],
  "icons": {
    "16": "images/logo16.png",
    "32": "images/logo32.png",
    "48": "images/logo48.png",
    "128": "images/logo128.png"
  },
  "browser_action": {
    "default_icon": {
      "128": "images/logo128.png"
    },
    "default_title": "__MSG_app_name__",
    "default_popup": "popup.html"
  },
  "options_ui": {
    "page": "options.html",
    "open_in_tab": true
  }
}


================================================
FILE: src/apis/baidu.js
================================================
import { DEFAULT_USER_AGENT } from "../config";

export const genBaidu = ({ texts, from, to }) => {
  const body = {
    from,
    to,
    query: texts.join(" "),
    source: "txt",
  };

  const url = "https://fanyi.baidu.com/transapi";
  const headers = {
    // Origin: "https://fanyi.baidu.com",
    "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
    "User-Agent": DEFAULT_USER_AGENT,
  };

  return { url, body, headers };
};


================================================
FILE: src/apis/deepl.js
================================================
let id = 1e4 * Math.round(1e4 * Math.random());

export const genDeeplFree = ({ texts, from, to }) => {
  const text = texts.join(" ");
  const iCount = (text.match(/[i]/g) || []).length + 1;
  let timestamp = Date.now();
  timestamp = timestamp + (iCount - (timestamp % iCount));
  id++;

  const url = "https://www2.deepl.com/jsonrpc";

  const body = {
    jsonrpc: "2.0",
    method: "LMT_handle_texts",
    params: {
      splitting: "newlines",
      lang: {
        target_lang: to,
        source_lang_user_selected: from,
      },
      commonJobParams: {
        wasSpoken: false,
        transcribe_as: "",
      },
      id,
      timestamp,
      texts: [
        {
          text,
          requestAlternatives: 3,
        },
      ],
    },
  };

  const headers = {
    "Content-Type": "application/json",
    Accept: "*/*",
    "x-app-os-name": "iOS",
    "x-app-os-version": "16.3.0",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "x-app-device": "iPhone13,2",
    "User-Agent": "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)",
    "x-app-build": "510265",
    "x-app-version": "2.9.1",
  };

  return { url, body, headers };
};


================================================
FILE: src/apis/history.js
================================================
import { DEFAULT_CONTEXT_SIZE } from "../config";

const historyMap = new Map();

const MsgHistory = (maxSize = DEFAULT_CONTEXT_SIZE) => {
  const messages = [];

  const add = (...msgs) => {
    messages.push(...msgs.filter(Boolean));
    const extra = messages.length - maxSize;
    if (extra > 0) {
      messages.splice(0, extra);
    }
  };

  const getAll = () => {
    return [...messages];
  };

  const clear = () => {
    messages.length = 0;
  };

  return {
    add,
    getAll,
    clear,
  };
};

export const getMsgHistory = (apiSlug, maxSize) => {
  if (historyMap.has(apiSlug)) {
    return historyMap.get(apiSlug);
  }

  const msgHistory = MsgHistory(maxSize);
  historyMap.set(apiSlug, msgHistory);
  return msgHistory;
};


================================================
FILE: src/apis/index.js
================================================
import queryString from "query-string";
import { fetchData } from "../libs/fetch";
import {
  URL_CACHE_TRAN,
  URL_CACHE_DELANG,
  URL_CACHE_BINGDICT,
  KV_SALT_SYNC,
  OPT_LANGS_TO_SPEC,
  OPT_LANGS_SPEC_DEFAULT,
  API_SPE_TYPES,
  DEFAULT_API_SETTING,
  OPT_TRANS_MICROSOFT,
  MSG_BUILTINAI_DETECT,
  MSG_BUILTINAI_TRANSLATE,
  OPT_TRANS_BUILTINAI,
  URL_CACHE_SUBTITLE,
  OPT_LANGS_TO_CODE,
} from "../config";
import { sha256, withTimeout } from "../libs/utils";
import {
  handleTranslate,
  handleSubtitle,
  handleMicrosoftLangdetect,
} from "./trans";
import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache";
import { getBatchQueue } from "../libs/batchQueue";
import { isBuiltinAIAvailable } from "../libs/browser";
import { chromeDetect, chromeTranslate } from "../libs/builtinAI";
import { fnPolyfill } from "../libs/fetch";
import { getFetchPool } from "../libs/pool";

/**
 * 同步数据
 * @param {*} url
 * @param {*} key
 * @param {*} data
 * @returns
 */
export const apiSyncData = async (url, key, data) =>
  fetchData(url, {
    headers: {
      "Content-type": "application/json",
      Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
    },
    method: "POST",
    body: JSON.stringify(data),
  });

/**
 * 下载数据
 * @param {*} url
 * @returns
 */
export const apiFetch = (url) => fetchData(url);

/**
 * Microsoft token
 * @returns
 */
export const apiMsAuth = async () =>
  fetchData("https://edge.microsoft.com/translate/auth");

/**
 * Google语言识别
 * @param {*} text
 * @returns
 */
export const apiGoogleLangdetect = async (text) => {
  const params = {
    client: "gtx",
    dt: "t",
    dj: 1,
    ie: "UTF-8",
    sl: "auto",
    tl: "zh-CN",
    q: text,
  };
  const input = `https://translate.googleapis.com/translate_a/single?${queryString.stringify(params)}`;
  const init = {
    headers: {
      "Content-type": "application/json",
    },
  };
  const res = await fetchData(input, init, { useCache: true });

  if (res?.src) {
    await putHttpCachePolyfill(input, init, res);
    return res.src;
  }

  return "";
};

/**
 * Microsoft语言识别
 * @param {*} text
 * @returns
 */
export const apiMicrosoftLangdetect = async (text) => {
  const cacheOpts = { text, detector: OPT_TRANS_MICROSOFT };
  const cacheInput = `${URL_CACHE_DELANG}?${queryString.stringify(cacheOpts)}`;
  const cache = await getHttpCachePolyfill(cacheInput);
  if (cache) {
    return cache;
  }

  const key = `${URL_CACHE_DELANG}_${OPT_TRANS_MICROSOFT}`;
  const queue = getBatchQueue(key, handleMicrosoftLangdetect, {
    batchInterval: 200,
    batchSize: 20,
    batchLength: 100000,
  });
  const lang = await queue.addTask(text);

  if (lang) {
    putHttpCachePolyfill(cacheInput, null, lang);
    return lang;
  }

  return "";
};

/**
 * Microsoft词典
 * @param {*} text
 * @returns
 */
export const apiMicrosoftDict = async (text) => {
  const cacheOpts = { text };
  const cacheInput = `${URL_CACHE_BINGDICT}?${queryString.stringify(cacheOpts)}`;
  const cache = await getHttpCachePolyfill(cacheInput);
  if (cache) {
    return cache;
  }

  const host = "https://www.bing.com";
  const url = `${host}/dict/search?q=${text}&FORM=BDVSP6&cc=cn`;
  const str = await fetchData(
    url,
    { credentials: "include" },
    { useCache: false }
  );
  if (!str) {
    return null;
  }

  const parser = new DOMParser();
  const doc = parser.parseFromString(str, "text/html");

  const word = doc.querySelector("#headword > h1")?.textContent.trim();
  if (!word) {
    return null;
  }

  const trs = [];
  doc.querySelectorAll("div.qdef > ul > li").forEach(($li) => {
    const pos = $li.querySelector(".pos")?.textContent?.trim();
    const def = $li.querySelector(".def")?.textContent?.trim();
    trs.push({ pos, def });
  });

  // 时态
  const presents = [];
  doc.querySelectorAll("div.hd_div1>.hd_if>.p1-5").forEach(($li) => {
    const present = $li.textContent?.trim();
    presents.push(present);
  });

  // 英汉双解
  const ecs = [];
  doc.querySelectorAll(".each_seg>.li_pos").forEach(($li) => {
    const pos = $li.querySelector(".pos_lin>.pos")?.textContent?.trim();
    const lis = [];
    $li.querySelectorAll(".de_seg>.se_lis").forEach(($l) => {
      lis.push($l.querySelector(".de_co")?.textContent?.trim());
    });
    ecs.push({ pos, lis });
  });

  // 添加例句信息
  const sentences = [];
  doc.querySelectorAll("#sentenceSeg .se_li").forEach(($li) => {
    const eng = $li.querySelector(".sen_en")?.textContent?.trim();
    const chs = $li.querySelector(".sen_cn")?.textContent?.trim();
    if (eng && chs) {
      sentences.push({ eng, chs });
    }
  });

  const aus = [];
  const $audioUK = doc.querySelector("#bigaud_uk");
  const $audioUS = doc.querySelector("#bigaud_us");

  // 检查 UK 音频和音标
  if ($audioUK) {
    const audioUK = host + $audioUK?.dataset?.mp3link;
    const $phoneticUK = $audioUK.parentElement?.previousElementSibling;
    const phoneticUK = $phoneticUK?.textContent
      ?.trim()
      ?.match(/\[(.*?)\]/)?.[1];
    aus.push({ key: "英", audio: audioUK, phonetic: phoneticUK });
  }

  // 检查 US 音频和音标
  if ($audioUS) {
    const audioUS = host + $audioUS?.dataset?.mp3link;
    const $phoneticUS = $audioUS.parentElement?.previousElementSibling;
    const phoneticUS = $phoneticUS?.textContent
      ?.trim()
      ?.match(/\[(.*?)\]/)?.[1];
    aus.push({ key: "美", audio: audioUS, phonetic: phoneticUS });
  }

  // 如果上面的方法没有获取到音标,尝试其他方式
  if (aus.length === 0) {
    const $pronInfo = doc.querySelector(".hd_pr");
    const $pronInfoUS = doc.querySelector(".hd_prUS");

    if ($pronInfo) {
      const phoneticText = $pronInfo.textContent?.trim();
      // 尝试提取音标部分
      const phoneticMatch = phoneticText?.match(/\[([^\]]+)\]/);
      if (phoneticMatch) {
        aus.push({ key: "英", phonetic: phoneticMatch[1] });
      }
    }

    if ($pronInfoUS) {
      const phoneticText = $pronInfoUS.textContent?.trim();
      // 尝试提取音标部分
      const phoneticMatch = phoneticText?.match(/\[([^\]]+)\]/);
      if (phoneticMatch) {
        aus.push({ key: "美", phonetic: phoneticMatch[1] });
      }
    }
  }

  const res = { word, trs, aus, ecs, sentences, presents };
  putHttpCachePolyfill(cacheInput, null, res);

  return res;
};

/**
 * 百度语言识别
 * @param {*} text
 * @returns
 */
export const apiBaiduLangdetect = async (text) => {
  const input = "https://fanyi.baidu.com/langdetect";
  const init = {
    headers: {
      "Content-type": "application/json",
    },
    method: "POST",
    body: JSON.stringify({
      query: text,
    }),
  };
  const res = await fetchData(input, init, { useCache: true });

  if (res?.error === 0) {
    await putHttpCachePolyfill(input, init, res);
    return res.lan;
  }

  return "";
};

/**
 * 百度翻译建议
 * @param {*} text
 * @returns
 */
export const apiBaiduSuggest = async (text) => {
  const input = "https://fanyi.baidu.com/sug";
  const init = {
    headers: {
      "Content-type": "application/json",
    },
    method: "POST",
    body: JSON.stringify({
      kw: text,
    }),
  };
  const res = await fetchData(input, init, { useCache: true });

  if (res?.errno === 0) {
    await putHttpCachePolyfill(input, init, res);
    return res.data;
  }

  return [];
};

/**
 * 有道翻译建议
 * @param {*} text
 * @returns
 */
export const apiYoudaoSuggest = async (text) => {
  const params = {
    num: 5,
    ver: 3.0,
    doctype: "json",
    cache: false,
    le: "en",
    q: text,
  };
  const input = `https://dict.youdao.com/suggest?${queryString.stringify(params)}`;
  const init = {
    headers: {
      accept: "application/json, text/plain, */*",
      "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
      "content-type": "application/x-www-form-urlencoded",
    },
    method: "GET",
  };
  const res = await fetchData(input, init, { useCache: true });

  if (res?.result?.code === 200) {
    await putHttpCachePolyfill(input, init, res);
    return res.data.entries;
  }

  return [];
};

/**
 * 有道词典
 * @param {*} text
 * @returns
 */
export const apiYoudaoDict = async (text) => {
  const params = {
    doctype: "json",
    jsonversion: 4,
  };
  const input = `https://dict.youdao.com/jsonapi_s?${queryString.stringify(params)}`;
  const body = queryString.stringify({
    q: text,
    le: "en",
    t: 3,
    client: "web",
    // sign: "",
    keyfrom: "webdict",
  });
  const init = {
    headers: {
      accept: "application/json, text/plain, */*",
      "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
      "content-type": "application/x-www-form-urlencoded",
    },
    method: "POST",
    body,
  };
  const res = await fetchData(input, init, { useCache: true });

  if (res) {
    await putHttpCachePolyfill(input, init, res);
    return res;
  }

  return null;
};

/**
 * 百度语音
 * @param {*} text
 * @param {*} lan
 * @param {*} spd
 * @returns
 */
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
  const input = `https://fanyi.baidu.com/gettts?${queryString.stringify({ lan, text, spd })}`;
  return fetchData(input);
};

/**
 * 腾讯语言识别
 * @param {*} text
 * @returns
 */
export const apiTencentLangdetect = async (text) => {
  const input = "https://transmart.qq.com/api/imt";
  const body = JSON.stringify({
    header: {
      fn: "text_analysis",
      client_key:
        "browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
    },
    text,
  });
  const init = {
    headers: {
      "Content-type": "application/json",
      "user-agent":
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
      referer: "https://transmart.qq.com/zh-CN/index",
    },
    method: "POST",
    body,
  };
  const res = await fetchData(input, init, { useCache: true });

  if (res?.language) {
    await putHttpCachePolyfill(input, init, res);
    return res.language;
  }

  return "";
};

/**
 * 浏览器内置AI语言识别
 * @param {*} text
 * @returns
 */
export const apiBuiltinAIDetect = async (text) => {
  if (!isBuiltinAIAvailable) {
    return "";
  }

  const [lang, error] = await fnPolyfill({
    fn: chromeDetect,
    msg: MSG_BUILTINAI_DETECT,
    text,
  });
  if (!error) {
    return lang;
  }

  return "";
};

/**
 * 浏览器内置AI翻译
 * @param {*} param0
 * @returns
 */
const apiBuiltinAITranslate = async ({ text, from, to, apiSetting }) => {
  if (!isBuiltinAIAvailable) {
    return ["", true];
  }

  const { fetchInterval, fetchLimit, httpTimeout } = apiSetting;
  const fetchPool = getFetchPool(fetchInterval, fetchLimit);
  const result = await withTimeout(
    fetchPool.push(fnPolyfill, {
      fn: chromeTranslate,
      msg: MSG_BUILTINAI_TRANSLATE,
      text,
      from,
      to,
    }),
    httpTimeout
  );

  if (!result) {
    throw new Error("apiBuiltinAITranslate got null reault");
  }

  const [trText, srLang, error] = result;
  if (error) {
    throw new Error("apiBuiltinAITranslate got error", error);
  }

  return [trText, srLang];
};

/**
 * 统一翻译接口
 * @param {*} param0
 * @returns
 */
export const apiTranslate = async ({
  text,
  fromLang = "auto",
  toLang,
  apiSetting = DEFAULT_API_SETTING,
  glossary,
  useCache = true,
  usePool = true,
}) => {
  if (!text) {
    throw new Error("The text cannot be empty.");
  }

  const { apiType, apiSlug, useBatchFetch } = apiSetting;
  const langMap = OPT_LANGS_TO_SPEC[apiType] || OPT_LANGS_SPEC_DEFAULT;
  const from = langMap.get(fromLang);
  const to = langMap.get(toLang);
  if (!to) {
    throw new Error(`The target lang: ${toLang} not support`);
  }

  // todo: 优化缓存失效因素
  const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
  const cacheOpts = {
    apiSlug,
    text,
    fromLang,
    toLang,
    version: [v1, v2].join("."),
  };
  const cacheInput = `${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`;

  // 查询缓存数据
  if (useCache) {
    const cache = await getHttpCachePolyfill(cacheInput);
    if (cache?.trText) {
      return cache;
    }
  }

  // 请求接口数据
  let translation = [];
  if (apiType === OPT_TRANS_BUILTINAI) {
    translation = await apiBuiltinAITranslate({
      text,
      from,
      to,
      apiSetting,
    });
  } else if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) {
    const { apiSlug, batchInterval, batchSize, batchLength, useStream } =
      apiSetting;
    const enableStream = useStream && API_SPE_TYPES.stream.has(apiType);
    const key = `${apiSlug}_${fromLang}_${toLang}_${enableStream ? "stream" : "batch"}`;
    const queue = getBatchQueue(key, handleTranslate, {
      batchInterval,
      batchSize,
      batchLength,
    });

    translation = await queue.addTask(text, {
      from,
      to,
      fromLang,
      toLang,
      langMap,
      glossary,
      apiSetting,
      usePool,
    });
  } else {
    const { value } = await handleTranslate([text], {
      from,
      to,
      fromLang,
      toLang,
      langMap,
      glossary,
      apiSetting,
      usePool,
    }).next();
    translation = value?.result;
  }

  let trText = "";
  let srLang = "";
  let srCode = "";
  if (Array.isArray(translation)) {
    [trText, srLang = ""] = translation;
    if (srLang) {
      srCode = OPT_LANGS_TO_CODE[apiType].get(srLang) || "";
    }
  } else if (typeof translation === "string") {
    trText = translation;
  }

  if (!trText) {
    throw new Error("tanslate api got empty trtext");
  }

  const isSame = fromLang === "auto" && srLang === to;

  // 插入缓存
  if (useCache) {
    putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang, srCode });
  }

  return { trText, srLang, srCode, isSame };
};

// 字幕处理/翻译
export const apiSubtitle = async ({
  videoId,
  chunkSign,
  fromLang = "auto",
  toLang,
  events = [],
  apiSetting,
}) => {
  const cacheOpts = {
    apiSlug: apiSetting.apiSlug,
    videoId,
    chunkSign,
    fromLang,
    toLang,
  };
  const cacheInput = `${URL_CACHE_SUBTITLE}?${queryString.stringify(cacheOpts)}`;
  const cache = await getHttpCachePolyfill(cacheInput);
  if (cache) {
    return cache;
  }

  const subtitles = await handleSubtitle({
    events,
    from: fromLang,
    to: toLang,
    apiSetting,
  });
  if (subtitles?.length) {
    putHttpCachePolyfill(cacheInput, null, subtitles);
    return subtitles;
  }

  return [];
};


================================================
FILE: src/apis/trans.js
================================================
import queryString from "query-string";
import {
  OPT_TRANS_GOOGLE,
  OPT_TRANS_GOOGLE_2,
  OPT_TRANS_MICROSOFT,
  OPT_TRANS_AZUREAI,
  OPT_TRANS_DEEPL,
  OPT_TRANS_DEEPLFREE,
  OPT_TRANS_DEEPLX,
  OPT_TRANS_NIUTRANS,
  OPT_TRANS_BAIDU,
  OPT_TRANS_TENCENT,
  OPT_TRANS_VOLCENGINE,
  OPT_TRANS_OPENAI,
  OPT_TRANS_GEMINI,
  OPT_TRANS_GEMINI_2,
  OPT_TRANS_CLAUDE,
  OPT_TRANS_CLOUDFLAREAI,
  OPT_TRANS_OLLAMA,
  OPT_TRANS_OPENROUTER,
  OPT_TRANS_CUSTOMIZE,
  API_SPE_TYPES,
  INPUT_PLACE_FROM,
  INPUT_PLACE_TO,
  INPUT_PLACE_TEXT,
  INPUT_PLACE_KEY,
  INPUT_PLACE_MODEL,
  DEFAULT_USER_AGENT,
  defaultSystemPrompt,
  defaultSubtitlePrompt,
  defaultNobatchPrompt,
  defaultNobatchUserPrompt,
  INPUT_PLACE_TONE,
  INPUT_PLACE_TITLE,
  INPUT_PLACE_DESCRIPTION,
  INPUT_PLACE_TO_LANG,
  INPUT_PLACE_FROM_LANG,
  defaultSystemPromptXml,
  defaultSystemPromptLines,
  INPUT_PLACE_SUMMARY,
} from "../config";
import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl";
import { genBaidu } from "./baidu";
import { interpreter } from "../libs/interpreter";
import {
  parseJsonObj,
  extractJson,
  stripMarkdownCodeBlock,
} from "../libs/utils";
import {
  parseStreamingSegments,
  createStreamingJsonParser,
  detectStreamFormat,
  getStreamDelta,
} from "../libs/stream";
import { kissLog } from "../libs/log";
import { fetchData, fetchStream } from "../libs/fetch";
import { getMsgHistory } from "./history";
import { parseBilingualVtt } from "../subtitle/vtt";
import { getDocInfo } from "../libs/docInfo";

const keyMap = new Map();
const urlMap = new Map();

// 轮询key/url
const keyPick = (apiSlug, key = "", cacheMap) => {
  const keys = key
    .split(/\n|,/)
    .map((item) => item.trim())
    .filter(Boolean);

  if (keys.length === 0) {
    return "";
  }

  const preIndex = cacheMap.get(apiSlug) ?? -1;
  const curIndex = (preIndex + 1) % keys.length;
  cacheMap.set(apiSlug, curIndex);

  return keys[curIndex];
};

const genSystemPrompt = ({
  systemPrompt,
  tone,
  from,
  to,
  fromLang,
  toLang,
  texts,
  docInfo: { title = "", description = "", summary = "" } = {},
}) =>
  systemPrompt
    .replaceAll(INPUT_PLACE_TITLE, title)
    .replaceAll(INPUT_PLACE_DESCRIPTION, description)
    .replaceAll(INPUT_PLACE_SUMMARY, summary)
    .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]);

const genUserPrompt = ({
  nobatchUserPrompt,
  useBatchFetch,
  tone,
  glossary,
  from,
  to,
  fromLang,
  toLang,
  texts,
  docInfo: { title = "", description = "", summary = "" } = {},
}) => {
  if (useBatchFetch) {
    const promptObj = {
      targetLanguage: toLang,
      segments: texts.map((text, i) => ({ id: i, text })),
    };

    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_SUMMARY, summary)
    .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]);
};

const genSubtitlePrompt = ({
  subtitlePrompt,
  tone,
  from,
  to,
  fromLang,
  toLang,
  docInfo: { title = "", description = "", summary = "" } = {},
}) =>
  subtitlePrompt
    .replaceAll(INPUT_PLACE_TITLE, title)
    .replaceAll(INPUT_PLACE_DESCRIPTION, description)
    .replaceAll(INPUT_PLACE_SUMMARY, summary)
    .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);

const parseAIRes = (raw, useBatchFetch = true) => {
  if (!raw) {
    return [];
  }

  if (!useBatchFetch) {
    return [[raw]];
  }

  // try {
  //   const jsonString = extractJson(raw);
  //   if (!jsonString) return [];

  //   const data = JSON.parse(jsonString);
  //   if (Array.isArray(data.translations)) {
  //     // todo: 考虑序号id可能会打乱
  //     return data.translations.map((item) => [
  //       item?.text ?? "",
  //       item?.sourceLanguage ?? "",
  //     ]);
  //   }
  // } catch (err) {
  //   kissLog("parse AI Res", err);
  // }
  // return [];

  let content = stripMarkdownCodeBlock(raw).trim();

  // JSON
  try {
    const start = content.search(/(\{|\[)/);
    const end = content.lastIndexOf(content.includes("}") ? "}" : "]");

    if (start > -1 && end > -1) {
      const jsonStr = content.substring(start, end + 1);
      const parsed = JSON.parse(jsonStr);

      const list = Array.isArray(parsed)
        ? parsed
        : parsed.translations || (parsed.result ? [parsed.result] : [parsed]);

      if (
        list.length > 0 &&
        (list[0].text !== undefined || list[0].translations)
      ) {
        return list.map((item) => [
          String(item.text || ""),
          String(item.sourceLanguage || ""),
        ]);
      }
    }
  } catch (e) {
    //
  }

  // XML
  const xmlTagPattern = /<(t|item|seg)\b/i;
  if (xmlTagPattern.test(content)) {
    try {
      const parser = new DOMParser();
      const doc = parser.parseFromString(content, "text/html");
      const elements = doc.querySelectorAll("t, item, seg");

      if (elements.length > 0) {
        return Array.from(elements).map((el) => [
          el.innerHTML.trim(),
          el.getAttribute("sourceLanguage") || "",
        ]);
      }
    } catch (e) {
      //
    }
  }

  // 纯文本换行
  return content.split("\n").map((line) => {
    const pipeMatch = line.match(/^\d+\s*\|\s*(.*)/);
    if (pipeMatch) {
      return [pipeMatch[1].trim(), ""];
    }

    const text = line.replace(/<br\s*\/?>/gi, "\n").trim();
    return [text, ""];
  });
};

const parseSTRes = (raw) => {
  if (!raw) {
    return [];
  }

  try {
    // const jsonString = extractJson(raw);
    // const data = JSON.parse(jsonString);
    const data = parseBilingualVtt(raw);
    if (Array.isArray(data)) {
      return data;
    }
  } catch (err) {
    kissLog("parse AI Res: subtitle", err);
  }

  return [];
};

const genGoogle = ({ texts, from, to, url, key }) => {
  const params = queryString.stringify({
    client: "gtx",
    dt: "t",
    dj: 1,
    ie: "UTF-8",
    sl: from,
    tl: to,
    q: texts.join(" "),
  });
  url = `${url}?${params}`;
  const headers = {
    "Content-type": "application/json",
  };
  if (key) {
    headers.Authorization = `Bearer ${key}`;
  }

  return { url, headers, method: "GET" };
};

const genGoogle2 = ({ texts, from, to, url, key }) => {
  const body = [[texts, from, to], "wt_lib"];
  const headers = {
    "Content-Type": "application/json+protobuf",
    "X-Goog-API-Key": key,
  };

  return { url, body, headers };
};

const genMicrosoft = ({ texts, from, to, token }) => {
  const params = queryString.stringify({
    from,
    to,
    "api-version": "3.0",
  });
  const url = `https://api-edge.cognitive.microsofttranslator.com/translate?${params}`;
  const headers = {
    "Content-type": "application/json",
    Authorization: `Bearer ${token}`,
  };
  const body = texts.map((text) => ({ Text: text }));

  return { url, body, headers };
};

const genAzureAI = ({ texts, from, to, url, key, region }) => {
  const params = queryString.stringify({
    from,
    to,
  });
  url = url.endsWith("&") ? `${url}${params}` : `${url}&${params}`;
  const headers = {
    "Content-type": "application/json",
    "Ocp-Apim-Subscription-Key": key,
    "Ocp-Apim-Subscription-Region": region,
  };
  const body = texts.map((text) => ({ Text: text }));

  return { url, body, headers };
};

const genDeepl = ({ texts, from, to, url, key }) => {
  const body = {
    text: texts,
    target_lang: to,
    source_lang: from,
    // split_sentences: "0",
  };
  const headers = {
    "Content-type": "application/json",
    Authorization: `DeepL-Auth-Key ${key}`,
  };

  return { url, body, headers };
};

const genDeeplX = ({ texts, from, to, url, key }) => {
  const body = {
    text: texts.join(" "),
    target_lang: to,
    source_lang: from,
  };

  const headers = {
    "Content-type": "application/json",
  };
  if (key) {
    headers.Authorization = `Bearer ${key}`;
  }

  return { url, body, headers };
};

const genNiuTrans = ({ texts, from, to, url, key, dictNo, memoryNo }) => {
  const body = {
    from,
    to,
    apikey: key,
    src_text: texts.join(" "),
    dictNo,
    memoryNo,
  };

  const headers = {
    "Content-type": "application/json",
  };

  return { url, body, headers };
};

const genTencent = ({ texts, from, to }) => {
  const body = {
    header: {
      fn: "auto_translation",
      client_key:
        "browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
    },
    type: "plain",
    model_category: "normal",
    source: {
      text_list: texts,
      lang: from,
    },
    target: {
      lang: to,
    },
  };

  const url = "https://transmart.qq.com/api/imt";
  const headers = {
    "Content-Type": "application/json",
    "user-agent": DEFAULT_USER_AGENT,
    referer: "https://transmart.qq.com/zh-CN/index",
  };

  return { url, body, headers };
};

const genVolcengine = ({ texts, from, to }) => {
  const body = {
    source_language: from,
    target_language: to,
    text: texts.join(" "),
  };

  const url = "https://translate.volcengine.com/crx/translate/v1";
  const headers = {
    "Content-type": "application/json",
  };

  return { url, body, headers };
};

const genOpenAI = ({
  url,
  key,
  systemPrompt,
  userPrompt,
  model,
  temperature,
  maxTokens,
  hisMsgs = [],
  useStream = false,
}) => {
  const userMsg = {
    role: "user",
    content: userPrompt,
  };
  const body = {
    model,
    messages: [
      {
        role: "system",
        content: systemPrompt,
      },
      ...hisMsgs,
      userMsg,
    ],
    temperature,
    max_completion_tokens: maxTokens,
    stream: useStream,
  };

  const headers = {
    "Content-type": "application/json",
    Authorization: `Bearer ${key}`, // OpenAI
    // "api-key": key, // Azure OpenAI
  };

  return { url, body, headers, userMsg };
};

const genGemini = ({
  url,
  key,
  systemPrompt,
  userPrompt,
  model,
  temperature,
  maxTokens,
  hisMsgs = [],
  useStream = false,
}) => {
  url = url
    .replaceAll(INPUT_PLACE_MODEL, model)
    .replaceAll(INPUT_PLACE_KEY, key);

  // 流式传输使用 streamGenerateContent 端点
  if (useStream) {
    url = url.replace(":generateContent", ":streamGenerateContent");
    url += (url.includes("?") ? "&" : "?") + "alt=sse";
  }

  const userMsg = { role: "user", parts: [{ text: userPrompt }] };
  const body = {
    // system_instruction: {
    //   parts: {
    //     text: systemPrompt,
    //   },
    // },
    contents: [
      {
        role: "model",
        parts: [{ text: systemPrompt }],
      },
      ...hisMsgs,
      userMsg,
    ],
    generationConfig: {
      maxOutputTokens: maxTokens,
      temperature,
      // topP: 0.8,
      // topK: 10,
    },
    // thinkingConfig: {
    //   thinkingBudget: 0,
    // },
    safetySettings: [
      {
        category: "HARM_CATEGORY_HARASSMENT",
        threshold: "BLOCK_NONE",
      },
      {
        category: "HARM_CATEGORY_HATE_SPEECH",
        threshold: "BLOCK_NONE",
      },
      {
        category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        threshold: "BLOCK_NONE",
      },
      {
        category: "HARM_CATEGORY_DANGEROUS_CONTENT",
        threshold: "BLOCK_NONE",
      },
    ],
  };
  const headers = {
    "Content-type": "application/json",
    "x-goog-api-key": key,
  };

  return { url, body, headers, userMsg };
};

const genGemini2 = ({
  url,
  key,
  systemPrompt,
  userPrompt,
  model,
  temperature,
  maxTokens,
  hisMsgs = [],
  useStream = false,
}) => {
  const userMsg = {
    role: "user",
    content: userPrompt,
  };
  const body = {
    model,
    messages: [
      {
        role: "system",
        content: systemPrompt,
      },
      ...hisMsgs,
      userMsg,
    ],
    temperature,
    max_tokens: maxTokens,
    stream: useStream,
  };

  const headers = {
    "Content-type": "application/json",
    Authorization: `Bearer ${key}`,
  };

  return { url, body, headers, userMsg };
};

const genClaude = ({
  url,
  key,
  systemPrompt,
  userPrompt,
  model,
  temperature,
  maxTokens,
  hisMsgs = [],
  useStream = false,
}) => {
  const userMsg = {
    role: "user",
    content: userPrompt,
  };
  const body = {
    model,
    system: systemPrompt,
    messages: [...hisMsgs, userMsg],
    temperature,
    max_tokens: maxTokens,
    stream: useStream,
  };

  const headers = {
    "Content-type": "application/json",
    "anthropic-version": "2023-06-01",
    "anthropic-dangerous-direct-browser-access": "true",
    "x-api-key": key,
  };

  return { url, body, headers, userMsg };
};

const genOpenRouter = ({
  url,
  key,
  systemPrompt,
  userPrompt,
  model,
  temperature,
  maxTokens,
  hisMsgs = [],
  useStream = false,
}) => {
  const userMsg = {
    role: "user",
    content: userPrompt,
  };
  const body = {
    model,
    messages: [
      {
        role: "system",
        content: systemPrompt,
      },
      ...hisMsgs,
      userMsg,
    ],
    temperature,
    max_tokens: maxTokens,
    stream: useStream,
  };

  const headers = {
    "Content-type": "application/json",
    Authorization: `Bearer ${key}`,
  };

  return { url, body, headers, userMsg };
};

const genOllama = ({
  // think,
  url,
  key,
  systemPrompt,
  userPrompt,
  model,
  temperature,
  maxTokens,
  hisMsgs = [],
  useStream = false,
}) => {
  const userMsg = {
    role: "user",
    content: userPrompt,
  };
  const body = {
    model,
    messages: [
      {
        role: "system",
        content: systemPrompt,
      },
      ...hisMsgs,
      userMsg,
    ],
    temperature,
    max_tokens: maxTokens,
    // think,
    stream: useStream,
  };

  const headers = {
    "Content-type": "application/json",
  };
  if (key) {
    headers.Authorization = `Bearer ${key}`;
  }

  return { url, body, headers, userMsg };
};

const genCloudflareAI = ({ texts, from, to, url, key }) => {
  const body = {
    text: texts.join(" "),
    source_lang: from,
    target_lang: to,
  };

  const headers = {
    "Content-type": "application/json",
    Authorization: `Bearer ${key}`,
  };

  return { url, body, headers };
};

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}`,
  };

  return { url, body, headers };
};

const genReqFuncs = {
  [OPT_TRANS_GOOGLE]: genGoogle,
  [OPT_TRANS_GOOGLE_2]: genGoogle2,
  [OPT_TRANS_MICROSOFT]: genMicrosoft,
  [OPT_TRANS_AZUREAI]: genAzureAI,
  [OPT_TRANS_DEEPL]: genDeepl,
  [OPT_TRANS_DEEPLFREE]: genDeeplFree,
  [OPT_TRANS_DEEPLX]: genDeeplX,
  [OPT_TRANS_NIUTRANS]: genNiuTrans,
  [OPT_TRANS_BAIDU]: genBaidu,
  [OPT_TRANS_TENCENT]: genTencent,
  [OPT_TRANS_VOLCENGINE]: genVolcengine,
  [OPT_TRANS_OPENAI]: genOpenAI,
  [OPT_TRANS_GEMINI]: genGemini,
  [OPT_TRANS_GEMINI_2]: genGemini2,
  [OPT_TRANS_CLAUDE]: genClaude,
  [OPT_TRANS_CLOUDFLAREAI]: genCloudflareAI,
  [OPT_TRANS_OLLAMA]: genOllama,
  [OPT_TRANS_OPENROUTER]: genOpenRouter,
  [OPT_TRANS_CUSTOMIZE]: genCustom,
};

const genInit = ({
  url = "",
  body = null,
  headers = {},
  userMsg = null,
  method = "POST",
}) => {
  if (!url) {
    throw new Error("genInit: url is empty");
  }

  const init = {
    method,
    headers,
  };
  if (method !== "GET" && method !== "HEAD" && body) {
    let payload = JSON.stringify(body);
    const id = body?.params?.id;
    if (id) {
      payload = payload.replace(
        'method":"',
        (id + 3) % 13 === 0 || (id + 5) % 29 === 0
          ? 'method" : "'
          : 'method": "'
      );
    }
    Object.assign(init, { body: payload });
  }

  return [url, init, userMsg];
};

/**
 * 构造翻译接口请求参数
 * @param {*}
 * @returns
 */
export const genTransReq = async ({ reqHook, ...args }) => {
  const {
    apiType,
    apiSlug,
    key,
    systemPrompt,
    subtitlePrompt,
    // userPrompt,
    nobatchPrompt = defaultNobatchPrompt,
    nobatchUserPrompt = defaultNobatchUserPrompt,
    useBatchFetch,
    from,
    to,
    fromLang,
    toLang,
    texts,
    glossary,
    customHeader,
    customBody,
    events,
    tone,
  } = args;

  if (API_SPE_TYPES.mulkeys.has(apiType)) {
    args.key = keyPick(apiSlug, key, keyMap);
  }

  if (apiType === OPT_TRANS_DEEPLX) {
    args.url = keyPick(apiSlug, args.url, urlMap);
  }

  if (API_SPE_TYPES.ai.has(apiType)) {
    const docInfo = getDocInfo();

    args.systemPrompt = events
      ? genSubtitlePrompt({
          subtitlePrompt,
          from,
          to,
          fromLang,
          toLang,
          texts,
          docInfo,
          tone,
        })
      : genSystemPrompt({
          systemPrompt: useBatchFetch ? systemPrompt : nobatchPrompt,
          from,
          to,
          fromLang,
          toLang,
          texts,
          docInfo,
          tone,
        });
    args.userPrompt = events
      ? JSON.stringify(events)
      : genUserPrompt({
          nobatchUserPrompt,
          useBatchFetch,
          from,
          to,
          fromLang,
          toLang,
          texts,
          docInfo,
          tone,
          glossary,
        });
  }

  const {
    url = "",
    body = null,
    headers = {},
    userMsg = null,
    method = "POST",
  } = genReqFuncs[apiType](args);

  // 合并用户自定义headers和body
  if (customHeader?.trim()) {
    Object.assign(headers, parseJsonObj(customHeader));
  }
  if (customBody?.trim()) {
    Object.assign(body, parseJsonObj(customBody));
  }

  // 执行 request hook
  if (reqHook?.trim() && !events) {
    try {
      const req = {
        url,
        body,
        headers,
        userMsg,
        method,
      };
      interpreter.run(`exports.reqHook = ${reqHook}`);
      const hookResult = await interpreter.exports.reqHook(
        {
          ...args,
          defaultSystemPrompt,
          defaultSystemPromptXml,
          defaultSystemPromptLines,
          defaultSubtitlePrompt,
          defaultNobatchPrompt,
          defaultNobatchUserPrompt,
          req,
        },
        req
      );
      if (hookResult && hookResult.url) {
        return genInit(hookResult);
      }
    } catch (err) {
      kissLog("run req hook", err);
      throw new Error(`Request hook error: ${err.message}`);
    }
  }

  return genInit({ url, body, headers, userMsg, method });
};

/**
 * 解析翻译接口返回数据
 * @param {*} res
 * @param {*} param3
 * @returns
 */
export const parseTransRes = async (
  res,
  {
    texts,
    from,
    to,
    fromLang,
    toLang,
    langMap,
    resHook,
    // thinkIgnore,
    history,
    userMsg,
    apiType,
    useBatchFetch,
  }
) => {
  // 执行 response hook
  if (resHook?.trim()) {
    try {
      interpreter.run(`exports.resHook = ${resHook}`);
      const hookResult = await interpreter.exports.resHook({
        apiType,
        userMsg,
        res,
        texts,
        from,
        to,
        fromLang,
        toLang,
        langMap,
        extractJson,
        parseAIRes,
      });
      if (hookResult && Array.isArray(hookResult.translations)) {
        if (history && userMsg && hookResult.modelMsg) {
          history.add(userMsg, hookResult.modelMsg);
        }
        return hookResult.translations;
      } else if (Array.isArray(hookResult)) {
        return hookResult;
      }
    } catch (err) {
      kissLog("run res hook", err);
      throw new Error(`Response hook error: ${err.message}`);
    }
  }

  let modelMsg = "";

  // todo: 根据结果抛出实际异常信息
  switch (apiType) {
    case OPT_TRANS_GOOGLE:
      return [[res?.sentences?.map((item) => item.trans).join(" "), res?.src]];
    case OPT_TRANS_GOOGLE_2:
      return res?.[0]?.map((_, i) => [res?.[0]?.[i], res?.[1]?.[i]]);
    case OPT_TRANS_MICROSOFT:
    case OPT_TRANS_AZUREAI:
      return res?.map((item) => [
        item.translations.map((item) => item.text).join(" "),
        item.detectedLanguage?.language,
      ]);
    case OPT_TRANS_DEEPL:
      return res?.translations?.map((item) => [
        item.text,
        item.detected_source_language,
      ]);
    case OPT_TRANS_DEEPLFREE:
      return [
        [
          res?.result?.texts?.map((item) => item.text).join(" "),
          res?.result?.lang,
        ],
      ];
    case OPT_TRANS_DEEPLX:
      return [[res?.data, res?.source_lang]];
    case OPT_TRANS_NIUTRANS:
      const json = JSON.parse(res);
      if (json.error_msg) {
        throw new Error(json.error_msg);
      }
      return [[json.tgt_text, json.from]];
    case OPT_TRANS_BAIDU:
      if (res.type === 1) {
        return [
          [
            Object.keys(JSON.parse(res.result).content[0].mean[0].cont)[0],
            res.from,
          ],
        ];
      } else if (res.type === 2) {
        return [[res.data.map((item) => item.dst).join(" "), res.from]];
      }
      break;
    case OPT_TRANS_TENCENT:
      return res?.auto_translation?.map((text) => [text, res?.src_lang]);
    case OPT_TRANS_VOLCENGINE:
      return [[res?.translation, res?.detected_language]];
    case OPT_TRANS_OPENAI:
    case OPT_TRANS_GEMINI_2:
    case OPT_TRANS_OPENROUTER:
      modelMsg = res?.choices?.[0]?.message;
      if (history && userMsg && modelMsg) {
        history.add(userMsg, {
          role: modelMsg.role,
          content: modelMsg.content,
        });
      }
      return parseAIRes(modelMsg?.content, useBatchFetch);
    case OPT_TRANS_GEMINI:
      modelMsg = res?.candidates?.[0]?.content;
      if (history && userMsg && modelMsg) {
        history.add(userMsg, modelMsg);
      }
      return parseAIRes(modelMsg?.parts?.[0]?.text ?? "", useBatchFetch);
    case OPT_TRANS_CLAUDE:
      modelMsg = { role: res?.role, content: res?.content?.text };
      if (history && userMsg && modelMsg) {
        history.add(userMsg, {
          role: modelMsg.role,
          content: modelMsg.content,
        });
      }
      return parseAIRes(res?.content?.[0]?.text ?? "", useBatchFetch);
    case OPT_TRANS_CLOUDFLAREAI:
      return [[res?.result?.translated_text]];
    case OPT_TRANS_OLLAMA:
      modelMsg = res?.choices?.[0]?.message;

      // const deepModels = thinkIgnore
      //   .split(",")
      //   .filter((model) => model?.trim());
      // if (deepModels.some((model) => res?.model?.startsWith(model))) {
      //   modelMsg?.content.replace(/<think>[\s\S]*<\/think>/i, "");
      // }

      if (history && userMsg && modelMsg) {
        history.add(userMsg, {
          role: modelMsg.role,
          content: modelMsg.content,
        });
      }
      return parseAIRes(modelMsg?.content, useBatchFetch);
    case OPT_TRANS_CUSTOMIZE:
      if (useBatchFetch) {
        return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
      }
      return [[res.text, res.src || res.from]];
    default:
  }

  throw new Error("parse translate result: apiType not matched", apiType);
};

/**
 * 发送翻译请求并解析
 * 支持流式和非流式两种模式
 * @param {*} texts 待翻译文本数组
 * @param {*} options 翻译选项
 * @yields {{id: number, result: [string, string]}} 流式模式下逐个返回结果
 * @returns {Promise<Array>} 非流式模式下返回完整结果数组
 */
export async function* handleTranslate(
  texts = [],
  { from, to, fromLang, toLang, langMap, glossary, apiSetting, usePool }
) {
  let history = null;
  let hisMsgs = [];
  const {
    apiType,
    apiSlug,
    contextSize,
    useContext,
    fetchInterval,
    fetchLimit,
    httpTimeout,
    useStream,
  } = apiSetting;
  if (useContext && API_SPE_TYPES.context.has(apiType)) {
    history = getMsgHistory(apiSlug, contextSize);
    hisMsgs = history.getAll();
  }

  const enableStream = useStream && API_SPE_TYPES.stream.has(apiType);

  let token = "";
  if (apiType === OPT_TRANS_MICROSOFT) {
    token = await msAuth();
    if (!token) {
      throw new Error("got msauth error");
    }
  }

  const [input, init, userMsg] = await genTransReq({
    texts,
    from,
    to,
    fromLang,
    toLang,
    langMap,
    glossary,
    hisMsgs,
    token,
    useStream: enableStream,
    ...apiSetting,
  });

  if (enableStream) {
    yield* handleTranslateStreamInternal(texts, input, init, {
      apiType,
      history,
      userMsg,
      usePool,
      fetchInterval,
      fetchLimit,
      httpTimeout,
    });
  } else {
    const response = await fetchData(input, init, {
      useCache: false,
      usePool,
      fetchInterval,
      fetchLimit,
      httpTimeout,
    });
    if (!response) {
      throw new Error("translate got empty response");
    }

    const result = await parseTransRes(response, {
      texts,
      from,
      to,
      fromLang,
      toLang,
      langMap,
      history,
      userMsg,
      ...apiSetting,
    });
    if (!result?.length) {
      throw new Error("translate got an unexpected result");
    }

    for (let i = 0; i < result.length; i++) {
      yield { id: i, result: result[i] };
    }
  }
}

/**
 * 内部流式翻译处理
 */
async function* handleTranslateStreamInternal(
  texts,
  input,
  init,
  { apiType, history, userMsg, usePool, fetchInterval, fetchLimit, httpTimeout }
) {
  const results = new Array(texts.length).fill(null);
  let fullContent = "";
  const processedIds = new Set();

  const jsonParser = createStreamingJsonParser();
  let isJsonFormat = false;
  let formatDetected = false;

  try {
    for await (const rawData of fetchStream(input, init, {
      useCache: false,
      usePool,
      fetchInterval,
      fetchLimit,
      httpTimeout,
    })) {
      try {
        const json = JSON.parse(rawData);
        const delta = getStreamDelta(json, apiType);

        if (delta) {
          fullContent += delta;
          fullContent = stripMarkdownCodeBlock(fullContent, true);

          if (!formatDetected) {
            const { isJson, detected } = detectStreamFormat(fullContent);
            if (detected) {
              formatDetected = true;
              isJsonFormat = isJson;
              // 格式检测成功后,将累积的内容写入解析器
              if (isJsonFormat) {
                for (const { id, translation } of jsonParser.write(
                  fullContent
                )) {
                  results[id] = translation;
                  yield { id, result: translation };
                }
              }
            }
          } else if (isJsonFormat) {
            for (const { id, translation } of jsonParser.write(delta)) {
              results[id] = translation;
              yield { id, result: translation };
            }
          } else {
            for (const { id, translation } of parseStreamingSegments(
              fullContent,
              processedIds
            )) {
              results[id] = translation;
              yield { id, result: translation };
            }
          }
        }
      } catch (e) {
        // 忽略解析错误
      }
    }

    if (isJsonFormat) {
      jsonParser.end();
    }
  } catch (error) {
    kissLog("handleTranslateStream error", error);
    throw error;
  }

  // 最终再解析一次,捕获可能遗漏的段落
  const hasEmpty = results.some((r) => !r);
  if (hasEmpty) {
    const parsed = parseAIRes(fullContent, true);
    for (let i = 0; i < texts.length && i < parsed.length; i++) {
      if (!results[i]) {
        results[i] = parsed[i];
        yield { id: i, result: results[i] };
      }
    }
  }

  if (history && userMsg) {
    if (apiType === OPT_TRANS_GEMINI) {
      history.add(userMsg, {
        role: "model",
        parts: [{ text: fullContent }],
      });
    } else {
      history.add(userMsg, {
        role: "assistant",
        content: fullContent,
      });
    }
  }
}

/**
 * Microsoft语言识别聚合及解析
 * @param {*} texts
 * @returns
 */
export const handleMicrosoftLangdetect = async (texts = []) => {
  const token = await msAuth();
  const input =
    "https://api-edge.cognitive.microsofttranslator.com/detect?api-version=3.0";
  const init = {
    headers: {
      "Content-type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    method: "POST",
    body: JSON.stringify(texts.map((text) => ({ Text: text }))),
  };

  const res = await fetchData(input, init, {
    useCache: false,
  });

  if (Array.isArray(res)) {
    return res.map((r) => r.language);
  }

  return [];
};

/**
 * 字幕翻译
 * @param {*} param0
 * @returns
 */
export const handleSubtitle = async ({ events, from, to, apiSetting }) => {
  const { apiType, fetchInterval, fetchLimit, httpTimeout } = apiSetting;

  const [input, init] = await genTransReq({
    ...apiSetting,
    events,
    from,
    to,
  });

  const res = await fetchData(input, init, {
    useCache: false,
    usePool: true,
    fetchInterval,
    fetchLimit,
    httpTimeout,
  });
  if (!res) {
    kissLog("subtitle got empty response");
    return [];
  }

  switch (apiType) {
    case OPT_TRANS_OPENAI:
    case OPT_TRANS_GEMINI_2:
    case OPT_TRANS_OPENROUTER:
    case OPT_TRANS_OLLAMA:
      return parseSTRes(res?.choices?.[0]?.message?.content ?? "");
    case OPT_TRANS_GEMINI:
      return parseSTRes(res?.candidates?.[0]?.content?.parts?.[0]?.text ?? "");
    case OPT_TRANS_CLAUDE:
      return parseSTRes(res?.content?.[0]?.text ?? "");
    case OPT_TRANS_CUSTOMIZE:
      return res;
    default:
  }

  return [];
};


================================================
FILE: src/background.js
================================================
import browser from "webextension-polyfill";
import {
  MSG_FETCH,
  MSG_GET_HTTPCACHE,
  MSG_PUT_HTTPCACHE,
  MSG_TRANS_TOGGLE,
  MSG_OPEN_OPTIONS,
  MSG_SAVE_RULE,
  MSG_TRANS_TOGGLE_STYLE,
  MSG_OPEN_TRANBOX,
  MSG_CONTEXT_MENUS,
  MSG_COMMAND_SHORTCUTS,
  MSG_INJECT_JS,
  MSG_INJECT_CSS,
  MSG_UPDATE_CSP,
  MSG_BUILTINAI_DETECT,
  MSG_BUILTINAI_TRANSLATE,
  CMD_TOGGLE_TRANSLATE,
  CMD_TOGGLE_STYLE,
  CMD_OPEN_OPTIONS,
  CMD_OPEN_TRANBOX,
  CMD_OPEN_SEPARATE_WINDOW,
  CLIENT_THUNDERBIRD,
  MSG_SET_LOGLEVEL,
  MSG_CLEAR_CACHES,
  MSG_OPEN_SEPARATE_WINDOW,
  STOKEY_SEPARATE_WINDOW,
  PORT_STREAM_FETCH,
  MSG_UPDATE_ICON,
} from "./config";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync";
import { fetchHandle, fetchStreamNative } from "./libs/fetch";
import { tryClearCaches, getHttpCache, putHttpCache } from "./libs/cache";
import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/subRules";
import { saveRule } from "./libs/rules";
import { getCurTabId } from "./libs/msg";
import { injectInlineJsBg, injectInternalCss } from "./libs/injector";
import { kissLog, logger } from "./libs/log";
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";

globalThis.__KISS_CONTEXT__ = "background";

async function updateIcon(isActive, tabId) {
  const suffix = isActive ? "_active" : "";
  const path = {
    16: `images/logo16${suffix}.png`,
    32: `images/logo32${suffix}.png`,
    48: `images/logo48${suffix}.png`,
    128: `images/logo128${suffix}.png`,
    192: `images/logo192${suffix}.png`,
  };
  try {
    // 兼容 v2 清单下的 Firefox
    if (browser.action) {
      await browser.action.setIcon({ path, tabId });
    } else {
      await browser.browserAction.setIcon({ path, tabId });
    }
  } catch (err) {
    kissLog("updateIcon error", err);
  }
}

const CSP_RULE_START_ID = 1;
const ORI_RULE_START_ID = 10000;
const CSP_REMOVE_HEADERS = [
  `content-security-policy`,
  `content-security-policy-report-only`,
  `x-webkit-csp`,
  `x-content-security-policy`,
];

// 独立窗口ID
let separateWindowId = null;
// 记录窗口最后一次有效的位置和大小
let lastKnownBounds = null;

const DEFAULT_SEPARATE_WINDOW_BOUNDS = {
  left: 100,
  top: 100,
  width: 400,
  height: 400,
};

/**
 * 将独立窗口数据写入存储
 */
async function persistSeparateWindowBounds(bounds) {
  if (!bounds) return;
  try {
    await browser.storage.local.set({ [STOKEY_SEPARATE_WINDOW]: bounds });
    kissLog("Final separate window bounds saved to storage", bounds);
  } catch (err) {
    kissLog("Save separate window bounds error", err);
  }
}

/**
 * 打开独立窗口
 */
async function openSeparateWindowWithSavedBounds() {
  try {
    // 如果窗口已存在,则聚焦它而不是重复创建
    if (separateWindowId !== null) {
      const allWindows = await browser.windows.getAll();
      const existingWin = allWindows.find((w) => w.id === separateWindowId);
      if (existingWin) {
        await browser.windows.update(separateWindowId, { focused: true });
        kissLog("Separate window is ready");
        return existingWin;
      }
    }

    const stored = await browser.storage.local.get(STOKEY_SEPARATE_WINDOW);
    const saved = stored && stored[STOKEY_SEPARATE_WINDOW];
    const bounds = Object.assign(
      {},
      DEFAULT_SEPARATE_WINDOW_BOUNDS,
      saved || {}
    );

    const win = await browser.windows.create({
      url: "popup.html#tranbox",
      type: "popup",
      left: Math.round(bounds.left),
      top: Math.round(bounds.top),
      width: Math.round(bounds.width),
      height: Math.round(bounds.height),
      focused: true,
    });

    separateWindowId = win.id;
    lastKnownBounds = {
      left: win.left,
      top: win.top,
      width: win.width,
      height: win.height,
    };

    return win;
  } catch (err) {
    kissLog("open separate window error", err);
  }
}

/**
 * 更新内存中的坐标缓存
 */
async function updateCacheFromActual(windowId) {
  try {
    const win = await browser.windows.get(windowId);
    if (win && win.state === "normal") {
      lastKnownBounds = {
        left: Math.round(win.left),
        top: Math.round(win.top),
        width: Math.round(win.width),
        height: Math.round(win.height),
      };
      kissLog("Bounds cached via fallback:", lastKnownBounds);
      // todo: 获取到的left和top均为0?
      // todo: firefox 每重新打开一次,窗口愈来愈大?
    }
  } catch (e) {
    // 窗口可能已关闭
  }
}

/**
 * 监听焦点变化(兼容桌面Firefox)
 * Firefox 移动端不支持
 */
browser.windows?.onFocusChanged?.addListener?.(async (windowId) => {
  if (separateWindowId !== null) {
    await updateCacheFromActual(separateWindowId);
  }
});

/**
 * 监听位置变化:仅更新内存,不操作 Storage
 * Firefox 不支持 browser.windows.onBoundsChanged
 */
browser.windows?.onBoundsChanged?.addListener?.((win) => {
  if (separateWindowId !== null && win.id === separateWindowId) {
    lastKnownBounds = {
      left: win.left ?? lastKnownBounds.left,
      top: win.top ?? lastKnownBounds.top,
      width: win.width ?? lastKnownBounds.width,
      height: win.height ?? lastKnownBounds.height,
    };
    // todo: 获取到的left和top均为0?
  }
});

/**
 * 监听窗口关闭:此时执行持久化
 * Firefox 移动端不支持
 */
browser.windows?.onRemoved?.addListener?.(async (windowId) => {
  if (windowId === separateWindowId) {
    if (lastKnownBounds) {
      await persistSeparateWindowBounds(lastKnownBounds);
    }

    separateWindowId = null;
    lastKnownBounds = null;
  }
});

/**
 * 添加右键菜单
 */
async function addContextMenus(contextMenuType = 1) {
  // 添加前先删除,避免重复ID的错误
  try {
    await browser.contextMenus.removeAll();
  } catch (err) {
    kissLog("remove contextMenus", err);
  }

  switch (contextMenuType) {
    case 1:
      browser.contextMenus.create({
        id: CMD_TOGGLE_TRANSLATE,
        title: browser.i18n.getMessage("app_name"),
        contexts: ["page", "selection"],
      });
      break;
    case 2:
      browser.contextMenus.create({
        id: CMD_TOGGLE_TRANSLATE,
        title: browser.i18n.getMessage("toggle_translate"),
        contexts: ["page", "selection"],
      });
      browser.contextMenus.create({
        id: CMD_TOGGLE_STYLE,
        title: browser.i18n.getMessage("toggle_style"),
        contexts: ["page", "selection"],
      });
      browser.contextMenus.create({
        id: CMD_OPEN_TRANBOX,
        title: browser.i18n.getMessage("open_tranbox"),
        contexts: ["page", "selection"],
      });
      browser.contextMenus.create({
        id: "options_separator",
        type: "separator",
        contexts: ["page", "selection"],
      });
      browser.contextMenus.create({
        id: CMD_OPEN_OPTIONS,
        title: browser.i18n.getMessage("open_options"),
        contexts: ["page", "selection"],
      });
      break;
    default:
  }
}

/**
 * 更新CSP策略
 * @param {*} csplist
 */
async function updateCspRules({ csplist, orilist }) {
  try {
    const oldRules = await browser.declarativeNetRequest.getDynamicRules();

    const rulesToAdd = [];
    const idsToRemove = [];

    if (csplist !== undefined) {
      let processedCspList = csplist;
      if (typeof processedCspList === "string") {
        processedCspList = processedCspList
          .split(/\n|,/)
          .map((url) => url.trim())
          .filter(Boolean);
      }

      const oldCspRuleIds = oldRules
        .filter(
          (rule) => rule.id >= CSP_RULE_START_ID && rule.id < ORI_RULE_START_ID
        )
        .map((rule) => rule.id);
      idsToRemove.push(...oldCspRuleIds);

      const newCspRules = processedCspList.map((url, index) => ({
        id: CSP_RULE_START_ID + index,
        action: {
          type: "modifyHeaders",
          responseHeaders: CSP_REMOVE_HEADERS.map((header) => ({
            operation: "remove",
            header,
          })),
        },
        condition: {
          urlFilter: url,
          resourceTypes: ["main_frame", "sub_frame"],
        },
      }));
      rulesToAdd.push(...newCspRules);
    }

    if (orilist !== undefined) {
      let processedOriList = orilist;
      if (typeof processedOriList === "string") {
        processedOriList = processedOriList
          .split(/\n|,/)
          .map((url) => url.trim())
          .filter(Boolean);
      }

      const oldOriRuleIds = oldRules
        .filter((rule) => rule.id >= ORI_RULE_START_ID)
        .map((rule) => rule.id);
      idsToRemove.push(...oldOriRuleIds);

      const newOriRules = processedOriList.map((url, index) => ({
        id: ORI_RULE_START_ID + index,
        action: {
          type: "modifyHeaders",
          requestHeaders: [{ header: "Origin", operation: "set", value: url }],
        },
        condition: {
          urlFilter: url,
          resourceTypes: ["xmlhttprequest"],
        },
      }));
      rulesToAdd.push(...newOriRules);
    }

    if (idsToRemove.length > 0 || rulesToAdd.length > 0) {
      await browser.declarativeNetRequest.updateDynamicRules({
        removeRuleIds: idsToRemove,
        addRules: rulesToAdd,
      });
    }
  } catch (err) {
    kissLog("update csp rules", err);
  }
}

/**
 * 注册邮件显示脚本
 */
async function registerMsgDisplayScript() {
  await messenger.messageDisplayScripts.register({
    js: [{ file: "/content.js" }],
  });
}

/**
 * 插件安装
 */
browser.runtime.onInstalled.addListener(async () => {
  await tryInitDefaultData();

  //在thunderbird中注册脚本
  if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
    registerMsgDisplayScript();
  }

  const { contextMenuType, csplist, orilist, subrulesList } =
    await getSettingWithDefault();

  // 右键菜单
  addContextMenus(contextMenuType);

  // 禁用CSP
  updateCspRules({ csplist, orilist });

  // 同步订阅规则
  trySyncAllSubRules({ subrulesList });
});

/**
 * 浏览器启动
 */
browser.runtime.onStartup.addListener(async () => {
  const {
    clearCache,
    contextMenuType,
    subrulesList,
    csplist,
    orilist,
    logLevel,
  } = await getSettingWithDefault();

  // 设置日志
  logger.setLevel(logLevel);

  // 清除缓存
  if (clearCache) {
    tryClearCaches();
  }

  //在thunderbird中注册脚本
  if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
    registerMsgDisplayScript();
  }

  // 右键菜单
  // firefox重启后菜单会消失,故重复添加
  addContextMenus(contextMenuType);

  // 禁用CSP
  updateCspRules({ csplist, orilist });

  // 同步数据
  trySyncSettingAndRules();

  // 同步订阅规则
  trySyncAllSubRules({ subrulesList });
});

/**
 * 向当前活动标签页注入脚本或CSS
 */
const injectToCurrentTab = async (func, args) => {
  const tabId = await getCurTabId();
  return browser.scripting.executeScript({
    target: { tabId, allFrames: true },
    func: func,
    args: [args],
    world: "MAIN",
  });
};

// 动作处理器映射表
const messageHandlers = {
  [MSG_FETCH]: (args) => fetchHandle(args),
  [MSG_GET_HTTPCACHE]: (args) => getHttpCache(args),
  [MSG_PUT_HTTPCACHE]: (args) => putHttpCache(args),
  [MSG_OPEN_OPTIONS]: () => browser.runtime.openOptionsPage(),
  [MSG_SAVE_RULE]: (args) => saveRule(args),
  [MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJsBg, args),
  [MSG_INJECT_CSS]: (args) => injectToCurrentTab(injectInternalCss, args),
  [MSG_UPDATE_CSP]: (args) => updateCspRules(args),
  [MSG_CONTEXT_MENUS]: (args) => addContextMenus(args),
  [MSG_COMMAND_SHORTCUTS]: () => browser.commands.getAll(),
  [MSG_BUILTINAI_DETECT]: (args) => chromeDetect(args),
  [MSG_BUILTINAI_TRANSLATE]: (args) => chromeTranslate(args),
  [MSG_SET_LOGLEVEL]: (args) => logger.setLevel(args),
  [MSG_CLEAR_CACHES]: () => tryClearCaches(),
  [MSG_OPEN_SEPARATE_WINDOW]: () => openSeparateWindowWithSavedBounds(),
  [MSG_UPDATE_ICON]: (args, sender) => updateIcon(args, sender?.tab?.id),
};

/**
 * 监听消息
 * todo: 返回含错误的结构化信息
 */
browser.runtime.onMessage.addListener(async ({ action, args }, sender) => {
  const handler = messageHandlers[action];
  if (!handler) {
    throw new Error(`Message action is unavailable: ${action}`);
  }

  return handler(args, sender);
});

/**
 * 监听快捷键
 * Firefox 移动端不支持
 */
browser.commands?.onCommand?.addListener?.((command) => {
  // console.log(`Command: ${command}`);
  switch (command) {
    case CMD_TOGGLE_TRANSLATE:
      sendTabMsg(MSG_TRANS_TOGGLE);
      break;
    case CMD_OPEN_TRANBOX:
      sendTabMsg(MSG_OPEN_TRANBOX);
      break;
    case CMD_TOGGLE_STYLE:
      sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
      break;
    case CMD_OPEN_OPTIONS:
      browser.runtime.openOptionsPage();
      break;
    case CMD_OPEN_SEPARATE_WINDOW:
      // invoke the handler to open the independent window
      if (messageHandlers[MSG_OPEN_SEPARATE_WINDOW]) {
        messageHandlers[MSG_OPEN_SEPARATE_WINDOW]();
      }
      break;
    default:
  }
});

/**
 * 监听右键菜单
 * Firefox 移动端不支持
 */
browser?.contextMenus?.onClicked?.addListener?.(({ menuItemId }) => {
  switch (menuItemId) {
    case CMD_TOGGLE_TRANSLATE:
      sendTabMsg(MSG_TRANS_TOGGLE);
      break;
    case CMD_TOGGLE_STYLE:
      sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
      break;
    case CMD_OPEN_TRANBOX:
      sendTabMsg(MSG_OPEN_TRANBOX);
      break;
    case CMD_OPEN_OPTIONS:
      browser.runtime.openOptionsPage();
      break;
    default:
  }
});

/**
 * 处理通用流式请求
 * 通过端口连接实现流式数据传输
 */
async function handleStreamFetch(port, args) {
  const { input, init, opts } = args;

  try {
    for await (const chunk of fetchStreamNative(
      input,
      init,
      opts.httpTimeout
    )) {
      port.postMessage({ type: "delta", data: chunk });
    }
    port.postMessage({ type: "done" });
  } catch (error) {
    if (error.name !== "AbortError") {
      port.postMessage({ type: "error", error: error.message });
    }
  }
}

/**
 * 监听端口连接(用于流式请求)
 */
browser.runtime.onConnect.addListener((port) => {
  if (port.name === PORT_STREAM_FETCH) {
    port.onMessage.addListener((message) => {
      if (message.action === "start") {
        handleStreamFetch(port, message.args);
      }
    });
  }
});


================================================
FILE: src/common.js
================================================
import { OPT_HIGHLIGHT_WORDS_DISABLE } from "./config";
import {
  getFabWithDefault,
  getSettingWithDefault,
  getWordsWithDefault,
} from "./libs/storage";
import { isIframe } from "./libs/iframe";
import { genEventName } from "./libs/utils";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
import { isInBlacklist } from "./libs/blacklist";
import { runSubtitle } from "./subtitle/subtitle";
import { logger } from "./libs/log";
import { injectInlineJs } from "./libs/injector";
import TranslatorManager from "./libs/translatorManager";

/**
 * 油猴脚本设置页面
 */
function runSettingPage() {
  if (GM.info?.script?.grant?.includes("unsafeWindow")) {
    unsafeWindow.GM = GM;
    unsafeWindow.APP_INFO = {
      name: process.env.REACT_APP_NAME,
      version: process.env.REACT_APP_VERSION,
    };
  } else {
    const ping = genEventName();
    window.addEventListener(ping, handlePing);
    // window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
    injectInlineJs(
      `(${injectScript})("${ping}")`,
      "kiss-translator-options-injector"
    );
  }
}

/**
 * 显示错误信息到页面顶部
 * @param {*} message
 */
function showErr(message) {
  const bannerId = "KISS-Translator-Message";
  const existingBanner = document.getElementById(bannerId);
  if (existingBanner) {
    existingBanner.remove();
  }

  const banner = document.createElement("div");
  banner.id = bannerId;

  Object.assign(banner.style, {
    position: "fixed",
    top: "0",
    left: "0",
    width: "100%",
    backgroundColor: "#f44336",
    color: "white",
    textAlign: "center",
    padding: "8px 16px",
    zIndex: "1001",
    boxSizing: "border-box",
    fontSize: "16px",
    boxShadow: "0 2px 5px rgba(0,0,0,0.2)",
  });

  const closeButton = document.createElement("span");
  closeButton.textContent = "×";

  Object.assign(closeButton.style, {
    position: "absolute",
    top: "50%",
    right: "20px",
    transform: "translateY(-50%)",
    cursor: "pointer",
    fontSize: "22px",
    fontWeight: "bold",
  });

  const messageText = document.createTextNode(`KISS-Translator: ${message}`);
  banner.appendChild(messageText);
  banner.appendChild(closeButton);

  document.body.appendChild(banner);

  const removeBanner = () => {
    banner.style.transition = "opacity 0.5s ease";
    banner.style.opacity = "0";
    setTimeout(() => {
      if (banner && banner.parentNode) {
        banner.parentNode.removeChild(banner);
      }
    }, 500);
  };

  closeButton.onclick = removeBanner;
  setTimeout(removeBanner, 10000);
}

async function getFavWords(rule) {
  if (
    rule.highlightWords &&
    rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
  ) {
    try {
      return Object.keys(await getWordsWithDefault());
    } catch (err) {
      logger.info("get fav words", err);
    }
  }

  return [];
}

/**
 * 入口函数
 */
export async function run(isUserscript = false) {
  try {
    // 读取设置信息
    const setting = await getSettingWithDefault();

    // 日志
    logger.setLevel(setting.logLevel);

    // if (document?.documentElement?.tagName?.toUpperCase() !== "HTML") {
    //   return;
    // }
    const contentType = document?.contentType?.toLowerCase() || "";
    if (!contentType.includes("text") && !contentType.includes("html")) {
      logger.info("Skip running in document content type: ", contentType);
      return;
    }

    const href = document?.location?.href || "";

    // 油猴脚本
    if (isUserscript) {
      if (!globalThis.GM) {
        globalThis.GM = {
          xmlHttpRequest: globalThis.GM_xmlhttpRequest,
          registerMenuCommand: globalThis.GM_registerMenuCommand,
          unregisterMenuCommand: globalThis.GM_unregisterMenuCommand,
          setValue: globalThis.GM_setValue,
          getValue: globalThis.GM_getValue,
          deleteValue: globalThis.GM_deleteValue,
          info: globalThis.GM_info,
        };
      }

      // 设置页面
      if (
        href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
        href.includes(process.env.REACT_APP_OPTIONSPAGE)
      ) {
        runSettingPage();
        return;
      }
    }

    // 黑名单
    if (isInBlacklist(href, setting)) {
      return;
    }

    // 翻译网页
    const rule = await matchRule(href, setting);
    const favWords = await getFavWords(rule);
    const fabConfig = await getFabWithDefault();
    const translatorManager = new TranslatorManager({
      setting,
      rule,
      fabConfig,
      favWords,
      isIframe,
      isUserscript,
    });
    translatorManager.start();

    if (isIframe) {
      return;
    }

    // 字幕翻译
    runSubtitle({ href, setting, rule, isUserscript });

    if (isUserscript) {
      trySyncAllSubRules(setting);
    }
  } catch (err) {
    console.error("[KISS-Translator]", err);
    showErr(err.message);
  }
}


================================================
FILE: src/components/Logo/icon.base64.js
================================================
export const FAVICON_BASE64 =
  "data:image/x-icon;base64,AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAAAABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAADunB8Q7pwgd+6cINTunCD67pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD67pwg1O6cIHfunR8QAAAAAO6cHxDunCCW7pwg+e6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIPnunCCW7pwfEO6cIHfunCD47pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD47pwgd+6cINPunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg0+6cIPnunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg+e6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6bHv/umx3/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unB//7psd/+6bHf/umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pse/+6bHf/umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7p0i//GsRP/ytlv/8a9K/+6eJf/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cH//vnyj/8bFP//K2Wv/wqj7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCH/8Kk9//KzVf/xrUb/7p4k/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx7/8rVY//3y4v/++/f//fbq//S+bP/umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/2yIT//vjw//779//87db/8axF/+6bHv/unCD/7pwg/+6cIP/unCD/7pwg/+6bHv/xsEz//O7Z//769P/99en/9MBx/+6bHv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9smG//////////////////jVn//unB//7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6fJv/64r7//////////////v3/9L9v/+6aHP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/1xHn/////////////////+Nmq/+6dIf/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suK//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/++fKP/65MH///////////////7/9MF0/+6aHP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dut/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/++fJ//65MH//////////////v7/9MFz/+6aHP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwf/++gKf/75cX//////////////v7/9MFz/+6aHP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pod//GuSP/98+T///////////////7/9MF0/+6aHP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx7/8a1G//rkw////////////////////vz/9L5t/+6aHP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwf/+6cH//ytlz//O3W///////////////////////98eH/8axE/+6bHv/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/umx//7p4k//TCdv/99ej///////////////////////3x4P/zu2b/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jWpP/unCD/7pwg/+6bHv/voSz/98+S//769P//////////////////////++jK//KxUP/umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpf/unB//7psd//CnOf/5267///37///////////////////9+//53K//8Kg7/+6bHf/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////vmx//wpjj/8rFQ//vmx////////////////////////vrz//fOkf/voSz/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ///////////////////9+v/87df//fXo///////////////////////99Ob/9MFz/+6dI//umx//7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////////////////////////////////////zr0v/ytVj/7psf/+6cH//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ////////////////////////////////////////////++fJ//GrQv/umx3/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ////////////////////////////////////////////9cN3/+6aG//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ////////////////////////////////////////////+Nai/+6eJf/unB//7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ/////////////////////////////////////////////vv2//bKiP/uniT/7pwf/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ///////////////////+/f/+9+3///z5//////////////////769f/2yYX/7p0j/+6cH//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////zr0v/xsE7/9siE//748P/////////////////++vT/9siD/+6dI//unB//7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jYp//umx7/7pwh//XFfP/++fH//////////////////vrz//XHgP/unSL/7pwf/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jWpP/unCD/7pwf/+6dIv/1xn3//vny//////////////////758v/1xn7/7p0i/+6cH//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cH//unSL/9cZ///758//////////////////++fH/9cV7/+6dIv/unB//7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unB//7p0j//XHgf/++vP//////////////////vjw//XDef/unSH/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwf/+6dI//1yIP//vr0//////////////////747//0wXP/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHf/1xn7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cH//uniP/9smF//769f/////////////////98d//8KpB/+6bHv/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/1xX7/////////////////+dqt/+6dIv/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unB//7p4k//fOkf///vz//////////////fz/9L1q/+6aHP/unB//7psf/+6bH//umx//7pse/+6aG//2yof/////////////////+d60/+6dIv/umx7/7psf/+6bH//umx//7pwf/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pse//CnOP/879r//////////////v7/9MFz/+6aHP/vnyf/8KY2//CmN//wpjf/8KY3//KxT//76Mv//////////////////fPk//O6ZP/wpjj/8KY3//CmN//wpjb/76Er/+6cH//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suJ//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwf/++gKP/65MP//////////////v7/9MBy/++kMf/5267//PDe//3x3//88d///fHf//737v////7///////////////////////768//98uH//PHf//3x3//88d//+uTB//GuSP/umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9suK//////////////////jXpP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/++fKP/65MH///////////////7/9MBw//O4X////fr///////////////////////////////////////////////////////////////////////////////////////bJhv/umx3/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx3/9smF//////////////////jUnv/unB//7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6fJv/64r3//////////////v3/9L1q//O6Y////fv///////////////////////////////////////////////////////////////////////////////////////bLif/umx3/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx7/8rNU//zv3P/++vP//fTl//O7Z//umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cH//1xn7//fbr//768//76tD/8KpA//CoO//75sj//vjw//748P/++PD//vjw//748P/++PD//vjw//748P/++PD//vjw//748P/++PD//vjw//748P/++PD//O7a//KzVf/umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwh//CpPv/ys1P/8axD/+6dI//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unib/8a5I//KyUv/wpzn/7pwg/+6cIP/wpTT/8a9M//GwTf/xsE3/8bBN//GwTf/xsE3/8bBN//GwTf/xsE3/8bBN//GwTf/xsE3/8bBN//GwTf/xsE3/8Kg7/+6cIf/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6bHv/umx3/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pse/+6bHf/umx7/7pwg/+6cIP/umx//7psd/+6bHf/umx3/7psd/+6bHf/umx3/7psd/+6bHf/umx3/7psd/+6bHf/umx3/7psd/+6bHf/umx3/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIPnunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg+e6cINTunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg1O6cIHfunCD47pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD47pwgd+6cHxDunCCW7pwg+e6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIPnunCCW7pwfEAAAAADunB8Q7pwgd+6cINTunCD67pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD67pwg1O6cIHfunB8QAAAAAOAAAAAABwAAgAAAAAABAACAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAQAAgAAAAAABAADgAAAAAAcAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAADunB8S7pwgie6cIOvunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIOvunCCJ7pwgEu6cIInunCD57pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIPnunCCJ7pwg6u6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIOrunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cH//uniX/7p8m/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cH//uniT/758n/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6dIf/vnyf/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx7/8KpA//netf/64bz/8a5J/+6bHv/unCD/7pwg/+6cIP/umx//76Qz//jZqf/65MP/87Zc/+6bHv/unCD/7pwg/+6cIP/uniX/9syL//vlxP/0wnb/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/zvGn///78///////1xHr/7poc/+6cIP/unCD/7pwg/+6bHf/yslP//vr0///////30pr/7pse/+6cIP/unCD/7psf/++kM//87tr///////rjwP/unyb/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7poc//S+bP///v3///////XFff/umhz/7pwg/+6cIP/unCD/7psd//KzVP/++vX///////jUnf/umx7/7pwg/+6cIP/umx//8KU1//zv3P//////+uTC/++fJ//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umhz/9L1s///+/f//////9cV9/+6aHP/unCD/7pwg/+6cIP/umhv/87de//78+P//////+NSd/+6bHv/unCD/7pwg/+6bH//wpTX//O/c///////65ML/758n/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/0vWz///79///////1xX3/7poc/+6cIP/unCD/7psd//CmN//637j////////////30pn/7pse/+6cIP/unCD/7psf//ClNf/879z///////rkwv/vnyf/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7poc//S9bP///v3///////XFff/umhz/7pwg/+6bHv/xr0r/++bG/////////////vft//O5Yf/umx3/7pwg/+6cIP/umx//8KU1//zv3P//////+uTC/++fJ//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umhz/9L1s///+/f//////9cV8/+6aHP/unCD/87lh//zv3P////////////3x3//zvGf/7p0h/+6cIP/unCD/7pwg/+6bH//wpTX//O/c///////65ML/758n/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/0vWz///79///////2yIP/7p0i//XFfP/+9uz////////////758n/8bBN/+6bHv/unCD/7pwg/+6cIP/unCD/7psf//ClNf/879z///////rkwv/vnyf/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7poc//S9bP///v3///////zt1v/53rT//vv1/////////fv/+dut//CnOf/umx3/7pwg/+6cIP/unCD/7pwg/+6cIP/umx//8KU1//zv3P//////+uTC/++fJ//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umhz/9L1s///+/f///////////////////////vr1//bOkP/voSv/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6bH//wpTX//O/c///////65ML/758n/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/0vWz///79///////////////////////526//7p0i/+6bH//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7psf//ClNf/879z///////rkwv/vnyf/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7poc//S9bP///v3///////////////////////voy//wpzj/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umx//8KU1//zv3P//////+uTC/++fJ//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umhz/9L1s///+/f///////vv3//768/////////////rguv/wpjb/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6bH//wpTX//O/c///////65ML/758n/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/0vWz///79///////41J7/87hf//zv2/////////////rft//wpTX/7pse/+6cIP/unCD/7pwg/+6cIP/unCD/7psf//ClNf/879z///////rkwv/vnyf/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7poc//S9bP///v3///////XFfP/tmRn/8rNV//zw3P////////////netf/wpTT/7pse/+6cIP/unCD/7pwg/+6cIP/umx//8KU1//zv3P//////+uTC/++fJ//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umhz/9L1s///+/f//////9cV9/+6aHP/umx7/8rRW//zw3v/////////+//nds//vpDP/7pse/+6cIP/unCD/7pwg/+6bH//wpTX//O/c///////65ML/758n/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/0vWz///79///////1xX3/7poc/+6cIP/umx7/8rVY//3x3//////////+//ncsP/vozD/7pwf/+6cIP/unCD/7psf//ClNf/879z///////rkwv/vnyf/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7poc//S9bP///v3///////XFff/umhz/7pwg/+6cIP/umx7/8rVZ//3y4v////////79//XEev/umx3/7psf/+6bHv/umx3/76Qz//zw3P//////+uTC/+6eJf/umx7/7pse/+6cH//unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/umhz/9L1s///+/f//////9cV9/+6aHP/unCD/7pwg/+6cIP/umx3/9cV7///+/f//////99Ob/+6cIf/wpTT/8Kc4//CmN//zu2b//vjv///////88N7/8rJT//CmN//wpzj/76Mx/+6cIf/unCD/7pwg/+6cIP/unCD/7pwg/+6aHP/0vmz///79///////1xX3/7poc/+6cIP/unCD/7pwg/+6bHf/ytFX//vv1///////30pv/87tn//zv2//98uH//fLh//758f///////////////v/+9+3//fHh//3y4f/76tD/8bBM/+6bHv/unCD/7pwg/+6cIP/unCD/7poc//O8af///vz///////XEef/umhz/7pwg/+6cIP/unCD/7psd//KyUv/++vP///////fRlv/3zpH////////////////////////////////////////////////////////+/P/0vWv/7poc/+6cIP/unCD/7pwg/+6cIP/umx7/8Kk+//ncsP/537b/8a1H/+6bHv/unCD/7pwg/+6cIP/unB//76Qx//jWo//64r7/8rRW//KyUv/537f/+uO///riv//64r//+uK///riv//64r//+uK///riv//647//+dqs//CpPv/umx7/7pwg/+6cIP/unCD/7pwg/+6cIP/unB//7p4k/+6eJP/unB//7pwg/+6cIP/unCD/7pwg/+6cIP/unB//7p0j/+6eJf/unB//7pwf/+6eJP/unib/7p4l/+6eJf/uniX/7p4l/+6eJf/uniX/7p4l/+6eJv/unSP/7pwf/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg6u6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIOrunCCJ7pwg+e6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD57pwgie6cIBLunCCJ7pwg6+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg6+6cIInunB8SgAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAEoAAAAEAAAACAAAAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAA7pwgnu6cIPjunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD47pwgnu6cIPfunB//7psd/+6cH//unCD/7pwg/+6bHv/umx3/7pwg/+6cIP/umx7/7psd/+6cIP/unCD/7pwg/+6cIPfunB//76Mv//S9a//vpDH/7pwf/+6cH//xsE7/87dc/+6cIf/umx//8axD//O5Yf/unSP/7pwg/+6cIP/unCD/7psd//KxT//99en/8rRX/+6bHf/umx7/+NWh//vmxv/vnyf/7poc//bLiv/87NX/76Iu/+6cH//unCD/7pwg/+6bHf/yslH//fbr//K1Wf/umhr/76Iu//rkw//75sb/7p8n/+6aHP/2zIz//O3X/++jL//unB//7pwg/+6cIP/umx3/8rJR//326//ytFX/8Kc4//netv/+9+3/9cN4/+6cIP/umh3/9syM//zt1//voy//7pwf/+6cIP/unCD/7psd//KyUf/++O//99Oc//voy//98eD/87tl/+6cIf/unCD/7psd//bMjP/87df/76Mv/+6cH//unCD/7pwg/+6bHf/ysVD//vrz///////98uL/8bBO/+6bHf/unCD/7pwg/+6aHf/2zIz//O3X/++jL//unB//7pwg/+6cIP/umx3/8rFQ//768//++/b//fXo//O3Xf/umx7/7pwg/+6cIP/umh3/9syM//zt1//voy//7pwf/+6cIP/unCD/7psd//KyUf/+9+3/9cV9//rguf/98+P/8rZb/+6bHv/unCD/7pod//bMjP/87df/76Mv/+6cH//unCD/7pwg/+6bHf/yslH//fbr//K0Vv/vpTT/+uK+//3y4f/ytFf/7pse/+6aG//2y4v//O3X/++iLf/umx7/7pwg/+6cIP/umx3/8rJR//326//ytVn/7pka//CnOf/869L/+uG7/++kM//wpjj/+NWh//3y4f/xr0z/8KU1/+6dI//unCD/7psd//KxT//99en/8rRX/+6bHf/umx3/+Nai//vnyv/52ar//fTm//779f///v3//fXp//zs0//xrET/7psd/+6cH//voi7/87xo/++jMf/unB//7pwf//GvTP/ytlv/8rZZ//XEev/1w3j/9cN3//XEef/0v3D/76Iu/+6cH//unCD37pwf/+6bHf/unB//7pwg/+6cIP/umx7/7psd/+6bHf/umhz/7poc/+6aHP/umhz/7poc/+6cH//unCD37pwgnu6cIPjunCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD/7pwg/+6cIP/unCD47pwgngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
export default FAVICON_BASE64;


================================================
FILE: src/components/Logo/index.js
================================================
import React from "react";
import { FAVICON_BASE64 } from "./icon.base64.js";

const Logo = ({ size = 16, className = "", style = {}, onClick }) => {
  return (
    <img
      src={FAVICON_BASE64}
      alt="Logo"
      className={className}
      onClick={onClick}
      style={{
        width: `${size}px`,
        height: `${size}px`,
        objectFit: "contain",
        display: "block",
        ...style,
      }}
    />
  );
};

export { FAVICON_BASE64 };
export default Logo;


================================================
FILE: src/config/api.js
================================================
export const DEFAULT_HTTP_TIMEOUT = 10000; // 调用超时时间
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
export const DEFAULT_BATCH_INTERVAL = 400; // 批处理请求间隔时间
export const DEFAULT_BATCH_SIZE = 10; // 每次最多发送段落数量
export const DEFAULT_BATCH_LENGTH = 10000; // 每次发送最大文字数量
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_SUMMARY = "{{summary}}"; // 摘要
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符

// export const OPT_DICT_BAIDU = "Baidu";
export const OPT_DICT_BING = "Bing";
export const OPT_DICT_YOUDAO = "Youdao";
export const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_YOUDAO];
export const OPT_DICT_MAP = new Set(OPT_DICT_ALL);

export const OPT_SUG_BAIDU = "Baidu";
export const OPT_SUG_YOUDAO = "Youdao";
export const OPT_SUG_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
export const OPT_SUG_MAP = new Set(OPT_SUG_ALL);

export const OPT_TRANS_BUILTINAI = "BuiltinAI";
export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_GOOGLE_2 = "Google2";
export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_AZUREAI = "AzureAI";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_DEEPLX = "DeepLX";
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_NIUTRANS = "NiuTrans";
export const OPT_TRANS_BAIDU = "Baidu";
export const OPT_TRANS_TENCENT = "Tencent";
export const OPT_TRANS_VOLCENGINE = "Volcengine";
export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_GEMINI = "Gemini";
export const OPT_TRANS_GEMINI_2 = "Gemini2";
export const OPT_TRANS_CLAUDE = "Claude";
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
export const OPT_TRANS_OLLAMA = "Ollama";
export const OPT_TRANS_OPENROUTER = "OpenRouter";
export const OPT_TRANS_CUSTOMIZE = "Custom";

// 内置支持的翻译引擎
export const OPT_ALL_TRANS_TYPES = [
  OPT_TRANS_BUILTINAI,
  OPT_TRANS_GOOGLE,
  OPT_TRANS_GOOGLE_2,
  OPT_TRANS_MICROSOFT,
  OPT_TRANS_AZUREAI,
  // OPT_TRANS_BAIDU,
  OPT_TRANS_TENCENT,
  OPT_TRANS_VOLCENGINE,
  OPT_TRANS_DEEPL,
  OPT_TRANS_DEEPLFREE,
  OPT_TRANS_DEEPLX,
  OPT_TRANS_NIUTRANS,
  OPT_TRANS_OPENAI,
  OPT_TRANS_GEMINI,
  OPT_TRANS_GEMINI_2,
  OPT_TRANS_CLAUDE,
  OPT_TRANS_CLOUDFLAREAI,
  OPT_TRANS_OLLAMA,
  OPT_TRANS_OPENROUTER,
  OPT_TRANS_CUSTOMIZE,
];

export const OPT_LANGDETECTOR_ALL = [
  OPT_TRANS_BUILTINAI,
  OPT_TRANS_GOOGLE,
  OPT_TRANS_MICROSOFT,
  OPT_TRANS_BAIDU,
  OPT_TRANS_TENCENT,
];

export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);

// 翻译引擎特殊集合
export const API_SPE_TYPES = {
  // 内置翻译
  builtin: new Set(OPT_ALL_TRANS_TYPES),
  // 机器翻译
  machine: new Set([
    OPT_TRANS_MICROSOFT,
    OPT_TRANS_DEEPLFREE,
    OPT_TRANS_BAIDU,
    OPT_TRANS_TENCENT,
    OPT_TRANS_VOLCENGINE,
  ]),
  // AI翻译
  ai: new Set([
    OPT_TRANS_OPENAI,
    OPT_TRANS_GEMINI,
    OPT_TRANS_GEMINI_2,
    OPT_TRANS_CLAUDE,
    OPT_TRANS_OLLAMA,
    OPT_TRANS_OPENROUTER,
    OPT_TRANS_CUSTOMIZE,
  ]),
  // 支持多key
  mulkeys: new Set([
    OPT_TRANS_AZUREAI,
    OPT_TRANS_DEEPL,
    OPT_TRANS_OPENAI,
    OPT_TRANS_GEMINI,
    OPT_TRANS_GEMINI_2,
    OPT_TRANS_CLAUDE,
    OPT_TRANS_CLOUDFLAREAI,
    OPT_TRANS_OLLAMA,
    OPT_TRANS_OPENROUTER,
    OPT_TRANS_NIUTRANS,
    OPT_TRANS_CUSTOMIZE,
  ]),
  // 支持批处理
  batch: new Set([
    OPT_TRANS_AZUREAI,
    OPT_TRANS_GOOGLE_2,
    OPT_TRANS_MICROSOFT,
    OPT_TRANS_TENCENT,
    OPT_TRANS_DEEPL,
    OPT_TRANS_OPENAI,
    OPT_TRANS_GEMINI,
    OPT_TRANS_GEMINI_2,
    OPT_TRANS_CLAUDE,
    OPT_TRANS_OLLAMA,
    OPT_TRANS_OPENROUTER,
    OPT_TRANS_CUSTOMIZE,
  ]),
  // 支持上下文
  context: new Set([
    OPT_TRANS_OPENAI,
    OPT_TRANS_GEMINI,
    OPT_TRANS_GEMINI_2,
    OPT_TRANS_CLAUDE,
    OPT_TRANS_OLLAMA,
    OPT_TRANS_OPENROUTER,
    OPT_TRANS_CUSTOMIZE,
  ]),
  // 支持流式传输
  stream: new Set([
    OPT_TRANS_OPENAI,
    OPT_TRANS_GEMINI,
    OPT_TRANS_GEMINI_2,
    OPT_TRANS_CLAUDE,
    OPT_TRANS_OLLAMA,
    OPT_TRANS_OPENROUTER,
  ]),
};

export const BUILTIN_STONES = [
  "formal", // 正式风格
  "casual", // 口语风格
  "neutral", // 中性风格
  "technical", // 技术风格
  "marketing", // 营销风格
  "Literary", // 文学风格
  "academic", // 学术风格
  "legal", // 法律风格
  "literal", // 直译风格
  "idiomatic", // 意译风格
  "transcreation", // 创译风格
  "machine-like", // 机器风格
  "concise", // 简明风格
];
export const BUILTIN_PLACEHOLDERS = ["{ }", "{{ }}", "[ ]", "[[ ]]"];
export const BUILTIN_PLACETAGS = ["i", "a", "b", "x", "span"];
export const PLACETAG_FORMATS = ["compact", "attribute"]; // 占位符格式:简洁格式、属性格式

export const OPT_LANGS_TO = [
  ["en", "English - English"],
  ["zh-CN", "Simplified Chinese - 简体中文"],
  ["zh-TW", "Traditional Chinese - 繁體中文"],
  ["ar", "Arabic - العربية"],
  ["bg", "Bulgarian - Български"],
  ["ca", "Catalan - Català"],
  ["hr", "Croatian - Hrvatski"],
  ["cs", "Czech - Čeština"],
  ["da", "Danish - Dansk"],
  ["nl", "Dutch - Nederlands"],
  ["fa", "Persian - فارسی"],
  ["fi", "Finnish - Suomi"],
  ["fr", "French - Français"],
  ["de", "German - Deutsch"],
  ["el", "Greek - Ελληνικά"],
  ["hi", "Hindi - हिन्दी"],
  ["hu", "Hungarian - Magyar"],
  ["id", "Indonesian - Indonesia"],
  ["it", "Italian - Italiano"],
  ["ja", "Japanese - 日本語"],
  ["ko", "Korean - 한국어"],
  ["ms", "Malay - Melayu"],
  ["mt", "Maltese - Malti"],
  ["nb", "Norwegian - Norsk Bokmål"],
  ["pl", "Polish - Polski"],
  ["pt", "Portuguese - Português"],
  ["ro", "Romanian - Română"],
  ["ru", "Russian - Русский"],
  ["sk", "Slovak - Slovenčina"],
  ["sl", "Slovenian - Slovenščina"],
  ["es", "Spanish - Español"],
  ["sv", "Swedish - Svenska"],
  ["ta", "Tamil - தமிழ்"],
  ["te", "Telugu - తెలుగు"],
  ["th", "Thai - ไทย"],
  ["tr", "Turkish - Türkçe"],
  ["uk", "Ukrainian - Українська"],
  ["vi", "Vietnamese - Tiếng Việt"],
];
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
export const OPT_LANGS_MAP = new Map(OPT_LANGS_TO);

// CODE->名称
export const OPT_LANGS_SPEC_NAME = new Map(
  OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
);
export const OPT_LANGS_SPEC_DEFAULT = new Map(
  OPT_LANGS_FROM.map(([key]) => [key, key])
);
export const OPT_LANGS_SPEC_DEFAULT_UC = new Map(
  OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()])
);
export const OPT_LANGS_TO_SPEC = {
  [OPT_TRANS_BUILTINAI]: new Map([
    ...OPT_LANGS_SPEC_DEFAULT,
    ["zh-CN", "zh"],
    ["zh-TW", "zh"],
  ]),
  [OPT_TRANS_GOOGLE]: OPT_LANGS_SPEC_DEFAULT,
  [OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT,
  [OPT_TRANS_MICROSOFT]: new Map([
    ...OPT_LANGS_SPEC_DEFAULT,
    ["auto", ""],
    ["zh-CN", "zh-Hans"],
 
Download .txt
gitextract_hbvdxx0r/

├── .babelrc
├── .github/
│   └── workflows/
│       └── release.yml
├── .gitignore
├── .pnpm-version
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.en.md
├── README.ja.md
├── README.ko.md
├── README.md
├── VERSION_MANAGEMENT.md
├── config-overrides.js
├── custom-api.md
├── custom-api_v2.md
├── kiss-translator.webm
├── package.json
├── public/
│   ├── .nojekyll
│   ├── _locales/
│   │   ├── de/
│   │   │   └── messages.json
│   │   ├── en/
│   │   │   └── messages.json
│   │   ├── es/
│   │   │   └── messages.json
│   │   ├── fr/
│   │   │   └── messages.json
│   │   ├── ja/
│   │   │   └── messages.json
│   │   ├── ko/
│   │   │   └── messages.json
│   │   ├── zh_CN/
│   │   │   └── messages.json
│   │   └── zh_TW/
│   │       └── messages.json
│   ├── content.html
│   ├── index.html
│   ├── manifest.firefox.json
│   ├── manifest.json
│   └── manifest.thunderbird.json
└── src/
    ├── apis/
    │   ├── baidu.js
    │   ├── deepl.js
    │   ├── history.js
    │   ├── index.js
    │   └── trans.js
    ├── background.js
    ├── common.js
    ├── components/
    │   └── Logo/
    │       ├── icon.base64.js
    │       └── index.js
    ├── config/
    │   ├── api.js
    │   ├── app.js
    │   ├── client.js
    │   ├── i18n.js
    │   ├── index.js
    │   ├── msg.js
    │   ├── quotes.js
    │   ├── rules.js
    │   ├── setting.js
    │   ├── storage.js
    │   ├── styles.js
    │   └── url.js
    ├── content.js
    ├── hooks/
    │   ├── Alert.js
    │   ├── Api.js
    │   ├── Audio.js
    │   ├── ColorMode.js
    │   ├── Confirm.js
    │   ├── CustomStyles.js
    │   ├── DebouncedCallback.js
    │   ├── Fab.js
    │   ├── FavWords.js
    │   ├── Fetch.js
    │   ├── I18n.js
    │   ├── InputRule.js
    │   ├── Loading.js
    │   ├── MouseHover.js
    │   ├── Rules.js
    │   ├── Setting.js
    │   ├── Shortcut.js
    │   ├── Storage.js
    │   ├── SubRules.js
    │   ├── Subtitle.js
    │   ├── Sync.js
    │   ├── Theme.js
    │   ├── Tranbox.js
    │   ├── ValidationInput.js
    │   ├── WindowSize.js
    │   ├── useAutoHideTranBtn.js
    │   ├── useSelectionController.js
    │   ├── useTranBoxState.js
    │   └── useTranboxShortcuts.js
    ├── index.js
    ├── injector-shadowroot.js
    ├── injector-subtitle.js
    ├── injectors/
    │   ├── index.js
    │   ├── shadowroot.js
    │   └── xmlhttp.js
    ├── libs/
    │   ├── auth.js
    │   ├── batchQueue.js
    │   ├── blacklist.js
    │   ├── browser.js
    │   ├── builtinAI.js
    │   ├── cache.js
    │   ├── client.js
    │   ├── detect.js
    │   ├── docInfo.js
    │   ├── domManager.js
    │   ├── fabManager.js
    │   ├── fetch.js
    │   ├── gm.js
    │   ├── iframe.js
    │   ├── injector.js
    │   ├── inputTranslate.js
    │   ├── interpreter.js
    │   ├── log.js
    │   ├── mobile.js
    │   ├── msg.js
    │   ├── pool.js
    │   ├── popupManager.js
    │   ├── rules.js
    │   ├── shadowDomManager.js
    │   ├── shortcut.js
    │   ├── storage.js
    │   ├── stream.js
    │   ├── style.js
    │   ├── subRules.js
    │   ├── svg.js
    │   ├── sync.js
    │   ├── touch.js
    │   ├── tranbox.js
    │   ├── translator.js
    │   ├── translatorManager.js
    │   ├── trustedTypes.js
    │   ├── url.js
    │   └── utils.js
    ├── options.js
    ├── popup.js
    ├── rules.js
    ├── scripts/
    │   ├── archive.mjs
    │   ├── build-ios.mjs
    │   ├── build-safari.js
    │   ├── build-safari.mjs
    │   ├── build-task.mjs
    │   ├── sync-version.mjs
    │   └── update-version.mjs
    ├── subtitle/
    │   ├── BilingualSubtitleManager.js
    │   ├── Menus.js
    │   ├── YouTubeCaptionProvider.js
    │   ├── YouTubeSubtitleList.js
    │   ├── subtitle.js
    │   └── vtt.js
    ├── userscript.js
    └── views/
        ├── Action/
        │   ├── ContentFab.js
        │   ├── Draggable.js
        │   └── index.js
        ├── Options/
        │   ├── About.js
        │   ├── Apis.js
        │   ├── DarkModeButton.js
        │   ├── DownloadButton.js
        │   ├── FavWords.js
        │   ├── Header.js
        │   ├── HelpButton.js
        │   ├── InputSetting.js
        │   ├── Layout.js
        │   ├── MouseHover.js
        │   ├── Navigator.js
        │   ├── Playground.js
        │   ├── ReusableAutocomplete.js
        │   ├── Rules.js
        │   ├── Setting.js
        │   ├── ShortcutInput.js
        │   ├── ShowMoreButton.js
        │   ├── StylesSetting.js
        │   ├── Subtitle.js
        │   ├── SyncSetting.js
        │   ├── Tranbox.js
        │   ├── UploadButton.js
        │   └── index.js
        ├── Popup/
        │   ├── Header.js
        │   ├── PopupCont.js
        │   └── index.js
        └── Selection/
            ├── AudioBtn.js
            ├── CopyBtn.js
            ├── DictCont.js
            ├── DictHandler.js
            ├── DraggableResizable.js
            ├── FavBtn.js
            ├── SugCont.js
            ├── TranBox.js
            ├── TranBtn.js
            ├── TranCont.js
            ├── TranForm.js
            └── index.js
Download .txt
SYMBOL INDEX (671 symbols across 109 files)

FILE: src/background.js
  function updateIcon (line 45) | async function updateIcon(isActive, tabId) {
  constant CSP_RULE_START_ID (line 66) | const CSP_RULE_START_ID = 1;
  constant ORI_RULE_START_ID (line 67) | const ORI_RULE_START_ID = 10000;
  constant CSP_REMOVE_HEADERS (line 68) | const CSP_REMOVE_HEADERS = [
  constant DEFAULT_SEPARATE_WINDOW_BOUNDS (line 80) | const DEFAULT_SEPARATE_WINDOW_BOUNDS = {
  function persistSeparateWindowBounds (line 90) | async function persistSeparateWindowBounds(bounds) {
  function openSeparateWindowWithSavedBounds (line 103) | async function openSeparateWindowWithSavedBounds() {
  function updateCacheFromActual (line 151) | async function updateCacheFromActual(windowId) {
  function addContextMenus (line 214) | async function addContextMenus(contextMenuType = 1) {
  function updateCspRules (line 265) | async function updateCspRules({ csplist, orilist }) {
  function registerMsgDisplayScript (line 347) | async function registerMsgDisplayScript() {
  function handleStreamFetch (line 518) | async function handleStreamFetch(port, args) {

FILE: src/common.js
  function runSettingPage (line 21) | function runSettingPage() {
  function showErr (line 43) | function showErr(message) {
  function getFavWords (line 101) | async function getFavWords(rule) {
  function run (line 119) | async function run(isUserscript = false) {

FILE: src/components/Logo/icon.base64.js
  constant FAVICON_BASE64 (line 1) | const FAVICON_BASE64 =

FILE: src/config/api.js
  constant DEFAULT_HTTP_TIMEOUT (line 1) | const DEFAULT_HTTP_TIMEOUT = 10000;
  constant DEFAULT_FETCH_LIMIT (line 2) | const DEFAULT_FETCH_LIMIT = 10;
  constant DEFAULT_FETCH_INTERVAL (line 3) | const DEFAULT_FETCH_INTERVAL = 100;
  constant DEFAULT_BATCH_INTERVAL (line 4) | const DEFAULT_BATCH_INTERVAL = 400;
  constant DEFAULT_BATCH_SIZE (line 5) | const DEFAULT_BATCH_SIZE = 10;
  constant DEFAULT_BATCH_LENGTH (line 6) | const DEFAULT_BATCH_LENGTH = 10000;
  constant DEFAULT_CONTEXT_SIZE (line 7) | const DEFAULT_CONTEXT_SIZE = 3;
  constant INPUT_PLACE_URL (line 9) | const INPUT_PLACE_URL = "{{url}}";
  constant INPUT_PLACE_FROM (line 10) | const INPUT_PLACE_FROM = "{{from}}";
  constant INPUT_PLACE_TO (line 11) | const INPUT_PLACE_TO = "{{to}}";
  constant INPUT_PLACE_FROM_LANG (line 12) | const INPUT_PLACE_FROM_LANG = "{{fromLang}}";
  constant INPUT_PLACE_TO_LANG (line 13) | const INPUT_PLACE_TO_LANG = "{{toLang}}";
  constant INPUT_PLACE_TEXT (line 14) | const INPUT_PLACE_TEXT = "{{text}}";
  constant INPUT_PLACE_TONE (line 15) | const INPUT_PLACE_TONE = "{{tone}}";
  constant INPUT_PLACE_TITLE (line 16) | const INPUT_PLACE_TITLE = "{{title}}";
  constant INPUT_PLACE_DESCRIPTION (line 17) | const INPUT_PLACE_DESCRIPTION = "{{description}}";
  constant INPUT_PLACE_SUMMARY (line 18) | const INPUT_PLACE_SUMMARY = "{{summary}}";
  constant INPUT_PLACE_KEY (line 19) | const INPUT_PLACE_KEY = "{{key}}";
  constant INPUT_PLACE_MODEL (line 20) | const INPUT_PLACE_MODEL = "{{model}}";
  constant OPT_DICT_BING (line 23) | const OPT_DICT_BING = "Bing";
  constant OPT_DICT_YOUDAO (line 24) | const OPT_DICT_YOUDAO = "Youdao";
  constant OPT_DICT_ALL (line 25) | const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_YOUDAO];
  constant OPT_DICT_MAP (line 26) | const OPT_DICT_MAP = new Set(OPT_DICT_ALL);
  constant OPT_SUG_BAIDU (line 28) | const OPT_SUG_BAIDU = "Baidu";
  constant OPT_SUG_YOUDAO (line 29) | const OPT_SUG_YOUDAO = "Youdao";
  constant OPT_SUG_ALL (line 30) | const OPT_SUG_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
  constant OPT_SUG_MAP (line 31) | const OPT_SUG_MAP = new Set(OPT_SUG_ALL);
  constant OPT_TRANS_BUILTINAI (line 33) | const OPT_TRANS_BUILTINAI = "BuiltinAI";
  constant OPT_TRANS_GOOGLE (line 34) | const OPT_TRANS_GOOGLE = "Google";
  constant OPT_TRANS_GOOGLE_2 (line 35) | const OPT_TRANS_GOOGLE_2 = "Google2";
  constant OPT_TRANS_MICROSOFT (line 36) | const OPT_TRANS_MICROSOFT = "Microsoft";
  constant OPT_TRANS_AZUREAI (line 37) | const OPT_TRANS_AZUREAI = "AzureAI";
  constant OPT_TRANS_DEEPL (line 38) | const OPT_TRANS_DEEPL = "DeepL";
  constant OPT_TRANS_DEEPLX (line 39) | const OPT_TRANS_DEEPLX = "DeepLX";
  constant OPT_TRANS_DEEPLFREE (line 40) | const OPT_TRANS_DEEPLFREE = "DeepLFree";
  constant OPT_TRANS_NIUTRANS (line 41) | const OPT_TRANS_NIUTRANS = "NiuTrans";
  constant OPT_TRANS_BAIDU (line 42) | const OPT_TRANS_BAIDU = "Baidu";
  constant OPT_TRANS_TENCENT (line 43) | const OPT_TRANS_TENCENT = "Tencent";
  constant OPT_TRANS_VOLCENGINE (line 44) | const OPT_TRANS_VOLCENGINE = "Volcengine";
  constant OPT_TRANS_OPENAI (line 45) | const OPT_TRANS_OPENAI = "OpenAI";
  constant OPT_TRANS_GEMINI (line 46) | const OPT_TRANS_GEMINI = "Gemini";
  constant OPT_TRANS_GEMINI_2 (line 47) | const OPT_TRANS_GEMINI_2 = "Gemini2";
  constant OPT_TRANS_CLAUDE (line 48) | const OPT_TRANS_CLAUDE = "Claude";
  constant OPT_TRANS_CLOUDFLAREAI (line 49) | const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
  constant OPT_TRANS_OLLAMA (line 50) | const OPT_TRANS_OLLAMA = "Ollama";
  constant OPT_TRANS_OPENROUTER (line 51) | const OPT_TRANS_OPENROUTER = "OpenRouter";
  constant OPT_TRANS_CUSTOMIZE (line 52) | const OPT_TRANS_CUSTOMIZE = "Custom";
  constant OPT_ALL_TRANS_TYPES (line 55) | const OPT_ALL_TRANS_TYPES = [
  constant OPT_LANGDETECTOR_ALL (line 78) | const OPT_LANGDETECTOR_ALL = [
  constant OPT_LANGDETECTOR_MAP (line 86) | const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
  constant API_SPE_TYPES (line 89) | const API_SPE_TYPES = {
  constant BUILTIN_STONES (line 160) | const BUILTIN_STONES = [
  constant BUILTIN_PLACEHOLDERS (line 175) | const BUILTIN_PLACEHOLDERS = ["{ }", "{{ }}", "[ ]", "[[ ]]"];
  constant BUILTIN_PLACETAGS (line 176) | const BUILTIN_PLACETAGS = ["i", "a", "b", "x", "span"];
  constant PLACETAG_FORMATS (line 177) | const PLACETAG_FORMATS = ["compact", "attribute"];
  constant OPT_LANGS_TO (line 179) | const OPT_LANGS_TO = [
  constant OPT_LANGS_LIST (line 219) | const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
  constant OPT_LANGS_FROM (line 220) | const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
  constant OPT_LANGS_MAP (line 221) | const OPT_LANGS_MAP = new Map(OPT_LANGS_TO);
  constant OPT_LANGS_SPEC_NAME (line 224) | const OPT_LANGS_SPEC_NAME = new Map(
  constant OPT_LANGS_SPEC_DEFAULT (line 227) | const OPT_LANGS_SPEC_DEFAULT = new Map(
  constant OPT_LANGS_SPEC_DEFAULT_UC (line 230) | const OPT_LANGS_SPEC_DEFAULT_UC = new Map(
  constant OPT_LANGS_TO_SPEC (line 233) | const OPT_LANGS_TO_SPEC = {
  constant OPT_LANGS_TO_CODE (line 361) | const OPT_LANGS_TO_CODE = {};
  constant DEFAULT_API_LIST (line 662) | const DEFAULT_API_LIST = OPT_ALL_TRANS_TYPES.map((apiType) => ({
  constant DEFAULT_API_TYPE (line 669) | const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
  constant DEFAULT_API_SETTING (line 670) | const DEFAULT_API_SETTING = DEFAULT_API_LIST.find(

FILE: src/config/app.js
  constant APP_NAME (line 1) | const APP_NAME = process.env.REACT_APP_NAME.trim()
  constant APP_LCNAME (line 4) | const APP_LCNAME = APP_NAME.toLowerCase();
  constant APP_UPNAME (line 5) | const APP_UPNAME = APP_NAME.toUpperCase();
  constant APP_CONSTS (line 6) | const APP_CONSTS = {
  constant APP_VERSION (line 12) | const APP_VERSION = process.env.REACT_APP_VERSION.split(".");
  constant THEME_LIGHT (line 14) | const THEME_LIGHT = "light";
  constant THEME_DARK (line 15) | const THEME_DARK = "dark";

FILE: src/config/client.js
  constant CLIENT_WEB (line 1) | const CLIENT_WEB = "web";
  constant CLIENT_CHROME (line 2) | const CLIENT_CHROME = "chrome";
  constant CLIENT_EDGE (line 3) | const CLIENT_EDGE = "edge";
  constant CLIENT_FIREFOX (line 4) | const CLIENT_FIREFOX = "firefox";
  constant CLIENT_USERSCRIPT (line 5) | const CLIENT_USERSCRIPT = "userscript";
  constant CLIENT_THUNDERBIRD (line 6) | const CLIENT_THUNDERBIRD = "thunderbird";
  constant CLIENT_EXTS (line 7) | const CLIENT_EXTS = [
  constant DEFAULT_USER_AGENT (line 14) | const DEFAULT_USER_AGENT =

FILE: src/config/i18n.js
  constant UI_LANGS (line 1) | const UI_LANGS = [
  constant I18N (line 183) | const I18N = {

FILE: src/config/msg.js
  constant CMD_TOGGLE_TRANSLATE (line 1) | const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
  constant CMD_TOGGLE_STYLE (line 2) | const CMD_TOGGLE_STYLE = "toggleStyle";
  constant CMD_OPEN_OPTIONS (line 3) | const CMD_OPEN_OPTIONS = "openOptions";
  constant CMD_OPEN_TRANBOX (line 4) | const CMD_OPEN_TRANBOX = "openTranbox";
  constant CMD_OPEN_SEPARATE_WINDOW (line 5) | const CMD_OPEN_SEPARATE_WINDOW = "openSeparateWindow";
  constant MSG_FETCH (line 7) | const MSG_FETCH = "kiss_fetch";
  constant MSG_GET_HTTPCACHE (line 8) | const MSG_GET_HTTPCACHE = "get_httpcache";
  constant MSG_PUT_HTTPCACHE (line 9) | const MSG_PUT_HTTPCACHE = "put_httpcache";
  constant MSG_OPEN_OPTIONS (line 10) | const MSG_OPEN_OPTIONS = "open_options";
  constant MSG_SAVE_RULE (line 11) | const MSG_SAVE_RULE = "save_rule";
  constant MSG_TRANS_TOGGLE (line 12) | const MSG_TRANS_TOGGLE = "toggle_translate";
  constant MSG_TRANS_TOGGLE_STYLE (line 13) | const MSG_TRANS_TOGGLE_STYLE = "toggle_styles";
  constant MSG_OPEN_TRANBOX (line 14) | const MSG_OPEN_TRANBOX = "open_tranbox";
  constant MSG_TRANS_GETRULE (line 15) | const MSG_TRANS_GETRULE = "trans_getrule";
  constant MSG_TRANS_PUTRULE (line 16) | const MSG_TRANS_PUTRULE = "trans_putrule";
  constant MSG_TRANS_CURRULE (line 17) | const MSG_TRANS_CURRULE = "trans_currule";
  constant MSG_TRANSBOX_TOGGLE (line 18) | const MSG_TRANSBOX_TOGGLE = "toggle_transbox";
  constant MSG_POPUP_TOGGLE (line 19) | const MSG_POPUP_TOGGLE = "toggle_popup";
  constant MSG_MOUSEHOVER_TOGGLE (line 20) | const MSG_MOUSEHOVER_TOGGLE = "toggle_mousehover";
  constant MSG_HOVERNODE_TOGGLE (line 21) | const MSG_HOVERNODE_TOGGLE = "toggle_hover_node";
  constant MSG_TRANSINPUT_TOGGLE (line 22) | const MSG_TRANSINPUT_TOGGLE = "toggle_input_translation";
  constant MSG_INPUT_TRANSLATE (line 23) | const MSG_INPUT_TRANSLATE = "input_translate";
  constant MSG_CONTEXT_MENUS (line 24) | const MSG_CONTEXT_MENUS = "context_menus";
  constant MSG_COMMAND_SHORTCUTS (line 25) | const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
  constant MSG_INJECT_JS (line 26) | const MSG_INJECT_JS = "inject_js";
  constant MSG_INJECT_CSS (line 27) | const MSG_INJECT_CSS = "inject_css";
  constant MSG_UPDATE_CSP (line 28) | const MSG_UPDATE_CSP = "update_csp";
  constant MSG_BUILTINAI_DETECT (line 29) | const MSG_BUILTINAI_DETECT = "builtinai_detect";
  constant MSG_BUILTINAI_TRANSLATE (line 30) | const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";
  constant MSG_SET_LOGLEVEL (line 31) | const MSG_SET_LOGLEVEL = "set_loglevel";
  constant MSG_CLEAR_CACHES (line 32) | const MSG_CLEAR_CACHES = "clear_caches";
  constant MSG_OPEN_SEPARATE_WINDOW (line 33) | const MSG_OPEN_SEPARATE_WINDOW = "open_separate_window";
  constant PORT_STREAM_FETCH (line 34) | const PORT_STREAM_FETCH = "kiss_stream_fetch";
  constant MSG_UPDATE_ICON (line 35) | const MSG_UPDATE_ICON = "update_icon";
  constant EVENT_KISS_INNER (line 37) | const EVENT_KISS_INNER = "kiss_translator_inner";
  constant EVENT_KISS_TRANSLATOR (line 38) | const EVENT_KISS_TRANSLATOR = "kiss_translator";
  constant MSG_XHR_DATA_YOUTUBE (line 40) | const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
  constant MSG_MENUS_PROGRESSED (line 44) | const MSG_MENUS_PROGRESSED = "progressed";
  constant MSG_MENUS_UPDATEFORM (line 45) | const MSG_MENUS_UPDATEFORM = "updateFormData";

FILE: src/config/quotes.js
  function getRandomQuote (line 710) | function getRandomQuote() {

FILE: src/config/rules.js
  constant GLOBAL_KEY (line 4) | const GLOBAL_KEY = "*";
  constant REMAIN_KEY (line 5) | const REMAIN_KEY = "-";
  constant SHADOW_KEY (line 6) | const SHADOW_KEY = ">>>";
  constant DEFAULT_COLOR (line 8) | const DEFAULT_COLOR = "#209CEE";
  constant DEFAULT_TRANS_TAG (line 10) | const DEFAULT_TRANS_TAG = "font";
  constant DEFAULT_SELECT_STYLE (line 11) | const DEFAULT_SELECT_STYLE =
  constant OPT_TIMING_PAGESCROLL (line 14) | const OPT_TIMING_PAGESCROLL = "mk_pagescroll";
  constant OPT_TIMING_PAGEOPEN (line 15) | const OPT_TIMING_PAGEOPEN = "mk_pageopen";
  constant OPT_TIMING_MOUSEOVER (line 16) | const OPT_TIMING_MOUSEOVER = "mk_mouseover";
  constant OPT_TIMING_CONTROL (line 17) | const OPT_TIMING_CONTROL = "mk_ctrlKey";
  constant OPT_TIMING_SHIFT (line 18) | const OPT_TIMING_SHIFT = "mk_shiftKey";
  constant OPT_TIMING_ALT (line 19) | const OPT_TIMING_ALT = "mk_altKey";
  constant OPT_TIMING_ALL (line 20) | const OPT_TIMING_ALL = [
  constant OPT_SPLIT_PARAGRAPH_DISABLE (line 29) | const OPT_SPLIT_PARAGRAPH_DISABLE = "split_disable";
  constant OPT_SPLIT_PARAGRAPH_TEXTLENGTH (line 30) | const OPT_SPLIT_PARAGRAPH_TEXTLENGTH = "split_textlength";
  constant OPT_SPLIT_PARAGRAPH_PUNCTUATION (line 31) | const OPT_SPLIT_PARAGRAPH_PUNCTUATION = "split_punctuation";
  constant OPT_SPLIT_PARAGRAPH_ALL (line 32) | const OPT_SPLIT_PARAGRAPH_ALL = [
  constant OPT_HIGHLIGHT_WORDS_DISABLE (line 38) | const OPT_HIGHLIGHT_WORDS_DISABLE = "highlight_disable";
  constant OPT_HIGHLIGHT_WORDS_BEFORETRANS (line 39) | const OPT_HIGHLIGHT_WORDS_BEFORETRANS = "highlight_beforetrans";
  constant OPT_HIGHLIGHT_WORDS_AFTERTRANS (line 40) | const OPT_HIGHLIGHT_WORDS_AFTERTRANS = "highlight_aftertrans";
  constant OPT_HIGHLIGHT_WORDS_ALL (line 41) | const OPT_HIGHLIGHT_WORDS_ALL = [
  constant DEFAULT_SELECTOR (line 47) | const DEFAULT_SELECTOR =
  constant DEFAULT_IGNORE_SELECTOR (line 49) | const DEFAULT_IGNORE_SELECTOR = "button, footer, pre, mark, nav";
  constant DEFAULT_KEEP_SELECTOR (line 50) | const DEFAULT_KEEP_SELECTOR = `code, cite, math, .math, a:has(code)`;
  constant DEFAULT_RULE (line 51) | const DEFAULT_RULE = {
  constant GLOBLA_RULE (line 96) | const GLOBLA_RULE = {
  constant DEFAULT_RULES (line 140) | const DEFAULT_RULES = [GLOBLA_RULE];
  constant RULES_MAP (line 143) | const RULES_MAP = {
  constant BUILTIN_RULES (line 187) | const BUILTIN_RULES = Object.entries(RULES_MAP).map(

FILE: src/config/setting.js
  constant OPT_SHORTCUT_TRANSLATE (line 12) | const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
  constant OPT_SHORTCUT_STYLE (line 13) | const OPT_SHORTCUT_STYLE = "toggleStyle";
  constant OPT_SHORTCUT_POPUP (line 14) | const OPT_SHORTCUT_POPUP = "togglePopup";
  constant OPT_SHORTCUT_SETTING (line 15) | const OPT_SHORTCUT_SETTING = "openSetting";
  constant DEFAULT_SHORTCUTS (line 16) | const DEFAULT_SHORTCUTS = {
  constant TRANS_MIN_LENGTH (line 23) | const TRANS_MIN_LENGTH = 2;
  constant TRANS_MAX_LENGTH (line 24) | const TRANS_MAX_LENGTH = 100000;
  constant TRANS_NEWLINE_LENGTH (line 25) | const TRANS_NEWLINE_LENGTH = 20;
  constant DEFAULT_BLACKLIST (line 26) | const DEFAULT_BLACKLIST = [
  constant DEFAULT_CSPLIST (line 31) | const DEFAULT_CSPLIST = [];
  constant DEFAULT_ORILIST (line 32) | const DEFAULT_ORILIST = ["https://dict.youdao.com"];
  constant OPT_SYNCTYPE_WORKER (line 35) | const OPT_SYNCTYPE_WORKER = "KISS-Worker";
  constant OPT_SYNCTYPE_WEBDAV (line 36) | const OPT_SYNCTYPE_WEBDAV = "WebDAV";
  constant OPT_SYNCTOKEN_PERFIX (line 37) | const OPT_SYNCTOKEN_PERFIX = "kt_";
  constant OPT_SYNCTYPE_ALL (line 38) | const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
  constant DEFAULT_SYNC (line 39) | const DEFAULT_SYNC = {
  constant OPT_INPUT_DOT_DISABLE (line 50) | const OPT_INPUT_DOT_DISABLE = "-";
  constant OPT_INPUT_DOT_MOBILE (line 51) | const OPT_INPUT_DOT_MOBILE = "mobile";
  constant OPT_INPUT_DOT_ALWAYS (line 52) | const OPT_INPUT_DOT_ALWAYS = "always";
  constant OPT_INPUT_TRANS_SIGNS (line 55) | const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
  constant DEFAULT_INPUT_SHORTCUT (line 56) | const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
  constant DEFAULT_INPUT_RULE (line 57) | const DEFAULT_INPUT_RULE = {
  constant PHONIC_MAP (line 70) | const PHONIC_MAP = {
  constant OPT_TRANBOX_TRIGGER_CLICK (line 74) | const OPT_TRANBOX_TRIGGER_CLICK = "click";
  constant OPT_TRANBOX_TRIGGER_HOVER (line 75) | const OPT_TRANBOX_TRIGGER_HOVER = "hover";
  constant OPT_TRANBOX_TRIGGER_SELECT (line 76) | const OPT_TRANBOX_TRIGGER_SELECT = "select";
  constant OPT_TRANBOX_TRIGGER_ALL (line 77) | const OPT_TRANBOX_TRIGGER_ALL = [
  constant DEFAULT_TRANBOX_SHORTCUT (line 82) | const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
  constant DEFAULT_TRANBOX_SETTING (line 83) | const DEFAULT_TRANBOX_SETTING = {
  constant SUBTITLE_WINDOW_STYLE (line 105) | const SUBTITLE_WINDOW_STYLE = `padding: 0.5em 1em;
  constant SUBTITLE_ORIGIN_STYLE (line 112) | const SUBTITLE_ORIGIN_STYLE = `font-size: clamp(1rem, 2cqw, 3rem);`;
  constant SUBTITLE_TRANSLATION_STYLE (line 113) | const SUBTITLE_TRANSLATION_STYLE = `font-size: clamp(1rem, 2cqw, 3rem);`;
  constant OPT_ENHANCE_ON (line 115) | const OPT_ENHANCE_ON = "on";
  constant OPT_ENHANCE_OFF (line 116) | const OPT_ENHANCE_OFF = "off";
  constant OPT_ENHANCE_MOBILE_OFF (line 117) | const OPT_ENHANCE_MOBILE_OFF = "mobile_off";
  constant DEFAULT_SUBTITLE_SETTING (line 119) | const DEFAULT_SUBTITLE_SETTING = {
  constant DEFAULT_SUBRULES_LIST (line 138) | const DEFAULT_SUBRULES_LIST = [
  constant DEFAULT_MOUSEHOVER_KEY (line 153) | const DEFAULT_MOUSEHOVER_KEY = ["ControlLeft"];
  constant DEFAULT_MOUSE_HOVER_SETTING (line 154) | const DEFAULT_MOUSE_HOVER_SETTING = {
  constant DEFAULT_SETTING (line 159) | const DEFAULT_SETTING = {

FILE: src/config/storage.js
  constant KV_RULES_KEY (line 3) | const KV_RULES_KEY = `kiss-rules_v${APP_VERSION[0]}.json`;
  constant KV_WORDS_KEY (line 4) | const KV_WORDS_KEY = "kiss-words.json";
  constant KV_RULES_SHARE_KEY (line 5) | const KV_RULES_SHARE_KEY = `kiss-rules-share_v${APP_VERSION[0]}.json`;
  constant KV_SETTING_KEY (line 6) | const KV_SETTING_KEY = `kiss-setting_v${APP_VERSION[0]}.json`;
  constant KV_SALT_SYNC (line 7) | const KV_SALT_SYNC = "KISS-Translator-SYNC";
  constant KV_SALT_SHARE (line 8) | const KV_SALT_SHARE = "KISS-Translator-SHARE";
  constant STOKEY_MSAUTH (line 10) | const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
  constant STOKEY_BDAUTH (line 11) | const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
  constant STOKEY_SETTING_OLD (line 12) | const STOKEY_SETTING_OLD = `${APP_NAME}_setting`;
  constant STOKEY_RULES_OLD (line 13) | const STOKEY_RULES_OLD = `${APP_NAME}_rules`;
  constant STOKEY_SETTING (line 14) | const STOKEY_SETTING = `${APP_NAME}_setting_v${APP_VERSION[0]}`;
  constant STOKEY_RULES (line 15) | const STOKEY_RULES = `${APP_NAME}_rules_v${APP_VERSION[0]}`;
  constant STOKEY_WORDS (line 16) | const STOKEY_WORDS = `${APP_NAME}_words`;
  constant STOKEY_SYNC (line 17) | const STOKEY_SYNC = `${APP_NAME}_sync`;
  constant STOKEY_FAB (line 18) | const STOKEY_FAB = `${APP_NAME}_fab`;
  constant STOKEY_TRANBOX (line 19) | const STOKEY_TRANBOX = `${APP_NAME}_tranbox`;
  constant STOKEY_SEPARATE_WINDOW (line 20) | const STOKEY_SEPARATE_WINDOW = `${APP_NAME}_separate_window`;
  constant STOKEY_RULESCACHE_PREFIX (line 21) | const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
  constant CACHE_NAME (line 23) | const CACHE_NAME = `${APP_NAME}_cache`;
  constant DEFAULT_CACHE_TIMEOUT (line 24) | const DEFAULT_CACHE_TIMEOUT = 3600 * 24 * 7;

FILE: src/config/styles.js
  constant OPT_STYLE_NONE (line 1) | const OPT_STYLE_NONE = "style_none";
  constant OPT_STYLE_LINE (line 2) | const OPT_STYLE_LINE = "under_line";
  constant OPT_STYLE_DOTLINE (line 3) | const OPT_STYLE_DOTLINE = "dot_line";
  constant OPT_STYLE_DASHLINE (line 4) | const OPT_STYLE_DASHLINE = "dash_line";
  constant OPT_STYLE_DASHLINE_BOLD (line 5) | const OPT_STYLE_DASHLINE_BOLD = "dash_line_bold";
  constant OPT_STYLE_DASHBOX (line 6) | const OPT_STYLE_DASHBOX = "dash_box";
  constant OPT_STYLE_DASHBOX_BOLD (line 7) | const OPT_STYLE_DASHBOX_BOLD = "dash_box_bold";
  constant OPT_STYLE_WAVYLINE (line 8) | const OPT_STYLE_WAVYLINE = "wavy_line";
  constant OPT_STYLE_WAVYLINE_BOLD (line 9) | const OPT_STYLE_WAVYLINE_BOLD = "wavy_line_bold";
  constant OPT_STYLE_MARKER (line 10) | const OPT_STYLE_MARKER = "marker";
  constant OPT_STYLE_GRADIENT_MARKER (line 11) | const OPT_STYLE_GRADIENT_MARKER = "gradient_marker";
  constant OPT_STYLE_FUZZY (line 12) | const OPT_STYLE_FUZZY = "fuzzy";
  constant OPT_STYLE_HIGHLIGHT (line 13) | const OPT_STYLE_HIGHLIGHT = "highlight";
  constant OPT_STYLE_BLOCKQUOTE (line 14) | const OPT_STYLE_BLOCKQUOTE = "blockquote";
  constant OPT_STYLE_GRADIENT (line 15) | const OPT_STYLE_GRADIENT = "gradient";
  constant OPT_STYLE_BLINK (line 16) | const OPT_STYLE_BLINK = "blink";
  constant OPT_STYLE_GLOW (line 17) | const OPT_STYLE_GLOW = "glow";
  constant OPT_STYLE_COLORFUL (line 18) | const OPT_STYLE_COLORFUL = "colorful";
  constant OPT_STYLE_ALL (line 19) | const OPT_STYLE_ALL = [
  constant DEFAULT_CUSTOM_STYLES (line 40) | const DEFAULT_CUSTOM_STYLES = [

FILE: src/config/url.js
  constant URL_CACHE_TRAN (line 3) | const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
  constant URL_CACHE_SUBTITLE (line 4) | const URL_CACHE_SUBTITLE = `https://${APP_LCNAME}/subtitle`;
  constant URL_CACHE_DELANG (line 5) | const URL_CACHE_DELANG = `https://${APP_LCNAME}/detectlang`;
  constant URL_CACHE_BINGDICT (line 6) | const URL_CACHE_BINGDICT = `https://${APP_LCNAME}/bingdict`;
  constant URL_KISS_WORKER (line 8) | const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
  constant URL_KISS_PROXY (line 9) | const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
  constant URL_KISS_RULES (line 10) | const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
  constant URL_KISS_RULES_NEW_ISSUE (line 11) | const URL_KISS_RULES_NEW_ISSUE =
  constant URL_RAW_PREFIX (line 13) | const URL_RAW_PREFIX =

FILE: src/hooks/Alert.js
  function AlertProvider (line 23) | function AlertProvider({ children }) {
  function useAlert (line 85) | function useAlert() {

FILE: src/hooks/Api.js
  function useApiState (line 5) | function useApiState() {
  function useApiList (line 19) | function useApiList() {
  function useApiItem (line 121) | function useApiItem(apiSlug) {

FILE: src/hooks/Audio.js
  function useAudio (line 10) | function useAudio(src) {

FILE: src/hooks/ColorMode.js
  function useDarkMode (line 8) | function useDarkMode() {

FILE: src/hooks/Confirm.js
  function ConfirmProvider (line 19) | function ConfirmProvider({ children }) {
  function useConfirm (line 91) | function useConfirm() {

FILE: src/hooks/CustomStyles.js
  function useStyleState (line 7) | function useStyleState() {
  function useStyleList (line 14) | function useStyleList() {
  function useAllTextStyles (line 65) | function useAllTextStyles() {

FILE: src/hooks/DebouncedCallback.js
  function useDebouncedCallback (line 4) | function useDebouncedCallback(callback, delay) {

FILE: src/hooks/Fab.js
  constant DEFAULT_FAB (line 4) | const DEFAULT_FAB = {};
  function useFab (line 10) | function useFab() {

FILE: src/hooks/FavWords.js
  constant DEFAULT_FAVWORDS (line 6) | const DEFAULT_FAVWORDS = {};
  function useFavWords (line 8) | function useFavWords() {

FILE: src/hooks/InputRule.js
  function useInputRule (line 4) | function useInputRule() {

FILE: src/hooks/Loading.js
  function Loading (line 5) | function Loading() {

FILE: src/hooks/MouseHover.js
  function useMouseHoverSetting (line 4) | function useMouseHoverSetting() {

FILE: src/hooks/Rules.js
  function useRules (line 11) | function useRules() {

FILE: src/hooks/Setting.js
  function SettingProvider (line 28) | function SettingProvider({ children, context }) {
  function useSetting (line 118) | function useSetting() {

FILE: src/hooks/Shortcut.js
  function useShortcut (line 5) | function useShortcut(action) {

FILE: src/hooks/Storage.js
  function useStorage (line 22) | function useStorage(key, defaultVal = null, syncKey = "") {

FILE: src/hooks/SubRules.js
  function useSubRules (line 11) | function useSubRules() {

FILE: src/hooks/Subtitle.js
  function useSubtitle (line 4) | function useSubtitle() {

FILE: src/hooks/Sync.js
  function useSync (line 9) | function useSync() {
  function useSyncMeta (line 18) | function useSyncMeta() {
  function useSyncCaches (line 45) | function useSyncCaches() {

FILE: src/hooks/Theme.js
  function Theme (line 12) | function Theme({ children, options = {}, styles = {} }) {

FILE: src/hooks/Tranbox.js
  function useTranbox (line 4) | function useTranbox() {

FILE: src/hooks/ValidationInput.js
  function ValidationInput (line 5) | function ValidationInput({

FILE: src/hooks/WindowSize.js
  function useWindowSize (line 4) | function useWindowSize() {

FILE: src/hooks/useAutoHideTranBtn.js
  function useAutoHideTranBtn (line 3) | function useAutoHideTranBtn(

FILE: src/hooks/useSelectionController.js
  function useSelectionController (line 10) | function useSelectionController({

FILE: src/hooks/useTranBoxState.js
  function useTranBoxState (line 7) | function useTranBoxState(tranboxSetting) {

FILE: src/hooks/useTranboxShortcuts.js
  function useTranboxShortcuts (line 12) | function useTranboxShortcuts({

FILE: src/index.js
  function App (line 13) | function App() {

FILE: src/injectors/index.js
  constant INJECTOR (line 7) | const INJECTOR = {
  function injectJs (line 17) | function injectJs(name, id = "kiss-translator-inject-js") {

FILE: src/libs/browser.js
  function _browser (line 7) | function _browser() {

FILE: src/libs/builtinAI.js
  class ChromeTranslator (line 6) | class ChromeTranslator {
    method constructor (line 10) | constructor(options = {}) {
    method #defaultProgressHandler (line 14) | #defaultProgressHandler(type, progress) {
    method #getDetectorPromise (line 18) | #getDetectorPromise() {
    method #createTranslator (line 40) | #createTranslator(sourceLanguage, targetLanguage) {
    method _monitorProgress (line 76) | _monitorProgress(monitorable, type) {
    method detectLanguage (line 83) | async detectLanguage(text, confidenceThreshold = 0.4) {
    method translateText (line 113) | async translateText(text, targetLanguage, sourceLanguage = "auto") {

FILE: src/libs/domManager.js
  class DomManager (line 12) | class DomManager {
    method constructor (line 24) | constructor({
    method isVisible (line 41) | get isVisible() {
    method show (line 49) | show(props) {
    method hide (line 74) | hide() {
    method destroy (line 85) | destroy() {
    method toggle (line 108) | toggle(props) {
    method updateProps (line 120) | updateProps(newProps) {
    method #mount (line 141) | #mount(props) {

FILE: src/libs/fabManager.js
  class FabManager (line 5) | class FabManager extends ShadowDomManager {
    method constructor (line 6) | constructor({ processActions, fabConfig }) {

FILE: src/libs/inputTranslate.js
  function getDeepActiveElement (line 24) | function getDeepActiveElement() {
  function isEditableTarget (line 36) | function isEditableTarget(node) {
  function getNodeText (line 58) | function getNodeText(node) {
  function setNativeValue (line 72) | function setNativeValue(element, value) {
  function smartReplaceText (line 92) | async function smartReplaceText(node, newText) {
  function checkSuccess (line 174) | function checkSuccess(node, targetText) {
  function addLoading (line 183) | function addLoading(node, loadingId) {
  function removeLoading (line 211) | function removeLoading(loadingId) {
  class InputTranslator (line 220) | class InputTranslator {
    method constructor (line 237) | constructor({ inputRule = DEFAULT_INPUT_RULE, transApis = [] } = {}) {
    method enable (line 255) | enable() {
    method disable (line 289) | disable() {
    method toggle (line 323) | toggle() {
    method handleFocusIn (line 331) | handleFocusIn() {
    method handleFocusOut (line 350) | handleFocusOut() {
    method showFloatButton (line 370) | showFloatButton(inputNode) {
    method createFloatButtonDOM (line 393) | createFloatButtonDOM() {
    method hideFloatButton (line 438) | hideFloatButton() {
    method removeFloatButton (line 445) | removeFloatButton() {
    method updateBtnPosition (line 452) | updateBtnPosition() {
    method handleTranslate (line 491) | async handleTranslate({ isBtnTrigger = false } = {}) {
    method updateConfig (line 586) | updateConfig({ inputRule, transApis }) {

FILE: src/libs/log.js
  function findLogLevelByValue (line 10) | function findLogLevelByValue(value) {
  function findLogLevelByName (line 14) | function findLogLevelByName(name) {
  class Logger (line 20) | class Logger {
    method constructor (line 26) | constructor(options = {}) {
    method setLevel (line 37) | setLevel(level) {
    method _log (line 79) | _log(level, ...args) {
    method _getConsoleMethod (line 114) | _getConsoleMethod(level) {
    method debug (line 131) | debug(...args) {
    method info (line 139) | info(...args) {
    method warn (line 147) | warn(...args) {
    method error (line 155) | error(...args) {

FILE: src/libs/pool.js
  class TaskPool (line 7) | class TaskPool {
    method constructor (line 19) | constructor(
    method #scheduleNext (line 32) | #scheduleNext() {
    method #execute (line 65) | async #execute(task) {
    method push (line 94) | push(fn, args) {
    method update (line 106) | update(interval, limit) {
    method clear (line 120) | clear() {

FILE: src/libs/popupManager.js
  class PopupManager (line 5) | class PopupManager extends ShadowDomManager {
    method constructor (line 6) | constructor({ translator, processActions }) {
    method toggle (line 15) | toggle(props) {

FILE: src/libs/rules.js
  function mergeSelectors (line 16) | function mergeSelectors(defaultStr, userStr) {

FILE: src/libs/shadowDomManager.js
  class ShadowDomManager (line 7) | class ShadowDomManager {
    method constructor (line 18) | constructor({
    method isVisible (line 35) | get isVisible() {
    method show (line 39) | show(props) {
    method hide (line 61) | hide() {
    method destroy (line 69) | destroy() {
    method toggle (line 88) | toggle(props) {
    method #mount (line 96) | #mount(props) {

FILE: src/libs/storage.js
  function set (line 23) | async function set(key, val) {
  function get (line 33) | async function get(key) {
  function del (line 44) | async function del(key) {
  function setObj (line 54) | async function setObj(key, obj) {
  function trySetObj (line 58) | async function trySetObj(key, obj) {
  function getObj (line 64) | async function getObj(key) {
  function putObj (line 75) | async function putObj(key, obj) {

FILE: src/libs/stream.js
  method iterate (line 66) | async *iterate() {
  function getStreamDelta (line 88) | function getStreamDelta(json, apiType) {
  function createStreamingJsonParser (line 165) | function createStreamingJsonParser() {
  function detectStreamFormat (line 214) | function detectStreamFormat(content) {

FILE: src/libs/svg.js
  function createSVGElement (line 15) | function createSVGElement(tag, attributes) {
  function createLoadingSVG (line 28) | function createLoadingSVG() {
  function createLogoSVG (line 69) | function createLogoSVG({

FILE: src/libs/touch.js
  function touchTapListener (line 1) | function touchTapListener(fn, options = {}) {

FILE: src/libs/tranbox.js
  class TransboxManager (line 8) | class TransboxManager {
    method constructor (line 14) | constructor(initialProps = {}) {
    method isEnabled (line 23) | isEnabled() {
    method enable (line 27) | enable() {
    method disable (line 56) | disable() {
    method toggle (line 67) | toggle() {
    method update (line 75) | update(newProps) {

FILE: src/libs/translator.js
  class Translator (line 46) | class Translator {
    method isElement (line 227) | static isElement(el) {
    method isElementOrFragment (line 231) | static isElementOrFragment(el) {
    method isBlockNode (line 236) | static isBlockNode(el) {
    method hasBlockNode (line 253) | static hasBlockNode(el) {
    method hasTextNode (line 264) | static hasTextNode(el) {
    method escapeRegex (line 275) | static escapeRegex(str) {
    method #ignoreSelector (line 336) | get #ignoreSelector() {
    method #apiSetting (line 356) | get #apiSetting() {
    method #placeholderConfig (line 366) | get #placeholderConfig() {
    method constructor (line 418) | constructor({ rule = {}, setting = {}, favWords = [] }) {
    method #run (line 461) | #run() {
    method #init (line 470) | #init() {
    method #handleWindowMessage (line 502) | #handleWindowMessage(event) {
    method #attachShadowRootListener (line 508) | #attachShadowRootListener() {
    method #removeShadowRootListener (line 519) | #removeShadowRootListener() {
    method #findAndObserveShadowRoot (line 524) | #findAndObserveShadowRoot() {
    method #createTextStyles (line 535) | #createTextStyles() {
    method #injectSheet (line 544) | #injectSheet(shadowRoot) {
    method #parseTerms (line 554) | #parseTerms(termsString) {
    method #parseAITerms (line 590) | #parseAITerms(termsString) {
    method #getDocDescription (line 609) | #getDocDescription() {
    method #createIntersectionObserver (line 621) | #createIntersectionObserver() {
    method #createMutationObserver (line 647) | #createMutationObserver() {
    method #createDebounceMouseMover (line 693) | #createDebounceMouseMover() {
    method #handleMouseMove (line 717) | #handleMouseMove(event) {
    method #handleKeyDown (line 723) | #handleKeyDown() {
    method toggleHoverNode (line 734) | toggleHoverNode() {
    method #toggleTargetNode (line 739) | #toggleTargetNode(targetNode) {
    method #getShadowRoot (line 748) | #getShadowRoot(element) {
    method #findAllShadowRoots (line 765) | #findAllShadowRoots(root = document.body, results = new Set()) {
    method #findChangeContainer (line 787) | #findChangeContainer(startNode) {
    method #queueForRescan (line 812) | #queueForRescan(target) {
    method #rescanContainer (line 825) | #rescanContainer(changedNode) {
    method #reIO (line 834) | #reIO(node) {
    method #reIOViewNodes (line 840) | #reIOViewNodes() {
    method #startObserveShadowRoot (line 845) | #startObserveShadowRoot(shadowRoot) {
    method #startObserveRoot (line 854) | #startObserveRoot(root) {
    method #startObserveNode (line 867) | #startObserveNode(node) {
    method #queryNode (line 899) | #queryNode(rootNode) {
    method #scanNode (line 913) | #scanNode(rootNode) {
    method #processNode (line 951) | async #processNode(node) {
    method #highlightTextNode (line 1021) | #highlightTextNode(textNode, wordRegex) {
    method #highlightWordsDeeply (line 1059) | #highlightWordsDeeply(parentNode) {
    method #splitTextNodesBySentence (line 1091) | #splitTextNodesBySentence(parentNode, splitParagraph, splitLength) {
    method #removeHighlights (line 1162) | #removeHighlights(parentNode) {
    method #removeBrTags (line 1178) | #removeBrTags(parentNode) {
    method #shouldBreak (line 1189) | #shouldBreak(node) {
    method #isInvalidText (line 1217) | #isInvalidText(text) {
    method #translateNodeGroup (line 1251) | async #translateNodeGroup(nodes, hostNode, deLang) {
    method #serializeForTranslation (line 1379) | #serializeForTranslation(nodes, termsStyle) {
    method #restoreFromTranslation (line 1498) | #restoreFromTranslation(translatedText, placeholderMap) {
    method #translateFetch (line 1555) | #translateFetch(text, deLang = "") {
    method #findTranslationWrappers (line 1590) | #findTranslationWrappers(parentNode) {
    method #cleanupAllNodes (line 1597) | #cleanupAllNodes() {
    method #cleanupAllTranslations (line 1602) | #cleanupAllTranslations(root) {
    method #cleanupDirectTranslations (line 1609) | #cleanupDirectTranslations(node) {
    method #removeTranslationElement (line 1616) | #removeTranslationElement(el) {
    method #restoreOriginal (line 1637) | #restoreOriginal(el, nodes) {
    method #removeNodes (line 1647) | #removeNodes(nodes) {
    method #toggleTranslationOnly (line 1655) | #toggleTranslationOnly(node, transOnly) {
    method #updateStyle (line 1674) | #updateStyle(node, oldStyle, newStyle) {
    method #refreshNode (line 1685) | #refreshNode(node) {
    method #performSyncNode (line 1691) | #performSyncNode(node) {
    method #resetOptions (line 1736) | #resetOptions() {
    method #enableMouseHover (line 1749) | #enableMouseHover() {
    method #disableMouseHover (line 1767) | #disableMouseHover() {
    method #initInjector (line 1777) | #initInjector() {
    method #removeInjector (line 1825) | #removeInjector() {
    method toggleMouseHover (line 1832) | toggleMouseHover() {
    method enable (line 1839) | enable() {
    method #translateTitle (line 1863) | async #translateTitle() {
    method disable (line 1878) | disable() {
    method rescan (line 1897) | rescan() {
    method toggle (line 1911) | toggle() {
    method toggleStyle (line 1916) | toggleStyle() {
    method toggleTransbox (line 1925) | toggleTransbox() {
    method toggleInputTranslate (line 1931) | toggleInputTranslate() {
    method stop (line 1936) | stop() {
    method updateRule (line 1945) | updateRule(newRule) {
    method setting (line 1980) | get setting() {
    method rule (line 1984) | get rule() {
    method eventName (line 1988) | get eventName() {

FILE: src/libs/translatorManager.js
  class TranslatorManager (line 34) | class TranslatorManager {
    method constructor (line 52) | constructor({ setting, rule, fabConfig, favWords, isIframe, isUserscri...
    method start (line 83) | start() {
    method stop (line 101) | stop() {
    method #setupMessageListeners (line 146) | #setupMessageListeners() {
    method #setupTouchOperations (line 160) | #setupTouchOperations() {
    method #handleWindowMessage (line 200) | #handleWindowMessage(event) {
    method #handleInnerMessage (line 205) | #handleInnerMessage(event) {
    method #handleBrowserMessage (line 209) | #handleBrowserMessage(message, sender, sendResponse) {
    method #registerShortcuts (line 219) | #registerShortcuts() {
    method #registerMenus (line 237) | #registerMenus() {
    method #processActions (line 268) | #processActions({ action, args } = {}, fromExt = false) {

FILE: src/libs/utils.js
  function stripMarkdownCodeBlock (line 7) | function stripMarkdownCodeBlock(text, startOnly = false) {
  function later (line 112) | function later() {
  function getMimeTypeFromFilename (line 440) | function getMimeTypeFromFilename(filename) {
  function downloadBlobFile (line 491) | function downloadBlobFile(str, filename = "kiss-file.txt") {
  function escapeHTML (line 509) | function escapeHTML(str) {

FILE: src/scripts/build-safari.js
  function main (line 7) | async function main() {

FILE: src/scripts/build-safari.mjs
  function main (line 11) | async function main() {
  function parseProjectVersion (line 147) | function parseProjectVersion(version) {

FILE: src/subtitle/BilingualSubtitleManager.js
  class BilingualSubtitleManager (line 116) | class BilingualSubtitleManager {
    method constructor (line 138) | constructor({ videoEl, formattedSubtitles, setting }) {
    method start (line 164) | start() {
    method destroy (line 179) | destroy() {
    method setIsAdPlaying (line 201) | setIsAdPlaying(isPlaying) {
    method #createCaptionWindow (line 209) | #createCaptionWindow() {
    method #handleWordHover (line 289) | #handleWordHover(event) {
    method #handleWordHoverOut (line 312) | #handleWordHoverOut(event) {
    method #handleWordMouseMove (line 331) | #handleWordMouseMove(event) {
    method #attachSpanListeners (line 336) | #attachSpanListeners() {
    method #showWordTooltip (line 350) | async #showWordTooltip(word, x, y) {
    method #hideWordTooltip (line 516) | #hideWordTooltip() {
    method #enableDragging (line 526) | #enableDragging(dragElement, boundaryContainer, handleElement) {
    method #attachEventListeners (line 604) | #attachEventListeners() {
    method #removeEventListeners (line 612) | #removeEventListeners() {
    method onTimeUpdate (line 620) | onTimeUpdate() {
    method onSeek (line 637) | onSeek() {
    method #findSubtitleIndexForTime (line 648) | #findSubtitleIndexForTime(currentTimeMs) {
    method #updateCaptionDisplay (line 658) | #updateCaptionDisplay(subtitle) {
    method #wrapWordsWithSpans (line 710) | #wrapWordsWithSpans(text) {
    method #triggerTranslations (line 723) | #triggerTranslations(currentTimeMs) {
    method #translateAndStore (line 743) | async #translateAndStore(subtitle) {
    method appendSubtitles (line 778) | appendSubtitles(newSubtitlesChunk) {
    method updateSetting (line 799) | updateSetting(obj) {
    method #getCurrentSubtitleStartTime (line 804) | #getCurrentSubtitleStartTime() {

FILE: src/subtitle/Menus.js
  function Label (line 4) | function Label({ children }) {
  function MenuItem (line 18) | function MenuItem({ children, onClick, disabled = false }) {
  function Switch (line 43) | function Switch({ label, name, value, onChange, disabled }) {
  function Select (line 79) | function Select({ label, name, value, options, onChange, disabled }) {
  function Button (line 164) | function Button({ label, onClick, disabled }) {
  function Menus (line 178) | function Menus({

FILE: src/subtitle/YouTubeCaptionProvider.js
  constant VIDEO_SELECT (line 23) | const VIDEO_SELECT = "#container video";
  constant CONTORLS_SELECT (line 24) | const CONTORLS_SELECT = ".ytp-right-controls";
  constant YT_CAPTION_SELECT (line 25) | const YT_CAPTION_SELECT = "#ytp-caption-window-container";
  constant YT_AD_SELECT (line 26) | const YT_AD_SELECT = ".video-ads";
  constant YT_SUBTITLE_BTN_SELECT (line 27) | const YT_SUBTITLE_BTN_SELECT = "button.ytp-subtitles-button";
  class YouTubeCaptionProvider (line 29) | class YouTubeCaptionProvider {
    method constructor (line 51) | constructor(setting = {}) {
    method #videoId (line 56) | get #videoId() {
    method #videoEl (line 61) | get #videoEl() {
    method #progressed (line 65) | set #progressed(num) {
    method #progressed (line 70) | get #progressed() {
    method initialize (line 74) | initialize() {
    method #moAds (line 117) | #moAds(adContainer) {
    method #waitForElement (line 179) | #waitForElement(selector, callback) {
    method updateSetting (line 200) | updateSetting({ name, value }) {
    method #toggleShowOrigin (line 217) | #toggleShowOrigin() {
    method downloadSubtitle (line 225) | downloadSubtitle() {
    method #getMenuProps (line 246) | #getMenuProps() {
    method #updateMenuProps (line 268) | #updateMenuProps() {
    method #injectToggleButton (line 274) | #injectToggleButton(ytControls) {
    method #isSameLang (line 317) | #isSameLang(lang1, lang2) {
    method #findCaptionTrack (line 322) | #findCaptionTrack(captionTracks, lang) {
    method #getCaptionTracks (line 355) | async #getCaptionTracks(videoId) {
    method #getSubtitleEvents (line 368) | async #getSubtitleEvents(capUrl, potUrl, responseText) {
    method #aiSegment (line 409) | async #aiSegment({ videoId, fromLang, toLang, chunkEvents, segApiSetti...
    method #getFromLang (line 439) | #getFromLang(lang) {
    method #handleInterceptedRequest (line 453) | async #handleInterceptedRequest(url, responseText) {
    method #processEvents (line 537) | async #processEvents({ videoId, flatEvents, fromLang }) {
    method #reProcessEvents (line 571) | #reProcessEvents() {
    method #eventsToSubtitles (line 589) | async #eventsToSubtitles({ videoId, flatEvents, fromLang }) {
    method #startManager (line 645) | #startManager() {
    method #destroyManager (line 713) | #destroyManager() {
    method #hideYtCaption (line 732) | #hideYtCaption() {
    method #showYtCaption (line 737) | #showYtCaption() {
    method #formatSubtitles (line 742) | #formatSubtitles(flatEvents, lang) {
    method #isQualityPoor (line 807) | #isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.2) {
    method #processSubtitles (line 815) | #processSubtitles({
    method #genFlatEvents (line 961) | #genFlatEvents(events = []) {
    method #splitEventsIntoChunks (line 994) | #splitEventsIntoChunks(flatEvents, chunkLength = 1000) {
    method #processRemainingChunksAsync (line 1042) | async #processRemainingChunksAsync({
    method #createNotificationElement (line 1113) | #createNotificationElement() {
    method #showNotification (line 1142) | #showNotification(message, duration = 2000) {

FILE: src/subtitle/YouTubeSubtitleList.js
  class YouTubeSubtitleList (line 14) | class YouTubeSubtitleList {
    method constructor (line 18) | constructor(videoElement) {
    method initialize (line 59) | initialize(subtitles) {
    method setBilingualSubtitles (line 71) | setBilingualSubtitles(subtitles) {
    method destroy (line 87) | destroy() {
    method jumpToTime (line 109) | jumpToTime(timeMs) {
    method handleWordAdded (line 121) | handleWordAdded(event) {
    method addWord (line 136) | addWord(
    method downloadSubtitles (line 167) | downloadSubtitles() {
    method createSubtitleList (line 193) | createSubtitleList() {
    method _ensureContainer (line 225) | _ensureContainer() {
    method _renderTabsAndStructure (line 314) | _renderTabsAndStructure() {
    method _createSubtitleListItem (line 450) | _createSubtitleListItem(sub, index) {
    method updateBilingualSubtitles (line 502) | updateBilingualSubtitles() {
    method _renderVocabulary (line 554) | _renderVocabulary() {
    method _createExportContainer (line 568) | _createExportContainer() {
    method _createVocabListContainer (line 606) | _createVocabListContainer() {
    method _createVocabItemElement (line 625) | _createVocabItemElement(item) {
    method exportVocabularyAsJson (line 689) | exportVocabularyAsJson() {
    method exportVocabularyAsCsv (line 719) | exportVocabularyAsCsv() {
    method exportVocabularyAsTxt (line 767) | exportVocabularyAsTxt() {
    method exportVocabularyAsMd (line 803) | exportVocabularyAsMd() {
    method setupEventListeners (line 845) | setupEventListeners() {
    method turnOnAutoSub (line 860) | turnOnAutoSub() {
    method _binarySearchSubtitle (line 919) | _binarySearchSubtitle(timeMs) {
    method turnOffAutoSub (line 940) | turnOffAutoSub() {
    method _downloadFile (line 954) | _downloadFile(content, mimeType, extension) {
    method millisToMinutesAndSeconds (line 965) | millisToMinutesAndSeconds(millis) {
    method _getYouTubeVideoId (line 972) | _getYouTubeVideoId() {
    method _getYouTubeVideoTitle (line 981) | _getYouTubeVideoTitle() {

FILE: src/subtitle/subtitle.js
  function runSubtitle (line 12) | function runSubtitle({ href, setting }) {

FILE: src/subtitle/vtt.js
  function parseTimestampToMilliseconds (line 14) | function parseTimestampToMilliseconds(timestamp) {
  function formatMillisecondsToTimestamp (line 63) | function formatMillisecondsToTimestamp(ms) {
  function parseBilingualVtt (line 81) | function parseBilingualVtt(vttText) {
  function buildBilingualVtt (line 125) | function buildBilingualVtt(cues) {

FILE: src/views/Action/ContentFab.js
  function ContentFab (line 10) | function ContentFab({

FILE: src/views/Action/Draggable.js
  function DraggableWrapper (line 36) | function DraggableWrapper({ children, usePaper, ...props }) {
  function Draggable (line 47) | function Draggable({

FILE: src/views/Action/index.js
  function Action (line 18) | function Action({ translator, processActions }) {

FILE: src/views/Options/About.js
  function About (line 6) | function About() {

FILE: src/views/Options/Apis.js
  function TestButton (line 55) | function TestButton({ api }) {
  function ApiFields (line 112) | function ApiFields({ apiSlug, isUserApi, deleteApi, copyApi }) {
  function ApiAccordion (line 878) | function ApiAccordion({ api, isUserApi, deleteApi, copyApi }) {
  function Apis (line 911) | function Apis() {

FILE: src/views/Options/DarkModeButton.js
  function DarkModeButton (line 7) | function DarkModeButton() {

FILE: src/views/Options/DownloadButton.js
  function DownloadButton (line 7) | function DownloadButton({ handleData, text, fileName }) {

FILE: src/views/Options/FavWords.js
  function FavAccordion (line 24) | function FavAccordion({ word, index, createdAt, timestamp }) {
  function FavWords (line 92) | function FavWords() {

FILE: src/views/Options/Header.js
  function Header (line 11) | function Header(props) {

FILE: src/views/Options/HelpButton.js
  function HelpButton (line 5) | function HelpButton({ url }) {

FILE: src/views/Options/InputSetting.js
  function InputSetting (line 23) | function InputSetting() {

FILE: src/views/Options/Layout.js
  function Layout (line 10) | function Layout() {

FILE: src/views/Options/MouseHover.js
  function MouseHoverSetting (line 12) | function MouseHoverSetting() {

FILE: src/views/Options/Navigator.js
  function LinkItem (line 22) | function LinkItem({ label, url, icon }) {
  function Navigator (line 32) | function Navigator(props) {

FILE: src/views/Options/Playground.js
  function Playgound (line 6) | function Playgound() {

FILE: src/views/Options/ReusableAutocomplete.js
  function ReusableAutocomplete (line 14) | function ReusableAutocomplete({

FILE: src/views/Options/Rules.js
  function RuleFields (line 70) | function RuleFields({ rule, rules, setShow, setKeyword }) {
  function RuleAccordion (line 780) | function RuleAccordion({ rule, rules, isExpanded = false }) {
  function ShareButton (line 809) | function ShareButton({ rules, injectRules, selectedUrl }) {
  function UserRules (line 851) | function UserRules({ subRules, rules }) {
  function SubRulesItem (line 989) | function SubRulesItem({
  function SubRulesEdit (line 1067) | function SubRulesEdit({ subList, addSub, updateDataCache }) {
  function SubRules (line 1178) | function SubRules({ subRules }) {
  function GlobalRule (line 1240) | function GlobalRule({ rules }) {
  function Rules (line 1262) | function Rules() {

FILE: src/views/Options/Setting.js
  function ShortcutItem (line 38) | function ShortcutItem({ action, label }) {
  function Settings (line 45) | function Settings() {

FILE: src/views/Options/ShortcutInput.js
  function ShortcutInput (line 10) | function ShortcutInput({

FILE: src/views/Options/ShowMoreButton.js
  function ShowMoreButton (line 6) | function ShowMoreButton({ onChange, showMore }) {

FILE: src/views/Options/StylesSetting.js
  function StyleFields (line 19) | function StyleFields({ customStyle, deleteStyle, updateStyle, isBuiltin ...
  function StyleAccordion (line 139) | function StyleAccordion({ customStyle, deleteStyle, updateStyle, isBuilt...
  function StylesSetting (line 171) | function StylesSetting() {

FILE: src/views/Options/Subtitle.js
  function StyleVisualEditor (line 159) | function StyleVisualEditor({ label, cssValue, onChange, type }) {
  function SubtitleSetting (line 438) | function SubtitleSetting() {

FILE: src/views/Options/SyncSetting.js
  function SyncSetting (line 27) | function SyncSetting() {

FILE: src/views/Options/Tranbox.js
  function Tranbox (line 27) | function Tranbox() {

FILE: src/views/Options/UploadButton.js
  function UploadButton (line 6) | function UploadButton({

FILE: src/views/Options/index.js
  function Options (line 30) | function Options() {

FILE: src/views/Popup/Header.js
  function Header (line 11) | function Header({ onClose, toggleTab, openSeparateWindow }) {

FILE: src/views/Popup/PopupCont.js
  function PopupCont (line 31) | function PopupCont({

FILE: src/views/Popup/index.js
  function Trantab (line 16) | function Trantab() {
  function Popup (line 45) | function Popup() {

FILE: src/views/Selection/AudioBtn.js
  function AudioBtn (line 6) | function AudioBtn({ src }) {
  function BaiduAudioBtn (line 32) | function BaiduAudioBtn({ text, lan = "uk", spd = 3 }) {

FILE: src/views/Selection/CopyBtn.js
  function CopyBtn (line 6) | function CopyBtn({ text, title = "copy" }) {

FILE: src/views/Selection/DictCont.js
  function DictBody (line 13) | function DictBody({ text, setCopyText, setRealWord, dict }) {
  function DictCont (line 50) | function DictCont({ text, enDict }) {

FILE: src/views/Selection/DraggableResizable.js
  function Pointer (line 8) | function Pointer({
  function DraggableResizable (line 138) | function DraggableResizable({

FILE: src/views/Selection/FavBtn.js
  function FavBtn (line 8) | function FavBtn({ word, title }) {

FILE: src/views/Selection/SugCont.js
  function SugBaidu (line 10) | function SugBaidu({ text }) {
  function SugYoudao (line 39) | function SugYoudao({ text }) {
  function SugCont (line 68) | function SugCont({ text, enSug }) {

FILE: src/views/Selection/TranBox.js
  function TranBoxHeader (line 25) | function TranBoxHeader({
  function TranBoxContent (line 260) | function TranBoxContent({
  function TranBox (line 331) | function TranBox(props) {

FILE: src/views/Selection/TranBtn.js
  function TranBtn (line 4) | function TranBtn({

FILE: src/views/Selection/TranCont.js
  function TranCont (line 12) | function TranCont({

FILE: src/views/Selection/TranForm.js
  function TranForm (line 30) | function TranForm({

FILE: src/views/Selection/index.js
  function Selection (line 7) | function Selection({
Condensed preview — 185 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (983K chars).
[
  {
    "path": ".babelrc",
    "chars": 55,
    "preview": "{\n    \"presets\": [\n        \"@babel/preset-env\"\n    ]\n}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2668,
    "preview": "name: publish release version\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  build:\n    runs-on: ubuntu-24.04\n    steps:\n "
  },
  {
    "path": ".gitignore",
    "chars": 364,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".pnpm-version",
    "chars": 7,
    "preview": "9.14.4\n"
  },
  {
    "path": ".prettierignore",
    "chars": 39,
    "preview": "node_modules\nbuild\npublic\npackage.json\n"
  },
  {
    "path": ".prettierrc",
    "chars": 566,
    "preview": "{\n  \"arrowParens\": \"always\",\n  \"bracketSpacing\": true,\n  \"endOfLine\": \"lf\",\n  \"htmlWhitespaceSensitivity\": \"css\",\n  \"ins"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 957,
    "preview": "## v2.0.20\n\n- 优化本地语言识别不准确时的处理逻辑\n- 修复 MacOS 编译 Safari 插件脚本错误\n- 优化调整划词翻译逻辑\n  - 修复移动端不显示header的bug\n  - 仅对翻译框的大小和位置持久化,其他设置不"
  },
  {
    "path": "LICENSE",
    "chars": 35149,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.en.md",
    "chars": 9013,
    "preview": "# KISS Translator\n\n[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)\n\nA simple, open"
  },
  {
    "path": "README.ja.md",
    "chars": 6155,
    "preview": "# KISS Translator シンプル翻訳\n\n[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)\n\nシンプルでオー"
  },
  {
    "path": "README.ko.md",
    "chars": 6250,
    "preview": "# KISS Translator 심플 번역\n\n[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)\n\n심플하고 오픈 "
  },
  {
    "path": "README.md",
    "chars": 5068,
    "preview": "# KISS Translator 简约翻译\n\n[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)\n\n一个简约、开源的 "
  },
  {
    "path": "VERSION_MANAGEMENT.md",
    "chars": 1552,
    "preview": "# 版本号管理说明\n\n## 📌 背景\n\n项目的版本号分散在多个文件中:\n- `package.json`\n- `.env` (REACT_APP_VERSION)\n- `public/manifest.json`\n- `public/man"
  },
  {
    "path": "config-overrides.js",
    "chars": 7044,
    "preview": "const paths = require(\"react-scripts/config/paths\");\nconst HtmlWebpackPlugin = require(\"html-webpack-plugin\");\nconst { W"
  },
  {
    "path": "custom-api.md",
    "chars": 7837,
    "preview": "# 自定义接口示例(本文档已过期,新版不再适用)\n\nV2版的示例请查看这里:[custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-a"
  },
  {
    "path": "custom-api_v2.md",
    "chars": 4740,
    "preview": "# 自定义接口说明及示例\n\n## 默认接口规范\n\n如果接口的请求数据和返回数据符合以下规范,\n则无需填写 `Request Hook` 或 `Response Hook`。\n\n\n### 非聚合翻译\n\nRequest body\n\n```jso"
  },
  {
    "path": "package.json",
    "chars": 3155,
    "preview": "{\n  \"name\": \"kiss-translator\",\n  \"description\": \"A minimalist bilingual translation Extension & Greasemonkey Script\",\n  "
  },
  {
    "path": "public/.nojekyll",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "public/_locales/de/messages.json",
    "chars": 566,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"KISS Übersetzer\"\n  },\n  \"app_description\": {\n    \"message\": \"Eine minimalistische zwei"
  },
  {
    "path": "public/_locales/en/messages.json",
    "chars": 525,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"KISS Translator\"\n  },\n  \"app_description\": {\n    \"message\": \"A minimalist bilingual tr"
  },
  {
    "path": "public/_locales/es/messages.json",
    "chars": 565,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"KISS Traductor\"\n  },\n  \"app_description\": {\n    \"message\": \"Una extensión de traducció"
  },
  {
    "path": "public/_locales/fr/messages.json",
    "chars": 605,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"KISS Traducteur\"\n  },\n  \"app_description\": {\n    \"message\": \"Une extension de traducti"
  },
  {
    "path": "public/_locales/ja/messages.json",
    "chars": 459,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"シンプル翻訳\"\n  },\n  \"app_description\": {\n    \"message\": \"Webサイト、テキスト選択、動画字幕などの翻訳に対応した、シンプルで"
  },
  {
    "path": "public/_locales/ko/messages.json",
    "chars": 464,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"심플 번역\"\n  },\n  \"app_description\": {\n    \"message\": \"웹페이지, 단어 선택, 동영상 자막 번역 등을 지원하는 심플한 "
  },
  {
    "path": "public/_locales/zh_CN/messages.json",
    "chars": 402,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"简约翻译\"\n  },\n  \"app_description\": {\n    \"message\": \"一个简约的双语对照翻译扩展,支持网页、划词、视频字幕翻译等功能,支持多种"
  },
  {
    "path": "public/_locales/zh_TW/messages.json",
    "chars": 408,
    "preview": "{\n  \"app_name\": {\n    \"message\": \"簡約翻譯\"\n  },\n  \"app_description\": {\n    \"message\": \"一個簡約的雙語對照翻譯擴充功能,支持網頁、劃詞、影片字幕翻譯等功能,支持"
  },
  {
    "path": "public/content.html",
    "chars": 730,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "public/index.html",
    "chars": 745,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "public/manifest.firefox.json",
    "chars": 1790,
    "preview": "{\n  \"manifest_version\": 2,\n  \"name\": \"__MSG_app_name__\",\n  \"description\": \"__MSG_app_description__\",\n  \"version\": \"2.0.2"
  },
  {
    "path": "public/manifest.json",
    "chars": 2063,
    "preview": "{\n  \"manifest_version\": 3,\n  \"name\": \"__MSG_app_name__\",\n  \"description\": \"__MSG_app_description__\",\n  \"version\": \"2.0.2"
  },
  {
    "path": "public/manifest.thunderbird.json",
    "chars": 1935,
    "preview": "{\n  \"manifest_version\": 2,\n  \"name\": \"__MSG_app_name__\",\n  \"description\": \"__MSG_app_description__\",\n  \"version\": \"2.0.2"
  },
  {
    "path": "src/apis/baidu.js",
    "chars": 452,
    "preview": "import { DEFAULT_USER_AGENT } from \"../config\";\n\nexport const genBaidu = ({ texts, from, to }) => {\n  const body = {\n   "
  },
  {
    "path": "src/apis/deepl.js",
    "chars": 1184,
    "preview": "let id = 1e4 * Math.round(1e4 * Math.random());\n\nexport const genDeeplFree = ({ texts, from, to }) => {\n  const text = t"
  },
  {
    "path": "src/apis/history.js",
    "chars": 743,
    "preview": "import { DEFAULT_CONTEXT_SIZE } from \"../config\";\n\nconst historyMap = new Map();\n\nconst MsgHistory = (maxSize = DEFAULT_"
  },
  {
    "path": "src/apis/index.js",
    "chars": 14235,
    "preview": "import queryString from \"query-string\";\nimport { fetchData } from \"../libs/fetch\";\nimport {\n  URL_CACHE_TRAN,\n  URL_CACH"
  },
  {
    "path": "src/apis/trans.js",
    "chars": 29872,
    "preview": "import queryString from \"query-string\";\nimport {\n  OPT_TRANS_GOOGLE,\n  OPT_TRANS_GOOGLE_2,\n  OPT_TRANS_MICROSOFT,\n  OPT_"
  },
  {
    "path": "src/background.js",
    "chars": 13747,
    "preview": "import browser from \"webextension-polyfill\";\nimport {\n  MSG_FETCH,\n  MSG_GET_HTTPCACHE,\n  MSG_PUT_HTTPCACHE,\n  MSG_TRANS"
  },
  {
    "path": "src/common.js",
    "chars": 4866,
    "preview": "import { OPT_HIGHLIGHT_WORDS_DISABLE } from \"./config\";\nimport {\n  getFabWithDefault,\n  getSettingWithDefault,\n  getWord"
  },
  {
    "path": "src/components/Logo/icon.base64.js",
    "chars": 20208,
    "preview": "export const FAVICON_BASE64 =\n  \"data:image/x-icon;base64,AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAG"
  },
  {
    "path": "src/components/Logo/index.js",
    "chars": 485,
    "preview": "import React from \"react\";\nimport { FAVICON_BASE64 } from \"./icon.base64.js\";\n\nconst Logo = ({ size = 16, className = \"\""
  },
  {
    "path": "src/config/api.js",
    "chars": 21183,
    "preview": "export const DEFAULT_HTTP_TIMEOUT = 10000; // 调用超时时间\nexport const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量\nexport const DEF"
  },
  {
    "path": "src/config/app.js",
    "chars": 454,
    "preview": "export const APP_NAME = process.env.REACT_APP_NAME.trim()\n  .split(/\\s+/)\n  .join(\"-\");\nexport const APP_LCNAME = APP_NA"
  },
  {
    "path": "src/config/client.js",
    "chars": 490,
    "preview": "export const CLIENT_WEB = \"web\";\nexport const CLIENT_CHROME = \"chrome\";\nexport const CLIENT_EDGE = \"edge\";\nexport const "
  },
  {
    "path": "src/config/i18n.js",
    "chars": 68635,
    "preview": "export const UI_LANGS = [\n  [\"en\", \"English\"],\n  [\"zh\", \"简体中文\"],\n  [\"zh_TW\", \"繁體中文\"],\n  [\"ja\", \"日本語\"],\n  [\"ko\", \"한국어\"],\n"
  },
  {
    "path": "src/config/index.js",
    "chars": 247,
    "preview": "export * from \"./app\";\nexport * from \"./rules\";\nexport * from \"./api\";\nexport * from \"./setting\";\nexport * from \"./i18n\""
  },
  {
    "path": "src/config/msg.js",
    "chars": 2151,
    "preview": "export const CMD_TOGGLE_TRANSLATE = \"toggleTranslate\";\nexport const CMD_TOGGLE_STYLE = \"toggleStyle\";\nexport const CMD_O"
  },
  {
    "path": "src/config/quotes.js",
    "chars": 22561,
    "preview": "const quotes = [\n  {\n    en: \"The unexamined life is not worth living.\",\n    zh: \"未经审视的人生不值得过。\",\n    zh_TW: \"未經審視的人生不值得過"
  },
  {
    "path": "src/config/rules.js",
    "chars": 7500,
    "preview": "import { OPT_TRANS_MICROSOFT } from \"./api\";\nimport { OPT_STYLE_NONE } from \"./styles\";\n\nexport const GLOBAL_KEY = \"*\";\n"
  },
  {
    "path": "src/config/setting.js",
    "chars": 6496,
    "preview": "import { LogLevel } from \"../libs/log\";\nimport {\n  OPT_DICT_BING,\n  OPT_SUG_YOUDAO,\n  DEFAULT_HTTP_TIMEOUT,\n  OPT_TRANS_"
  },
  {
    "path": "src/config/storage.js",
    "chars": 1212,
    "preview": "import { APP_NAME, APP_VERSION } from \"./app\";\n\nexport const KV_RULES_KEY = `kiss-rules_v${APP_VERSION[0]}.json`;\nexport"
  },
  {
    "path": "src/config/styles.js",
    "chars": 1548,
    "preview": "export const OPT_STYLE_NONE = \"style_none\"; // 无\nexport const OPT_STYLE_LINE = \"under_line\"; // 下划线\nexport const OPT_STY"
  },
  {
    "path": "src/config/url.js",
    "chars": 716,
    "preview": "import { APP_LCNAME } from \"./app\";\n\nexport const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;\nexport const URL_C"
  },
  {
    "path": "src/content.js",
    "chars": 82,
    "preview": "import { run } from \"./common\";\n\nglobalThis.__KISS_CONTEXT__ = \"content\";\n\nrun();\n"
  },
  {
    "path": "src/hooks/Alert.js",
    "chars": 2002,
    "preview": "import {\n  createContext,\n  useContext,\n  useState,\n  forwardRef,\n  useCallback,\n  useMemo,\n} from \"react\";\nimport Snack"
  },
  {
    "path": "src/hooks/Api.js",
    "chars": 4023,
    "preview": "import { useCallback, useEffect, useMemo } from \"react\";\nimport { DEFAULT_API_LIST, API_SPE_TYPES } from \"../config\";\nim"
  },
  {
    "path": "src/hooks/Audio.js",
    "chars": 2576,
    "preview": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { logger } from \"../libs/log\";\nimport { fetchDa"
  },
  {
    "path": "src/hooks/ColorMode.js",
    "chars": 495,
    "preview": "import { useCallback } from \"react\";\nimport { useSetting } from \"./Setting\";\n\n/**\n * 深色模式hook\n * @returns\n */\nexport fun"
  },
  {
    "path": "src/hooks/Confirm.js",
    "chars": 2635,
    "preview": "import {\n  useState,\n  useContext,\n  createContext,\n  useCallback,\n  useRef,\n  useMemo,\n} from \"react\";\nimport Dialog fr"
  },
  {
    "path": "src/hooks/CustomStyles.js",
    "chars": 2153,
    "preview": "import { useCallback, useMemo } from \"react\";\nimport { useSetting } from \"./Setting\";\nimport { DEFAULT_CUSTOM_STYLES, OP"
  },
  {
    "path": "src/hooks/DebouncedCallback.js",
    "chars": 530,
    "preview": "import { useMemo, useEffect, useRef } from \"react\";\nimport { debounce } from \"../libs/utils\";\n\nexport function useDeboun"
  },
  {
    "path": "src/hooks/Fab.js",
    "chars": 274,
    "preview": "import { STOKEY_FAB } from \"../config\";\nimport { useStorage } from \"./Storage\";\n\nconst DEFAULT_FAB = {};\n\n/**\n * fab hoo"
  },
  {
    "path": "src/hooks/FavWords.js",
    "chars": 2089,
    "preview": "import { STOKEY_WORDS, KV_WORDS_KEY } from \"../config\";\nimport { useCallback, useMemo } from \"react\";\nimport { useStorag"
  },
  {
    "path": "src/hooks/Fetch.js",
    "chars": 3275,
    "preview": "import { useEffect, useState, useCallback } from \"react\";\n\nexport const useAsync = () => {\n  const [data, setData] = use"
  },
  {
    "path": "src/hooks/I18n.js",
    "chars": 682,
    "preview": "import { useSetting } from \"./Setting\";\nimport { I18N, URL_RAW_PREFIX } from \"../config\";\nimport { useGet } from \"./Fetc"
  },
  {
    "path": "src/hooks/InputRule.js",
    "chars": 329,
    "preview": "import { DEFAULT_INPUT_RULE } from \"../config\";\nimport { useSetting } from \"./Setting\";\n\nexport function useInputRule() "
  },
  {
    "path": "src/hooks/Loading.js",
    "chars": 421,
    "preview": "import CircularProgress from \"@mui/material/CircularProgress\";\nimport Link from \"@mui/material/Link\";\nimport Divider fro"
  },
  {
    "path": "src/hooks/MouseHover.js",
    "chars": 407,
    "preview": "import { DEFAULT_MOUSE_HOVER_SETTING } from \"../config\";\nimport { useSetting } from \"./Setting\";\n\nexport function useMou"
  },
  {
    "path": "src/hooks/Rules.js",
    "chars": 2567,
    "preview": "import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from \"../config\";\nimport { useStorage } from \"./Storage\";\nimport { "
  },
  {
    "path": "src/hooks/Setting.js",
    "chars": 2699,
    "preview": "import {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useEffect,\n} from \"react\";\nimport Alert from \"@mui/m"
  },
  {
    "path": "src/hooks/Shortcut.js",
    "chars": 575,
    "preview": "import { useCallback } from \"react\";\nimport { DEFAULT_SHORTCUTS } from \"../config\";\nimport { useSetting } from \"./Settin"
  },
  {
    "path": "src/hooks/Storage.js",
    "chars": 3825,
    "preview": "import { useCallback, useEffect, useState } from \"react\";\nimport { storage } from \"../libs/storage\";\nimport { kissLog } "
  },
  {
    "path": "src/hooks/SubRules.js",
    "chars": 1891,
    "preview": "import { DEFAULT_SUBRULES_LIST } from \"../config\";\nimport { useSetting } from \"./Setting\";\nimport { useCallback, useEffe"
  },
  {
    "path": "src/hooks/Subtitle.js",
    "chars": 362,
    "preview": "import { DEFAULT_SUBTITLE_SETTING } from \"../config\";\nimport { useSetting } from \"./Setting\";\n\nexport function useSubtit"
  },
  {
    "path": "src/hooks/Sync.js",
    "chars": 1685,
    "preview": "import { useCallback, useMemo } from \"react\";\nimport { STOKEY_SYNC, DEFAULT_SYNC } from \"../config\";\nimport { useStorage"
  },
  {
    "path": "src/hooks/Theme.js",
    "chars": 1763,
    "preview": "import { useEffect, useMemo, useState } from \"react\";\nimport { ThemeProvider, createTheme } from \"@mui/material/styles\";"
  },
  {
    "path": "src/hooks/Tranbox.js",
    "chars": 353,
    "preview": "import { DEFAULT_TRANBOX_SETTING } from \"../config\";\nimport { useSetting } from \"./Setting\";\n\nexport function useTranbox"
  },
  {
    "path": "src/hooks/ValidationInput.js",
    "chars": 1150,
    "preview": "import { useState, useEffect } from \"react\";\nimport TextField from \"@mui/material/TextField\";\nimport { limitNumber, limi"
  },
  {
    "path": "src/hooks/WindowSize.js",
    "chars": 686,
    "preview": "import { useState, useEffect } from \"react\";\nimport { useDebouncedCallback } from \"./DebouncedCallback\";\n\nfunction useWi"
  },
  {
    "path": "src/hooks/useAutoHideTranBtn.js",
    "chars": 1239,
    "preview": "import { useEffect, useRef } from \"react\";\n\nexport default function useAutoHideTranBtn(\n  showBtn,\n  setShowBtn,\n  posit"
  },
  {
    "path": "src/hooks/useSelectionController.js",
    "chars": 4207,
    "preview": "import { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { sleep, limitNumber } from \"../libs/utils\";\ni"
  },
  {
    "path": "src/hooks/useTranBoxState.js",
    "chars": 2280,
    "preview": "import { useState, useEffect } from \"react\";\nimport { limitNumber } from \"../libs/utils\";\nimport { isMobile } from \"../l"
  },
  {
    "path": "src/hooks/useTranboxShortcuts.js",
    "chars": 2007,
    "preview": "import { useEffect, useCallback } from \"react\";\nimport { shortcutRegister } from \"../libs/shortcut\";\nimport { isGm, isEx"
  },
  {
    "path": "src/index.js",
    "chars": 1892,
    "preview": "import React, { useState } from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport CircularProgress from \"@mui/mat"
  },
  {
    "path": "src/injector-shadowroot.js",
    "chars": 84,
    "preview": "import { shadowRootInjector } from \"./injectors/shadowroot\";\n\nshadowRootInjector();\n"
  },
  {
    "path": "src/injector-subtitle.js",
    "chars": 89,
    "preview": "import { XMLHttpRequestInjector } from \"./injectors/xmlhttp\";\n\nXMLHttpRequestInjector();\n"
  },
  {
    "path": "src/injectors/index.js",
    "chars": 763,
    "preview": "import { browser } from \"../libs/browser\";\nimport { isExt } from \"../libs/client\";\nimport { injectExternalJs, injectInli"
  },
  {
    "path": "src/injectors/shadowroot.js",
    "chars": 364,
    "preview": "export const shadowRootInjector = () => {\n  try {\n    const orig = Element.prototype.attachShadow;\n    Element.prototype"
  },
  {
    "path": "src/injectors/xmlhttp.js",
    "chars": 689,
    "preview": "export const XMLHttpRequestInjector = () => {\n  try {\n    const originalOpen = XMLHttpRequest.prototype.open;\n    XMLHtt"
  },
  {
    "path": "src/libs/auth.js",
    "chars": 1708,
    "preview": "import { getMsauth, setMsauth } from \"./storage\";\nimport { kissLog } from \"./log\";\nimport { apiMsAuth } from \"../apis\";\n"
  },
  {
    "path": "src/libs/batchQueue.js",
    "chars": 4020,
    "preview": "import {\n  DEFAULT_BATCH_INTERVAL,\n  DEFAULT_BATCH_SIZE,\n  DEFAULT_BATCH_LENGTH,\n} from \"../config\";\n\n/**\n * 批处理队列\n * 支持"
  },
  {
    "path": "src/libs/blacklist.js",
    "chars": 231,
    "preview": "import { isMatch } from \"./utils\";\n\n/**\n * 检查是否在黑名单中\n * @param {*} href\n * @param {*} param1\n * @returns\n */\nexport cons"
  },
  {
    "path": "src/libs/browser.js",
    "chars": 1204,
    "preview": "// import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from \"../config\";\n\n/**\n * 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发\n * @re"
  },
  {
    "path": "src/libs/builtinAI.js",
    "chars": 4778,
    "preview": "import { kissLog, logger } from \"./log\";\n\n/**\n * Chrome 浏览器内置翻译\n */\nclass ChromeTranslator {\n  #translatorMap = new Map("
  },
  {
    "path": "src/libs/cache.js",
    "chars": 3894,
    "preview": "import {\n  CACHE_NAME,\n  DEFAULT_CACHE_TIMEOUT,\n  MSG_CLEAR_CACHES,\n  MSG_GET_HTTPCACHE,\n  MSG_PUT_HTTPCACHE,\n} from \".."
  },
  {
    "path": "src/libs/client.js",
    "chars": 347,
    "preview": "import {\n  CLIENT_EXTS,\n  CLIENT_USERSCRIPT,\n  CLIENT_WEB,\n  CLIENT_FIREFOX,\n} from \"../config\";\n\nexport const client = "
  },
  {
    "path": "src/libs/detect.js",
    "chars": 1498,
    "preview": "import {\n  OPT_TRANS_GOOGLE,\n  OPT_TRANS_MICROSOFT,\n  OPT_TRANS_BAIDU,\n  OPT_TRANS_TENCENT,\n  OPT_LANGS_TO_CODE,\n  OPT_L"
  },
  {
    "path": "src/libs/docInfo.js",
    "chars": 1632,
    "preview": "import { truncateWords } from \"./utils\";\n\n// 清洗文本,移除换行符\nconst cleanText = (text) => {\n  if (!text) return \"\";\n  return t"
  },
  {
    "path": "src/libs/domManager.js",
    "chars": 3633,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { CacheProvider } from \"@emotion/react\";\nimpo"
  },
  {
    "path": "src/libs/fabManager.js",
    "chars": 463,
    "preview": "import ShadowDomManager from \"./shadowDomManager\";\nimport { APP_CONSTS } from \"../config\";\nimport ContentFab from \"../vi"
  },
  {
    "path": "src/libs/fetch.js",
    "chars": 8420,
    "preview": "import { isExt, isGm } from \"./client\";\nimport { sendBgMsg } from \"./msg\";\nimport { getSettingWithDefault } from \"./stor"
  },
  {
    "path": "src/libs/gm.js",
    "chars": 2668,
    "preview": "import { fetchGM } from \"./fetch\";\nimport { genEventName } from \"./utils\";\n\nconst MSG_GM_xmlHttpRequest = \"xmlHttpReques"
  },
  {
    "path": "src/libs/iframe.js",
    "chars": 337,
    "preview": "export const isIframe = window.self !== window.top;\n\nexport const sendIframeMsg = (action, args) => {\n  document.querySe"
  },
  {
    "path": "src/libs/injector.js",
    "chars": 1960,
    "preview": "import { trustedTypesHelper } from \"./trustedTypes\";\n\n// Function to inject inline JavaScript code\nexport const injectIn"
  },
  {
    "path": "src/libs/inputTranslate.js",
    "chars": 16207,
    "preview": "import {\n  DEFAULT_INPUT_RULE,\n  DEFAULT_INPUT_SHORTCUT,\n  OPT_LANGS_LIST,\n  DEFAULT_API_SETTING,\n  OPT_INPUT_DOT_DISABL"
  },
  {
    "path": "src/libs/interpreter.js",
    "chars": 398,
    "preview": "import Sval from \"sval\";\n\nexport const interpreter = new Sval({\n  // ECMA Version of the code\n  // 3 | 5 | 6 | 7 | 8 | 9"
  },
  {
    "path": "src/libs/log.js",
    "chars": 4132,
    "preview": "// 定义日志级别\nexport const LogLevel = {\n  DEBUG: { value: 0, name: \"DEBUG\", color: \"#6495ED\" }, // 宝蓝色\n  INFO: { value: 1, n"
  },
  {
    "path": "src/libs/mobile.js",
    "chars": 496,
    "preview": "export const isMobile = (() => {\n  try {\n    if (typeof navigator === \"undefined\") return false;\n    const ua = navigato"
  },
  {
    "path": "src/libs/msg.js",
    "chars": 1000,
    "preview": "import { browser } from \"./browser\";\n\n/**\n * 获取当前tab信息\n * @returns\n */\nexport const getCurTab = async () => {\n  const [t"
  },
  {
    "path": "src/libs/pool.js",
    "chars": 3453,
    "preview": "import { DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_LIMIT } from \"../config\";\nimport { kissLog } from \"./log\";\n\n/**\n * 任务池\n *"
  },
  {
    "path": "src/libs/popupManager.js",
    "chars": 679,
    "preview": "import ShadowDomManager from \"./shadowDomManager\";\nimport { APP_CONSTS, EVENT_KISS_INNER, MSG_POPUP_TOGGLE } from \"../co"
  },
  {
    "path": "src/libs/rules.js",
    "chars": 9558,
    "preview": "import { matchValue, type, isMatch } from \"./utils\";\nimport {\n  GLOBAL_KEY,\n  OPT_LANGS_FROM,\n  OPT_LANGS_TO,\n  DEFAULT_"
  },
  {
    "path": "src/libs/shadowDomManager.js",
    "chars": 2971,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { CacheProvider } from \"@emotion/react\";\nimpo"
  },
  {
    "path": "src/libs/shortcut.js",
    "chars": 3183,
    "preview": "import { isSameSet } from \"./utils\";\n\n/**\n * 键盘快捷键监听器\n * @param {(pressedKeys: Set<string>, event: KeyboardEvent) => voi"
  },
  {
    "path": "src/libs/storage.js",
    "chars": 4614,
    "preview": "import {\n  STOKEY_SETTING,\n  STOKEY_SETTING_OLD,\n  STOKEY_RULES,\n  STOKEY_RULES_OLD,\n  STOKEY_WORDS,\n  STOKEY_FAB,\n  STO"
  },
  {
    "path": "src/libs/stream.js",
    "chars": 5736,
    "preview": "import { JSONParser } from \"@streamparser/json\";\nimport {\n  OPT_TRANS_OPENAI,\n  OPT_TRANS_GEMINI,\n  OPT_TRANS_GEMINI_2,\n"
  },
  {
    "path": "src/libs/style.js",
    "chars": 4328,
    "preview": "import { css, keyframes } from \"@emotion/css\";\nimport {\n  OPT_STYLE_NONE,\n  OPT_STYLE_LINE,\n  OPT_STYLE_DOTLINE,\n  OPT_S"
  },
  {
    "path": "src/libs/subRules.js",
    "chars": 1906,
    "preview": "import { GLOBAL_KEY } from \"../config\";\nimport {\n  getSyncWithDefault,\n  putSync,\n  setSubRules,\n  getSubRules,\n} from \""
  },
  {
    "path": "src/libs/svg.js",
    "chars": 3703,
    "preview": "export const loadingSvg = `<svg viewBox=\"-20 0 100 100\" \n     style=\"display: inline-block; width: 1em; height: 1em; ver"
  },
  {
    "path": "src/libs/sync.js",
    "chars": 4376,
    "preview": "import {\n  APP_LCNAME,\n  KV_SETTING_KEY,\n  KV_RULES_KEY,\n  KV_WORDS_KEY,\n  KV_RULES_SHARE_KEY,\n  KV_SALT_SHARE,\n  OPT_SY"
  },
  {
    "path": "src/libs/touch.js",
    "chars": 1118,
    "preview": "export function touchTapListener(fn, options = {}) {\n  const config = {\n    taps: 2,\n    fingers: 1,\n    delay: 300,\n   "
  },
  {
    "path": "src/libs/tranbox.js",
    "chars": 2207,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport createCache from \"@emotion/cache\";\nimport { C"
  },
  {
    "path": "src/libs/translator.js",
    "chars": 51089,
    "preview": "import {\n  APP_LCNAME,\n  APP_CONSTS,\n  OPT_STYLE_FUZZY,\n  GLOBLA_RULE,\n  DEFAULT_SETTING,\n  // DEFAULT_MOUSEHOVER_KEY,\n "
  },
  {
    "path": "src/libs/translatorManager.js",
    "chars": 8598,
    "preview": "import { browser } from \"./browser\";\nimport { Translator } from \"./translator\";\nimport { InputTranslator } from \"./input"
  },
  {
    "path": "src/libs/trustedTypes.js",
    "chars": 1060,
    "preview": "import { logger } from \"./log\";\n\nexport const trustedTypesHelper = (() => {\n  const POLICY_NAME = \"kiss-translator-polic"
  },
  {
    "path": "src/libs/url.js",
    "chars": 4823,
    "preview": "/**\n * URL 處理工具函數\n */\n\n/**\n * 檢查是否為 IP 位址 (v4 或 v6)\n * @param {string} hostname - 主機名稱\n * @returns {boolean}\n * @example"
  },
  {
    "path": "src/libs/utils.js",
    "chars": 10459,
    "preview": "/**\n * 移除 Markdown 代码块标记\n * @param {string} text 原始文本\n * @param {boolean} startOnly 是否只处理开头\n * @returns {string} 移除代码块标记"
  },
  {
    "path": "src/options.js",
    "chars": 292,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport Options from \"./views/Options\";\n\nglobalThis._"
  },
  {
    "path": "src/popup.js",
    "chars": 488,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { SettingProvider } from \"./hooks/Setting\";\ni"
  },
  {
    "path": "src/rules.js",
    "chars": 702,
    "preview": "import fs from \"fs\";\nimport path from \"path\";\nimport { BUILTIN_RULES } from \"./config/rules\";\n\n(() => {\n  // rules\n  try"
  },
  {
    "path": "src/scripts/archive.mjs",
    "chars": 1404,
    "preview": "#!/usr/bin/env zx\n\nconsole.log(chalk.cyan(\"\\nStarting compression tasks...\\n\"));\n\n// 1. 进入 build 目录\ncd(\"build\");\n\n// 2. "
  },
  {
    "path": "src/scripts/build-ios.mjs",
    "chars": 1320,
    "preview": "#!/usr/bin/env zx\n\nconsole.log(chalk.cyan(\"\\nBuilding iOS Userscript...\\n\"));\n\nconst srcFile = \"build/web/kiss-translato"
  },
  {
    "path": "src/scripts/build-safari.js",
    "chars": 3044,
    "preview": "import { $, globby } from \"zx\";\nimport path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimport dotenv from \"dot"
  },
  {
    "path": "src/scripts/build-safari.mjs",
    "chars": 4350,
    "preview": "#!/usr/bin/env zx\nimport { $, globby } from \"zx\";\nimport path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimpor"
  },
  {
    "path": "src/scripts/build-task.mjs",
    "chars": 3776,
    "preview": "#!/usr/bin/env zx\nimport { argv, quote, $ } from \"zx\";\n\n// 在 Windows 上使用 cmd.exe,避免 zx 默认使用 WSL bash 导致 node not found\ni"
  },
  {
    "path": "src/scripts/sync-version.mjs",
    "chars": 2379,
    "preview": "#!/usr/bin/env zx\nimport { $ } from \"zx\";\n\n/**\n * 版本号同步脚本\n * 从 package.json 读取版本号,自动同步到其他配置文件\n */\n\nconst rootDir = path."
  },
  {
    "path": "src/scripts/update-version.mjs",
    "chars": 1693,
    "preview": "#!/usr/bin/env zx\nimport { $, argv } from \"zx\";\n\n/**\n * 版本号更新脚本\n * 使用 npm version 命令更新 package.json 中的版本号,然后自动同步到其他文件\n *"
  },
  {
    "path": "src/subtitle/BilingualSubtitleManager.js",
    "chars": 22977,
    "preview": "import { logger } from \"../libs/log.js\";\nimport { truncateWords, throttle } from \"../libs/utils.js\";\nimport { apiTransla"
  },
  {
    "path": "src/subtitle/Menus.js",
    "chars": 6651,
    "preview": "import { useCallback, useMemo, useState } from \"react\";\nimport { API_SPE_TYPES } from \"../config\";\n\nfunction Label({ chi"
  },
  {
    "path": "src/subtitle/YouTubeCaptionProvider.js",
    "chars": 31205,
    "preview": "import { logger } from \"../libs/log.js\";\nimport { apiSubtitle } from \"../apis/index.js\";\nimport { BilingualSubtitleManag"
  },
  {
    "path": "src/subtitle/YouTubeSubtitleList.js",
    "chars": 33037,
    "preview": "import { logger } from \"../libs/log.js\";\nimport { downloadBlobFile } from \"../libs/utils.js\";\nimport { buildBilingualVtt"
  },
  {
    "path": "src/subtitle/subtitle.js",
    "chars": 1193,
    "preview": "import { YouTubeInitializer } from \"./YouTubeCaptionProvider.js\";\nimport { isMatch } from \"../libs/utils.js\";\nimport { D"
  },
  {
    "path": "src/subtitle/vtt.js",
    "chars": 4017,
    "preview": "/**\n * 将多种格式的VTT时间戳字符串转换为毫秒数。\n * 兼容以下格式:\n * - mmm (e.g., \"291040\")\n * - MM:SS (e.g., \"00:03\")\n * - HH:MM:SS (e.g., \"01:0"
  },
  {
    "path": "src/userscript.js",
    "chars": 44,
    "preview": "import { run } from \"./common\";\n\nrun(true);\n"
  },
  {
    "path": "src/views/Action/ContentFab.js",
    "chars": 1833,
    "preview": "import Fab from \"@mui/material/Fab\";\nimport TranslateIcon from \"@mui/icons-material/Translate\";\nimport ThemeProvider fro"
  },
  {
    "path": "src/views/Action/Draggable.js",
    "chars": 4081,
    "preview": "import { useEffect, useMemo, useState } from \"react\";\nimport { limitNumber } from \"../../libs/utils\";\nimport { isMobile "
  },
  {
    "path": "src/views/Action/index.js",
    "chars": 3070,
    "preview": "import ThemeProvider from \"../../hooks/Theme\";\nimport Draggable from \"./Draggable\";\nimport { useEffect, useMemo, useCall"
  },
  {
    "path": "src/views/Options/About.js",
    "chars": 538,
    "preview": "import Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport ReactMarkdown"
  },
  {
    "path": "src/views/Options/Apis.js",
    "chars": 28847,
    "preview": "import { useState, useEffect, useMemo } from \"react\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@m"
  },
  {
    "path": "src/views/Options/DarkModeButton.js",
    "chars": 650,
    "preview": "import IconButton from \"@mui/material/IconButton\";\nimport { useDarkMode } from \"../../hooks/ColorMode\";\nimport LightMode"
  },
  {
    "path": "src/views/Options/DownloadButton.js",
    "chars": 859,
    "preview": "import FileDownloadIcon from \"@mui/icons-material/FileDownload\";\nimport LoadingButton from \"@mui/lab/LoadingButton\";\nimp"
  },
  {
    "path": "src/views/Options/FavWords.js",
    "chars": 11682,
    "preview": "import Stack from \"@mui/material/Stack\";\nimport { useState } from \"react\";\nimport Typography from \"@mui/material/Typogra"
  },
  {
    "path": "src/views/Options/Header.js",
    "chars": 1346,
    "preview": "import AppBar from \"@mui/material/AppBar\";\nimport IconButton from \"@mui/material/IconButton\";\nimport MenuIcon from \"@mui"
  },
  {
    "path": "src/views/Options/HelpButton.js",
    "chars": 423,
    "preview": "import Button from \"@mui/material/Button\";\nimport { useI18n } from \"../../hooks/I18n\";\nimport HelpIcon from \"@mui/icons-"
  },
  {
    "path": "src/views/Options/InputSetting.js",
    "chars": 6525,
    "preview": "import Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextF"
  },
  {
    "path": "src/views/Options/Layout.js",
    "chars": 1336,
    "preview": "import { useEffect, useState } from \"react\";\nimport { Outlet, useLocation } from \"react-router-dom\";\nimport useMediaQuer"
  },
  {
    "path": "src/views/Options/MouseHover.js",
    "chars": 1796,
    "preview": "import Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport { useI18n } from \"../../hooks/I18n\""
  },
  {
    "path": "src/views/Options/Navigator.js",
    "chars": 3188,
    "preview": "import Drawer from \"@mui/material/Drawer\";\nimport List from \"@mui/material/List\";\nimport ListItemButton from \"@mui/mater"
  },
  {
    "path": "src/views/Options/Playground.js",
    "chars": 850,
    "preview": "import { useState } from \"react\";\nimport TranForm from \"../Selection/TranForm\";\nimport { DEFAULT_SETTING, DEFAULT_TRANBO"
  },
  {
    "path": "src/views/Options/ReusableAutocomplete.js",
    "chars": 1831,
    "preview": "import { useState, useEffect, useRef } from \"react\";\nimport Autocomplete from \"@mui/material/Autocomplete\";\nimport TextF"
  },
  {
    "path": "src/views/Options/Rules.js",
    "chars": 36883,
    "preview": "import Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextF"
  },
  {
    "path": "src/views/Options/Setting.js",
    "chars": 15154,
    "preview": "import Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextF"
  },
  {
    "path": "src/views/Options/ShortcutInput.js",
    "chars": 2166,
    "preview": "import Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextField\";\nimport IconButton from \"@mui/m"
  },
  {
    "path": "src/views/Options/ShowMoreButton.js",
    "chars": 761,
    "preview": "import ExpandMoreIcon from \"@mui/icons-material/ExpandMore\";\nimport ExpandLessIcon from \"@mui/icons-material/ExpandLess\""
  },
  {
    "path": "src/views/Options/StylesSetting.js",
    "chars": 5521,
    "preview": "import { useState, useEffect, useMemo } from \"react\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@m"
  },
  {
    "path": "src/views/Options/Subtitle.js",
    "chars": 23662,
    "preview": "import Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextF"
  },
  {
    "path": "src/views/Options/SyncSetting.js",
    "chars": 5822,
    "preview": "import Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextF"
  },
  {
    "path": "src/views/Options/Tranbox.js",
    "chars": 11603,
    "preview": "import Box from \"@mui/material/Box\";\nimport Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextF"
  },
  {
    "path": "src/views/Options/UploadButton.js",
    "chars": 1167,
    "preview": "import { useRef } from \"react\";\nimport FileUploadIcon from \"@mui/icons-material/FileUpload\";\nimport { useI18n } from \".."
  },
  {
    "path": "src/views/Options/index.js",
    "chars": 4553,
    "preview": "import { Routes, Route, HashRouter } from \"react-router-dom\";\nimport About from \"./About\";\nimport Rules from \"./Rules\";\n"
  },
  {
    "path": "src/views/Popup/Header.js",
    "chars": 1914,
    "preview": "import IconButton from \"@mui/material/IconButton\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport HomeIcon fr"
  },
  {
    "path": "src/views/Popup/PopupCont.js",
    "chars": 14435,
    "preview": "import { useState, useEffect, useMemo } from \"react\";\nimport Stack from \"@mui/material/Stack\";\nimport MenuItem from \"@mu"
  },
  {
    "path": "src/views/Popup/index.js",
    "chars": 3876,
    "preview": "import { useState, useEffect, useCallback } from \"react\";\nimport Box from \"@mui/material/Box\";\nimport Stack from \"@mui/m"
  },
  {
    "path": "src/views/Selection/AudioBtn.js",
    "chars": 944,
    "preview": "import IconButton from \"@mui/material/IconButton\";\nimport VolumeUpIcon from \"@mui/icons-material/VolumeUp\";\nimport { use"
  },
  {
    "path": "src/views/Selection/CopyBtn.js",
    "chars": 922,
    "preview": "import IconButton from \"@mui/material/IconButton\";\nimport ContentCopyIcon from \"@mui/icons-material/ContentCopy\";\nimport"
  },
  {
    "path": "src/views/Selection/DictCont.js",
    "chars": 2255,
    "preview": "import { useState, useEffect, useMemo } from \"react\";\nimport Stack from \"@mui/material/Stack\";\nimport FavBtn from \"./Fav"
  },
  {
    "path": "src/views/Selection/DictHandler.js",
    "chars": 6560,
    "preview": "import Typography from \"@mui/material/Typography\";\nimport { AudioBtn, BaiduAudioBtn } from \"./AudioBtn\";\nimport { OPT_DI"
  },
  {
    "path": "src/views/Selection/DraggableResizable.js",
    "chars": 8237,
    "preview": "import { useState } from \"react\";\nimport Paper from \"@mui/material/Paper\";\nimport Box from \"@mui/material/Box\";\nimport {"
  },
  {
    "path": "src/views/Selection/FavBtn.js",
    "chars": 937,
    "preview": "import IconButton from \"@mui/material/IconButton\";\nimport FavoriteIcon from \"@mui/icons-material/Favorite\";\nimport Favor"
  },
  {
    "path": "src/views/Selection/SugCont.js",
    "chars": 1954,
    "preview": "import Typography from \"@mui/material/Typography\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport"
  },
  {
    "path": "src/views/Selection/TranBox.js",
    "chars": 12030,
    "preview": "import { SettingProvider } from \"../../hooks/Setting\";\nimport ThemeProvider from \"../../hooks/Theme\";\nimport DraggableRe"
  },
  {
    "path": "src/views/Selection/TranBtn.js",
    "chars": 1943,
    "preview": "import { isMobile } from \"../../libs/mobile\";\nimport { limitNumber } from \"../../libs/utils\";\n\nexport default function T"
  },
  {
    "path": "src/views/Selection/TranCont.js",
    "chars": 2396,
    "preview": "import TextField from \"@mui/material/TextField\";\nimport Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui"
  },
  {
    "path": "src/views/Selection/TranForm.js",
    "chars": 13081,
    "preview": "import Stack from \"@mui/material/Stack\";\nimport TextField from \"@mui/material/TextField\";\nimport MenuItem from \"@mui/mat"
  },
  {
    "path": "src/views/Selection/index.js",
    "chars": 2165,
    "preview": "import TranBtn from \"./TranBtn\";\nimport TranBox from \"./TranBox\";\nimport useTranBoxState from \"../../hooks/useTranBoxSta"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the fishjar/kiss-translator GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 185 files (903.6 KB), approximately 273.5k tokens, and a symbol index with 671 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!