Merge branch 'feature/bergamot'
24
sdkjs-plugins/content/bergamot/3rd-Party.txt
Normal file
@ -0,0 +1,24 @@
|
||||
Bergamot Translator Plugin uses the following third-party components:
|
||||
|
||||
1. Bergamot Translator (https://github.com/browsermt/bergamot-translator)
|
||||
License: Mozilla Public License 2.0
|
||||
Copyright: Bergamot Project Contributors
|
||||
|
||||
2. Firefox Translations Models (https://github.com/mozilla/firefox-translations-models)
|
||||
License: Creative Commons Attribution-ShareAlike 4.0 International
|
||||
Copyright: Mozilla Foundation
|
||||
|
||||
3. jQuery (https://jquery.com/)
|
||||
License: MIT
|
||||
Copyright: OpenJS Foundation and other contributors
|
||||
|
||||
4. Select2 (https://select2.org/)
|
||||
License: MIT
|
||||
Copyright: Kevin Brown and other contributors
|
||||
|
||||
5. Marian NMT (https://marian-nmt.github.io/)
|
||||
License: MIT
|
||||
Copyright: Microsoft Corporation and contributors
|
||||
|
||||
The Bergamot Project has received funding from the European Union's Horizon 2020
|
||||
research and innovation programme under grant agreement No 825303.
|
||||
5
sdkjs-plugins/content/bergamot/CHANGELOG.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Initial release.
|
||||
64
sdkjs-plugins/content/bergamot/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Bergamot Translator Plugin for ONLYOFFICE
|
||||
|
||||
An offline machine translation plugin for ONLYOFFICE editors, powered by the Bergamot neural machine translation engine.
|
||||
|
||||
## Features
|
||||
|
||||
- **Offline Translation**: Works without internet connection after models are downloaded
|
||||
- **Privacy-Friendly**: All translation happens locally in your browser using WebAssembly
|
||||
- **Neural Machine Translation**: High-quality translations using the same technology as Firefox Translations
|
||||
- **Multi-Language Support**: Supports translation between many European languages
|
||||
- **Automatic Text Detection**: Automatically translates selected text in the document
|
||||
- **Manual Input**: Option to manually enter text for translation
|
||||
|
||||
## Supported Language Pairs
|
||||
|
||||
The plugin supports translation between the following languages:
|
||||
|
||||
- English
|
||||
- German
|
||||
- Spanish
|
||||
- French
|
||||
- Italian
|
||||
- Portuguese
|
||||
- Polish
|
||||
- Dutch
|
||||
- Russian
|
||||
- Ukrainian
|
||||
- Czech
|
||||
- Bulgarian
|
||||
- Estonian
|
||||
- Norwegian (Bokmal and Nynorsk)
|
||||
- And more...
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **First Use**: The plugin downloads the translation engine (WASM) and selected language models
|
||||
2. **Model Caching**: Models are cached in your browser for offline use
|
||||
3. **Translation**: Text is translated locally using neural machine translation
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Uses [Bergamot Translator](https://github.com/browsermt/bergamot-translator) - a WebAssembly port of the Marian NMT framework
|
||||
- Models are provided by [Mozilla Firefox Translations](https://github.com/mozilla/firefox-translations-models)
|
||||
- Part of the [Bergamot Project](https://browser.mt/) funded by the European Union
|
||||
|
||||
## Usage
|
||||
|
||||
1. Install the plugin in your ONLYOFFICE editor
|
||||
2. Select text you want to translate
|
||||
3. Choose source and target languages
|
||||
4. Click "Insert to document" to replace selected text with translation
|
||||
5. Or click "Copy" to copy the translation to clipboard
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0
|
||||
|
||||
## Third-party
|
||||
|
||||
* Bergamot Translator ([MPL 2.0](https://www.mozilla.org/en-US/MPL/2.0/))
|
||||
* Firefox Translations Models ([CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/))
|
||||
* jQuery ([MIT](https://opensource.org/licenses/MIT))
|
||||
* Select2 ([MIT](https://opensource.org/licenses/MIT))
|
||||
* Marian NMT ([MIT](https://opensource.org/licenses/MIT))
|
||||
71
sdkjs-plugins/content/bergamot/config.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "Bergamot Translator",
|
||||
"nameLocale": {
|
||||
"cs-CS": "Bergamot Translator",
|
||||
"de-DE": "Bergamot Translator",
|
||||
"es-ES": "Bergamot Translator",
|
||||
"fr-FR": "Bergamot Translator",
|
||||
"it-IT": "Bergamot Translator",
|
||||
"ja-JA": "Bergamot Translator",
|
||||
"nl-NL": "Bergamot Translator",
|
||||
"pt-BR": "Bergamot Translator",
|
||||
"pt-PT": "Bergamot Translator",
|
||||
"ru-RU": "Bergamot Translator",
|
||||
"zh-ZH": "Bergamot Translator"
|
||||
},
|
||||
"guid": "asc.{e3c4f4a2-8b7d-4f6e-9a1c-5d2b8f0c7e3a}",
|
||||
"version": "1.0.0",
|
||||
|
||||
"variations": [
|
||||
{
|
||||
"description": "Offline machine translation powered by Bergamot - privacy-friendly, works without internet",
|
||||
"descriptionLocale": {
|
||||
"cs-CS": "Offline strojový překlad poháněný Bergamot - šetrný k soukromí, funguje bez internetu",
|
||||
"de-DE": "Offline-Maschinenübersetzung mit Bergamot - datenschutzfreundlich, funktioniert ohne Internet",
|
||||
"es-ES": "Traducción automática sin conexión con Bergamot - respetuoso con la privacidad, funciona sin internet",
|
||||
"fr-FR": "Traduction automatique hors ligne par Bergamot - respectueux de la vie privée, fonctionne sans internet",
|
||||
"it-IT": "Traduzione automatica offline con Bergamot - rispettoso della privacy, funziona senza internet",
|
||||
"ja-JA": "Bergamotによるオフライン機械翻訳 - プライバシーに配慮し、インターネットなしで動作",
|
||||
"nl-NL": "Offline machinevertaling met Bergamot - privacyvriendelijk, werkt zonder internet",
|
||||
"pt-BR": "Tradução automática offline com Bergamot - amigável à privacidade, funciona sem internet",
|
||||
"pt-PT": "Tradução automática offline com Bergamot - amigo da privacidade, funciona sem internet",
|
||||
"ru-RU": "Офлайн машинный перевод на основе Bergamot - конфиденциальность, работает без интернета",
|
||||
"zh-ZH": "Bergamot 离线机器翻译 - 隐私友好,无需互联网即可工作"
|
||||
},
|
||||
"url": "index.html",
|
||||
|
||||
"icons": "resources/%theme-type%(light|dark)/icon%scale%(default).%extension%(png)",
|
||||
"isViewer": false,
|
||||
"EditorsSupport": [
|
||||
"word",
|
||||
"slide",
|
||||
"cell",
|
||||
"pdf"
|
||||
],
|
||||
|
||||
"isVisual": true,
|
||||
"isModal": false,
|
||||
"isInsideMode": true,
|
||||
|
||||
"initDataType": "text",
|
||||
"initOnSelectionChanged": true,
|
||||
|
||||
"store": {
|
||||
"background": {
|
||||
"light" : "#E2E88C",
|
||||
"dark" : "#E2E88C"
|
||||
},
|
||||
"screenshots" : [
|
||||
"resources/store/screenshots/screen_1.png",
|
||||
"resources/store/screenshots/screen_2.png",
|
||||
"resources/store/screenshots/screen_3.png"
|
||||
],
|
||||
"icons" : {
|
||||
"light" : "resources/store/icons",
|
||||
"dark" : "resources/store/icons"
|
||||
},
|
||||
"categories": ["specAbilities", "work"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
sdkjs-plugins/content/bergamot/deploy/bergamot.plugin
Normal file
234
sdkjs-plugins/content/bergamot/index.html
Normal file
@ -0,0 +1,234 @@
|
||||
<!--
|
||||
(c) Copyright Ascensio System SIA 2024
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bergamot Translator</title>
|
||||
<link rel="stylesheet" href="plugin_style.css">
|
||||
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
|
||||
<link rel="stylesheet" href="vendor/select2-4.0.13/css/select2.css"/>
|
||||
<script src="vendor/select2-4.0.13/js/select2.js"></script>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
|
||||
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
|
||||
<script src="scripts/bergamot.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
min-height: 100% !important;
|
||||
height: 100%;
|
||||
font-family: "Arial";
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#main-container-id {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#copy {
|
||||
width:40%;
|
||||
margin-left: 12px;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
#paste {
|
||||
float: right;
|
||||
width:40%;
|
||||
margin-right: 12px;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
#display {
|
||||
width:100%;
|
||||
margin-left: 12px;
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input__head {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.header {
|
||||
margin-right: 3px;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
#source {
|
||||
margin-top: 5px;
|
||||
width: calc(100% - 67px);
|
||||
}
|
||||
#target {
|
||||
float:right;
|
||||
margin-top: 5px;
|
||||
width: calc(100% - 67px);
|
||||
}
|
||||
.select2-results__options {
|
||||
font-size: 11px !important;
|
||||
}
|
||||
.select2-selection__rendered {
|
||||
font-size: 11px !important;
|
||||
margin-top: -1px;
|
||||
}
|
||||
#button_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn-text-default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#hide_show {
|
||||
width: 130px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
#show_manually, #hide_manually {
|
||||
cursor:pointer;
|
||||
border-bottom: 1px dashed #444444;
|
||||
}
|
||||
.noselect {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.ps__rail-y {
|
||||
cursor: default !important;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
#status_bar {
|
||||
margin: 8px 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
display: none;
|
||||
}
|
||||
#status_bar.info {
|
||||
background-color: #e8f4fd;
|
||||
color: #1976d2;
|
||||
display: block;
|
||||
}
|
||||
#status_bar.success {
|
||||
background-color: #e8f5e9;
|
||||
color: #388e3c;
|
||||
display: block;
|
||||
}
|
||||
#status_bar.warning {
|
||||
background-color: #fff3e0;
|
||||
color: #f57c00;
|
||||
display: block;
|
||||
}
|
||||
#status_bar.error {
|
||||
background-color: #ffebee;
|
||||
color: #d32f2f;
|
||||
display: block;
|
||||
}
|
||||
#model_progress {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
margin-top: 4px;
|
||||
display: none;
|
||||
}
|
||||
#model_progress_bar {
|
||||
height: 100%;
|
||||
background-color: #1976d2;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
#txt_shower {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body id="body" style="left:0px; top:0px; margin:0px; padding:0px;">
|
||||
<div id="main_container-id" style="width:100%; height:100%;margin:0;padding:0; overflow: hidden; display: flex; flex-direction: column">
|
||||
|
||||
<div id="loader-container" class="asc-plugin-loader display-none">
|
||||
<div class="asc-loader-image">
|
||||
<div class="asc-loader-title"></div>
|
||||
</div>
|
||||
<div class="asc-loader-title i18n">
|
||||
Translating...
|
||||
</div>
|
||||
</div>
|
||||
<div id="loader-container2" class="asc-plugin-loader display-none">
|
||||
<div class="asc-loader-image">
|
||||
<div class="asc-loader-title"></div>
|
||||
</div>
|
||||
<div class="asc-loader-title i18n">
|
||||
Loading translation engine...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status_bar"></div>
|
||||
<div id="model_progress">
|
||||
<div id="model_progress_bar"></div>
|
||||
</div>
|
||||
|
||||
<div id="main_panel" style="display: flex; flex-direction: column; max-height: 55%;">
|
||||
<div class="input__head" style="display: flex; flex-direction: row; justify-content: space-between;">
|
||||
<span class="prefs__locale_source" style="margin-left: 12px; width: calc(40% + 2px);">
|
||||
<select id="source" class="prefs__set-locale select_example" data-title-id="locale" title="Source Language">
|
||||
</select>
|
||||
</span>
|
||||
<span style="display:flex;">
|
||||
<svg id="arrow" style="margin-top: 6px;" width="11" height="9" viewBox="0 0 11 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="arrow-svg-path" d="M10.3537 4.85344C10.5489 4.65812 10.5488 4.34153 10.3534 4.14633L7.17045 0.965364C6.97513 0.770164 6.65854 0.770264 6.46334 0.965588C6.26814 1.16091 6.26824 1.47749 6.46357 1.6727L9.29289 4.50022L6.46536 7.32955C6.27016 7.52487 6.27026 7.84146 6.46559 8.03666C6.66091 8.23186 6.97749 8.23176 7.1727 8.03643L10.3537 4.85344ZM0.000158691 5.00317L10.0002 5L9.99984 4L-0.000158691 4.00317L0.000158691 5.00317Z" fill="#444444"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="prefs__locale_target" style="margin-right: 12px; width: calc(40% + 2px);">
|
||||
<select id="target" class="prefs__set-locale select_example" data-title-id="locale" title="Target Language">
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div id="hide_show">
|
||||
<label style="display:none;" class="i18n" id="hide_manually">Hide field</label>
|
||||
<label id="show_manually" class="i18n">Enter text manually</label>
|
||||
</div>
|
||||
<textarea id="enter_container" oninput='this.style.height = "";this.style.height = this.scrollHeight + 2 + "px"' class="form-control" style="cursor: text; position: relative; margin: 16px 12px 0px 12px; min-height: 100px; max-height: 100%; height: fit-content; display:none;">
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="separator horizontal" style="width: calc(100% - 24px); margin: 16px 0 16px 12px;"></div>
|
||||
<div id="display" style="width: calc(100% - 12px); position: relative;">
|
||||
<div id="txt_shower" style="width:calc(100% - 20px); margin-right:8px; white-space: normal; word-wrap: break-word;">
|
||||
</div>
|
||||
</div>
|
||||
<div id="vanish_container" class="display-none">
|
||||
<div id="button_wrapper">
|
||||
<div id="copy" class="noselect btn-text-default i18n" style="height: auto; min-height: 20px;">Copy</div>
|
||||
<div id="paste" class="noselect btn-text-default i18n" style="height: auto; min-height: 20px;">Insert to document</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
188
sdkjs-plugins/content/bergamot/plugin_style.css
Normal file
@ -0,0 +1,188 @@
|
||||
.blur {
|
||||
filter: blur(3px);
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flexSize {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flexCenter {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flexCol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#errorWrapper {
|
||||
background-color: #ffffff;
|
||||
padding: 10px 12px;
|
||||
color: #D9534F;
|
||||
}
|
||||
|
||||
/* loader */
|
||||
|
||||
.cssload-container {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cssload-loading i {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
background: rgb(68, 68, 68);
|
||||
}
|
||||
|
||||
.cssload-loading i:first-child {
|
||||
opacity: 0;
|
||||
animation: cssload-loading-ani2 0.58s linear infinite;
|
||||
-o-animation: cssload-loading-ani2 0.58s linear infinite;
|
||||
-ms-animation: cssload-loading-ani2 0.58s linear infinite;
|
||||
-webkit-animation: cssload-loading-ani2 0.58s linear infinite;
|
||||
-moz-animation: cssload-loading-ani2 0.58s linear infinite;
|
||||
transform: translate(-19px);
|
||||
-o-transform: translate(-19px);
|
||||
-ms-transform: translate(-19px);
|
||||
-webkit-transform: translate(-19px);
|
||||
-moz-transform: translate(-19px);
|
||||
}
|
||||
|
||||
.cssload-loading i:nth-child(2),
|
||||
.cssload-loading i:nth-child(3) {
|
||||
animation: cssload-loading-ani3 0.58s linear infinite;
|
||||
-o-animation: cssload-loading-ani3 0.58s linear infinite;
|
||||
-ms-animation: cssload-loading-ani3 0.58s linear infinite;
|
||||
-webkit-animation: cssload-loading-ani3 0.58s linear infinite;
|
||||
-moz-animation: cssload-loading-ani3 0.58s linear infinite;
|
||||
}
|
||||
|
||||
.cssload-loading i:last-child {
|
||||
animation: cssload-loading-ani1 0.58s linear infinite;
|
||||
-o-animation: cssload-loading-ani1 0.58s linear infinite;
|
||||
-ms-animation: cssload-loading-ani1 0.58s linear infinite;
|
||||
-webkit-animation: cssload-loading-ani1 0.58s linear infinite;
|
||||
-moz-animation: cssload-loading-ani1 0.58s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes cssload-loading-ani1 {
|
||||
100% {
|
||||
transform: translate(39px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes cssload-loading-ani1 {
|
||||
100% {
|
||||
-o-transform: translate(39px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-ms-keyframes cssload-loading-ani1 {
|
||||
100% {
|
||||
-ms-transform: translate(39px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes cssload-loading-ani1 {
|
||||
100% {
|
||||
-webkit-transform: translate(39px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes cssload-loading-ani1 {
|
||||
100% {
|
||||
-moz-transform: translate(39px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cssload-loading-ani2 {
|
||||
100% {
|
||||
transform: translate(19px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes cssload-loading-ani2 {
|
||||
100% {
|
||||
-o-transform: translate(19px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-ms-keyframes cssload-loading-ani2 {
|
||||
100% {
|
||||
-ms-transform: translate(19px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes cssload-loading-ani2 {
|
||||
100% {
|
||||
-webkit-transform: translate(19px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes cssload-loading-ani2 {
|
||||
100% {
|
||||
-moz-transform: translate(19px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cssload-loading-ani3 {
|
||||
100% {
|
||||
transform: translate(19px);
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes cssload-loading-ani3 {
|
||||
100% {
|
||||
-o-transform: translate(19px);
|
||||
}
|
||||
}
|
||||
|
||||
@-ms-keyframes cssload-loading-ani3 {
|
||||
100% {
|
||||
-ms-transform: translate(19px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes cssload-loading-ani3 {
|
||||
100% {
|
||||
-webkit-transform: translate(19px);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes cssload-loading-ani3 {
|
||||
100% {
|
||||
-moz-transform: translate(19px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* end loader */
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
BIN
sdkjs-plugins/content/bergamot/resources/dark/icon.png
Normal file
|
After Width: | Height: | Size: 334 B |
3
sdkjs-plugins/content/bergamot/resources/dark/icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.15 1.975c2.678 0 4.209 1.148 4.591 3.171C15.31 4.37 16.221 4 17.5 4a.5.5 0 0 1 0 1c-1.143 0-1.7.334-2.016.831-.31.487-.45 1.214-.48 2.227A9 9 0 1 1 14 8h.008l.002-.05c-3.278-.248-4.565-2.447-3.86-5.975m5.666 10.367a5.002 5.002 0 0 1 .318 9.18l-.32-.725-1.534 2.098 2.583.278-.324-.735a6.001 6.001 0 0 0-.36-11.028zm-4.357-.779a6 6 0 0 0 .36 11.028l.363-.932a5.002 5.002 0 0 1-.318-9.18l.32.725 1.534-2.098-2.583-.278z" fill="#eaeaea"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 556 B |
BIN
sdkjs-plugins/content/bergamot/resources/dark/icon@1.25x.png
Normal file
|
After Width: | Height: | Size: 377 B |
BIN
sdkjs-plugins/content/bergamot/resources/dark/icon@1.5x.png
Normal file
|
After Width: | Height: | Size: 467 B |
BIN
sdkjs-plugins/content/bergamot/resources/dark/icon@1.75x.png
Normal file
|
After Width: | Height: | Size: 536 B |
BIN
sdkjs-plugins/content/bergamot/resources/dark/icon@2x.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
sdkjs-plugins/content/bergamot/resources/light/icon.png
Normal file
|
After Width: | Height: | Size: 325 B |
3
sdkjs-plugins/content/bergamot/resources/light/icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.15 1.975c2.678 0 4.209 1.148 4.591 3.171C15.31 4.37 16.221 4 17.5 4a.5.5 0 0 1 0 1c-1.143 0-1.7.334-2.016.831-.31.487-.45 1.214-.48 2.227A9 9 0 1 1 14 8h.008l.002-.05c-3.278-.248-4.565-2.447-3.86-5.975m5.666 10.367a5.002 5.002 0 0 1 .318 9.18l-.32-.725-1.534 2.098 2.583.278-.324-.735a6.001 6.001 0 0 0-.36-11.028zm-4.357-.779a6 6 0 0 0 .36 11.028l.363-.932a5.002 5.002 0 0 1-.318-9.18l.32.725 1.534-2.098-2.583-.278z" fill="#383838"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 556 B |
BIN
sdkjs-plugins/content/bergamot/resources/light/icon@1.25x.png
Normal file
|
After Width: | Height: | Size: 367 B |
BIN
sdkjs-plugins/content/bergamot/resources/light/icon@1.5x.png
Normal file
|
After Width: | Height: | Size: 457 B |
BIN
sdkjs-plugins/content/bergamot/resources/light/icon@1.75x.png
Normal file
|
After Width: | Height: | Size: 529 B |
BIN
sdkjs-plugins/content/bergamot/resources/light/icon@2x.png
Normal file
|
After Width: | Height: | Size: 587 B |
BIN
sdkjs-plugins/content/bergamot/resources/store/icons/icon.png
Normal file
|
After Width: | Height: | Size: 325 B |
@ -0,0 +1,3 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.15 1.975c2.678 0 4.209 1.148 4.591 3.171C15.31 4.37 16.221 4 17.5 4a.5.5 0 0 1 0 1c-1.143 0-1.7.334-2.016.831-.31.487-.45 1.214-.48 2.227A9 9 0 1 1 14 8h.008l.002-.05c-3.278-.248-4.565-2.447-3.86-5.975m5.666 10.367a5.002 5.002 0 0 1 .318 9.18l-.32-.725-1.534 2.098 2.583.278-.324-.735a6.001 6.001 0 0 0-.36-11.028zm-4.357-.779a6 6 0 0 0 .36 11.028l.363-.932a5.002 5.002 0 0 1-.318-9.18l.32.725 1.534-2.098-2.583-.278z" fill="#383838"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 556 B |
|
After Width: | Height: | Size: 367 B |
|
After Width: | Height: | Size: 457 B |
|
After Width: | Height: | Size: 529 B |
BIN
sdkjs-plugins/content/bergamot/resources/store/icons/icon@2x.png
Normal file
|
After Width: | Height: | Size: 587 B |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 203 KiB |
732
sdkjs-plugins/content/bergamot/scripts/bergamot.js
Normal file
@ -0,0 +1,732 @@
|
||||
/**
|
||||
* Bergamot Translator Plugin for ONLYOFFICE
|
||||
*
|
||||
* Uses the Bergamot neural machine translation engine (WASM-based)
|
||||
* for privacy-friendly offline translation.
|
||||
*
|
||||
* Based on: https://github.com/browsermt/bergamot-translator
|
||||
* Models from: https://github.com/mozilla/firefox-translations-models
|
||||
*
|
||||
* (c) Copyright Ascensio System SIA 2024
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
*/
|
||||
|
||||
(function (window, undefined) {
|
||||
"use strict";
|
||||
|
||||
// Configuration
|
||||
// TRANSLATOR_MODULE_URL: relative to this script (scripts/), so go up one level
|
||||
// MODELS_REGISTRY_URL: online registry with full model URLs
|
||||
const TRANSLATOR_MODULE_URL = "../vendor/bergamot/translator.js";
|
||||
const MODELS_REGISTRY_URL = "https://bergamot.s3.amazonaws.com/models/index.json";
|
||||
const DEBUG = false; // Set to true to enable debug logging
|
||||
|
||||
// State
|
||||
let translator = null;
|
||||
let modelsRegistry = null;
|
||||
let allLanguagePairs = {};
|
||||
let isInitialized = false;
|
||||
let isInitializing = false;
|
||||
let txt = "";
|
||||
let translatedText = "";
|
||||
let paste_done = true;
|
||||
let loadedModels = []; // Track loaded models (max 3 to prevent memory overflow)
|
||||
const MAX_LOADED_MODELS = 3;
|
||||
|
||||
// Debug logging function
|
||||
function debugLog(...args) {
|
||||
if (DEBUG) {
|
||||
console.log("[Bergamot]", ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// Language names mapping (ISO 639-1 to display names)
|
||||
const LANGUAGE_NAMES = {
|
||||
"en": "English",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fr": "French",
|
||||
"it": "Italian",
|
||||
"pt": "Portuguese",
|
||||
"pl": "Polish",
|
||||
"nl": "Dutch",
|
||||
"ru": "Russian",
|
||||
"uk": "Ukrainian",
|
||||
"cs": "Czech",
|
||||
"bg": "Bulgarian",
|
||||
"et": "Estonian",
|
||||
"is": "Icelandic",
|
||||
"nb": "Norwegian Bokmal",
|
||||
"nn": "Norwegian Nynorsk",
|
||||
"fa": "Persian",
|
||||
"th": "Thai"
|
||||
};
|
||||
|
||||
// Load the translator module dynamically using ES module import
|
||||
async function loadTranslatorModule() {
|
||||
if (translator) {
|
||||
return translator;
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import of ES module
|
||||
const module = await import(TRANSLATOR_MODULE_URL);
|
||||
|
||||
// Create a BatchTranslator instance
|
||||
// BatchTranslator is optimized for translating multiple texts
|
||||
// Note: workerUrl is resolved relative to translator.js via import.meta.url
|
||||
// so we don't need to specify it explicitly when using local files
|
||||
translator = new module.BatchTranslator({
|
||||
registryUrl: MODELS_REGISTRY_URL
|
||||
});
|
||||
|
||||
return translator;
|
||||
} catch (error) {
|
||||
console.error("Failed to load translator module:", error);
|
||||
throw new Error("Failed to load translation engine: " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch models registry to build available language pairs
|
||||
async function fetchModelsRegistry() {
|
||||
if (modelsRegistry) {
|
||||
return modelsRegistry;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(MODELS_REGISTRY_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch models registry");
|
||||
}
|
||||
modelsRegistry = await response.json();
|
||||
return modelsRegistry;
|
||||
} catch (error) {
|
||||
console.error("Error fetching models registry:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Build language pairs from registry
|
||||
function buildLanguagePairs(registry) {
|
||||
allLanguagePairs = {};
|
||||
|
||||
for (const pairKey of Object.keys(registry)) {
|
||||
// pairKey format: "ende" (en->de) or "deen" (de->en)
|
||||
if (pairKey.length !== 4) continue;
|
||||
|
||||
const sourceLang = pairKey.substring(0, 2);
|
||||
const targetLang = pairKey.substring(2, 4);
|
||||
|
||||
if (!allLanguagePairs[sourceLang]) {
|
||||
allLanguagePairs[sourceLang] = [];
|
||||
}
|
||||
if (!allLanguagePairs[sourceLang].includes(targetLang)) {
|
||||
allLanguagePairs[sourceLang].push(targetLang);
|
||||
}
|
||||
}
|
||||
|
||||
return allLanguagePairs;
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
function updateProgress(percent) {
|
||||
const progressBar = document.getElementById("model_progress_bar");
|
||||
const progressContainer = document.getElementById("model_progress");
|
||||
if (progressBar && progressContainer) {
|
||||
progressContainer.style.display = "block";
|
||||
progressBar.style.width = percent + "%";
|
||||
}
|
||||
}
|
||||
|
||||
// Hide progress bar
|
||||
function hideProgress() {
|
||||
const progressContainer = document.getElementById("model_progress");
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Update status bar
|
||||
function updateStatus(message, type) {
|
||||
const statusBar = document.getElementById("status_bar");
|
||||
if (statusBar) {
|
||||
statusBar.textContent = message;
|
||||
statusBar.className = type || "";
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up old translation models to free memory
|
||||
async function freeOldestModel() {
|
||||
if (!translator || !translator.workers || translator.workers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the oldest model (first in array)
|
||||
const oldestModel = loadedModels.shift();
|
||||
if (!oldestModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Free the model from all workers
|
||||
for (const workerEntry of translator.workers) {
|
||||
if (workerEntry && workerEntry.exports && workerEntry.exports.freeTranslationModel) {
|
||||
try {
|
||||
await workerEntry.exports.freeTranslationModel({
|
||||
from: oldestModel.from,
|
||||
to: oldestModel.to
|
||||
});
|
||||
debugLog(`Freed model: ${oldestModel.from}->${oldestModel.to}`);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to free model ${oldestModel.from}->${oldestModel.to}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error during model cleanup:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Perform translation using the high-level API
|
||||
async function translate(text, sourceLang, targetLang) {
|
||||
if (!text || !text.trim()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
if (!translator) {
|
||||
await loadTranslatorModule();
|
||||
}
|
||||
|
||||
// Track this model and clean up old ones if needed
|
||||
const modelKey = `${sourceLang}-${targetLang}`;
|
||||
const existingIndex = loadedModels.findIndex(m => `${m.from}-${m.to}` === modelKey);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
// New model - check if we need to free space
|
||||
if (loadedModels.length >= MAX_LOADED_MODELS) {
|
||||
await freeOldestModel();
|
||||
}
|
||||
// Add to the end (most recent)
|
||||
loadedModels.push({ from: sourceLang, to: targetLang });
|
||||
} else {
|
||||
// Move existing model to end (mark as most recently used)
|
||||
const model = loadedModels.splice(existingIndex, 1)[0];
|
||||
loadedModels.push(model);
|
||||
}
|
||||
|
||||
updateStatus(getMessage("Translating..."), "info");
|
||||
updateProgress(50);
|
||||
|
||||
// Use the translator's translate method
|
||||
const result = await translator.translate({
|
||||
from: sourceLang,
|
||||
to: targetLang,
|
||||
text: text,
|
||||
html: false
|
||||
});
|
||||
|
||||
hideProgress();
|
||||
return result.target?.text || result.translation || result;
|
||||
} catch (error) {
|
||||
hideProgress();
|
||||
console.error("Translation error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the plugin
|
||||
async function initializeTranslator() {
|
||||
if (isInitialized) return;
|
||||
if (isInitializing) return;
|
||||
|
||||
isInitializing = true;
|
||||
showLoader(["#loader-container2"], true);
|
||||
updateStatus(getMessage("Loading translation engine..."), "info");
|
||||
|
||||
try {
|
||||
// Fetch models registry to know available language pairs
|
||||
await fetchModelsRegistry();
|
||||
|
||||
// Build language pairs
|
||||
buildLanguagePairs(modelsRegistry);
|
||||
|
||||
// Populate dropdowns
|
||||
populateLanguageDropdowns();
|
||||
|
||||
// Pre-load the translator module (but don't download models yet)
|
||||
await loadTranslatorModule();
|
||||
|
||||
isInitialized = true;
|
||||
updateStatus(getMessage("Ready - Select languages to translate"), "success");
|
||||
} catch (error) {
|
||||
console.error("Initialization error:", error);
|
||||
updateStatus(getMessage("Failed to initialize: ") + error.message, "error");
|
||||
} finally {
|
||||
isInitializing = false;
|
||||
showLoader(["#loader-container2"], false);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate language dropdowns
|
||||
function populateLanguageDropdowns() {
|
||||
const sourceSelect = document.getElementById("source");
|
||||
const targetSelect = document.getElementById("target");
|
||||
|
||||
if (!sourceSelect || !targetSelect) return;
|
||||
|
||||
// Clear existing options
|
||||
sourceSelect.innerHTML = "";
|
||||
targetSelect.innerHTML = "";
|
||||
|
||||
// Get all source languages
|
||||
const sourceLanguages = Object.keys(allLanguagePairs).sort((a, b) => {
|
||||
const nameA = LANGUAGE_NAMES[a] || a;
|
||||
const nameB = LANGUAGE_NAMES[b] || b;
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
// Populate source dropdown
|
||||
sourceLanguages.forEach(lang => {
|
||||
const option = document.createElement("option");
|
||||
option.value = lang;
|
||||
option.textContent = LANGUAGE_NAMES[lang] || lang.toUpperCase();
|
||||
sourceSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set default source language (English if available)
|
||||
if (sourceLanguages.includes("en")) {
|
||||
sourceSelect.value = "en";
|
||||
}
|
||||
|
||||
// Update target dropdown based on source
|
||||
updateTargetLanguages();
|
||||
|
||||
// Initialize Select2
|
||||
$(sourceSelect).select2({
|
||||
minimumResultsForSearch: Infinity,
|
||||
width: "100%"
|
||||
});
|
||||
|
||||
$(targetSelect).select2({
|
||||
minimumResultsForSearch: Infinity,
|
||||
width: "100%"
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
$(sourceSelect).on("change", function() {
|
||||
updateTargetLanguages();
|
||||
RunTranslate(txt);
|
||||
});
|
||||
|
||||
$(targetSelect).on("change", function() {
|
||||
RunTranslate(txt);
|
||||
});
|
||||
}
|
||||
|
||||
// Update target languages based on selected source
|
||||
function updateTargetLanguages() {
|
||||
const sourceSelect = document.getElementById("source");
|
||||
const targetSelect = document.getElementById("target");
|
||||
|
||||
if (!sourceSelect || !targetSelect) return;
|
||||
|
||||
const sourceLang = sourceSelect.value;
|
||||
const targetLanguages = allLanguagePairs[sourceLang] || [];
|
||||
const currentTarget = targetSelect.value;
|
||||
|
||||
// Clear and repopulate
|
||||
targetSelect.innerHTML = "";
|
||||
|
||||
targetLanguages.sort((a, b) => {
|
||||
const nameA = LANGUAGE_NAMES[a] || a;
|
||||
const nameB = LANGUAGE_NAMES[b] || b;
|
||||
return nameA.localeCompare(nameB);
|
||||
}).forEach(lang => {
|
||||
const option = document.createElement("option");
|
||||
option.value = lang;
|
||||
option.textContent = LANGUAGE_NAMES[lang] || lang.toUpperCase();
|
||||
targetSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Try to keep the previous selection if still valid
|
||||
if (targetLanguages.includes(currentTarget)) {
|
||||
targetSelect.value = currentTarget;
|
||||
}
|
||||
|
||||
// Update Select2
|
||||
$(targetSelect).trigger("change.select2");
|
||||
}
|
||||
|
||||
// Run translation based on editor type
|
||||
function RunTranslate(sText) {
|
||||
switch (window.Asc.plugin.info.editorType) {
|
||||
case 'word':
|
||||
case 'slide': {
|
||||
window.Asc.plugin.executeMethod("GetSelectedText",
|
||||
[{Numbering:false, Math: false, TableCellSeparator: '\n', ParaSeparator: '\n'}],
|
||||
function(data) {
|
||||
sText = data.replace(/\r/g, ' ');
|
||||
runTranslation(sText);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'cell': {
|
||||
window.Asc.plugin.executeMethod("GetSelectedText",
|
||||
[{Numbering:false, Math: false, TableCellSeparator: '\n', ParaSeparator: '\n'}],
|
||||
function(data) {
|
||||
if (data == '')
|
||||
sText = txt.replace(/\r/g, ' ').replace(/\t/g, '\n');
|
||||
else
|
||||
sText = data.replace(/\r/g, ' ');
|
||||
runTranslation(sText);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'pdf': {
|
||||
window.Asc.plugin.executeMethod("GetSelectedText",
|
||||
[{Numbering:false, Math: false, TableCellSeparator: '\n', ParaSeparator: '\n'}],
|
||||
function(data) {
|
||||
if (data && data.trim() !== '')
|
||||
sText = data.replace(/\r/g, ' ');
|
||||
runTranslation(sText);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Fallback for other editor types
|
||||
runTranslation(sText);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run translation
|
||||
async function runTranslation(textToTranslate) {
|
||||
const textInput = textToTranslate || txt;
|
||||
|
||||
if (!textInput || !textInput.trim()) {
|
||||
clearTranslation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update txt with the actual text to translate
|
||||
txt = textInput;
|
||||
|
||||
if (!isInitialized) {
|
||||
await initializeTranslator();
|
||||
}
|
||||
|
||||
const sourceLang = document.getElementById("source")?.value;
|
||||
const targetLang = document.getElementById("target")?.value;
|
||||
|
||||
if (!sourceLang || !targetLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(getMessage("Translating..."), "info");
|
||||
|
||||
try {
|
||||
translatedText = await translate(txt, sourceLang, targetLang);
|
||||
displayTranslation(translatedText);
|
||||
updateStatus(getMessage("Translation complete"), "success");
|
||||
} catch (error) {
|
||||
displayTranslation(getMessage("Translation failed: ") + error.message);
|
||||
updateStatus(getMessage("Translation failed"), "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Display translation result
|
||||
function displayTranslation(text) {
|
||||
const display = document.getElementById("txt_shower");
|
||||
const vanishContainer = document.getElementById("vanish_container");
|
||||
|
||||
if (display) {
|
||||
display.textContent = text;
|
||||
}
|
||||
|
||||
if (vanishContainer && text) {
|
||||
vanishContainer.classList.remove("display-none");
|
||||
}
|
||||
|
||||
updateScroll();
|
||||
}
|
||||
|
||||
// Clear translation
|
||||
function clearTranslation() {
|
||||
const display = document.getElementById("txt_shower");
|
||||
const vanishContainer = document.getElementById("vanish_container");
|
||||
|
||||
if (display) {
|
||||
display.textContent = "";
|
||||
}
|
||||
|
||||
if (vanishContainer) {
|
||||
vanishContainer.classList.add("display-none");
|
||||
}
|
||||
|
||||
translatedText = "";
|
||||
}
|
||||
|
||||
// Show/hide loader
|
||||
function showLoader(selectors, show) {
|
||||
selectors.forEach(selector => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
if (show) {
|
||||
el.classList.remove("display-none");
|
||||
} else {
|
||||
el.classList.add("display-none");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update scroll
|
||||
function updateScroll() {
|
||||
if (window.Ps && typeof window.Ps.update === "function") {
|
||||
window.Ps.update();
|
||||
}
|
||||
}
|
||||
|
||||
// Get translated message
|
||||
function getMessage(key) {
|
||||
if (window.Asc && window.Asc.plugin && typeof window.Asc.plugin.tr === "function") {
|
||||
const translated = window.Asc.plugin.tr(key);
|
||||
return translated || key;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// Copy text to clipboard
|
||||
function copyToClipboard(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Plugin initialization
|
||||
window.Asc.plugin.init = function(text) {
|
||||
// Hide paste button in view mode (e.g., PDF viewer)
|
||||
if (window.Asc.plugin.info.isViewMode)
|
||||
document.getElementById("paste").classList.add('hidden');
|
||||
|
||||
txt = text || "";
|
||||
|
||||
// Populate manual entry field with selected text
|
||||
if (txt.trim() !== "") {
|
||||
const enterContainer = document.getElementById("enter_container");
|
||||
if (enterContainer) {
|
||||
enterContainer.value = txt;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInitialized && !isInitializing) {
|
||||
initializeTranslator().then(() => {
|
||||
if (txt) {
|
||||
RunTranslate(txt);
|
||||
}
|
||||
});
|
||||
} else if (txt) {
|
||||
RunTranslate(txt);
|
||||
}
|
||||
};
|
||||
|
||||
// Selection changed handler
|
||||
window.Asc.plugin.onExternalMouseUp = function() {
|
||||
// Re-run translation if text changed
|
||||
};
|
||||
|
||||
// Button handlers
|
||||
window.Asc.plugin.button = function(id) {
|
||||
this.executeCommand("close", "");
|
||||
};
|
||||
|
||||
// Theme change handler
|
||||
window.Asc.plugin.onThemeChanged = function(theme) {
|
||||
window.Asc.plugin.onThemeChangedBase(theme);
|
||||
|
||||
// Update arrow color based on theme
|
||||
const arrowPath = document.getElementById("arrow-svg-path");
|
||||
if (arrowPath) {
|
||||
const isDark = theme && theme.type === "dark";
|
||||
arrowPath.setAttribute("fill", isDark ? "#ffffff" : "#444444");
|
||||
}
|
||||
|
||||
// Update manual entry link colors
|
||||
const showManually = document.getElementById("show_manually");
|
||||
const hideManually = document.getElementById("hide_manually");
|
||||
const isDark = theme && theme.type === "dark";
|
||||
const borderColor = isDark ? "#ffffff" : "#444444";
|
||||
|
||||
if (showManually) {
|
||||
showManually.style.borderBottomColor = borderColor;
|
||||
}
|
||||
if (hideManually) {
|
||||
hideManually.style.borderBottomColor = borderColor;
|
||||
}
|
||||
|
||||
// Update status bar colors for dark theme
|
||||
const statusBar = document.getElementById("status_bar");
|
||||
if (statusBar && isDark) {
|
||||
if (statusBar.classList.contains("info")) {
|
||||
statusBar.style.backgroundColor = "#1e3a5f";
|
||||
statusBar.style.color = "#90caf9";
|
||||
} else if (statusBar.classList.contains("success")) {
|
||||
statusBar.style.backgroundColor = "#1b5e20";
|
||||
statusBar.style.color = "#a5d6a7";
|
||||
} else if (statusBar.classList.contains("warning")) {
|
||||
statusBar.style.backgroundColor = "#e65100";
|
||||
statusBar.style.color = "#ffcc80";
|
||||
} else if (statusBar.classList.contains("error")) {
|
||||
statusBar.style.backgroundColor = "#b71c1c";
|
||||
statusBar.style.color = "#ef9a9a";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Translation handler for UI strings
|
||||
window.Asc.plugin.onTranslate = function() {
|
||||
const elements = document.querySelectorAll(".i18n");
|
||||
elements.forEach(el => {
|
||||
const text = el.textContent.trim();
|
||||
if (text) {
|
||||
const translated = getMessage(text);
|
||||
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
||||
el.placeholder = translated;
|
||||
} else {
|
||||
el.textContent = translated;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// DOM Ready handler
|
||||
$(document).ready(function() {
|
||||
// Initialize manual text entry field
|
||||
const enterContainer = document.getElementById("enter_container");
|
||||
if (enterContainer) {
|
||||
enterContainer.value = "";
|
||||
}
|
||||
|
||||
// Manual text entry toggle
|
||||
const showManually = document.getElementById("show_manually");
|
||||
const hideManually = document.getElementById("hide_manually");
|
||||
|
||||
if (showManually) {
|
||||
showManually.addEventListener("click", function() {
|
||||
if (enterContainer) {
|
||||
enterContainer.style.display = "block";
|
||||
}
|
||||
showManually.style.display = "none";
|
||||
if (hideManually) {
|
||||
hideManually.style.display = "inline";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hideManually) {
|
||||
hideManually.addEventListener("click", function() {
|
||||
if (enterContainer) {
|
||||
enterContainer.style.display = "none";
|
||||
}
|
||||
hideManually.style.display = "none";
|
||||
if (showManually) {
|
||||
showManually.style.display = "inline";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Manual text entry handler with debounce
|
||||
if (enterContainer) {
|
||||
const debouncedTranslate = debounce(function() {
|
||||
txt = enterContainer.value;
|
||||
RunTranslate(txt);
|
||||
}, 500);
|
||||
|
||||
enterContainer.addEventListener("input", debouncedTranslate);
|
||||
}
|
||||
|
||||
// Copy button handler
|
||||
const copyBtn = document.getElementById("copy");
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener("click", function() {
|
||||
if (translatedText) {
|
||||
copyToClipboard(translatedText);
|
||||
updateStatus(getMessage("Copied to clipboard"), "success");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Paste/Insert button handler
|
||||
const pasteBtn = document.getElementById("paste");
|
||||
if (pasteBtn) {
|
||||
pasteBtn.addEventListener("click", function() {
|
||||
if (!paste_done)
|
||||
return;
|
||||
|
||||
if (!translatedText)
|
||||
return;
|
||||
|
||||
paste_done = false;
|
||||
|
||||
// Store translated text in Asc.scope so it's accessible in callCommand context
|
||||
Asc.scope.translatedText = translatedText;
|
||||
window.Asc.plugin.info.recalculate = true;
|
||||
|
||||
window.Asc.plugin.executeMethod("GetVersion", [], function(version) {
|
||||
if (version === undefined) {
|
||||
window.Asc.plugin.executeMethod("PasteText", [$("#txt_shower")[0].innerText], function(result) {
|
||||
paste_done = true;
|
||||
});
|
||||
} else {
|
||||
window.Asc.plugin.executeMethod("GetSelectionType", [], function(selectionType) {
|
||||
switch (selectionType) {
|
||||
case "none":
|
||||
case "drawing":
|
||||
window.Asc.plugin.executeMethod("PasteText", [$("#txt_shower")[0].innerText], function(result) {
|
||||
paste_done = true;
|
||||
});
|
||||
break;
|
||||
case "text":
|
||||
window.Asc.plugin.callCommand(function() {
|
||||
Api.ReplaceTextSmart([Asc.scope.translatedText]);
|
||||
}, undefined, undefined, function(result) {
|
||||
paste_done = true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize translator
|
||||
initializeTranslator();
|
||||
});
|
||||
|
||||
})(window);
|
||||
18
sdkjs-plugins/content/bergamot/translations/cs-CS.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Zadat text ručně",
|
||||
"Hide field": "Skrýt pole",
|
||||
"Translating...": "Překládám...",
|
||||
"Loading translation engine...": "Načítání překladače...",
|
||||
"Downloading model...": "Stahování modelu...",
|
||||
"Model loaded - Ready for offline translation": "Model načten - Připraven k offline překladu",
|
||||
"Failed to load model: ": "Nepodařilo se načíst model: ",
|
||||
"Ready - Select languages to translate": "Připraven - Vyberte jazyky k překladu",
|
||||
"Failed to initialize: ": "Nepodařilo se inicializovat: ",
|
||||
"Translation complete": "Překlad dokončen",
|
||||
"Translation failed: ": "Překlad selhal: ",
|
||||
"Translation failed": "Překlad selhal",
|
||||
"Copied to clipboard": "Zkopírováno do schránky",
|
||||
"Copy": "Kopírovat",
|
||||
"Insert to document": "Vložit do dokumentu",
|
||||
"Offline": "Offline"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/de-DE.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Text manuell eingeben",
|
||||
"Hide field": "Feld ausblenden",
|
||||
"Translating...": "Übersetzen...",
|
||||
"Loading translation engine...": "Übersetzungsmodul wird geladen...",
|
||||
"Downloading model...": "Modell wird heruntergeladen...",
|
||||
"Model loaded - Ready for offline translation": "Modell geladen - Bereit für Offline-Übersetzung",
|
||||
"Failed to load model: ": "Fehler beim Laden des Modells: ",
|
||||
"Ready - Select languages to translate": "Bereit - Sprachen zum Übersetzen auswählen",
|
||||
"Failed to initialize: ": "Initialisierung fehlgeschlagen: ",
|
||||
"Translation complete": "Übersetzung abgeschlossen",
|
||||
"Translation failed: ": "Übersetzung fehlgeschlagen: ",
|
||||
"Translation failed": "Übersetzung fehlgeschlagen",
|
||||
"Copied to clipboard": "In Zwischenablage kopiert",
|
||||
"Copy": "Kopieren",
|
||||
"Insert to document": "In Dokument einfügen",
|
||||
"Offline": "Offline"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/en-US.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Enter text manually",
|
||||
"Hide field": "Hide field",
|
||||
"Translating...": "Translating...",
|
||||
"Loading translation engine...": "Loading translation engine...",
|
||||
"Downloading model...": "Downloading model...",
|
||||
"Model loaded - Ready for offline translation": "Model loaded - Ready for offline translation",
|
||||
"Failed to load model: ": "Failed to load model: ",
|
||||
"Ready - Select languages to translate": "Ready - Select languages to translate",
|
||||
"Failed to initialize: ": "Failed to initialize: ",
|
||||
"Translation complete": "Translation complete",
|
||||
"Translation failed: ": "Translation failed: ",
|
||||
"Translation failed": "Translation failed",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy": "Copy",
|
||||
"Insert to document": "Insert to document",
|
||||
"Offline": "Offline"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/es-ES.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Introducir texto manualmente",
|
||||
"Hide field": "Ocultar campo",
|
||||
"Translating...": "Traduciendo...",
|
||||
"Loading translation engine...": "Cargando motor de traducción...",
|
||||
"Downloading model...": "Descargando modelo...",
|
||||
"Model loaded - Ready for offline translation": "Modelo cargado - Listo para traducción sin conexión",
|
||||
"Failed to load model: ": "Error al cargar el modelo: ",
|
||||
"Ready - Select languages to translate": "Listo - Seleccione idiomas para traducir",
|
||||
"Failed to initialize: ": "Error de inicialización: ",
|
||||
"Translation complete": "Traducción completada",
|
||||
"Translation failed: ": "Error de traducción: ",
|
||||
"Translation failed": "Error de traducción",
|
||||
"Copied to clipboard": "Copiado al portapapeles",
|
||||
"Copy": "Copiar",
|
||||
"Insert to document": "Insertar en documento",
|
||||
"Offline": "Sin conexión"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/fr-FR.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Saisir le texte manuellement",
|
||||
"Hide field": "Masquer le champ",
|
||||
"Translating...": "Traduction en cours...",
|
||||
"Loading translation engine...": "Chargement du moteur de traduction...",
|
||||
"Downloading model...": "Téléchargement du modèle...",
|
||||
"Model loaded - Ready for offline translation": "Modèle chargé - Prêt pour la traduction hors ligne",
|
||||
"Failed to load model: ": "Échec du chargement du modèle : ",
|
||||
"Ready - Select languages to translate": "Prêt - Sélectionnez les langues à traduire",
|
||||
"Failed to initialize: ": "Échec de l'initialisation : ",
|
||||
"Translation complete": "Traduction terminée",
|
||||
"Translation failed: ": "Échec de la traduction : ",
|
||||
"Translation failed": "Échec de la traduction",
|
||||
"Copied to clipboard": "Copié dans le presse-papiers",
|
||||
"Copy": "Copier",
|
||||
"Insert to document": "Insérer dans le document",
|
||||
"Offline": "Hors ligne"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/it-IT.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Inserisci testo manualmente",
|
||||
"Hide field": "Nascondi campo",
|
||||
"Translating...": "Traduzione in corso...",
|
||||
"Loading translation engine...": "Caricamento motore di traduzione...",
|
||||
"Downloading model...": "Download del modello...",
|
||||
"Model loaded - Ready for offline translation": "Modello caricato - Pronto per la traduzione offline",
|
||||
"Failed to load model: ": "Errore nel caricamento del modello: ",
|
||||
"Ready - Select languages to translate": "Pronto - Seleziona le lingue da tradurre",
|
||||
"Failed to initialize: ": "Errore di inizializzazione: ",
|
||||
"Translation complete": "Traduzione completata",
|
||||
"Translation failed: ": "Traduzione fallita: ",
|
||||
"Translation failed": "Traduzione fallita",
|
||||
"Copied to clipboard": "Copiato negli appunti",
|
||||
"Copy": "Copia",
|
||||
"Insert to document": "Inserisci nel documento",
|
||||
"Offline": "Offline"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/ja-JA.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "手動でテキストを入力",
|
||||
"Hide field": "フィールドを非表示",
|
||||
"Translating...": "翻訳中...",
|
||||
"Loading translation engine...": "翻訳エンジンを読み込み中...",
|
||||
"Downloading model...": "モデルをダウンロード中...",
|
||||
"Model loaded - Ready for offline translation": "モデル読み込み完了 - オフライン翻訳準備完了",
|
||||
"Failed to load model: ": "モデルの読み込みに失敗しました:",
|
||||
"Ready - Select languages to translate": "準備完了 - 翻訳する言語を選択してください",
|
||||
"Failed to initialize: ": "初期化に失敗しました:",
|
||||
"Translation complete": "翻訳完了",
|
||||
"Translation failed: ": "翻訳に失敗しました:",
|
||||
"Translation failed": "翻訳に失敗しました",
|
||||
"Copied to clipboard": "クリップボードにコピーしました",
|
||||
"Copy": "コピー",
|
||||
"Insert to document": "ドキュメントに挿入",
|
||||
"Offline": "オフライン"
|
||||
}
|
||||
13
sdkjs-plugins/content/bergamot/translations/langs.json
Normal file
@ -0,0 +1,13 @@
|
||||
[
|
||||
"cs-CS",
|
||||
"de-DE",
|
||||
"es-ES",
|
||||
"fr-FR",
|
||||
"it-IT",
|
||||
"ja-JA",
|
||||
"nl-NL",
|
||||
"pt-PT",
|
||||
"ru-RU",
|
||||
"pt-BR",
|
||||
"zh-ZH"
|
||||
]
|
||||
18
sdkjs-plugins/content/bergamot/translations/nl-NL.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Tekst handmatig invoeren",
|
||||
"Hide field": "Veld verbergen",
|
||||
"Translating...": "Vertalen...",
|
||||
"Loading translation engine...": "Vertaalengine laden...",
|
||||
"Downloading model...": "Model downloaden...",
|
||||
"Model loaded - Ready for offline translation": "Model geladen - Klaar voor offline vertaling",
|
||||
"Failed to load model: ": "Model laden mislukt: ",
|
||||
"Ready - Select languages to translate": "Gereed - Selecteer talen om te vertalen",
|
||||
"Failed to initialize: ": "Initialisatie mislukt: ",
|
||||
"Translation complete": "Vertaling voltooid",
|
||||
"Translation failed: ": "Vertaling mislukt: ",
|
||||
"Translation failed": "Vertaling mislukt",
|
||||
"Copied to clipboard": "Gekopieerd naar klembord",
|
||||
"Copy": "Kopiëren",
|
||||
"Insert to document": "Invoegen in document",
|
||||
"Offline": "Offline"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/pt-BR.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Inserir texto manualmente",
|
||||
"Hide field": "Ocultar campo",
|
||||
"Translating...": "Traduzindo...",
|
||||
"Loading translation engine...": "Carregando motor de tradução...",
|
||||
"Downloading model...": "Baixando modelo...",
|
||||
"Model loaded - Ready for offline translation": "Modelo carregado - Pronto para tradução offline",
|
||||
"Failed to load model: ": "Erro ao carregar o modelo: ",
|
||||
"Ready - Select languages to translate": "Pronto - Selecione idiomas para traduzir",
|
||||
"Failed to initialize: ": "Erro de inicialização: ",
|
||||
"Translation complete": "Tradução concluída",
|
||||
"Translation failed: ": "Erro na tradução: ",
|
||||
"Translation failed": "Erro na tradução",
|
||||
"Copied to clipboard": "Copiado para a área de transferência",
|
||||
"Copy": "Copiar",
|
||||
"Insert to document": "Inserir no documento",
|
||||
"Offline": "Offline"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/pt-PT.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Introduzir texto manualmente",
|
||||
"Hide field": "Ocultar campo",
|
||||
"Translating...": "A traduzir...",
|
||||
"Loading translation engine...": "A carregar motor de tradução...",
|
||||
"Downloading model...": "A transferir modelo...",
|
||||
"Model loaded - Ready for offline translation": "Modelo carregado - Pronto para tradução offline",
|
||||
"Failed to load model: ": "Erro ao carregar o modelo: ",
|
||||
"Ready - Select languages to translate": "Pronto - Selecione idiomas para traduzir",
|
||||
"Failed to initialize: ": "Erro de inicialização: ",
|
||||
"Translation complete": "Tradução concluída",
|
||||
"Translation failed: ": "Erro na tradução: ",
|
||||
"Translation failed": "Erro na tradução",
|
||||
"Copied to clipboard": "Copiado para a área de transferência",
|
||||
"Copy": "Copiar",
|
||||
"Insert to document": "Inserir no documento",
|
||||
"Offline": "Offline"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/ru-RU.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "Ввести текст вручную",
|
||||
"Hide field": "Скрыть поле для ввода",
|
||||
"Translating...": "Перевод...",
|
||||
"Loading translation engine...": "Загрузка движка перевода...",
|
||||
"Downloading model...": "Загрузка модели...",
|
||||
"Model loaded - Ready for offline translation": "Модель загружена - Готов к офлайн переводу",
|
||||
"Failed to load model: ": "Ошибка загрузки модели: ",
|
||||
"Ready - Select languages to translate": "Готов - Выберите языки для перевода",
|
||||
"Failed to initialize: ": "Ошибка инициализации: ",
|
||||
"Translation complete": "Перевод завершён",
|
||||
"Translation failed: ": "Ошибка перевода: ",
|
||||
"Translation failed": "Ошибка перевода",
|
||||
"Copied to clipboard": "Скопировано в буфер обмена",
|
||||
"Copy": "Копировать",
|
||||
"Insert to document": "Вставить в документ",
|
||||
"Offline": "Офлайн"
|
||||
}
|
||||
18
sdkjs-plugins/content/bergamot/translations/zh-ZH.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Enter text manually": "手动输入文本",
|
||||
"Hide field": "隐藏输入框",
|
||||
"Translating...": "翻译中...",
|
||||
"Loading translation engine...": "加载翻译引擎...",
|
||||
"Downloading model...": "下载模型...",
|
||||
"Model loaded - Ready for offline translation": "模型已加载 - 可进行离线翻译",
|
||||
"Failed to load model: ": "模型加载失败:",
|
||||
"Ready - Select languages to translate": "就绪 - 选择要翻译的语言",
|
||||
"Failed to initialize: ": "初始化失败:",
|
||||
"Translation complete": "翻译完成",
|
||||
"Translation failed: ": "翻译失败:",
|
||||
"Translation failed": "翻译失败",
|
||||
"Copied to clipboard": "已复制到剪贴板",
|
||||
"Copy": "复制",
|
||||
"Insert to document": "插入到文档",
|
||||
"Offline": "离线"
|
||||
}
|
||||
238
sdkjs-plugins/content/bergamot/vendor/bergamot/README.md
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
# Installation
|
||||
|
||||
```bash
|
||||
npm install @mkljczk/bergamot-translator
|
||||
```
|
||||
|
||||
# Quick start
|
||||
|
||||
```js
|
||||
import { BatchTranslator } from "@mkljczk/bergamot-translator/translator.js";
|
||||
|
||||
const translator = new BatchTranslator();
|
||||
|
||||
const response = await translator.translate({
|
||||
from: "en",
|
||||
to: "es",
|
||||
text: "Hello <em>world</em>!",
|
||||
html: true,
|
||||
});
|
||||
|
||||
console.log(response.target.text);
|
||||
|
||||
// Stops worker threads
|
||||
translator.delete();
|
||||
```
|
||||
|
||||
# Throughput vs Latency
|
||||
|
||||
This package comes with two translator implementations:
|
||||
|
||||
- [LatencyOptimisedTranslator](#latencyoptimisedtranslator) is more useful for an interactive session, say like Google Translate, where you're only working on translating one input at a time.
|
||||
- [BatchTranslator](#batchtranslator) is optimised for processing a large number of translations as fast as possible (but individual translations might take some time), e.g. translating a large number of strings or all paragraphs in a document.
|
||||
|
||||
## LantencyOptimisedTranslator
|
||||
|
||||
Translator best suited for interactive usage. Runs with a single worker thread and a batch-size of 1 to give you a response as quickly as possible. It will cancel any pending translations that aren't currently being processed if you submit a new one.
|
||||
|
||||
```js
|
||||
const translator = new LatencyOptimisedTranslator({
|
||||
pivotLanguage?: string?,
|
||||
registryUrl?: string,
|
||||
workerUrl?: string,
|
||||
downloadTimeout?: number,
|
||||
cacheSize?: number,
|
||||
useNativeIntGemm?: boolean,
|
||||
})
|
||||
```
|
||||
|
||||
- `pivotLanguage` - language code for the language to use as an intermediate if there is no direct translation model available. Defaults to `"en"`. Set to `null` to disable pivoting.
|
||||
- `registryUrl` - url to a list of models and their paths. Defaults to `https://storage.googleapis.com/bergamot-models-sandbox/0.3.3/registry.json`.
|
||||
- `workerUrl` - url to `translator-worker.js`. Defaults to `"worker/translator-worker.js"` relative to the path of `translator.js`.
|
||||
- `downloadTimeout` - Maximum time we're attempting to download model files before failing. Defaults to `60000` or 60 seconds. Set to `0` to disable.
|
||||
- `cacheSize` - Maximum number of sentences in kept translation cache (per worker, workers do not share their cache). This is an ideal maximum as it is a hash-map, in practice about 1/3th is occupied. If set to `0`, translation cache is disabled (the default).
|
||||
- `useNativeIntGemm` - Try to link to native IntGEMM implementation when loading the WASM binary. This is only implemented in the privileged extension context of Firefox Nightly. If it fails, it will always fall back to the included implementation. Defaults to `false`.
|
||||
|
||||
### translate()
|
||||
|
||||
```js
|
||||
const {request, target: {text:string}} = await translator.translate({
|
||||
from: string,
|
||||
to: string,
|
||||
text: string,
|
||||
html?: boolean,
|
||||
qualityScores?: boolean
|
||||
})
|
||||
```
|
||||
|
||||
Submits a translation request. Multiple of these are processed in a batch. A batch will be started the next tick (if there is a worker available).
|
||||
|
||||
- `from` - language code of the source language, e.g. `"de"`
|
||||
- `to` - language code of the target language, e.g. `"en"`
|
||||
- `text` - string of text to translate, e.g. `"Hallo Welt!"`
|
||||
- `html` - boolean indicating whether `text` contains just plain text or HTML
|
||||
- `qualityScores` - whether to calculate quality scores. Not all models support this, and you need to load a separate quality scores model file for it. Quality scores are returned as `<font x-bergamot-sentence-quality="">` and `<font x-bergamot-word-quality="">` wrapped around sentences and words in the output. When enabled, the output is always HTML, regardless of whether the input was.
|
||||
|
||||
Returns:
|
||||
|
||||
A promise to a translation response object, with `target.text` being the text or HTML of the translated output, and `request` a reference to the original translation request.
|
||||
|
||||
### delete()
|
||||
|
||||
```js
|
||||
translator.delete()
|
||||
```
|
||||
|
||||
Cancels all pending requests with a `CancelledError` and terminates the worker immediately. This will free all the resources used.
|
||||
|
||||
In a nodejs context you'll need to call this, otherwise your script won't exit because the translator will still be listening for messages from the worker.
|
||||
|
||||
## BatchTranslator
|
||||
|
||||
```js
|
||||
const translator = new BatchTranslator({
|
||||
pivotLanguage?: string?,
|
||||
registryUrl?: string,
|
||||
workerUrl?: string,
|
||||
downloadTimeout?: number,
|
||||
cacheSize?: number,
|
||||
useNativeIntGemm?: boolean,
|
||||
workers?: number,
|
||||
batchSize?: number,
|
||||
})
|
||||
```
|
||||
|
||||
General translator options:
|
||||
|
||||
See [LatencyOptimisedTranslator](#latencyoptimisedtranslator).
|
||||
|
||||
BatchTranslator-specific options:
|
||||
|
||||
- `workers` - Number of worker threads. These are full-on instances of the translator, with their own copy of the model loaded. This is an upper bound. If not that many workers can be fed, it won't create new ones. Minimally 1. Default is `1`.
|
||||
- `batchSize` - Number of translation requests per batch. All sentences from all translation requests are packed into a bunch of matrix operations. With a larger batch size the translator has more material to find ideal sets of sentences for filling the matrix. However, you'll only get the results for each of the requests in a batch once the whole batch is finished. Defaults to 8.
|
||||
|
||||
### translate()
|
||||
|
||||
```js
|
||||
const {target: {text:string}} = await translator.translate({
|
||||
from: string,
|
||||
to: string,
|
||||
text: string,
|
||||
html?: boolean,
|
||||
qualityScores?: boolean,
|
||||
priority?: number
|
||||
})
|
||||
```
|
||||
|
||||
Submits a translation request. Multiple of these are processed in a batch. A batch will be started the next tick (if there is a worker available).
|
||||
|
||||
- (See [LatencyOptimisedTranslator.translate()](#translate) for most options)
|
||||
- `priority` - When grouping translation requests into batches to give to workers, requests with a lower number are considered first. For example, if you're translating a web page, you can give requests of parts that are in the current frame a lower number to make sure they're processed first.
|
||||
|
||||
### remove()
|
||||
|
||||
```js
|
||||
translator.remove(request => {
|
||||
// true deletes the request from the queue.
|
||||
return true;
|
||||
})
|
||||
```
|
||||
|
||||
Removes requests from the translation queue, i.e. only when they haven't been sent to a worker yet.
|
||||
|
||||
The filter function should return true-ish for each request that should be cancelled. Their promises are rejected with a `CancelledError` error.
|
||||
|
||||
|
||||
### delete()
|
||||
|
||||
```js
|
||||
translator.delete()
|
||||
```
|
||||
|
||||
Cancels all pending requests with a `CancelledError` and terminates all workers immediately. This will free all the resources used.
|
||||
|
||||
|
||||
# Models
|
||||
|
||||
Both translators accept a `backing` option, which tells it where to get model data and the translation engine implementation from. They default to using `BergamotTranslator` which gets its models from the same repository as [firefox-translations](https://github.com/mozilla/firefox-translations).
|
||||
|
||||
To customize the model, reimplement the `loadModelRegistry` and `loadTranslationModel` methods.
|
||||
|
||||
`loadModelRegistry()` has the hard requirement to return a promise to a list that looks like `{from: string, to: string, ...}[]`. The `from` and `to` keys are used as key for model selection.
|
||||
|
||||
`loadTranslationModel()` should return a promise with ArrayBuffers for `model`, `shortlist`, `vocabs`, and optionally `qualityModel`. It can include a `config` object as well.
|
||||
|
||||
Example of an alternative implementation that loads models from data.statmt.org, i.e. the same as [translateLocally](https://translateLocally.com):
|
||||
|
||||
```js
|
||||
class CustomBacking extends TranslatorBacking {
|
||||
async loadModelRegistery() {
|
||||
const response = await fetch('https://translatelocally.com/models.json');
|
||||
const {models} = await response.json();
|
||||
|
||||
// Add 'from' and 'to' keys for each model. Since theoretically a model
|
||||
// can have multiple froms keys in TranslateLocally, we do a little
|
||||
// product here.
|
||||
return models.reduce((list, model) => {
|
||||
try {
|
||||
const to = first(Intl.getCanonicalLocales(model.trgTag));
|
||||
for (let from of Intl.getCanonicalLocales(Object.keys(model.srcTags))) {
|
||||
list.push({from, to, model});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Skipped model', model, 'because', err);
|
||||
}
|
||||
|
||||
return list;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async loadTranslationModel({from, to}) {
|
||||
// Find that model in the registry which will tell us about its files
|
||||
const entries = (await this.registry).filter(model => model.from === from && model.to === to);
|
||||
|
||||
// Prefer tiny models above non-tiny ones
|
||||
entries.sort(({model: a}, {model: b}) => (a.shortName.indexOf('tiny') === -1 ? 1 : 0) - (b.shortName.indexOf('tiny') === -1 ? 1 : 0));
|
||||
|
||||
if (!entries)
|
||||
throw new Error(`No model for '${from}' -> '${to}'`);
|
||||
|
||||
const entry = entries[0].model;
|
||||
|
||||
const response = await fetch(entry.url, {
|
||||
integrity: `sha256-${entry.checksum}`
|
||||
});
|
||||
|
||||
// pako from https://www.npmjs.com/package/pako
|
||||
const archive = pako.inflate(await response.arrayBuffer());
|
||||
|
||||
// untar from https://www.npmjs.com/package/js-untar
|
||||
const files = await untar(archive.buffer);
|
||||
|
||||
const find = (filename) => {
|
||||
const found = files.find(file => file.name.match(/(?:^|\/)([^\/]+)$/)[1] === filename)
|
||||
if (found === undefined)
|
||||
throw new Error(`Could not find '${filename}' in model archive`);
|
||||
return found;
|
||||
};
|
||||
|
||||
// YAML.parse is found in worker/translator-worker.js
|
||||
const config = YAML.parse(find('config.intgemm8bitalpha.yml').readAsString());
|
||||
|
||||
const model = find(config.models[0]).buffer;
|
||||
|
||||
const vocabs = config.vocabs.map(vocab => find(vocab).buffer);
|
||||
|
||||
const shortlist = find(config.shortlist[0]).buffer;
|
||||
|
||||
// Return the buffers
|
||||
return {model, vocabs, shortlist, config};
|
||||
}
|
||||
}
|
||||
|
||||
const translator = new BatchTranslator(options, new CustomBacking(options));
|
||||
```
|
||||
|
||||
# Supported languages
|
||||
|
||||
See https://github.com/mozilla/firefox-translations-models#currently-supported-languages. You may need to set the `registryUrl` option to point to the latest release.
|
||||
21
sdkjs-plugins/content/bergamot/vendor/bergamot/main.js
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
import { stdin, stdout } from "node:process";
|
||||
import * as readline from "node:readline/promises";
|
||||
import { BatchTranslator } from "./translator.js";
|
||||
|
||||
const rl = readline.createInterface({ input: stdin, output: stdout });
|
||||
|
||||
const translator = new BatchTranslator();
|
||||
|
||||
for await (const line of rl) {
|
||||
const response = await translator.translate({
|
||||
from: "en",
|
||||
to: "es",
|
||||
text: line,
|
||||
html: false,
|
||||
qualityScores: false,
|
||||
});
|
||||
|
||||
console.log(response.target.text);
|
||||
}
|
||||
|
||||
translator.delete();
|
||||
45
sdkjs-plugins/content/bergamot/vendor/bergamot/package.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mkljczk/bergamot-translator",
|
||||
"version": "0.4.16",
|
||||
"description": "Cross platform C++ library focusing on optimized machine translation on the consumer-grade device.",
|
||||
"homepage": "https://codeberg.org/mkljczk/bergamot-translator#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@codeberg.org/mkljczk/bergamot-translator.git"
|
||||
},
|
||||
"keywords": [
|
||||
"machine",
|
||||
"translation"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MPL-2.0",
|
||||
"bugs": {
|
||||
"url": "https://codeberg.org/mkljczk/bergamot-translator/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "translator.js",
|
||||
"scripts": {
|
||||
"prepare": "test -f worker/bergamot-translator-worker.wasm || npm run build",
|
||||
"build": "tsc && mkdir -p ../../build-wasm && docker run --rm -v $(realpath ../../):/src -v $(realpath ../../build-wasm):/build -v $(pwd)/worker:/dst -w /build emscripten/emsdk:$npm_package_config_emscripten_version sh -c \"emcmake cmake -DCOMPILE_WASM=on -DWORMHOLE=off /src && emmake make -j2 && cp bergamot-translator-worker.wasm bergamot-translator-worker.js /dst\"",
|
||||
"test": "echo \"Hello world!\" | node main.js"
|
||||
},
|
||||
"files": [
|
||||
"worker/bergamot-translator-worker.js",
|
||||
"worker/bergamot-translator-worker.wasm",
|
||||
"worker/translator-worker.js",
|
||||
"translator.d.ts",
|
||||
"translator.js",
|
||||
"main.js"
|
||||
],
|
||||
"types": "translator.d.ts",
|
||||
"config": {
|
||||
"emscripten_version": "3.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oneidentity/zstd-js": "^1.0.3",
|
||||
"@types/node": "^25.0.1"
|
||||
}
|
||||
}
|
||||
301
sdkjs-plugins/content/bergamot/vendor/bergamot/translator.d.ts
vendored
Normal file
@ -0,0 +1,301 @@
|
||||
interface TranslationRequest {
|
||||
from: string;
|
||||
to: string;
|
||||
text: string;
|
||||
html: boolean;
|
||||
priority?: number;
|
||||
qualityScores?: boolean;
|
||||
}
|
||||
interface TranslationRequestOptions {
|
||||
signal?: AbortSignal;
|
||||
onDownloadProgress?: (current: number, total: number) => void;
|
||||
}
|
||||
interface TranslationResponse {
|
||||
request: TranslationRequest;
|
||||
target: {
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
interface TranslationModel {
|
||||
model: ArrayBuffer;
|
||||
shortlist: ArrayBuffer;
|
||||
vocabs: ArrayBuffer[];
|
||||
qualityModel?: ArrayBuffer;
|
||||
config?: Record<string, any>;
|
||||
}
|
||||
interface TranslatorBackingOptions {
|
||||
cacheSize?: number;
|
||||
useNativeIntGemm?: boolean;
|
||||
downloadTimeout?: number;
|
||||
workerUrl?: string;
|
||||
registryUrl?: string;
|
||||
pivotLanguage?: string;
|
||||
onerror?: (error: Error) => void;
|
||||
}
|
||||
interface Model {
|
||||
from: string;
|
||||
to: string;
|
||||
files: Record<string, {
|
||||
name: string;
|
||||
size: number;
|
||||
expectedSha256Hash: string;
|
||||
}>;
|
||||
}
|
||||
/**
|
||||
* Thrown when a pending translation is replaced by another newer pending
|
||||
* translation.
|
||||
*/
|
||||
export declare class SupersededError extends Error {
|
||||
}
|
||||
/**
|
||||
* Thrown when a translation was removed from the queue.
|
||||
*/
|
||||
export declare class CancelledError extends Error {
|
||||
}
|
||||
/**
|
||||
* Wrapper around bergamot-translator loading and model management.
|
||||
*/
|
||||
export declare class TranslatorBacking {
|
||||
options: TranslatorBackingOptions;
|
||||
registryUrl: string;
|
||||
workerUrl: string;
|
||||
downloadTimeout: number;
|
||||
/**
|
||||
* registry of all available models and their urls
|
||||
*/
|
||||
registry: Promise<Model[]>;
|
||||
/**
|
||||
* Map of downloaded model data files as buffers per model.
|
||||
*/
|
||||
buffers: Map<string, Promise<TranslationModel>>;
|
||||
pivotLanguage: string;
|
||||
/**
|
||||
* A map of language-pairs to a list of models you need for it.
|
||||
*/
|
||||
models: Map<string, Promise<Omit<Model, "files">[]>>;
|
||||
/**
|
||||
* Map of model download state.
|
||||
*/
|
||||
state: Map<string, "unavailable" | "downloadable" | "downloading" | "available">;
|
||||
/**
|
||||
* Error handler for all errors that are async, not tied to a specific
|
||||
* call and that are unrecoverable.
|
||||
*/
|
||||
onerror: (error: Error) => void;
|
||||
constructor(options: TranslatorBackingOptions);
|
||||
/**
|
||||
* Loads a worker thread, and wraps it in a message passing proxy. I.e. it
|
||||
* exposes the entire interface of TranslationWorker here, and all calls
|
||||
* to it are async. Do note that you can only pass arguments that survive
|
||||
* being copied into a message.
|
||||
* @return {Promise<{worker:Worker, exports:Proxy<TranslationWorker>}>}
|
||||
*/
|
||||
loadWorker(): Promise<{
|
||||
worker: Worker;
|
||||
exports: {};
|
||||
}>;
|
||||
/**
|
||||
* Loads the model registry. Uses the registry shipped with this extension,
|
||||
* but formatted a bit easier to use, and future-proofed to be swapped out
|
||||
* with a TranslateLocally type registry.
|
||||
*/
|
||||
loadModelRegistry(): Promise<Model[]>;
|
||||
/**
|
||||
* Gets or loads translation model data. Caching wrapper around
|
||||
* `loadTranslationModel()`.
|
||||
*/
|
||||
getTranslationModel({ from, to }: Omit<Model, "files">, options?: {
|
||||
signal?: AbortSignal;
|
||||
}): Promise<TranslationModel>;
|
||||
/**
|
||||
* Downloads a translation model and returns a set of
|
||||
* ArrayBuffers. These can then be passed to a TranslationWorker thread
|
||||
* to instantiate a TranslationModel inside the WASM vm.
|
||||
*/
|
||||
loadTranslationModel({ from, to }: Omit<Model, "files">, options?: {
|
||||
signal?: AbortSignal;
|
||||
}): Promise<TranslationModel>;
|
||||
/**
|
||||
* Helper to download file from the web. Verifies the checksum.
|
||||
*/
|
||||
fetch(url: string, checksum: string | undefined, extra: {
|
||||
signal?: AbortSignal;
|
||||
} | undefined): Promise<ArrayBuffer>;
|
||||
/**
|
||||
* Converts the hexadecimal hashes from the registry to something we can use with
|
||||
* the fetch() method.
|
||||
*/
|
||||
hexToBase64(hexstring: any): string;
|
||||
/**
|
||||
* Crappy named method that gives you a list of models to translate from
|
||||
* one language into the other. Generally this will be the same as you
|
||||
* just put in if there is a direct model, but it could return a list of
|
||||
* two models if you need to pivot through a third language.
|
||||
* Returns just [{from:str,to:str}...]. To be used something like this:
|
||||
* ```
|
||||
* const models = await this.getModels(from, to);
|
||||
* models.forEach(({from, to}) => {
|
||||
* const buffers = await this.loadTranslationModel({from,to});
|
||||
* [TranslationWorker].loadTranslationModel({from,to}, buffers)
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getModels({ from, to }: {
|
||||
from: any;
|
||||
to: any;
|
||||
}): Promise<Omit<Model, "files">[]>;
|
||||
/**
|
||||
* Find model (or model pair) to translate from `from` to `to`.
|
||||
*/
|
||||
findModels(from: string, to: string): Promise<Omit<Model, "files">[]>;
|
||||
/**
|
||||
* Checks whether a model is available or being downloaded.
|
||||
*/
|
||||
getState({ from, to, }: Omit<Model, "files">): Promise<"unavailable" | "downloadable" | "downloading" | "available">;
|
||||
}
|
||||
interface BatchTranslatorOptions extends TranslatorBackingOptions {
|
||||
workers?: number;
|
||||
batchSize?: number;
|
||||
}
|
||||
interface Batch {
|
||||
id: number;
|
||||
key: string;
|
||||
priority: number;
|
||||
models: Omit<Model, "files">[];
|
||||
requests: Array<{
|
||||
request: TranslationRequest;
|
||||
resolve: (response: TranslationResponse) => void;
|
||||
reject: (error: Error) => void;
|
||||
}>;
|
||||
}
|
||||
/**
|
||||
* Translator balancing between throughput and latency. Can use multiple worker
|
||||
* threads.
|
||||
*/
|
||||
export declare class BatchTranslator {
|
||||
backing: TranslatorBacking;
|
||||
/**
|
||||
* List of active workers
|
||||
* (and a flag to mark them idle or not)
|
||||
*/
|
||||
workers: Array<{
|
||||
idle: boolean;
|
||||
worker?: Worker;
|
||||
exports?: any;
|
||||
}>;
|
||||
/**
|
||||
* Maximum number of workers
|
||||
*/
|
||||
workerLimit: number;
|
||||
/**
|
||||
* List of batches we push() to & shift() from using `enqueue`.
|
||||
*/
|
||||
queue: Array<Batch>;
|
||||
/**
|
||||
* batch serial to help keep track of batches when debugging
|
||||
*/
|
||||
batchSerial: number;
|
||||
/**
|
||||
* Number of requests in a batch before it is ready to be translated in
|
||||
* a single call. Bigger is better for throughput (better matrix packing)
|
||||
* but worse for latency since you'll have to wait for the entire batch
|
||||
* to be translated.
|
||||
*/
|
||||
batchSize: number;
|
||||
onerror: (error: Error) => void;
|
||||
constructor(options: BatchTranslatorOptions, backing?: TranslatorBacking);
|
||||
/**
|
||||
* Destructor that stops and cleans up.
|
||||
*/
|
||||
delete(): Promise<void>;
|
||||
/**
|
||||
* Makes sure queued work gets send to a worker. Will delay it till `idle`
|
||||
* to make sure the batches have been filled to some degree. Will keep
|
||||
* calling itself as long as there is work in the queue, but it does not
|
||||
* hurt to call it multiple times. This function always returns immediately.
|
||||
*/
|
||||
notify(): void;
|
||||
/**
|
||||
* The only real public call you need!
|
||||
* ```
|
||||
* const {target: {text:string}} = await this.translate({
|
||||
* from: 'de',
|
||||
* to: 'en',
|
||||
* text: 'Hallo Welt!',
|
||||
* html: false, // optional
|
||||
* priority: 0 // optional, like `nice` lower numbers are translated first
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
translate(request: TranslationRequest): Promise<TranslationResponse>;
|
||||
/**
|
||||
* Prune pending requests by testing each one of them to whether they're
|
||||
* still relevant. Used to prune translation requests from tabs that got
|
||||
* closed.
|
||||
*/
|
||||
remove(
|
||||
/** evaluates to true if request should be removed */
|
||||
filter: (request: TranslationRequest) => boolean): void;
|
||||
/**
|
||||
* Internal function used to put a request in a batch that still has space.
|
||||
* Also responsible for keeping the batches in order of priority. Called by
|
||||
* `translate()` but also used when filtering pending requests.
|
||||
*/
|
||||
enqueue({ key, models, request, resolve, reject, priority, }: {
|
||||
key: string;
|
||||
models: Omit<Model, "files">[];
|
||||
request: TranslationRequest;
|
||||
resolve: (response: TranslationResponse) => void;
|
||||
reject: (error: Error) => void;
|
||||
priority?: number;
|
||||
}): void;
|
||||
/**
|
||||
* Internal method that uses a worker thread to process a batch. You can
|
||||
* wait for the batch to be done by awaiting this call. You should only
|
||||
* then reuse the worker otherwise you'll just clog up its message queue.
|
||||
*/
|
||||
consumeBatch(batch: Batch, worker: any): Promise<void>;
|
||||
}
|
||||
interface LatencyOptimisedTranslatorOptions extends TranslatorBackingOptions {
|
||||
onDownloadProgress?: (current: number, total: number) => void;
|
||||
}
|
||||
/**
|
||||
* Translator optimised for interactive use.
|
||||
*/
|
||||
export declare class LatencyOptimisedTranslator {
|
||||
backing: TranslatorBacking;
|
||||
worker: Promise<{
|
||||
idle: boolean;
|
||||
worker: Worker;
|
||||
exports: any;
|
||||
}> | null;
|
||||
pending: {
|
||||
request: TranslationRequest;
|
||||
accept: (response: TranslationResponse) => void;
|
||||
reject: (error: Error) => void;
|
||||
options?: {
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
} | null;
|
||||
constructor(request: {
|
||||
from: string;
|
||||
to: string;
|
||||
}, options: LatencyOptimisedTranslatorOptions, backing?: TranslatorBacking);
|
||||
/**
|
||||
* Destructor that stops and cleans up.
|
||||
*/
|
||||
delete(): Promise<void>;
|
||||
/**
|
||||
* Sets `request` as the next translation to process. If there was already
|
||||
* a translation waiting to be processed, their promise is rejected with a
|
||||
* SupersededError.
|
||||
*/
|
||||
translate(request: TranslationRequest, options?: TranslationRequestOptions): Promise<TranslationResponse>;
|
||||
notify(): void;
|
||||
fetchTranslationModels(worker: {
|
||||
idle: boolean;
|
||||
worker: Worker;
|
||||
exports: any;
|
||||
}, request: Pick<TranslationRequest, "from" | "to">, options?: TranslationRequestOptions): Promise<Omit<Model, "files">[]>;
|
||||
}
|
||||
export {};
|
||||
723
sdkjs-plugins/content/bergamot/vendor/bergamot/translator.js
vendored
Normal file
@ -0,0 +1,723 @@
|
||||
/**
|
||||
* NodeJS compatibility, a thin WebWorker layer around node:worker_threads.
|
||||
*/
|
||||
if (!(typeof window !== "undefined" && window.Worker)) {
|
||||
globalThis.Worker = class {
|
||||
worker;
|
||||
constructor(url) {
|
||||
this.worker = new Promise(async (accept) => {
|
||||
// @ts-ignore
|
||||
const { Worker } = await import(/* webpackIgnore: true */ "node:worker_threads");
|
||||
accept(new Worker(url));
|
||||
});
|
||||
}
|
||||
addEventListener(eventName, callback) {
|
||||
this.worker.then((worker) => worker.on(eventName, (data) => callback({ data })));
|
||||
}
|
||||
postMessage(message) {
|
||||
this.worker.then((worker) => worker.postMessage(message));
|
||||
}
|
||||
terminate() {
|
||||
this.worker.then((worker) => worker.terminate());
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Thrown when a pending translation is replaced by another newer pending
|
||||
* translation.
|
||||
*/
|
||||
export class SupersededError extends Error {
|
||||
}
|
||||
/**
|
||||
* Thrown when a translation was removed from the queue.
|
||||
*/
|
||||
export class CancelledError extends Error {
|
||||
}
|
||||
/**
|
||||
* Wrapper around bergamot-translator loading and model management.
|
||||
*/
|
||||
export class TranslatorBacking {
|
||||
options;
|
||||
registryUrl;
|
||||
workerUrl;
|
||||
downloadTimeout;
|
||||
/**
|
||||
* registry of all available models and their urls
|
||||
*/
|
||||
registry;
|
||||
/**
|
||||
* Map of downloaded model data files as buffers per model.
|
||||
*/
|
||||
buffers = new Map();
|
||||
pivotLanguage;
|
||||
/**
|
||||
* A map of language-pairs to a list of models you need for it.
|
||||
*/
|
||||
models;
|
||||
/**
|
||||
* Map of model download state.
|
||||
*/
|
||||
state;
|
||||
/**
|
||||
* Error handler for all errors that are async, not tied to a specific
|
||||
* call and that are unrecoverable.
|
||||
*/
|
||||
onerror;
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.registryUrl =
|
||||
this.options.registryUrl ||
|
||||
"https://bergamot.s3.amazonaws.com/models/index.json";
|
||||
this.workerUrl = this.options.workerUrl || "./worker/translator-worker.js";
|
||||
this.downloadTimeout =
|
||||
"downloadTimeout" in this.options
|
||||
? parseInt(this.options.downloadTimeout)
|
||||
: 60000;
|
||||
this.registry = this.loadModelRegistry();
|
||||
this.buffers = new Map();
|
||||
this.pivotLanguage =
|
||||
"pivotLanguage" in this.options ? options.pivotLanguage : "en";
|
||||
this.models = new Map();
|
||||
this.state = new Map();
|
||||
this.onerror =
|
||||
this.options.onerror ||
|
||||
((err) => console.error("WASM Translation Worker error:", err));
|
||||
}
|
||||
/**
|
||||
* Loads a worker thread, and wraps it in a message passing proxy. I.e. it
|
||||
* exposes the entire interface of TranslationWorker here, and all calls
|
||||
* to it are async. Do note that you can only pass arguments that survive
|
||||
* being copied into a message.
|
||||
* @return {Promise<{worker:Worker, exports:Proxy<TranslationWorker>}>}
|
||||
*/
|
||||
async loadWorker() {
|
||||
const worker = new Worker(new URL(this.workerUrl, import.meta.url));
|
||||
/**
|
||||
* Incremental counter to derive request/response ids from.
|
||||
*/
|
||||
let serial = 0;
|
||||
/**
|
||||
* Map of pending requests
|
||||
*/
|
||||
const pending = new Map();
|
||||
// Function to send requests
|
||||
const call = (name, ...args) => new Promise((accept, reject) => {
|
||||
const id = ++serial;
|
||||
pending.set(id, {
|
||||
accept,
|
||||
reject,
|
||||
callsite: {
|
||||
// for debugging which call caused the error
|
||||
message: `${name}(${args.map((arg) => String(arg)).join(", ")})`,
|
||||
stack: new Error().stack,
|
||||
},
|
||||
});
|
||||
worker.postMessage({ id, name, args });
|
||||
});
|
||||
// … receive responses
|
||||
worker.addEventListener("message", function ({ data: { id, result, error } }) {
|
||||
if (!pending.has(id)) {
|
||||
console.debug("Received message with unknown id:", arguments[0]);
|
||||
throw new Error(`BergamotTranslator received response from worker to unknown call '${id}'`);
|
||||
}
|
||||
const { accept, reject, callsite } = pending.get(id);
|
||||
pending.delete(id);
|
||||
if (error !== undefined)
|
||||
reject(Object.assign(new Error(), error, {
|
||||
message: error.message + ` (response to ${callsite.message})`,
|
||||
stack: error.stack
|
||||
? `${error.stack}\n${callsite.stack}`
|
||||
: callsite.stack,
|
||||
}));
|
||||
else
|
||||
accept(result);
|
||||
});
|
||||
// … and general errors
|
||||
worker.addEventListener("error", this.onerror.bind(this));
|
||||
// Await initialisation. This will also nicely error out if the WASM
|
||||
// runtime fails to load.
|
||||
await call("initialize", this.options);
|
||||
/**
|
||||
* Little wrapper around the message passing api of Worker to make it
|
||||
* easy to await a response to a sent message. This wraps the worker in
|
||||
* a Proxy so you can treat it as if it is an instance of the
|
||||
* TranslationWorker class that lives inside the worker. All function
|
||||
* calls to it are transparently passed through the message passing
|
||||
* channel.
|
||||
*/
|
||||
return {
|
||||
worker,
|
||||
exports: new Proxy({}, {
|
||||
get(target, name, receiver) {
|
||||
// Prevent this object from being marked "then-able"
|
||||
if (name !== "then")
|
||||
return (...args) => call(name, ...args);
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Loads the model registry. Uses the registry shipped with this extension,
|
||||
* but formatted a bit easier to use, and future-proofed to be swapped out
|
||||
* with a TranslateLocally type registry.
|
||||
*/
|
||||
async loadModelRegistry() {
|
||||
const response = await fetch(this.registryUrl, {
|
||||
headers: { Accept: "application/json" },
|
||||
credentials: "omit",
|
||||
});
|
||||
const registry = await response.json();
|
||||
// Add 'from' and 'to' keys for each model.
|
||||
return Array.from(Object.entries(registry), ([key, files]) => {
|
||||
return {
|
||||
from: key.substring(0, 2),
|
||||
to: key.substring(2, 4),
|
||||
files: files,
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Gets or loads translation model data. Caching wrapper around
|
||||
* `loadTranslationModel()`.
|
||||
*/
|
||||
getTranslationModel({ from, to }, options) {
|
||||
const key = JSON.stringify({ from, to });
|
||||
if (!this.buffers.has(key)) {
|
||||
const promise = this.loadTranslationModel({ from, to }, options);
|
||||
// set the promise so we return the same promise when its still pending
|
||||
this.buffers.set(key, promise);
|
||||
this.state.set(key, "downloading");
|
||||
// But if loading fails, remove the promise again so we can try again later
|
||||
promise.catch((err) => {
|
||||
this.buffers.delete(key);
|
||||
this.state.set(key, "available");
|
||||
});
|
||||
}
|
||||
return this.buffers.get(key);
|
||||
}
|
||||
/**
|
||||
* Downloads a translation model and returns a set of
|
||||
* ArrayBuffers. These can then be passed to a TranslationWorker thread
|
||||
* to instantiate a TranslationModel inside the WASM vm.
|
||||
*/
|
||||
async loadTranslationModel({ from, to }, options) {
|
||||
const key = JSON.stringify({ from, to });
|
||||
performance.mark(`loadTranslationModule.${key}`);
|
||||
// Find that model in the registry which will tell us about its files
|
||||
const entries = (await this.registry).filter((model) => model.from == from && model.to == to);
|
||||
if (!entries)
|
||||
throw new Error(`No model for '${from}' -> '${to}'`);
|
||||
const files = entries[0].files;
|
||||
let abort;
|
||||
// Promise that resolves (or rejects really) when the abort signal hits
|
||||
const escape = new Promise((accept, reject) => {
|
||||
if (options?.signal) {
|
||||
abort = () => reject(new CancelledError("abort signal"));
|
||||
options.signal.addEventListener("abort", abort);
|
||||
}
|
||||
});
|
||||
// Download all files mentioned in the registry entry. Race the promise
|
||||
// of all fetch requests, and a promise that rejects on the abort signal
|
||||
const buffersEntries = (await Promise.race([
|
||||
Promise.all(Object.entries(files).map(async ([part, file]) => {
|
||||
// Special case where qualityModel is not part of the model, and this
|
||||
// should also catch the `config` case.
|
||||
if (file === undefined || file.name === undefined)
|
||||
return [part, null];
|
||||
try {
|
||||
return [
|
||||
part,
|
||||
await this.fetch(file.name, file.expectedSha256Hash, options),
|
||||
];
|
||||
}
|
||||
catch (cause) {
|
||||
throw new Error(`Could not fetch ${file.name} for ${from}->${to} model. Cause: ${cause}`);
|
||||
}
|
||||
})),
|
||||
escape,
|
||||
]));
|
||||
const buffers = Object.fromEntries(buffersEntries);
|
||||
// Nothing to abort now, clean up abort promise
|
||||
if (options?.signal)
|
||||
options.signal.removeEventListener("abort", abort);
|
||||
performance.measure("loadTranslationModel", `loadTranslationModule.${key}`);
|
||||
let vocabs = [];
|
||||
if (buffers.vocab)
|
||||
vocabs = [buffers.vocab];
|
||||
else if (buffers.trgvocab && buffers.srcvocab)
|
||||
vocabs = [buffers.srcvocab, buffers.trgvocab];
|
||||
else
|
||||
throw new Error(`Could not identify vocab files for ${from}->${to} model among: ${Array.from(Object.keys(files)).join(" ")}`);
|
||||
const config = {};
|
||||
// For the Ukrainian models we need to override the gemm-precision
|
||||
if (files.model.name.endsWith("intgemm8.bin"))
|
||||
config["gemm-precision"] = "int8shiftAll";
|
||||
// If quality estimation is used, we need to turn off skip-cost. Turning
|
||||
// this off causes quite the slowdown.
|
||||
if (files.qualityModel)
|
||||
config["skip-cost"] = false;
|
||||
// Allow the registry to also specify marian configuration parameters
|
||||
if (files.config)
|
||||
Object.assign(config, files.config);
|
||||
this.state.set(key, "available");
|
||||
// Translate to generic bergamot-translator format that also supports
|
||||
// separate vocabularies for input & output language, and calls 'lex'
|
||||
// a more descriptive 'shortlist'.
|
||||
return {
|
||||
model: buffers.model,
|
||||
shortlist: buffers.lex,
|
||||
vocabs,
|
||||
qualityModel: buffers.qualityModel,
|
||||
config,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Helper to download file from the web. Verifies the checksum.
|
||||
*/
|
||||
async fetch(url, checksum, extra) {
|
||||
// Rig up a timeout cancel signal for our fetch
|
||||
const controller = new AbortController();
|
||||
const abort = () => controller.abort();
|
||||
const timeout = this.downloadTimeout
|
||||
? setTimeout(abort, this.downloadTimeout)
|
||||
: null;
|
||||
try {
|
||||
// Also maintain the original abort signal
|
||||
if (extra?.signal)
|
||||
extra.signal.addEventListener("abort", abort);
|
||||
const options = {
|
||||
credentials: "omit",
|
||||
signal: controller.signal,
|
||||
};
|
||||
if (checksum)
|
||||
options["integrity"] = `sha256-${this.hexToBase64(checksum)}`;
|
||||
// Disable the integrity check for NodeJS because of
|
||||
// https://github.com/nodejs/undici/issues/1594
|
||||
if (typeof window === "undefined")
|
||||
delete options["integrity"];
|
||||
// Start downloading the url, using the hex checksum to ask
|
||||
// `fetch()` to verify the download using subresource integrity
|
||||
const response = await fetch(url, options);
|
||||
// Finish downloading (or crash due to timeout)
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
finally {
|
||||
if (timeout)
|
||||
clearTimeout(timeout);
|
||||
if (extra?.signal)
|
||||
extra.signal.removeEventListener("abort", abort);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Converts the hexadecimal hashes from the registry to something we can use with
|
||||
* the fetch() method.
|
||||
*/
|
||||
hexToBase64(hexstring) {
|
||||
return btoa(hexstring
|
||||
.match(/\w{2}/g)
|
||||
.map((a) => String.fromCharCode(parseInt(a, 16)))
|
||||
.join(""));
|
||||
}
|
||||
/**
|
||||
* Crappy named method that gives you a list of models to translate from
|
||||
* one language into the other. Generally this will be the same as you
|
||||
* just put in if there is a direct model, but it could return a list of
|
||||
* two models if you need to pivot through a third language.
|
||||
* Returns just [{from:str,to:str}...]. To be used something like this:
|
||||
* ```
|
||||
* const models = await this.getModels(from, to);
|
||||
* models.forEach(({from, to}) => {
|
||||
* const buffers = await this.loadTranslationModel({from,to});
|
||||
* [TranslationWorker].loadTranslationModel({from,to}, buffers)
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getModels({ from, to }) {
|
||||
const key = JSON.stringify({ from, to });
|
||||
// Note that the `this.models` map stores Promises. This so that
|
||||
// multiple calls to `getModels` that ask for the same model will
|
||||
// return the same promise, and the actual lookup is only done once.
|
||||
// The lookup is async because we need to await `this.registry`
|
||||
if (!this.models.has(key))
|
||||
this.models.set(key, this.findModels(from, to));
|
||||
return this.models.get(key);
|
||||
}
|
||||
/**
|
||||
* Find model (or model pair) to translate from `from` to `to`.
|
||||
*/
|
||||
async findModels(from, to) {
|
||||
const registry = await this.registry;
|
||||
const direct = [], outbound = [], inbound = [];
|
||||
registry.forEach((model) => {
|
||||
if (model.from === from && model.to === to)
|
||||
direct.push(model);
|
||||
else if (model.from === from && model.to === this.pivotLanguage)
|
||||
outbound.push(model);
|
||||
else if (model.to === to && model.from === this.pivotLanguage)
|
||||
inbound.push(model);
|
||||
});
|
||||
if (direct.length)
|
||||
return [direct[0]];
|
||||
if (outbound.length && inbound.length)
|
||||
return [outbound[0], inbound[0]];
|
||||
throw new Error(`No model available to translate from '${from}' to '${to}'`);
|
||||
}
|
||||
/**
|
||||
* Checks whether a model is available or being downloaded.
|
||||
*/
|
||||
async getState({ from, to, }) {
|
||||
const key = JSON.stringify({ from, to });
|
||||
try {
|
||||
const models = await this.findModels(from, to);
|
||||
if (!models.length)
|
||||
return "unavailable";
|
||||
return this.state.get(key) || "downloadable";
|
||||
}
|
||||
catch (e) {
|
||||
return "unavailable";
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Translator balancing between throughput and latency. Can use multiple worker
|
||||
* threads.
|
||||
*/
|
||||
export class BatchTranslator {
|
||||
backing;
|
||||
/**
|
||||
* List of active workers
|
||||
* (and a flag to mark them idle or not)
|
||||
*/
|
||||
workers = [];
|
||||
/**
|
||||
* Maximum number of workers
|
||||
*/
|
||||
workerLimit;
|
||||
/**
|
||||
* List of batches we push() to & shift() from using `enqueue`.
|
||||
*/
|
||||
queue = [];
|
||||
/**
|
||||
* batch serial to help keep track of batches when debugging
|
||||
*/
|
||||
batchSerial = 0;
|
||||
/**
|
||||
* Number of requests in a batch before it is ready to be translated in
|
||||
* a single call. Bigger is better for throughput (better matrix packing)
|
||||
* but worse for latency since you'll have to wait for the entire batch
|
||||
* to be translated.
|
||||
*/
|
||||
batchSize;
|
||||
onerror;
|
||||
constructor(options, backing) {
|
||||
if (!backing)
|
||||
backing = new TranslatorBacking(options);
|
||||
this.backing = backing;
|
||||
this.workerLimit = Math.max(options?.workers || 0, 1);
|
||||
this.batchSize = Math.max(options?.batchSize || 8, 1);
|
||||
this.onerror =
|
||||
options?.onerror ||
|
||||
((err) => console.error("WASM Translation Worker error:", err));
|
||||
}
|
||||
/**
|
||||
* Destructor that stops and cleans up.
|
||||
*/
|
||||
async delete() {
|
||||
// Empty the queue
|
||||
this.remove(() => true);
|
||||
// Terminate the workers
|
||||
this.workers.forEach(({ worker }) => worker.terminate());
|
||||
}
|
||||
/**
|
||||
* Makes sure queued work gets send to a worker. Will delay it till `idle`
|
||||
* to make sure the batches have been filled to some degree. Will keep
|
||||
* calling itself as long as there is work in the queue, but it does not
|
||||
* hurt to call it multiple times. This function always returns immediately.
|
||||
*/
|
||||
notify() {
|
||||
setTimeout(async () => {
|
||||
// Is there work to be done?
|
||||
if (!this.queue.length)
|
||||
return;
|
||||
// Find an idle worker
|
||||
let worker = this.workers.find((worker) => worker.idle);
|
||||
// No worker free, but space for more?
|
||||
if (!worker && this.workers.length < this.workerLimit) {
|
||||
try {
|
||||
// Claim a place in the workers array (but mark it busy so
|
||||
// it doesn't get used by any other `notify()` calls).
|
||||
const placeholder = { idle: false };
|
||||
this.workers.push(placeholder);
|
||||
// adds `worker` and `exports` props
|
||||
Object.assign(placeholder, await this.backing.loadWorker());
|
||||
// At this point we know our new worker will be usable.
|
||||
worker = placeholder;
|
||||
}
|
||||
catch (e) {
|
||||
this.onerror(new Error(`Could not initialise translation worker: ${e.message}`));
|
||||
}
|
||||
}
|
||||
// If no worker, that's the end of it.
|
||||
if (!worker)
|
||||
return;
|
||||
// Up to this point, this function has not used await, so no
|
||||
// chance that another call stole our batch since we did the check
|
||||
// at the beginning of this function and JavaScript is only
|
||||
// cooperatively parallel.
|
||||
const batch = this.queue.shift();
|
||||
// Put this worker to work, marking as busy
|
||||
worker.idle = false;
|
||||
try {
|
||||
await this.consumeBatch(batch, worker.exports);
|
||||
}
|
||||
catch (e) {
|
||||
batch.requests.forEach(({ reject }) => reject(e));
|
||||
}
|
||||
worker.idle = true;
|
||||
// Is there more work to be done? Do another idleRequest
|
||||
if (this.queue.length)
|
||||
this.notify();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* The only real public call you need!
|
||||
* ```
|
||||
* const {target: {text:string}} = await this.translate({
|
||||
* from: 'de',
|
||||
* to: 'en',
|
||||
* text: 'Hallo Welt!',
|
||||
* html: false, // optional
|
||||
* priority: 0 // optional, like `nice` lower numbers are translated first
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
translate(request) {
|
||||
const { from, to, priority } = request;
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// Batching key: only requests with the same key can be batched
|
||||
// together. Think same translation model, same options.
|
||||
const key = JSON.stringify({ from, to });
|
||||
// (Fetching models first because if we would do it between looking
|
||||
// for a batch and making a new one, we end up with a race condition.)
|
||||
const models = await this.backing.getModels(request);
|
||||
// Put the request and its callbacks into a fitting batch
|
||||
this.enqueue({ key, models, request, resolve, reject, priority });
|
||||
// Tell a worker to pick up the work at some point.
|
||||
this.notify();
|
||||
}
|
||||
catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Prune pending requests by testing each one of them to whether they're
|
||||
* still relevant. Used to prune translation requests from tabs that got
|
||||
* closed.
|
||||
*/
|
||||
remove(
|
||||
/** evaluates to true if request should be removed */
|
||||
filter) {
|
||||
const queue = this.queue;
|
||||
this.queue = [];
|
||||
queue.forEach((batch) => {
|
||||
batch.requests.forEach(({ request, resolve, reject }) => {
|
||||
if (filter(request)) {
|
||||
// Add error.request property to match response.request for
|
||||
// a resolve() callback. Pretty useful if you don't want to
|
||||
// do all kinds of Funcion.bind() dances.
|
||||
reject(Object.assign(new CancelledError("removed by filter"), { request }));
|
||||
return;
|
||||
}
|
||||
this.enqueue({
|
||||
key: batch.key,
|
||||
priority: batch.priority,
|
||||
models: batch.models,
|
||||
request,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Internal function used to put a request in a batch that still has space.
|
||||
* Also responsible for keeping the batches in order of priority. Called by
|
||||
* `translate()` but also used when filtering pending requests.
|
||||
*/
|
||||
enqueue({ key, models, request, resolve, reject, priority, }) {
|
||||
if (priority === undefined)
|
||||
priority = 0;
|
||||
// Find a batch in the queue that we can add to
|
||||
// (TODO: can we search backwards? that would speed things up)
|
||||
let batch = this.queue.find((batch) => {
|
||||
return (batch.key === key &&
|
||||
batch.priority === priority &&
|
||||
batch.requests.length < this.batchSize);
|
||||
});
|
||||
// No batch or full batch? Queue up a new one
|
||||
if (!batch) {
|
||||
batch = { id: ++this.batchSerial, key, priority, models, requests: [] };
|
||||
this.queue.push(batch);
|
||||
this.queue.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
batch.requests.push({ request, resolve, reject });
|
||||
}
|
||||
/**
|
||||
* Internal method that uses a worker thread to process a batch. You can
|
||||
* wait for the batch to be done by awaiting this call. You should only
|
||||
* then reuse the worker otherwise you'll just clog up its message queue.
|
||||
*/
|
||||
async consumeBatch(batch, worker) {
|
||||
performance.mark("BergamotBatchTranslator.start");
|
||||
// Make sure the worker has all necessary models loaded. If not, tell it
|
||||
// first to load them.
|
||||
await Promise.all(batch.models.map(async ({ from, to }) => {
|
||||
if (!(await worker.hasTranslationModel({ from, to }))) {
|
||||
const buffers = await this.backing.getTranslationModel({ from, to });
|
||||
await worker.loadTranslationModel({ from, to }, buffers);
|
||||
}
|
||||
}));
|
||||
// Call the worker to translate. Only sending the actually necessary
|
||||
// parts of the batch to avoid trying to send things that don't survive
|
||||
// the message passing API between this thread and the worker thread.
|
||||
const responses = await worker.translate({
|
||||
models: batch.models.map(({ from, to }) => ({ from, to })),
|
||||
texts: batch.requests.map(({ request: { text, html, qualityScores } }) => ({
|
||||
text: text.toString(),
|
||||
html: !!html,
|
||||
qualityScores: !!qualityScores,
|
||||
})),
|
||||
});
|
||||
// Responses are in! Connect them back to their requests and call their
|
||||
// callbacks.
|
||||
batch.requests.forEach(({ request, resolve, reject }, i) => {
|
||||
// TODO: look at response.ok and reject() if it is false
|
||||
resolve({
|
||||
request, // Include request for easy reference? Will allow you
|
||||
// to specify custom properties and use that to link
|
||||
// request & response back to each other.
|
||||
...responses[i], // {target: {text: String}}
|
||||
});
|
||||
});
|
||||
performance.measure("BergamotBatchTranslator", "BergamotBatchTranslator.start");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Translator optimised for interactive use.
|
||||
*/
|
||||
export class LatencyOptimisedTranslator {
|
||||
backing;
|
||||
worker;
|
||||
pending;
|
||||
constructor(request, options, backing) {
|
||||
if (!backing)
|
||||
backing = new TranslatorBacking(options);
|
||||
this.backing = backing;
|
||||
// Exposing the this.loadWorker() returned promise through this.worker
|
||||
// so that you can use that to catch any errors that happened during
|
||||
// loading.
|
||||
this.worker = this.backing
|
||||
.loadWorker()
|
||||
.then((worker) => ({ ...worker, idle: true }));
|
||||
this.worker.then((worker) => {
|
||||
this.fetchTranslationModels(worker, request, options);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Destructor that stops and cleans up.
|
||||
*/
|
||||
async delete() {
|
||||
// Cancel pending translation
|
||||
if (this.pending) {
|
||||
this.pending.reject(new CancelledError("translator got deleted"));
|
||||
this.pending = null;
|
||||
}
|
||||
// Terminate the worker (I don't care if this fails)
|
||||
try {
|
||||
const { worker } = await this.worker;
|
||||
worker.terminate();
|
||||
}
|
||||
finally {
|
||||
this.worker = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets `request` as the next translation to process. If there was already
|
||||
* a translation waiting to be processed, their promise is rejected with a
|
||||
* SupersededError.
|
||||
*/
|
||||
translate(request, options) {
|
||||
if (this.pending)
|
||||
this.pending.reject(new SupersededError());
|
||||
return new Promise((accept, reject) => {
|
||||
const pending = { request, accept, reject, options };
|
||||
if (options?.signal) {
|
||||
options.signal.addEventListener("abort", (e) => {
|
||||
reject(new CancelledError("abort signal"));
|
||||
if (this.pending === pending)
|
||||
this.pending = null;
|
||||
});
|
||||
}
|
||||
this.pending = pending;
|
||||
this.notify();
|
||||
});
|
||||
}
|
||||
notify() {
|
||||
setTimeout(async () => {
|
||||
if (!this.pending)
|
||||
return;
|
||||
// Catch errors such as the worker not working
|
||||
try {
|
||||
// Possibly wait for the worker to finish loading. After it loaded
|
||||
// these calls are pretty much instantaneous.
|
||||
const worker = await this.worker;
|
||||
// Is another notify() call hogging the worker? Then stop.
|
||||
if (!worker.idle)
|
||||
return;
|
||||
// Claim the pending translation request.
|
||||
const { request, accept, reject, options } = this.pending;
|
||||
this.pending = null;
|
||||
// Mark the worker as occupied
|
||||
worker.idle = false;
|
||||
try {
|
||||
console.log("huj");
|
||||
const models = await this.fetchTranslationModels(worker, request, options);
|
||||
console.log("chuj");
|
||||
const { text, html, qualityScores } = request;
|
||||
const responses = await worker.exports.translate({
|
||||
models: models.map(({ from, to }) => ({ from, to })),
|
||||
texts: [{ text, html, qualityScores }],
|
||||
});
|
||||
accept({ request, ...responses[0] });
|
||||
}
|
||||
catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
worker.idle = true;
|
||||
// Is there more work to be done? Do another idleRequest
|
||||
if (this.pending)
|
||||
this.notify();
|
||||
}
|
||||
catch (e) {
|
||||
this.backing.onerror(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
async fetchTranslationModels(worker, request, options) {
|
||||
// Fetch the models needed for translation
|
||||
const models = await this.backing.getModels(request);
|
||||
options?.onDownloadProgress?.(0, models.length);
|
||||
let downloaded = 0;
|
||||
// Ensure all required translation models are loaded in the worker
|
||||
await Promise.all(models.map(async ({ from, to }) => {
|
||||
if (!(await worker.exports.hasTranslationModel({ from, to }))) {
|
||||
const buffers = await this.backing.getTranslationModel({ from, to }, options);
|
||||
await worker.exports.loadTranslationModel({ from, to }, buffers);
|
||||
}
|
||||
options?.onDownloadProgress?.(++downloaded, models.length);
|
||||
}));
|
||||
return models;
|
||||
}
|
||||
}
|
||||
2919
sdkjs-plugins/content/bergamot/vendor/bergamot/worker/bergamot-translator-worker.js
vendored
Normal file
BIN
sdkjs-plugins/content/bergamot/vendor/bergamot/worker/bergamot-translator-worker.wasm
vendored
Executable file
475
sdkjs-plugins/content/bergamot/vendor/bergamot/worker/translator-worker.js
vendored
Normal file
@ -0,0 +1,475 @@
|
||||
/**
|
||||
* Wrapper around the dirty bits of Bergamot's WASM bindings.
|
||||
*/
|
||||
|
||||
// Global because importScripts is global.
|
||||
var Module = {};
|
||||
|
||||
/**
|
||||
* node.js compatibility: Fake GlobalWorkerScope that emulates being inside a
|
||||
* WebWorker
|
||||
*/
|
||||
if (typeof self === 'undefined') {
|
||||
global.Module = Module;
|
||||
|
||||
global.self = new class GlobalWorkerScope {
|
||||
/** @type {import("node:worker_threads").MessagePort} */
|
||||
#port;
|
||||
|
||||
constructor() {
|
||||
const {parentPort} = require(/* webpackIgnore: true */ 'node:worker_threads');
|
||||
this.#port = parentPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener to listen for messages posted to the worker.
|
||||
* @param {string} eventName
|
||||
* @param {(object)} callback
|
||||
*/
|
||||
addEventListener(eventName, callback) {
|
||||
this.#port.on(eventName, (data) => callback({data}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Post message outside, to the owner of the Worker.
|
||||
* @param {any} message
|
||||
*/
|
||||
postMessage(message) {
|
||||
this.#port.postMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...string} scripts - Paths to scripts to import in that order
|
||||
*/
|
||||
importScripts(...scripts) {
|
||||
const {readFileSync} = require(/* webpackIgnore: true */ 'node:fs');
|
||||
const {join} = require(/* webpackIgnore: true */ 'node:path');
|
||||
for (let pathname of scripts) {
|
||||
const script = readFileSync(join(__dirname, pathname), {encoding: 'utf-8'});
|
||||
eval.call(global, script);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds support for local file urls. Assumes anything that doesn't start
|
||||
* with "http" to be a local path.
|
||||
* @param {string} url - path or url
|
||||
* @param {object?} options - See `fetch()` options
|
||||
* @return {Promise<Response>}
|
||||
*/
|
||||
async fetch(url, options) {
|
||||
if (url.protocol === 'file:') {
|
||||
const {readFile} = require(/* webpackIgnore: true */ 'node:fs/promises');
|
||||
const buffer = await readFile(url.pathname);
|
||||
const blob = new Blob([buffer]);
|
||||
return new Response(blob, {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {
|
||||
'Content-Type': 'application/wasm',
|
||||
'Content-Length': blob.size.toString()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return await fetch(url, options);
|
||||
}
|
||||
|
||||
get location() {
|
||||
return new URL(`file://${__filename}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class YAML {
|
||||
/**
|
||||
* Parses YAML into dictionary. Does not interpret types, all values are a
|
||||
* string or a list of strings. No support for objects other than the top
|
||||
* level.
|
||||
* @param {string} yaml
|
||||
* @return {{[string]: string | string[]}}
|
||||
*/
|
||||
static parse(yaml) {
|
||||
const out = {};
|
||||
|
||||
yaml.split('\n').reduce((key, line, i) => {
|
||||
let match;
|
||||
if (match = line.match(/^\s*-\s+(.+?)$/)) {
|
||||
if (!Array.isArray(out[key]))
|
||||
out[key] = out[key].trim() ? [out[key]] : [];
|
||||
out[key].push(match[1].trim());
|
||||
}
|
||||
else if (match = line.match(/^\s*([A-Za-z0-9_][A-Za-z0-9_-]*):\s*(.*)$/)) {
|
||||
key = match[1];
|
||||
out[key] = match[2].trim();
|
||||
}
|
||||
else if (!line.trim()) {
|
||||
// whitespace, ignore
|
||||
}
|
||||
else {
|
||||
throw Error(`Could not parse line ${i+1}: "${line}"`);
|
||||
}
|
||||
return key;
|
||||
}, null);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an object into a YAML string. No support for objects, only simple
|
||||
* types and lists of simple types.
|
||||
* @param {{[string]: string | number | boolean | string[]}} data
|
||||
* @return {string}
|
||||
*/
|
||||
static stringify(data) {
|
||||
return Object.entries(data).reduce((str, [key, value]) => {
|
||||
let valstr = '';
|
||||
if (Array.isArray(value))
|
||||
valstr = value.map(val => `\n - ${val}`).join('');
|
||||
else if (typeof value === 'number' || typeof value === 'boolean' || value.match(/^\d*(\.\d+)?$/))
|
||||
valstr = `${value}`;
|
||||
else
|
||||
valstr = `${value}`; // Quote?
|
||||
|
||||
return `${str}${key}: ${valstr}\n`;
|
||||
}, '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the bergamot-translator exported module that hides the need
|
||||
* of working with C++ style data structures and does model management.
|
||||
*/
|
||||
class BergamotTranslatorWorker {
|
||||
/**
|
||||
* Map of expected symbol -> name of fallback symbol for functions that can
|
||||
* be swizzled for a faster implementation. Firefox Nightly makes use of
|
||||
* this.
|
||||
*/
|
||||
static GEMM_TO_FALLBACK_FUNCTIONS_MAP = {
|
||||
'int8_prepare_a': 'int8PrepareAFallback',
|
||||
'int8_prepare_b': 'int8PrepareBFallback',
|
||||
'int8_prepare_b_from_transposed': 'int8PrepareBFromTransposedFallback',
|
||||
'int8_prepare_b_from_quantized_transposed': 'int8PrepareBFromQuantizedTransposedFallback',
|
||||
'int8_prepare_bias': 'int8PrepareBiasFallback',
|
||||
'int8_multiply_and_add_bias': 'int8MultiplyAndAddBiasFallback',
|
||||
'int8_select_columns_of_b': 'int8SelectColumnsOfBFallback'
|
||||
};
|
||||
|
||||
/**
|
||||
* Name of module exported by Firefox Nightly that exports an optimised
|
||||
* implementation of the symbols mentioned above.
|
||||
*/
|
||||
static NATIVE_INT_GEMM = 'mozIntGemm';
|
||||
|
||||
/**
|
||||
* Empty because we can't do async constructors yet. It is the
|
||||
* responsibility of whoever owns this WebWorker to call `initialize()`.
|
||||
*/
|
||||
constructor(options) {}
|
||||
|
||||
/**
|
||||
* Instantiates a new translation worker with optional options object.
|
||||
* If this call succeeds, the WASM runtime is loaded and ready.
|
||||
*
|
||||
* Available options are:
|
||||
* useNativeIntGemm: {true | false} defaults to false. If true, it will
|
||||
* attempt to link to the intgemm module available in
|
||||
* Firefox Nightly which makes translations much faster.
|
||||
* cacheSize: {Number} defaults to 0 which disables translation
|
||||
* cache entirely. Note that this is a theoretical
|
||||
* upper bound. In practice it will use about 1/3th of
|
||||
* the cache specified here. 2^14 is not a bad starting
|
||||
* value.
|
||||
* @param {{useNativeIntGemm: boolean, cacheSize: number}} options
|
||||
*/
|
||||
async initialize(options) {
|
||||
this.options = options || {};
|
||||
this.models = new Map(); // Map<str,Promise<TranslationModel>>
|
||||
this.module = await this.loadModule();
|
||||
this.service = await this.loadTranslationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load native IntGEMM module for bergamot-translator. If that
|
||||
* fails because it or any of the expected functions is not available, it
|
||||
* falls back to using the naive implementations that come with the wasm
|
||||
* binary itself through `linkFallbackIntGemm()`.
|
||||
* @param {{env: {memory: WebAssembly.Memory}}} info
|
||||
* @return {{[method:string]: (...any) => any}}
|
||||
*/
|
||||
linkNativeIntGemm(info) {
|
||||
if (!WebAssembly['mozIntGemm']) {
|
||||
console.warn('Native gemm requested but not available, falling back to embedded gemm');
|
||||
return this.linkFallbackIntGemm(info);
|
||||
}
|
||||
|
||||
const instance = new WebAssembly.Instance(WebAssembly['mozIntGemm'](), {
|
||||
'': {memory: info['env']['memory']}
|
||||
});
|
||||
|
||||
if (!Array.from(Object.keys(BergamotTranslatorWorker.GEMM_TO_FALLBACK_FUNCTIONS_MAP)).every(fun => instance.exports[fun])) {
|
||||
console.warn('Native gemm is missing expected functions, falling back to embedded gemm');
|
||||
return this.linkFallbackIntGemm(info);
|
||||
}
|
||||
|
||||
return instance.exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links intgemm functions that are already available in the wasm binary,
|
||||
* but just exports them under the name that is expected by
|
||||
* bergamot-translator.
|
||||
* @param {{env: {memory: WebAssembly.Memory}}} info
|
||||
* @return {{[method:string]: (...any) => any}}
|
||||
*/
|
||||
linkFallbackIntGemm(info) {
|
||||
const mapping = Object.entries(BergamotTranslatorWorker.GEMM_TO_FALLBACK_FUNCTIONS_MAP).map(([key, name]) => {
|
||||
return [key, (...args) => Module['asm'][name](...args)]
|
||||
});
|
||||
|
||||
return Object.fromEntries(mapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method. Reads and instantiates the WASM binary. Returns a
|
||||
* promise for the exported Module object that contains all the classes
|
||||
* and functions exported by bergamot-translator.
|
||||
* @return {Promise<BergamotTranslator>}
|
||||
*/
|
||||
loadModule() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await self.fetch(new URL('./bergamot-translator-worker.wasm', self.location));
|
||||
|
||||
Object.assign(Module, {
|
||||
instantiateWasm: (info, accept) => {
|
||||
try {
|
||||
WebAssembly.instantiateStreaming(response, {
|
||||
...info,
|
||||
'wasm_gemm': this.options.useNativeIntGemm
|
||||
? this.linkNativeIntGemm(info)
|
||||
: this.linkFallbackIntGemm(info)
|
||||
}).then(({instance}) => accept(instance)).catch(reject);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
onRuntimeInitialized: () => {
|
||||
resolve(Module);
|
||||
}
|
||||
});
|
||||
|
||||
// Emscripten glue code. Webpack et al. should not mangle the `Module` property name!
|
||||
self.Module = Module;
|
||||
self.importScripts('bergamot-translator-worker.js');
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method. Instantiates a BlockingService()
|
||||
* @return {BergamotTranslator.BlockingService}
|
||||
*/
|
||||
loadTranslationService() {
|
||||
return new this.module.BlockingService({
|
||||
cacheSize: Math.max(this.options.cacheSize || 0, 0)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a model has already been loaded in this worker. Marked
|
||||
* async because the message passing interface we use expects async methods.
|
||||
* @param {{from:string, to:string}}
|
||||
* @return boolean
|
||||
*/
|
||||
hasTranslationModel({from,to}) {
|
||||
const key = JSON.stringify({from,to});
|
||||
return this.models.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a translation model from a set of file buffers. After this, the
|
||||
* model is available to translate with and `hasTranslationModel()` will
|
||||
* return true for this pair.
|
||||
* @param {{from:string, to:string}}
|
||||
* @param {{
|
||||
* model: ArrayBuffer,
|
||||
* shortlist: ArrayBuffer,
|
||||
* vocabs: ArrayBuffer[],
|
||||
* qualityModel: ArrayBuffer?,
|
||||
* config?: {
|
||||
* [key:string]: string
|
||||
* }
|
||||
* }} buffers
|
||||
*/
|
||||
loadTranslationModel({from, to}, buffers) {
|
||||
// This because service_bindings.cpp:prepareVocabsSmartMemories :(
|
||||
const uniqueVocabs = buffers.vocabs.filter((vocab, index, vocabs) => {
|
||||
return !vocabs.slice(0, index).includes(vocab);
|
||||
});
|
||||
|
||||
const [modelMemory, shortlistMemory, qualityModel, ...vocabMemory] = [
|
||||
this.prepareAlignedMemoryFromBuffer(buffers.model, 256),
|
||||
this.prepareAlignedMemoryFromBuffer(buffers.shortlist, 64),
|
||||
buffers.qualityModel // optional quality model
|
||||
? this.prepareAlignedMemoryFromBuffer(buffers.qualityModel, 64)
|
||||
: null,
|
||||
...uniqueVocabs.map(vocab => this.prepareAlignedMemoryFromBuffer(vocab, 64))
|
||||
];
|
||||
|
||||
const vocabs = new this.module.AlignedMemoryList();
|
||||
vocabMemory.forEach(vocab => vocabs.push_back(vocab));
|
||||
|
||||
// Defaults
|
||||
let modelConfig = YAML.parse(`
|
||||
beam-size: 1
|
||||
normalize: 1.0
|
||||
word-penalty: 0
|
||||
cpu-threads: 0
|
||||
gemm-precision: int8shiftAlphaAll
|
||||
skip-cost: true
|
||||
`);
|
||||
|
||||
if (buffers.config)
|
||||
Object.assign(modelConfig, buffers.config);
|
||||
|
||||
// WASM marian is only compiled with support for shiftedAll.
|
||||
if (modelConfig['gemm-precision'] === 'int8')
|
||||
modelConfig['gemm-precision'] = 'int8shiftAll';
|
||||
|
||||
// Override these
|
||||
Object.assign(modelConfig, YAML.parse(`
|
||||
alignment: soft
|
||||
quiet: true
|
||||
quiet-translation: true
|
||||
max-length-break: 128
|
||||
mini-batch-words: 1024
|
||||
workspace: 128
|
||||
max-length-factor: 2.0
|
||||
`));
|
||||
|
||||
const key = JSON.stringify({from,to});
|
||||
this.models.set(key, new this.module.TranslationModel(YAML.stringify(modelConfig), modelMemory, shortlistMemory, vocabs, qualityModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees up memory used by old translation model. Does nothing if model is
|
||||
* already deleted.
|
||||
* @param {{from:string, to:string}}
|
||||
*/
|
||||
freeTranslationModel({from, to}) {
|
||||
const key = JSON.stringify({from,to});
|
||||
|
||||
if (!this.models.has(key))
|
||||
return;
|
||||
|
||||
const model = this.models.get(key);
|
||||
this.models.delete(key);
|
||||
|
||||
model.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function. Copies the data from an ArrayBuffer into memory that
|
||||
* can be used inside the WASM vm by Marian.
|
||||
* @param {{ArrayBuffer}} buffer
|
||||
* @param {number} alignmentSize
|
||||
* @return {BergamotTranslator.AlignedMemory}
|
||||
*/
|
||||
prepareAlignedMemoryFromBuffer(buffer, alignmentSize) {
|
||||
const bytes = new Int8Array(buffer);
|
||||
const memory = new this.module.AlignedMemory(bytes.byteLength, alignmentSize);
|
||||
memory.getByteArrayView().set(bytes);
|
||||
return memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public. Does actual translation work. You have to make sure that the
|
||||
* models necessary for translating text are already loaded before calling
|
||||
* this method. Returns a promise with translation responses.
|
||||
* @param {{models: {from:string, to:string}[], texts: {text: string, html: boolean}[]}}
|
||||
* @return {Promise<{target: {text: string}}[]>}
|
||||
*/
|
||||
translate({models, texts}) {
|
||||
// Convert texts array into a std::vector<std::string>.
|
||||
let input = new this.module.VectorString();
|
||||
texts.forEach(({text}) => input.push_back(text));
|
||||
|
||||
// Extracts the texts[].html options into ResponseOption objects
|
||||
let options = new this.module.VectorResponseOptions();
|
||||
texts.forEach(({html, qualityScores}) => options.push_back({alignment: false, html, qualityScores}));
|
||||
|
||||
// Turn our model names into a list of TranslationModel pointers
|
||||
const translationModels = models.map(({from,to}) => {
|
||||
const key = JSON.stringify({from,to});
|
||||
return this.models.get(key);
|
||||
});
|
||||
|
||||
// translate the input, which is a vector<String>; the result is a vector<Response>
|
||||
const responses = models.length > 1
|
||||
? this.service.translateViaPivoting(...translationModels, input, options)
|
||||
: this.service.translate(...translationModels, input, options);
|
||||
|
||||
input.delete();
|
||||
options.delete();
|
||||
|
||||
// Convert the Response WASM wrappers into native JavaScript types we
|
||||
// can send over the 'wire' (message passing) in the same format as we
|
||||
// use in bergamot-translator.
|
||||
const translations = texts.map((_, i) => ({
|
||||
target: {
|
||||
text: responses.get(i).getTranslatedText()
|
||||
}
|
||||
}));
|
||||
|
||||
responses.delete();
|
||||
|
||||
return translations;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Because you can't put an Error object in a message. But you can post a
|
||||
* generic object!
|
||||
* @param {Error} error
|
||||
* @return {{
|
||||
* name: string?,
|
||||
* message: string?,
|
||||
* stack: string?
|
||||
* }}
|
||||
*/
|
||||
function cloneError(error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
};
|
||||
}
|
||||
|
||||
// (Constructor doesn't really do anything, we need to call `initialize()`
|
||||
// first before using it. That happens from outside the worker.)
|
||||
const worker = new BergamotTranslatorWorker();
|
||||
|
||||
self.addEventListener('message', async function({data: {id, name, args}}) {
|
||||
if (!id)
|
||||
console.error('Received message without id', arguments[0]);
|
||||
|
||||
try {
|
||||
if (typeof worker[name] !== 'function')
|
||||
throw TypeError(`worker[${name}] is not a function`);
|
||||
|
||||
// Using `Promise.resolve` to await any promises that worker[name]
|
||||
// possibly returns.
|
||||
const result = await Promise.resolve(Reflect.apply(worker[name], worker, args));
|
||||
self.postMessage({id, result});
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id,
|
||||
error: cloneError(error)
|
||||
})
|
||||
}
|
||||
});
|
||||
2
sdkjs-plugins/content/bergamot/vendor/jquery/jquery-3.7.1.min.js
vendored
Normal file
21
sdkjs-plugins/content/bergamot/vendor/select2-4.0.13/LICENSE.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
157
sdkjs-plugins/content/bergamot/vendor/select2-4.0.13/README.md
vendored
Normal file
@ -0,0 +1,157 @@
|
||||
Select2
|
||||
=======
|
||||
![Build Status][github-actions-image]
|
||||
[](https://opencollective.com/select2) [][cdnjs]
|
||||
[][jsdelivr]
|
||||
|
||||
Select2 is a jQuery-based replacement for select boxes. It supports searching,
|
||||
remote data sets, and pagination of results.
|
||||
|
||||
To get started, checkout examples and documentation at
|
||||
https://select2.org/
|
||||
|
||||
Use cases
|
||||
---------
|
||||
* Enhancing native selects with search.
|
||||
* Enhancing native selects with a better multi-select interface.
|
||||
* Loading data from JavaScript: easily load items via AJAX and have them
|
||||
searchable.
|
||||
* Nesting optgroups: native selects only support one level of nesting. Select2
|
||||
does not have this restriction.
|
||||
* Tagging: ability to add new items on the fly.
|
||||
* Working with large, remote datasets: ability to partially load a dataset based
|
||||
on the search term.
|
||||
* Paging of large datasets: easy support for loading more pages when the results
|
||||
are scrolled to the end.
|
||||
* Templating: support for custom rendering of results and selections.
|
||||
|
||||
Browser compatibility
|
||||
---------------------
|
||||
* IE 8+
|
||||
* Chrome 8+
|
||||
* Firefox 10+
|
||||
* Safari 3+
|
||||
* Opera 10.6+
|
||||
|
||||
Usage
|
||||
-----
|
||||
You can source Select2 directly from a CDN like [jsDelivr][jsdelivr] or
|
||||
[cdnjs][cdnjs], [download it from this GitHub repo][releases], or use one of
|
||||
the integrations below.
|
||||
|
||||
Integrations
|
||||
------------
|
||||
Third party developers have created plugins for platforms which allow Select2 to be integrated more natively and quickly. For many platforms, additional plugins are not required because Select2 acts as a standard `<select>` box.
|
||||
|
||||
Plugins
|
||||
|
||||
* [Django]
|
||||
- [django-autocomplete-light]
|
||||
- [django-easy-select2]
|
||||
- [django-select2]
|
||||
* [Drupal] - [drupal-select2]
|
||||
* [Meteor] - [meteor-select2]
|
||||
* [Ruby on Rails][ruby-on-rails] - [select2-rails]
|
||||
* [Wicket] - [wicketstuff-select2]
|
||||
* [Yii 2][yii2] - [yii2-widget-select2]
|
||||
* [Angularjs][angularjs] - [mdr-angular-select2]
|
||||
|
||||
Themes
|
||||
|
||||
- [Bootstrap 3][bootstrap3] - [select2-bootstrap-theme]
|
||||
- [Bootstrap 4][bootstrap4] - [select2-bootstrap4-theme]
|
||||
- [Flat UI][flat-ui] - [select2-flat-theme]
|
||||
- [Metro UI][metro-ui] - [select2-metro]
|
||||
|
||||
Missing an integration? Modify this `README` and make a pull request back here to Select2 on GitHub.
|
||||
|
||||
Internationalization (i18n)
|
||||
---------------------------
|
||||
Select2 supports multiple languages by simply including the right language JS
|
||||
file (`dist/js/i18n/it.js`, `dist/js/i18n/nl.js`, etc.) after
|
||||
`dist/js/select2.js`.
|
||||
|
||||
Missing a language? Just copy `src/js/select2/i18n/en.js`, translate it, and
|
||||
make a pull request back to Select2 here on GitHub.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
The documentation for Select2 is available
|
||||
[online at the documentation website][documentation] and is located within the
|
||||
[`docs` directory of this repository][documentation-directory].
|
||||
|
||||
Community
|
||||
---------
|
||||
You can find out about the different ways to get in touch with the Select2
|
||||
community at the [Select2 community page][community].
|
||||
|
||||
Copyright and license
|
||||
---------------------
|
||||
The license is available within the repository in the [LICENSE][license] file.
|
||||
|
||||
[cdnjs]: http://www.cdnjs.com/libraries/select2
|
||||
[community]: https://select2.org/getting-help
|
||||
[documentation]: https://select2.org
|
||||
[documentation-directory]: https://github.com/select2/select2/tree/develop/docs
|
||||
[freenode]: https://freenode.net/
|
||||
[github-actions-image]: https://github.com/select2/select2/workflows/CI/badge.svg
|
||||
[jsdelivr]: https://www.jsdelivr.com/package/npm/select2
|
||||
[license]: LICENSE.md
|
||||
[releases]: https://github.com/select2/select2/releases
|
||||
|
||||
[angularjs]: https://angularjs.org/
|
||||
[bootstrap3]: https://getbootstrap.com/
|
||||
[bootstrap4]: https://getbootstrap.com/
|
||||
[django]: https://www.djangoproject.com/
|
||||
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
|
||||
[django-easy-select2]: https://github.com/asyncee/django-easy-select2
|
||||
[django-select2]: https://github.com/applegrew/django-select2
|
||||
[drupal]: https://www.drupal.org/
|
||||
[drupal-select2]: https://www.drupal.org/project/select2
|
||||
[flat-ui]: http://designmodo.github.io/Flat-UI/
|
||||
[mdr-angular-select2]: https://github.com/modulr/mdr-angular-select2
|
||||
[meteor]: https://www.meteor.com/
|
||||
[meteor-select2]: https://github.com/nate-strauser/meteor-select2
|
||||
[metro-ui]: http://metroui.org.ua/
|
||||
[select2-metro]: http://metroui.org.ua/select2.html
|
||||
[ruby-on-rails]: http://rubyonrails.org/
|
||||
[select2-bootstrap-theme]: https://github.com/select2/select2-bootstrap-theme
|
||||
[select2-bootstrap4-theme]: https://github.com/ttskch/select2-bootstrap4-theme
|
||||
[select2-flat-theme]: https://github.com/techhysahil/select2-Flat_Theme
|
||||
[select2-rails]: https://github.com/argerim/select2-rails
|
||||
[vue.js]: http://vuejs.org/
|
||||
[select2-vue]: http://vuejs.org/examples/select2.html
|
||||
[wicket]: https://wicket.apache.org/
|
||||
[wicketstuff-select2]: https://github.com/wicketstuff/core/tree/master/select2-parent
|
||||
[yii2]: http://www.yiiframework.com/
|
||||
[yii2-widget-select2]: https://github.com/kartik-v/yii2-widget-select2
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](.github/CONTRIBUTING.md)].
|
||||
<a href="https://github.com/select2/select2/graphs/contributors"><img src="https://opencollective.com/select2/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/select2/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/select2"><img src="https://opencollective.com/select2/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/select2/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/select2/organization/0/website"><img src="https://opencollective.com/select2/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/1/website"><img src="https://opencollective.com/select2/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/2/website"><img src="https://opencollective.com/select2/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/3/website"><img src="https://opencollective.com/select2/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/4/website"><img src="https://opencollective.com/select2/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/5/website"><img src="https://opencollective.com/select2/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/6/website"><img src="https://opencollective.com/select2/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/7/website"><img src="https://opencollective.com/select2/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/8/website"><img src="https://opencollective.com/select2/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/select2/organization/9/website"><img src="https://opencollective.com/select2/organization/9/avatar.svg"></a>
|
||||
484
sdkjs-plugins/content/bergamot/vendor/select2-4.0.13/css/select2.css
vendored
Normal file
@ -0,0 +1,484 @@
|
||||
.select2-container {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
vertical-align: middle; }
|
||||
.select2-container .select2-selection--single {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 22px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
padding-left: 8px;
|
||||
padding-right: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-selection--single .select2-selection__clear {
|
||||
position: relative; }
|
||||
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
|
||||
padding-right: 8px;
|
||||
padding-left: 20px; }
|
||||
.select2-container .select2-selection--multiple {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
min-height: 32px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
padding-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-search--inline {
|
||||
float: left; }
|
||||
.select2-container .select2-search--inline .select2-search__field {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
font-size: 100%;
|
||||
margin-top: 5px;
|
||||
padding: 0; }
|
||||
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
|
||||
.select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -100000px;
|
||||
width: 100%;
|
||||
z-index: 1051; }
|
||||
|
||||
.select2-results {
|
||||
display: block; }
|
||||
|
||||
.select2-results__options {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
.select2-results__option {
|
||||
padding: 6px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-results__option[aria-selected] {
|
||||
cursor: pointer; }
|
||||
|
||||
.select2-container--open .select2-dropdown {
|
||||
left: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--above {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-search--dropdown {
|
||||
display: block;
|
||||
padding: 4px; }
|
||||
.select2-search--dropdown .select2-search__field {
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
.select2-search--dropdown.select2-search--hide {
|
||||
display: none; }
|
||||
|
||||
.select2-close-mask {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
z-index: 99;
|
||||
background-color: #fff;
|
||||
filter: alpha(opacity=0); }
|
||||
|
||||
.select2-hidden-accessible {
|
||||
border: 0 !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
-webkit-clip-path: inset(50%) !important;
|
||||
clip-path: inset(50%) !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
white-space: nowrap !important; }
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 22px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
cursor: text; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
width: 100%; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
margin-right: 10px;
|
||||
padding: 1px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #333; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||
border: solid black 1px;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa; }
|
||||
|
||||
.select2-container--default .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield; }
|
||||
|
||||
.select2-container--default .select2-results > .select2-results__options {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--default .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-disabled=true] {
|
||||
color: #999; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-selected=true] {
|
||||
background-color: #7d858c; }
|
||||
|
||||
.select2-container--default .select2-results__option .select2-results__option {
|
||||
padding-left: 1em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em; }
|
||||
|
||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #5897fb;
|
||||
color: white; }
|
||||
|
||||
.select2-container--default .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic .select2-selection--single {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
outline: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
.select2-container--classic .select2-selection--single:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 22px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-right: 10px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__arrow {
|
||||
background-color: #ddd;
|
||||
border: none;
|
||||
border-left: 1px solid #aaa;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
border: none;
|
||||
border-right: 1px solid #aaa;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--single {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
|
||||
background: transparent;
|
||||
border: none; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
|
||||
|
||||
.select2-container--classic .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
cursor: text;
|
||||
outline: 0; }
|
||||
.select2-container--classic .select2-selection--multiple:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
|
||||
display: none; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #555; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--multiple {
|
||||
border: 1px solid #5897fb; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--inline .select2-search__field {
|
||||
outline: 0;
|
||||
box-shadow: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid transparent; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--above {
|
||||
border-bottom: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--below {
|
||||
border-top: none; }
|
||||
|
||||
.select2-container--classic .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--classic .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--classic .select2-results__option[aria-disabled=true] {
|
||||
color: grey; }
|
||||
|
||||
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #3875d7;
|
||||
color: white; }
|
||||
|
||||
.select2-container--classic .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-dropdown {
|
||||
border-color: #5897fb; }
|
||||
|
||||
6110
sdkjs-plugins/content/bergamot/vendor/select2-4.0.13/js/select2.js
vendored
Normal file
@ -38,5 +38,6 @@
|
||||
{ "name": "icons", "discussion": "" },
|
||||
{ "name": "datepicker", "discussion": "" },
|
||||
{ "name": "news", "discussion": "" },
|
||||
{ "name": "texthighlighter", "discussion": "" }
|
||||
{ "name": "texthighlighter", "discussion": "" },
|
||||
{ "name": "bergamot", "discussion": "" }
|
||||
]
|
||||
|
||||
@ -45,6 +45,9 @@
|
||||
},
|
||||
"200%": {
|
||||
"normal": "resources/light/icon@2x.png"
|
||||
},
|
||||
"*": {
|
||||
"normal": "resources/light/icon.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -64,6 +67,9 @@
|
||||
},
|
||||
"200%": {
|
||||
"normal": "resources/dark/icon@2x.png"
|
||||
},
|
||||
"*": {
|
||||
"normal": "resources/dark/icon.svg"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
3
store/plugin/resources/dark/icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28">
|
||||
<path fill="#fff" d="M18 4a2 2 0 1 0-3 1.732V7H7v8H5.732a2 2 0 1 0 0 2H7v8h6.764a3 3 0 1 1 4.472 0H25v-6.764a3 3 0 1 1 0-4.472V7h-8V5.732A2 2 0 0 0 18 4m1 0a3 3 0 0 1-.764 2H25a1 1 0 0 1 1 1v8h-1.268A2 2 0 0 0 21 16a2 2 0 0 0 3.732 1H26v8a1 1 0 0 1-1 1h-8v-1.268A2 2 0 0 0 16 21a2 2 0 0 0-1 3.732V26H7a1 1 0 0 1-1-1v-6.764a3 3 0 1 1 0-4.472V7a1 1 0 0 1 1-1h6.764A3 3 0 1 1 19 4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |
3
store/plugin/resources/light/icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28">
|
||||
<path fill="#000" d="M18 4a2 2 0 1 0-3 1.732V7H7v8H5.732a2 2 0 1 0 0 2H7v8h6.764a3 3 0 1 1 4.472 0H25v-6.764a3 3 0 1 1 0-4.472V7h-8V5.732A2 2 0 0 0 18 4m1 0a3 3 0 0 1-.764 2H25a1 1 0 0 1 1 1v8h-1.268A2 2 0 0 0 21 16a2 2 0 0 0 3.732 1H26v8a1 1 0 0 1-1 1h-8v-1.268A2 2 0 0 0 16 21a2 2 0 0 0-1 3.732V26H7a1 1 0 0 1-1-1v-6.764a3 3 0 1 1 0-4.472V7a1 1 0 0 1 1-1h6.764A3 3 0 1 1 19 4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |