Repository: crimx/ext-saladict Branch: dev Commit: ffb478cd2e32 Files: 707 Total size: 2.1 MB Directory structure: gitextract_w8wgf1mm/ ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── _bug_report_chs.md │ │ ├── _feature_request_chs.md │ │ ├── _new_dict_chs.md │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── config.yml │ └── no-response.yml ├── .gitignore ├── .neutrinorc.js ├── .prettierrc ├── .storybook/ │ ├── addons.ts │ ├── config.ts │ ├── configs/ │ │ └── contexts.tsx │ ├── manager-head.html │ ├── preview-head.html │ ├── style.css │ └── webpack.config.js ├── .travis.yml ├── .vscode/ │ ├── locales.schema.json │ └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING-zh.md ├── CONTRIBUTING.md ├── LICENSE ├── README-zh.md ├── README.md ├── assets/ │ ├── content.css │ ├── fanyi.youdao.2.0/ │ │ ├── all-packed.css │ │ ├── conn.html │ │ ├── conn.js │ │ └── main.js │ ├── google-page-trans.js │ ├── inject-dict-panel.js │ └── vimium-c-injector.js ├── commitlint.config.js ├── config/ │ └── jest/ │ ├── cssTransform.js │ ├── fileTransform.js │ └── setupTests.js ├── jest.config.js ├── jsconfig.json ├── mac-app/ │ └── Saladict - Pop-up Dictionary and Page Translator/ │ ├── Saladict - Pop-up Dictionary and Page Translator/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── Saladict___Pop_up_Dictionary_and_Page_Translator.entitlements │ │ └── ViewController.swift │ ├── Saladict - Pop-up Dictionary and Page Translator Extension/ │ │ ├── Info.plist │ │ ├── SafariWebExtensionHandler.swift │ │ └── Saladict___Pop_up_Dictionary_and_Page_Translator_Extension.entitlements │ └── Saladict - Pop-up Dictionary and Page Translator.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata/ │ │ └── crimx.xcuserdatad/ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata/ │ └── crimx.xcuserdatad/ │ └── xcschemes/ │ └── xcschememanagement.plist ├── package.json ├── postcss.config.js ├── scripts/ │ ├── after-build.js │ ├── build.js │ ├── firefox-fix.js │ ├── fixtures.js │ ├── pdf.js │ ├── setup-env.js │ ├── start.js │ ├── style-extractor.js │ └── test.js ├── src/ │ ├── _helpers/ │ │ ├── __mocks__/ │ │ │ ├── browser-api.ts │ │ │ ├── config-manager.ts │ │ │ └── selection.ts │ │ ├── analytics/ │ │ │ ├── events.ts │ │ │ └── index.ts │ │ ├── browser-api.ts │ │ ├── check-update.ts │ │ ├── chs-to-chz.ts │ │ ├── config-manager.ts │ │ ├── dom.ts │ │ ├── fetch-dom.ts │ │ ├── getSuggests.ts │ │ ├── hooks.ts │ │ ├── i18n.ts │ │ ├── injectSaladictInternal.ts │ │ ├── integrity.ts │ │ ├── lang-check.ts │ │ ├── matchPatternToRegExpStr.ts │ │ ├── observables.ts │ │ ├── permission-manager.ts │ │ ├── profile-manager.ts │ │ ├── promise-more.ts │ │ ├── record-manager.ts │ │ ├── saladict.ts │ │ ├── scrollbar-width.ts │ │ ├── storybook.tsx │ │ ├── titlebar-offset.ts │ │ ├── translateCtx.ts │ │ ├── uniqueKey.ts │ │ └── wordoftheday.ts │ ├── _locales/ │ │ ├── en/ │ │ │ ├── background.ts │ │ │ ├── common.ts │ │ │ ├── content.ts │ │ │ ├── langcode.ts │ │ │ ├── menus.ts │ │ │ ├── options.ts │ │ │ ├── popup.ts │ │ │ └── wordpage.ts │ │ ├── es/ │ │ │ ├── background.ts │ │ │ ├── common.ts │ │ │ ├── content.ts │ │ │ ├── langcode.ts │ │ │ ├── menus.ts │ │ │ ├── options.ts │ │ │ ├── popup.ts │ │ │ └── wordpage.ts │ │ ├── manifest/ │ │ │ ├── en/ │ │ │ │ └── messages.json │ │ │ ├── np/ │ │ │ │ └── messages.json │ │ │ ├── zh_CN/ │ │ │ │ └── messages.json │ │ │ └── zh_TW/ │ │ │ └── messages.json │ │ ├── ne/ │ │ │ ├── background.ts │ │ │ ├── common.ts │ │ │ ├── content.ts │ │ │ ├── langcode.ts │ │ │ ├── menus.ts │ │ │ ├── options.ts │ │ │ ├── popup.ts │ │ │ └── wordpage.ts │ │ ├── zh-CN/ │ │ │ ├── background.ts │ │ │ ├── common.ts │ │ │ ├── content.ts │ │ │ ├── langcode.ts │ │ │ ├── menus.ts │ │ │ ├── options.ts │ │ │ ├── popup.ts │ │ │ └── wordpage.ts │ │ └── zh-TW/ │ │ ├── background.ts │ │ ├── common.ts │ │ ├── content.ts │ │ ├── langcode.ts │ │ ├── menus.ts │ │ ├── options.ts │ │ ├── popup.ts │ │ └── wordpage.ts │ ├── _sass_shared/ │ │ ├── _fancy-scrollbar.scss │ │ ├── _global/ │ │ │ ├── _interfaces.scss │ │ │ ├── _mixins.scss │ │ │ ├── _variables.scss │ │ │ └── _z-indices.scss │ │ ├── _namespace.scss │ │ ├── _reset.scss │ │ └── _theme.scss │ ├── app-config/ │ │ ├── auth.ts │ │ ├── context-menus.ts │ │ ├── dicts.ts │ │ ├── index.ts │ │ ├── merge-config.ts │ │ ├── merge-profile.ts │ │ └── profiles.ts │ ├── audio-control/ │ │ ├── audio-control.scss │ │ └── index.tsx │ ├── background/ │ │ ├── __fake__/ │ │ │ └── env.ts │ │ ├── __mocks__/ │ │ │ └── database.ts │ │ ├── audio-manager.ts │ │ ├── badge.ts │ │ ├── clipboard-manager.ts │ │ ├── context-menus.ts │ │ ├── database/ │ │ │ ├── core.ts │ │ │ ├── index.ts │ │ │ ├── read.ts │ │ │ ├── sync-meta.ts │ │ │ └── write.ts │ │ ├── env.ts │ │ ├── i18n-manager.ts │ │ ├── index.ts │ │ ├── initialization.ts │ │ ├── page-translate/ │ │ │ └── caiyun.ts │ │ ├── pdf-sniffer.ts │ │ ├── server.ts │ │ ├── sync-manager/ │ │ │ ├── __mocks__/ │ │ │ │ └── helpers.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── services/ │ │ │ ├── ankiconnect/ │ │ │ │ ├── _locales/ │ │ │ │ │ ├── en.ts │ │ │ │ │ ├── zh-CN.ts │ │ │ │ │ └── zh-TW.ts │ │ │ │ └── index.ts │ │ │ ├── eudic/ │ │ │ │ ├── _locales/ │ │ │ │ │ ├── en.ts │ │ │ │ │ ├── zh-CN.ts │ │ │ │ │ └── zh-TW.ts │ │ │ │ └── index.ts │ │ │ ├── shanbay/ │ │ │ │ ├── _locales/ │ │ │ │ │ ├── en.ts │ │ │ │ │ ├── zh-CN.ts │ │ │ │ │ └── zh-TW.ts │ │ │ │ └── index.ts │ │ │ └── webdav/ │ │ │ ├── _locales/ │ │ │ │ ├── en.ts │ │ │ │ ├── zh-CN.ts │ │ │ │ └── zh-TW.ts │ │ │ └── index.ts │ │ ├── types.ts │ │ └── windows-manager.ts │ ├── components/ │ │ ├── AntdRoot/ │ │ │ ├── AntdRootContainer.tsx │ │ │ ├── _style.scss │ │ │ └── index.tsx │ │ ├── EntryBox/ │ │ │ ├── EntryBox.scss │ │ │ ├── EntryBox.stories.tsx │ │ │ └── index.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── FloatBox/ │ │ │ ├── FloatBox.scss │ │ │ ├── FloatBox.stories.tsx │ │ │ └── index.tsx │ │ ├── HoverBox/ │ │ │ ├── HoverBox.scss │ │ │ └── index.tsx │ │ ├── MachineTrans/ │ │ │ ├── MachineTrans.scss │ │ │ ├── MachineTrans.stories.tsx │ │ │ ├── MachineTrans.tsx │ │ │ └── engine.ts │ │ ├── ShadowPortal/ │ │ │ ├── ShadowPortal.scss │ │ │ └── index.tsx │ │ ├── Speaker/ │ │ │ ├── Speaker.scss │ │ │ ├── Speaker.stories.tsx │ │ │ └── index.tsx │ │ ├── StarRates/ │ │ │ └── index.tsx │ │ ├── StrElm/ │ │ │ └── index.tsx │ │ ├── Waveform/ │ │ │ ├── Waveform.scss │ │ │ ├── Waveform.stories.tsx │ │ │ └── Waveform.tsx │ │ ├── WordPage/ │ │ │ ├── ExportModal/ │ │ │ │ ├── Linebreak.tsx │ │ │ │ ├── PlaceholderTable.tsx │ │ │ │ └── index.tsx │ │ │ ├── Header.tsx │ │ │ ├── WordTable.tsx │ │ │ ├── _style.scss │ │ │ └── index.tsx │ │ └── dictionaries/ │ │ ├── ahdict/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── baidu/ │ │ │ ├── View.tsx │ │ │ ├── _locales.ts │ │ │ ├── _style.shadow.scss │ │ │ ├── auth.ts │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── bing/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── caiyun/ │ │ │ ├── View.tsx │ │ │ ├── _locales.ts │ │ │ ├── _style.shadow.scss │ │ │ ├── auth.ts │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── cambridge/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── cnki/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── cobuild/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── dictionaries.stories.tsx │ │ ├── etymonline/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── eudic/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── google/ │ │ │ ├── View.tsx │ │ │ ├── _locales.ts │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── googledict/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── guoyu/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── helpers.ts │ │ ├── hjdict/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── jikipedia/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── jukuu/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── lexico/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── liangan/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── locales.ts │ │ ├── longman/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── macmillan/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── merriamwebster/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── mojidict/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── naver/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── oaldict/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── renren/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── shanbay/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── sogou/ │ │ │ ├── View.tsx │ │ │ ├── _locales.ts │ │ │ ├── _style.shadow.scss │ │ │ ├── auth.ts │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── tencent/ │ │ │ ├── View.tsx │ │ │ ├── _locales.ts │ │ │ ├── _style.shadow.scss │ │ │ ├── auth.ts │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── urban/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── vocabulary/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── weblio/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── weblioejje/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── websterlearner/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── wikipedia/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── youdao/ │ │ │ ├── View.tsx │ │ │ ├── _locales.json │ │ │ ├── _style.shadow.scss │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ ├── youdaotrans/ │ │ │ ├── View.tsx │ │ │ ├── _locales.ts │ │ │ ├── _style.shadow.scss │ │ │ ├── auth.ts │ │ │ ├── config.ts │ │ │ └── engine.ts │ │ └── zdic/ │ │ ├── View.tsx │ │ ├── _locales.json │ │ ├── _style.shadow.scss │ │ ├── config.ts │ │ └── engine.ts │ ├── content/ │ │ ├── __fake__/ │ │ │ ├── env-instant-capture.ts │ │ │ ├── env-select-text.ts │ │ │ └── env.ts │ │ ├── _style.scss │ │ ├── components/ │ │ │ ├── DictItem/ │ │ │ │ ├── DictItem.scss │ │ │ │ ├── DictItem.stories.tsx │ │ │ │ ├── DictItem.tsx │ │ │ │ ├── DictItemBody.tsx │ │ │ │ ├── DictItemContent.shadow.scss │ │ │ │ ├── DictItemHead.scss │ │ │ │ └── DictItemHead.tsx │ │ │ ├── DictList/ │ │ │ │ ├── DictList.container.tsx │ │ │ │ ├── DictList.scss │ │ │ │ ├── DictList.stories.tsx │ │ │ │ └── DictList.tsx │ │ │ ├── DictPanel/ │ │ │ │ ├── DictPanel.container.tsx │ │ │ │ ├── DictPanel.portal.tsx │ │ │ │ ├── DictPanel.scss │ │ │ │ ├── DictPanel.shadow.scss │ │ │ │ ├── DictPanel.stories.tsx │ │ │ │ ├── DictPanel.tsx │ │ │ │ ├── DictPanelStandalone.container.tsx │ │ │ │ ├── DictPanelStandalone.scss │ │ │ │ └── DictPanelStandalone.tsx │ │ │ ├── MenuBar/ │ │ │ │ ├── MenuBar.container.tsx │ │ │ │ ├── MenuBar.scss │ │ │ │ ├── MenuBar.stories.tsx │ │ │ │ ├── MenuBar.tsx │ │ │ │ ├── MenubarBtns.scss │ │ │ │ ├── MenubarBtns.stories.tsx │ │ │ │ ├── MenubarBtns.tsx │ │ │ │ ├── Profiles.scss │ │ │ │ ├── Profiles.stories.tsx │ │ │ │ ├── Profiles.tsx │ │ │ │ ├── SearchBox.scss │ │ │ │ ├── SearchBox.stories.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ ├── Suggest.scss │ │ │ │ ├── Suggest.stories.tsx │ │ │ │ └── Suggest.tsx │ │ │ ├── MtaBox/ │ │ │ │ ├── MtaBox.container.tsx │ │ │ │ ├── MtaBox.scss │ │ │ │ ├── MtaBox.stories.tsx │ │ │ │ └── MtaBox.tsx │ │ │ ├── SaladBowl/ │ │ │ │ ├── SaladBowl.container.tsx │ │ │ │ ├── SaladBowl.portal.tsx │ │ │ │ ├── SaladBowl.shadow.scss │ │ │ │ ├── SaladBowl.stories.tsx │ │ │ │ └── SaladBowl.tsx │ │ │ ├── WaveformBox/ │ │ │ │ ├── WaveformBox.container.tsx │ │ │ │ ├── WaveformBox.scss │ │ │ │ ├── WaveformBox.stories.tsx │ │ │ │ └── WaveformBox.tsx │ │ │ └── WordEditor/ │ │ │ ├── CtxTransList.scss │ │ │ ├── CtxTransList.stories.tsx │ │ │ ├── CtxTransList.tsx │ │ │ ├── Notes.scss │ │ │ ├── Notes.tsx │ │ │ ├── WordCards.scss │ │ │ ├── WordCards.tsx │ │ │ ├── WordEditor.container.tsx │ │ │ ├── WordEditor.portal.tsx │ │ │ ├── WordEditor.scss │ │ │ ├── WordEditor.shadow.scss │ │ │ ├── WordEditor.stories.tsx │ │ │ ├── WordEditor.tsx │ │ │ ├── WordEditorPanel.scss │ │ │ ├── WordEditorPanel.stories.tsx │ │ │ ├── WordEditorPanel.tsx │ │ │ └── WordEditorStandalone.container.tsx │ │ ├── index.tsx │ │ └── redux/ │ │ ├── epics/ │ │ │ ├── index.ts │ │ │ ├── newSelection.epic.ts │ │ │ ├── searchStart.epic.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── init.ts │ │ └── modules/ │ │ ├── action-catalog.ts │ │ ├── action-handlers/ │ │ │ ├── index.ts │ │ │ ├── new-selection.ts │ │ │ ├── open-qs-panel.ts │ │ │ └── search-start.ts │ │ ├── index.ts │ │ └── state.ts │ ├── history/ │ │ ├── env.ts │ │ └── index.tsx │ ├── manifest/ │ │ ├── chrome.manifest.json │ │ ├── common.manifest.js │ │ ├── edge.manifest.json │ │ ├── firefox.manifest.json │ │ └── safari.manifest.json │ ├── notebook/ │ │ ├── env.ts │ │ └── index.tsx │ ├── options/ │ │ ├── __fake__/ │ │ │ └── env.ts │ │ ├── _style.scss │ │ ├── acknowledgement.ts │ │ ├── components/ │ │ │ ├── BtnPreview/ │ │ │ │ ├── PreviewIcon.tsx │ │ │ │ ├── _style.scss │ │ │ │ └── index.tsx │ │ │ ├── Entries/ │ │ │ │ ├── BlackWhiteList.tsx │ │ │ │ ├── ContextMenus/ │ │ │ │ │ ├── AddModal.tsx │ │ │ │ │ ├── EditeModal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── DictAuths.tsx │ │ │ │ ├── DictPanel.tsx │ │ │ │ ├── Dictionaries/ │ │ │ │ │ ├── AllDicts.tsx │ │ │ │ │ ├── DictTitle/ │ │ │ │ │ │ ├── _style.scss │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── EditModal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── General.tsx │ │ │ │ ├── ImportExport.tsx │ │ │ │ ├── Notebook/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── sync-services/ │ │ │ │ │ ├── ankiconnect.tsx │ │ │ │ │ ├── eudic.tsx │ │ │ │ │ ├── shanbay.tsx │ │ │ │ │ └── webdav.tsx │ │ │ │ ├── PDF.tsx │ │ │ │ ├── Permissions.tsx │ │ │ │ ├── Popup.tsx │ │ │ │ ├── Privacy.tsx │ │ │ │ ├── Profiles/ │ │ │ │ │ ├── EditNameModal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Pronunciation.tsx │ │ │ │ ├── QuickSearch/ │ │ │ │ │ ├── StandaloneModal.tsx │ │ │ │ │ ├── TitlebarOffsetModal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── SearchModes/ │ │ │ │ ├── index.tsx │ │ │ │ └── searchMode.tsx │ │ │ ├── EntryError.tsx │ │ │ ├── EntrySideBar/ │ │ │ │ ├── _style.scss │ │ │ │ └── index.tsx │ │ │ ├── Header/ │ │ │ │ ├── HeadInfo/ │ │ │ │ │ ├── AckList.tsx │ │ │ │ │ ├── _style.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── _style.scss │ │ │ │ └── index.tsx │ │ │ ├── InputNumberGroup/ │ │ │ │ ├── _style.scss │ │ │ │ └── index.tsx │ │ │ ├── MainEntry.tsx │ │ │ ├── MatchPatternModal/ │ │ │ │ ├── PatternItem.tsx │ │ │ │ └── index.tsx │ │ │ ├── SaladictForm/ │ │ │ │ ├── SaveBtn.tsx │ │ │ │ ├── _style.scss │ │ │ │ └── index.tsx │ │ │ ├── SaladictModalForm.tsx │ │ │ └── SortableList/ │ │ │ ├── _style.scss │ │ │ ├── index.tsx │ │ │ └── reorder.ts │ │ ├── env.ts │ │ ├── helpers/ │ │ │ ├── change-entry.ts │ │ │ ├── layout.ts │ │ │ ├── panel-store.ts │ │ │ ├── path-joiner.ts │ │ │ ├── upload.ts │ │ │ ├── use-check-dict-auth.ts │ │ │ └── use-form-dirty.ts │ │ └── index.tsx │ ├── popup/ │ │ ├── Notebook.tsx │ │ ├── Popup.tsx │ │ ├── __fake__/ │ │ │ ├── _style.scss │ │ │ └── env.ts │ │ ├── _style.scss │ │ ├── env.ts │ │ └── index.tsx │ ├── quick-search/ │ │ ├── env.ts │ │ ├── index.tsx │ │ └── quick-search.scss │ ├── selection/ │ │ ├── helper.ts │ │ ├── index.ts │ │ ├── instant-capture.ts │ │ ├── message.ts │ │ ├── quick-search.ts │ │ └── select-text.ts │ ├── typings/ │ │ ├── css.d.ts │ │ ├── global.d.ts │ │ ├── helpers.ts │ │ └── message.ts │ └── word-editor/ │ ├── env.ts │ ├── index.tsx │ └── word-editor.scss ├── test/ │ ├── helper.ts │ └── specs/ │ ├── _helpers/ │ │ ├── browser-api.spec.ts │ │ ├── check-update.spec.ts │ │ ├── chs-to-chz.spec.ts │ │ ├── lang-check.spec.ts │ │ ├── profile-manager.spec.ts │ │ └── promise-more.spec.ts │ ├── background/ │ │ ├── audio-manager.spec.ts │ │ ├── context-menus.spec.ts │ │ ├── initialization.spec.ts │ │ ├── pdf-sniffer.spec.ts │ │ └── sync-manager/ │ │ └── services/ │ │ ├── ankiconnect.spec.ts │ │ └── webdav.spec.ts │ └── components/ │ └── dictionaries/ │ ├── ahdict/ │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── bing/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── cambridge/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── cnki/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── cobuild/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── etymonline/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── eudic/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── googledict/ │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── guoyu/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── helpers.ts │ ├── hjdict/ │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── jikipedia/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── jukuu/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── lexico/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── liangan/ │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── longman/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── macmillan/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── merriamwebster/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ ├── requests.mock.ts │ │ └── testCases.ts │ ├── mojidict/ │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── naver/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── oaldict/ │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── renren/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── shanbay/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── urban/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── vocabulary/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── weblio/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── weblioejje/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── websterlearner/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── wikipedia/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ ├── youdao/ │ │ ├── engine.spec.ts │ │ ├── fixtures.js │ │ └── requests.mock.ts │ └── zdic/ │ ├── engine.spec.ts │ ├── fixtures.js │ └── requests.mock.ts ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ Firefox > 67 Chrome >= 63 ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { browser: true, node: true, 'jest/globals': true }, extends: [ 'standard', 'plugin:prettier/recommended', 'plugin:react/recommended' ], plugins: ['@typescript-eslint', 'jest'], parser: '@typescript-eslint/parser', rules: { '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/no-unused-vars': [ 'error', { args: 'none', ignoreRestSiblings: true } ], 'dot-notation': 'off', 'import/first': 'off', 'import/no-webpack-loader-syntax': 'off', 'no-dupe-class-members': 'off', 'no-unused-vars': 'off', 'no-useless-return': 'off', 'prefer-promise-reject-errors': 'off', 'prettier/prettier': ['error', { singleQuote: true, semi: false }], 'react/display-name': 'off', 'react/prop-types': 'off', 'standard/computed-property-even-spacing': 'off', 'standard/no-callback-literal': 'off', camelcase: 'off', yoda: 'off' }, globals: { browser: true }, settings: { react: { version: 'detect' } } } ================================================ FILE: .gitattributes ================================================ *.min.js binary /public/** binary # For Github language details /test/**/response/*.html linguist-vendored /public/** linguist-vendored ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: saladict open_collective: # Replace with a single Open Collective username ko_fi: saladict tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ['https://saladict.crimx.com/support.html'] ================================================ FILE: .github/ISSUE_TEMPLATE/_bug_report_chs.md ================================================ --- name: 反馈 Bug about: 沙拉查词运行出现不正确行为。 title: '' labels: '' assignees: '' --- ## 设备信息 - 操作系统: [] - 浏览器版本: [] - 沙拉查词版本: [] ## 描述问题 ## 复现步骤 ## 期待的正常行为 ## 截图 ## 额外信息 ================================================ FILE: .github/ISSUE_TEMPLATE/_feature_request_chs.md ================================================ --- name: 功能建议 about: 请求实现新功能或改进已有功能。 title: '' labels: '' assignees: '' --- ## 设备信息 - 操作系统: [] - 浏览器版本: [] - 沙拉查词版本: [] ## 请描述目前使用沙拉查词遇到什么不便 ## 理想情况下,沙拉查词应该怎么做 ## 替代方案 ## 额外信息 ================================================ FILE: .github/ISSUE_TEMPLATE/_new_dict_chs.md ================================================ --- name: 词典推荐 about: 请求沙拉查词添加新词典。 title: '' labels: '' assignees: '' --- ## 词典名称以及链接 ## 沙拉查词的已有的词典为什么不能满足? ## 单词举例 ## 截图 ## 额外信息 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Bug related issue. title: '' labels: '' assignees: '' --- ## Device info - OS: [e.g. Window10] - Browser Version [e.g. Chrome77] - Saladict Version [e.g. v7.0.0] ## Describe the bug ## To Reproduce 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ## Expected behavior ## Screenshots ## Additional context ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for Saladict title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** **Describe the solution you'd like** **Describe alternatives you've considered** **Additional context** ================================================ FILE: .github/config.yml ================================================ # Configuration for request-info - https://github.com/behaviorbot/request-info # *OPTIONAL* Comment to reply with # Can be either a string : requestInfoReplyComment: | 请填写模板描述问题,以便别人理解、定位和解决问题。 We would appreciate it if you could provide more info about this issue. # Or an array: # requestInfoReplyComment: # - Ah no! young blade! That was a trifle short! # - Tell me more ! # - I am sure you can be more effusive # *OPTIONAL* default titles to check against for lack of descriptiveness # MUST BE ALL LOWERCASE # requestInfoDefaultTitles: # - update readme.md # - updates # *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given requestInfoLabelToAdd: needs-more-info # *OPTIONAL* Require Issues to contain more information than what is provided in the issue templates # Will fail if the issue's body is equal to a provided template checkIssueTemplate: true # *OPTIONAL* Require Pull Requests to contain more information than what is provided in the PR template # Will fail if the pull request's body is equal to the provided template checkPullRequestTemplate: false # *OPTIONAL* Only warn about insufficient information on these events type # Keys must be lowercase. Valid values are 'issue' and 'pullRequest' requestInfoOn: pullRequest: false issue: true # *OPTIONAL* Add a list of people whose Issues/PRs will not be commented on # keys must be GitHub usernames # requestInfoUserstoExclude: # - hiimbex # - bexo ================================================ FILE: .github/no-response.yml ================================================ # Configuration for probot-no-response - https://github.com/probot/no-response # Number of days of inactivity before an Issue is closed for lack of response daysUntilClose: 14 # Label requiring a response responseRequiredLabel: needs-more-info # Comment to post when closing an Issue for lack of response. Set to `false` to disable closeComment: > This issue has been automatically closed because there has been no response to our request for more information from the original author. With only the information that is currently in the issue, we don't have enough information to take action. Please reach out if you have or find the answers we need so that we can investigate further. ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Neutrino build directory build assets/pdf/ deps/ test/**/response .idea .webext_tmp/* **/.DS_Store ================================================ FILE: .neutrinorc.js ================================================ const path = require('path') const fs = require('fs') const webpack = require('webpack') const react = require('@neutrinojs/react') const copy = require('@neutrinojs/copy') const jest = require('@neutrinojs/jest') const wext = require('neutrino-webextension') const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') const MomentLocalesPlugin = require('moment-locales-webpack-plugin') const dotenv = require('dotenv') const argv = require('yargs').argv const AfterBuildPlugin = require('./scripts/after-build') const svgToMiniDataURI = require('mini-svg-data-uri') const isAnalyze = argv.analyze || argv.analyse module.exports = { options: { mains: { content: { entry: 'content', webext: { type: 'content_scripts', manifest: { css: ['assets/content.css'], matches: [''] }, setup: 'content/__fake__/env.ts' } }, selection: { entry: 'selection', webext: { type: 'content_scripts', manifest: { match_about_blank: true, all_frames: true, matches: [''] } } }, popup: { entry: 'popup', webext: { type: 'browser_action', manifest: { default_icon: { '16': 'assets/icon-16.png', '19': 'assets/icon-19.png', '24': 'assets/icon-24.png', '38': 'assets/icon-38.png', '48': 'assets/icon-48.png', '128': 'assets/icon-128.png' } }, setup: 'popup/__fake__/env.ts' } }, options: { entry: 'options', webext: { type: 'options_ui', manifest: { open_in_tab: true }, setup: 'options/__fake__/env.ts' } }, background: { entry: 'background', webext: { type: 'background', setup: 'background/__fake__/env.ts' } }, notebook: { entry: 'notebook' }, history: { entry: 'history' }, 'quick-search': { entry: 'quick-search' }, 'word-editor': { entry: 'word-editor' }, 'audio-control': { entry: 'audio-control' } } }, use: [ react({ html: { title: 'Saladict' }, image: false, style: { test: /\.(css|scss)$/, modulesTest: /\.module\.(css|scss)$/, loaders: [ // Define loaders as objects. Note: loaders must be specified in reverse order. // ie: for the loaders below the actual execution order would be: // input file -> sass-loader -> postcss-loader -> css-loader -> style-loader/mini-css-extract-plugin { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] }, useId: 'postcss' }, { loader: 'sass-loader', useId: 'scss' }, { loader: 'sass-resources-loader', useId: 'sass-resources', options: { sourceMap: process.env.NODE_ENV !== 'production', resources: [ path.join(__dirname, 'src/_sass_shared/_namespace.scss'), ...fs .readdirSync(path.join(__dirname, 'src/_sass_shared/_global/')) .map(filename => path.join(__dirname, 'src/_sass_shared/_global/', filename) ) ] } } ] }, babel: { presets: [ [ '@babel/preset-env', { /* remove targets set by neutrino web preset preferring browserslistrc */ } ], [ '@babel/preset-typescript', { isTSX: true, allExtensions: true } ] ], plugins: [ [ 'import', { libraryName: 'antd' }, 'antd' ], [ 'import', { libraryName: '@ant-design/icons', libraryDirectory: '', camel2DashComponentName: false, style: false }, '@ant-design/icons' ] ] } }), copy({ patterns: [ { context: 'assets', from: '**/*', to: 'assets/', toType: 'dir' }, { context: 'src/_locales/manifest', from: '**/*', to: '_locales/', toType: 'dir' }, { context: 'node_modules/antd/dist/', from: '+(antd|antd.dark).min.css', to: 'assets/', toType: 'dir' }, // caiyunapp { context: 'node_modules/trsjs/build/sala', from: 'trs.js', to: 'assets/', toType: 'dir' } ] }), neutrino => { /* eslint-disable indent */ // images neutrino.config.module.rules .delete('image') .end() .rule('svg') .test(/\.(svg)(\?v=\d+\.\d+\.\d+)?$/) .use('svg-url') .loader(require.resolve('url-loader')) .options({ limit: 8192, // remove `default` when `require` image // due to legacy code esModule: false, generator: content => svgToMiniDataURI(content.toString()) }) .end() .end() .rule('pixel') .test(/\.(ico|png|jpg|jpeg|gif|webp)(\?v=\d+\.\d+\.\d+)?$/) .use('img-url') .loader(require.resolve('file-loader')) .options({ // dev-server image name collision name: resourcePath => { if (process.env.NODE_ENV === 'development') { return '[path]/[name].[ext]' } const dictMatch = /\/dictionaries\/([^/]+)\/favicon.png/.exec( resourcePath ) if (dictMatch) { return `assets/favicon-${dictMatch[1]}.[contenthash:8].[ext]` } return 'assets/[name].[contenthash:8].[ext]' }, limit: 0, esModule: false }) // avoid collision neutrino.config.output.jsonpFunction('saladictEntry') // transform *.shadow.(css|scss) to string // this will be injected into shadow-dom style tag // prettier-ignore const shadowStyleRules = neutrino.config.module .rule('style') .oneOf('shadow') .before('normal') .test(/\.shadow\.(css|scss)$/) .use('tostring') .loader('to-string-loader') .end() .use('minify') .after('css') .loader('clean-css-loader') .options({ level: 1, }) .end() // copy loaders from normal to shadow // prettier-ignore neutrino.config.module .rule('style') .oneOf('normal') .uses.values() .filter(rule => !/^(extract|style)$/.test(rule.name)) .forEach(rule => { shadowStyleRules .use(rule.name) .loader(rule.get('loader')) .options(rule.get('options')) }) // prettier-ignore neutrino.config .module .rule('compile') // add ts extensions for babel ect .test(/\.(mjs|jsx|js|ts|tsx)$/) .end() .end() .resolve .extensions // typescript extensions .add('.ts') .add('.tsx') .end() .alias // '@' src alias .set('@', path.join(__dirname, 'src')) .end() .end() // remove locales neutrino.config .plugin('momentjs') .use(MomentLocalesPlugin, [{ localesToKeep: ['zh-cn', 'zh-tw'] }]) .end() // prettier-ignore neutrino.config .plugin('process.env') .use(webpack.DefinePlugin, [{ 'process.env': JSON.stringify(Object.assign( { DEBUG: !!argv.debug }, dotenv.config().parsed )) }]) /* eslint-enable indent */ if (argv.mode === 'production') { // prettier-ignore neutrino.config .performance .hints(false) .end() .optimization .merge({ splitChunks: { cacheGroups: { react: { test: /[\\/]node_modules[\\/](react|react-dom|i18next)[\\/]/, name: 'view-vendor', chunks: 'all', priority: 100 }, franc: { test: /[\\/]node_modules[\\/]franc/, name: 'franc', chunks: 'all', priority: 100 }, dexie: { test: /[\\/]node_modules[\\/]dexie/, name: 'dexie', chunks: 'all', priority: 100 }, wordpage: { test: (module, chunks) => module.resource && module.resource.includes(`${path.sep}src${path.sep}`) && !module.resource.includes(`${path.sep}node_modules${path.sep}`), name: 'wordpage', chunks: ({ name }) => /^(notebook|history)$/.test(name), }, antd: { test: /[\\/]node_modules[\\/]/, name: 'antd', chunks: ({ name }) => /^(options|notebook|history)$/.test(name), } } }, }) } if (argv.debug) { // prettier-ignore neutrino.config .devtool('inline-source-map') .optimization .minimize(false) } if (isAnalyze) { // prettier-ignore neutrino.config .plugin('bundle-analyze') .use(BundleAnalyzerPlugin); } }, jest({ testRegex: ['test/specs/.*\\.spec\\.(ts|tsx|js|jsx)'], setupFilesAfterEnv: ['/config/jest/setupTests.js'], moduleNameMapper: { '^@/(.*)$': '/src/$1' }, transform: { '\\.(mjs|jsx|js|ts|tsx)$': require.resolve( '@neutrinojs/jest/src/transformer' ) }, testTimeout: 20000 }), wext({ polyfill: true }), neutrino => { // prettier-ignore neutrino.config .plugin('after-build') .use(AfterBuildPlugin); } ] } ================================================ FILE: .prettierrc ================================================ tabWidth: 2 semi: false singleQuote: true ================================================ FILE: .storybook/addons.ts ================================================ import '@storybook/addon-knobs/register' import '@storybook/addon-contexts/register' import '@storybook/addon-actions/register' import '@storybook/addon-backgrounds/register' import 'storybook-addon-jsx/register' import 'storybook-addon-react-docgen/register' import addons from '@storybook/addons' import { STORY_RENDERED } from '@storybook/core-events' addons.register('TitleAddon', api => { api.on(STORY_RENDERED, () => { const storyData = api.getCurrentStoryData() document.title = `${storyData.name} - Saladict Storybook` }) }) ================================================ FILE: .storybook/config.ts ================================================ import { configure, addDecorator, StoryDecorator, addParameters } from '@storybook/react' import { withContexts } from '@storybook/addon-contexts/react' import { i18nContexts } from './configs/contexts' import { StyleWrap } from '../src/_helpers/storybook' import './style.css' addParameters({ options: { // bug https://github.com/storybookjs/storybook/issues/6569 enableShortcuts: false }, props: { propTablesExclude: [StyleWrap], styles: styles => ({ ...styles, infoBody: { ...styles.infoBody, marginTop: 0, padding: '0 40px' }, propTableHead: { ...styles.propTableHead, margin: 0 }, h1: { display: 'none' } }) }, jsx: { functionValue: (fn: Function) => `${fn.name}()` } }) // place after the info addon so that wrappers get removed addDecorator(withContexts(i18nContexts) as StoryDecorator) function loadStories() { const req = require.context('../src', true, /\.stories\.tsx$/) let files = req.keys() if (process.env.STORYBOOK_PATH_PATTERN) { const tester = new RegExp(process.env.STORYBOOK_PATH_PATTERN) files = files.filter(filename => tester.test(filename)) } files.forEach(filename => req(filename)) } configure(loadStories, module) ================================================ FILE: .storybook/configs/contexts.tsx ================================================ import React, { FC, useContext, useEffect } from 'react' import { I18nContextProvider, I18nContext, i18nLoader } from '../../src/_helpers/i18n' import i18next from 'i18next' interface I18nWrapProps { lang: string } const I18nWrapInner: FC = props => { const lang = useContext(I18nContext) useEffect(() => { if (lang) { if (lang && props.lang !== lang) { i18next.changeLanguage(props.lang) } } else { i18nLoader() } }, [lang, props.lang]) return <>{props.children} } const I18nWrap: FC = props => ( {props.children} ) export const i18nContexts = [ { // https://storybooks-official.netlify.com/?path=/story/basics-icon--labels icon: 'globe', title: 'i18n', components: [I18nWrap], params: [ { name: 'English', props: { lang: 'en' }, default: 'en' === navigator.language }, { name: '简体中文', props: { lang: 'zh-CN' }, default: 'zh-CN' === navigator.language }, { name: '繁体中文', props: { lang: 'zh-TW' }, default: 'zh-TW' === navigator.language } ] } ] ================================================ FILE: .storybook/manager-head.html ================================================ ================================================ FILE: .storybook/preview-head.html ================================================ ================================================ FILE: .storybook/style.css ================================================ body { width: unset; height: unset; overflow-y: scroll; margin: 0; padding: 0; box-sizing: border-box; } ================================================ FILE: .storybook/webpack.config.js ================================================ const path = require('path') const fs = require('fs') const Neutrino = require('neutrino/Neutrino') const neutrinorc = require('../.neutrinorc.js') const neutrino = new Neutrino(neutrinorc.options) neutrinorc.use.forEach(middleware => neutrino.use(middleware)) const babelOptions = neutrino.config.module .rule('compile') .use('babel') .get('options') // babelOptions.plugins.push([ // 'babel-plugin-react-docgen-typescript', // { // docgenCollectionName: 'STORYBOOK_REACT_CLASSES', // include: 'components.*\\.tsx$', // exclude: '__mocks__|(\\.stories\\.tsx$)' // } // ]) const sassGlobals = [ path.join(__dirname, '../src/_sass_shared/_namespace.scss'), ...fs .readdirSync(path.join(__dirname, '../src/_sass_shared/_global/')) .map(filename => path.join(__dirname, '../src/_sass_shared/_global/', filename)) ] module.exports = ({ config }) => { config.module.rules.push({ test: /\.mjs$/, type: 'javascript/auto' }) config.module.rules.push({ test: /\.(ts|tsx)$/, use: [ { loader: require.resolve('babel-loader'), options: babelOptions }, { loader: require.resolve('react-docgen-typescript-loader'), options: { tsconfigPath: path.join(__dirname, '../tsconfig.json') } } ] }) config.module.rules.push({ oneOf: [ { test: /\.module\.(css|scss)$/, use: [ 'to-string-loader', { loader: 'css-loader', options: { importLoaders: 2, modules: true } }, { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } }, 'sass-loader' ], include: path.resolve(__dirname, '../src') }, { test: /\.shadow\.(css|scss)$/, use: [ 'to-string-loader', { loader: 'css-loader', options: { importLoaders: 2 } }, { loader: 'clean-css-loader', options: { level: 1 } }, { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } }, 'sass-loader', { loader: 'sass-resources-loader', options: { sourceMap: true, resources: sassGlobals } } ], include: path.resolve(__dirname, '../src') }, { test: /\.(css|scss)$/, use: [ 'to-string-loader', { loader: 'css-loader', options: { importLoaders: 2 } }, { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } }, 'sass-loader', { loader: 'sass-resources-loader', options: { sourceMap: true, resources: sassGlobals } } ], include: path.resolve(__dirname, '../src') } ] }) if (Array.isArray(config.entry)) { config.entry.unshift('webextensions-emulator/dist/core') } else { Object.keys(config.entry).forEach(id => { if (!Array.isArray(config.entry[id])) { config.entry[id] = [config.entry[id]] } config.entry[id].unshift('webextensions-emulator/dist/core') }) } config.resolve.extensions.push('.ts', '.tsx') config.resolve.alias['@'] = path.join(__dirname, '../src') config.resolve.alias['@sb'] = path.join(__dirname) return config } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 'stable' script: - yarn lint - yarn build # remove agnostic tests - yarn test --testPathIgnorePatterns 'components/dictionaries' ================================================ FILE: .vscode/locales.schema.json ================================================ { "type": "object", "properties": { "name": { "$ref": "#/definitions/locale" }, "options": { "$ref": "#/definitions/locales" }, "helps": { "$ref": "#/definitions/locales" } }, "required": ["name"], "additionalProperties": false, "definitions": { "locale": { "type": "object", "properties": { "en": { "type": "string" }, "zh-CN": { "type": "string" }, "zh-TW": { "type": "string" } }, "required": ["en", "zh-CN", "zh-TW"], "additionalProperties": false }, "locales": { "type": "object", "patternProperties": { ".+": { "$ref": "#/definitions/locale" } } } } } ================================================ FILE: .vscode/settings.json ================================================ { "json.schemas": [ { "fileMatch": ["src/components/dictionaries/**/_locales.json"], "url": ".vscode/locales.schema.json" } ], "files.watcherExclude": { "**/.git/objects/**": true, "**/.git/subtree-cache/**": true, "**/node_modules/*/**": true, "**/build/*/**": true, "**/assets/*/**": true }, "conventionalCommits.scopes": [ "audio-control", "background", "components", "config", "dicts", "history", "locales", "notebook", "options", "panel", "popup", "selecion", "sync-services", "word-editor" ] } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. ## [7.20.0](https://github.com/crimx/ext-saladict/compare/v7.19.1...v7.20.0) (2021-10-17) ### Features * **pdf:** inject vimium-c ([#1462](https://github.com/crimx/ext-saladict/issues/1462)) ([#1463](https://github.com/crimx/ext-saladict/issues/1463)) ([029c07a](https://github.com/crimx/ext-saladict/commit/029c07a0801177bf9d8137163fc43f70fe1e7a30)) * **sync-services:** add eudic ([#1467](https://github.com/crimx/ext-saladict/issues/1467)) ([452bf53](https://github.com/crimx/ext-saladict/commit/452bf537a4aee7d1e3d79b7960cd5183cdff88af)) * **wordpage:** add context cloze ([b910769](https://github.com/crimx/ext-saladict/commit/b91076941940dc82961830f43dcb9ee081596aa4)) * add Oxford Learner's Dict ([#1458](https://github.com/crimx/ext-saladict/issues/1458)) ([aaffe00](https://github.com/crimx/ext-saladict/commit/aaffe00d760adb44676042142a5ce2af34ce5001)), closes [#1253](https://github.com/crimx/ext-saladict/issues/1253) ### Bug Fixes * **dictpanel:** remove waveform box if option is off ([e979c1c](https://github.com/crimx/ext-saladict/commit/e979c1c62fb022f23e4e3edc600e5d0abee830c1)) * **dicts:** fix srcset protocol ([21a0032](https://github.com/crimx/ext-saladict/commit/21a0032a3acb6b25cc5444409218b689176a9cb1)), closes [#1366](https://github.com/crimx/ext-saladict/issues/1366) * **hot-words:** remove daily hot words of urban dict ([#1428](https://github.com/crimx/ext-saladict/issues/1428)) ([5dc29cd](https://github.com/crimx/ext-saladict/commit/5dc29cd61fc181929804dcf4d1cd4337289a062b)) * **notebook:** make export panel textarea background transparent ([09bddc0](https://github.com/crimx/ext-saladict/commit/09bddc0579f7844b4688aca4e6e60e409407d292)) * **panel:** pin panel by default ([e4ee931](https://github.com/crimx/ext-saladict/commit/e4ee931efe04f13946a1883425ced8fca7f52931)), closes [#1505](https://github.com/crimx/ext-saladict/issues/1505) * **selecion:** check range count before getting range ([dfc46a0](https://github.com/crimx/ext-saladict/commit/dfc46a0378b3c71bbc9f0febf5d2b0058fdb3826)), closes [#1144](https://github.com/crimx/ext-saladict/issues/1144) ### [7.19.1](https://github.com/crimx/ext-saladict/compare/v7.19.0...v7.19.1) (2021-07-25) ### Bug Fixes * **no-typefield:** add support for content editable ([#1334](https://github.com/crimx/ext-saladict/issues/1334)) ([7984991](https://github.com/crimx/ext-saladict/commit/7984991e0e7adfb0b5fa16d604ffa3da6be65a40)) * **pdf:** update pdf dir ([a8df19f](https://github.com/crimx/ext-saladict/commit/a8df19f12bcc5000eedaf40f6c0d3e0438386371)) ## [7.19.0](https://github.com/crimx/ext-saladict/compare/v7.18.2...v7.19.0) (2021-05-23) ### Features * **dict-panel:** add a option to pin panel by default ([#1296](https://github.com/crimx/ext-saladict/issues/1296)) ([bbaeb76](https://github.com/crimx/ext-saladict/commit/bbaeb76c77d62e18e3d6a0af52d7bad983dbcd69)) ### Bug Fixes * **fixtures:** cnki url ([0fd0125](https://github.com/crimx/ext-saladict/commit/0fd012596c4e82e5acf7fa91469bdb78209fbbbc)) ### [7.18.2](https://github.com/crimx/ext-saladict/compare/v7.18.1...v7.18.2) (2021-05-23) ### Bug Fixes * **dict-panel:** move root el to document element ([aaeae1c](https://github.com/crimx/ext-saladict/commit/aaeae1cc6f4ea9ad43e2042b8b670f8480aaefb1)), closes [#1293](https://github.com/crimx/ext-saladict/issues/1293) [#1190](https://github.com/crimx/ext-saladict/issues/1190) [#474](https://github.com/crimx/ext-saladict/issues/474) [#421](https://github.com/crimx/ext-saladict/issues/421) [#398](https://github.com/crimx/ext-saladict/issues/398) [#278](https://github.com/crimx/ext-saladict/issues/278) * **dict-panel:** prevent input method conflict on first input ([e4dda57](https://github.com/crimx/ext-saladict/commit/e4dda573263f77ad9bfbf10c577971b9a9631fb8)), closes [#1149](https://github.com/crimx/ext-saladict/issues/1149) * **dicts:** fix zdic icon ([9aea435](https://github.com/crimx/ext-saladict/commit/9aea435fc8f8141d448d2f8309fef3d9872ea75a)), closes [#1244](https://github.com/crimx/ext-saladict/issues/1244) ### [7.18.1](https://github.com/crimx/ext-saladict/compare/v7.18.0...v7.18.1) (2021-01-18) ### Bug Fixes * **fixtures:** read url of undefined ([52e209d](https://github.com/crimx/ext-saladict/commit/52e209d2ca55715bdc5f574ae9f10420ce87306b)) ## [7.18.0](https://github.com/crimx/ext-saladict/compare/v7.17.0...v7.18.0) (2020-11-02) ### Features * **panel:** add shortcut for switch search history ([0a61c49](https://github.com/crimx/ext-saladict/commit/0a61c4982a0232daaecf9c9c5573858a2a2cd1a1)), closes [#1063](https://github.com/crimx/ext-saladict/issues/1063) * add options for browser action panel width and height ([fab67dd](https://github.com/crimx/ext-saladict/commit/fab67dd5d82a1fc13d15886d92b5e03f518c434f)), closes [#983](https://github.com/crimx/ext-saladict/issues/983) ### Bug Fixes * **background:** query error on http url ([1922c09](https://github.com/crimx/ext-saladict/commit/1922c0914f46c306ce905d87b298b5ccd7e15e5a)) * **dicts:** sogou access token is required Closes [#1011](https://github.com/crimx/ext-saladict/issues/1011) ([a449119](https://github.com/crimx/ext-saladict/commit/a449119ae1643b2cb19b23e68aa1a998382c6cb9)) * **options:** hide unsupported features on Firefox ([516e030](https://github.com/crimx/ext-saladict/commit/516e03048525f426ac9145db96866fe4673ee25c)), closes [#1062](https://github.com/crimx/ext-saladict/issues/1062) ## [7.17.0](https://github.com/crimx/ext-saladict/compare/v7.15.1...v7.17.0) (2020-09-08) ### Features * add lingocloud browser shortcut ([badd839](https://github.com/crimx/ext-saladict/commit/badd839a0c40602a79337b646a0b6baa6f66f834)) * add Lingocloud trs ([fe75c7a](https://github.com/crimx/ext-saladict/commit/fe75c7a6c9431aeeaa63138b4fd1c1eff153124f)) ### Bug Fixes * **dicts:** add google tts ([bbbe0a1](https://github.com/crimx/ext-saladict/commit/bbbe0a10a6def4608ff850e849005997abb37ad6)) * **options:** fix dict title overflows on small screens ([54d4fe3](https://github.com/crimx/ext-saladict/commit/54d4fe30ddd0a62f0c41bc7423bf0eb3c3a9309e)) * **options:** replace react-sortable-hoc with react-beautiful-dnd ([7e3d0c7](https://github.com/crimx/ext-saladict/commit/7e3d0c744f1d1bc18c8778460f811f2a55d029a3)), closes [#966](https://github.com/crimx/ext-saladict/issues/966) * **panel:** remove text loading delay on standalone panel ([42827bb](https://github.com/crimx/ext-saladict/commit/42827bb30499c7b5788be9cb10f6c9f4cf191508)), closes [#974](https://github.com/crimx/ext-saladict/issues/974) * **panel:** simplify summoned panel initialization ([ae322be](https://github.com/crimx/ext-saladict/commit/ae322beb688414264ce6a2a4b958e69392ab4c2c)) * **popup:** close popup panel after menus being triggered ([931afc2](https://github.com/crimx/ext-saladict/commit/931afc279ce6f9ff1e4ad211eccf2fd86f95f330)) ### [7.16.1](https://github.com/crimx/ext-saladict/compare/v7.15.1...v7.16.1) (2020-09-05) ### Features * add lingocloud browser shortcut ([badd839](https://github.com/crimx/ext-saladict/commit/badd839a0c40602a79337b646a0b6baa6f66f834)) * add Lingocloud trs ([fe75c7a](https://github.com/crimx/ext-saladict/commit/fe75c7a6c9431aeeaa63138b4fd1c1eff153124f)) ### Bug Fixes * **dicts:** add google tts ([bbbe0a1](https://github.com/crimx/ext-saladict/commit/bbbe0a10a6def4608ff850e849005997abb37ad6)) * **options:** fix dict title overflows on small screens ([54d4fe3](https://github.com/crimx/ext-saladict/commit/54d4fe30ddd0a62f0c41bc7423bf0eb3c3a9309e)) * **options:** replace react-sortable-hoc with react-beautiful-dnd ([7e3d0c7](https://github.com/crimx/ext-saladict/commit/7e3d0c744f1d1bc18c8778460f811f2a55d029a3)), closes [#966](https://github.com/crimx/ext-saladict/issues/966) * **panel:** remove text loading delay on standalone panel ([42827bb](https://github.com/crimx/ext-saladict/commit/42827bb30499c7b5788be9cb10f6c9f4cf191508)), closes [#974](https://github.com/crimx/ext-saladict/issues/974) * **popup:** close popup panel after menus being triggered ([931afc2](https://github.com/crimx/ext-saladict/commit/931afc279ce6f9ff1e4ad211eccf2fd86f95f330)) ## [7.16.0](https://github.com/crimx/ext-saladict/compare/v7.15.1...v7.16.0) (2020-09-04) ### Features * add lingocloud browser shortcut ([badd839](https://github.com/crimx/ext-saladict/commit/badd839a0c40602a79337b646a0b6baa6f66f834)) * add Lingocloud trs ([fe75c7a](https://github.com/crimx/ext-saladict/commit/fe75c7a6c9431aeeaa63138b4fd1c1eff153124f)) ### Bug Fixes * **dicts:** add google tts ([bbbe0a1](https://github.com/crimx/ext-saladict/commit/bbbe0a10a6def4608ff850e849005997abb37ad6)) * **options:** fix dict title overflows on small screens ([54d4fe3](https://github.com/crimx/ext-saladict/commit/54d4fe30ddd0a62f0c41bc7423bf0eb3c3a9309e)) * **options:** replace react-sortable-hoc with react-beautiful-dnd ([7e3d0c7](https://github.com/crimx/ext-saladict/commit/7e3d0c744f1d1bc18c8778460f811f2a55d029a3)), closes [#966](https://github.com/crimx/ext-saladict/issues/966) * **panel:** remove text loading delay on standalone panel ([42827bb](https://github.com/crimx/ext-saladict/commit/42827bb30499c7b5788be9cb10f6c9f4cf191508)), closes [#974](https://github.com/crimx/ext-saladict/issues/974) * **popup:** close popup panel after menus being triggered ([931afc2](https://github.com/crimx/ext-saladict/commit/931afc279ce6f9ff1e4ad211eccf2fd86f95f330)) ### [7.15.1](https://github.com/crimx/ext-saladict/compare/v7.15.0...v7.15.1) (2020-08-07) ## [7.15.0](https://github.com/crimx/ext-saladict/compare/v7.14.5...v7.15.0) (2020-08-07) ### Features * **sync-services:** add sync server to ankiconnect ([b6a7487](https://github.com/crimx/ext-saladict/commit/b6a74873b71282805892e8172961dec1a77e13bb)) ### Bug Fixes * **background:** remove background permission on Opera ([151b0a1](https://github.com/crimx/ext-saladict/commit/151b0a16320ff7ab875ad970525b358459f733a3)), closes [#916](https://github.com/crimx/ext-saladict/issues/916) * **dicts:** cambridge amp-img replacement ([1aed3f4](https://github.com/crimx/ext-saladict/commit/1aed3f41f24e0b91e5bf9b20ab11a3f38967d8a7)), closes [#939](https://github.com/crimx/ext-saladict/issues/939) * **dicts:** cambridge idiom-only entry ([0135b3e](https://github.com/crimx/ext-saladict/commit/0135b3e84ec85af382e2e94470b5c1705e1e830e)), closes [#940](https://github.com/crimx/ext-saladict/issues/940) * **dicts:** replace cambridge amp-audio ([aac184c](https://github.com/crimx/ext-saladict/commit/aac184cd9d9ddb035f8331457baf8b0a160a3224)), closes [#943](https://github.com/crimx/ext-saladict/issues/943) * **dicts:** same url for src page ([a0696e1](https://github.com/crimx/ext-saladict/commit/a0696e1709dd700d20d182c0f277025090a14608)), closes [#935](https://github.com/crimx/ext-saladict/issues/935) * **locales:** typo ([8dcb8e1](https://github.com/crimx/ext-saladict/commit/8dcb8e11cf5f9e40dd424fa332525329d3385ca4)) * **panel:** reset opacity on root container ([40abbbc](https://github.com/crimx/ext-saladict/commit/40abbbc884f6ac3fba44e9423e9d4887048e91e0)), closes [#904](https://github.com/crimx/ext-saladict/issues/904) * **panel:** search box on in-page panel loses focus ([c1d5984](https://github.com/crimx/ext-saladict/commit/c1d598473d02dc247c218d48e183bfa58251def2)), closes [#927](https://github.com/crimx/ext-saladict/issues/927) * **panel:** select background color ([2a9c144](https://github.com/crimx/ext-saladict/commit/2a9c14484d56a2815bb9fb09d98f361e41616f52)) * **panel:** support Super Dark Mode ([6e6164e](https://github.com/crimx/ext-saladict/commit/6e6164eeb9ec1f5eeff80bad3f3b9715a3627447)), closes [#947](https://github.com/crimx/ext-saladict/issues/947) * **sync-services:** shanbay batch upload interrupting ([5881389](https://github.com/crimx/ext-saladict/commit/5881389b05823258383ba7448eb836e6cb59bc30)), closes [#932](https://github.com/crimx/ext-saladict/issues/932) * **word-editor:** correct container dimension ([0b51e6b](https://github.com/crimx/ext-saladict/commit/0b51e6bb0026d363f728cde758e299219580c791)) ### Build System * **deps-dev:** bump standard-version from 6.0.1 to 8.0.1 ([#903](https://github.com/crimx/ext-saladict/issues/903)) ([d1ac2b5](https://github.com/crimx/ext-saladict/commit/d1ac2b57d576f4eaaa94f3a8a6595b44a28b76f1)) ### [7.14.5](https://github.com/crimx/ext-saladict/compare/v7.14.4...v7.14.5) (2020-07-12) ### Bug Fixes * **panel:** set missing initial config ([791cb57](https://github.com/crimx/ext-saladict/commit/791cb57cbd40264b255da03576693af3a15daf24)) ### [7.14.4](https://github.com/crimx/ext-saladict/compare/v7.14.3...v7.14.4) (2020-07-12) ### Bug Fixes * **dicts:** correct machine translator rtl source text collapse fading ([42003e5](https://github.com/crimx/ext-saladict/commit/42003e51c0c3778c3ecbc0d6771142baae720ab9)) ### Build System * always strip momentjs locales ([7dbb309](https://github.com/crimx/ext-saladict/commit/7dbb309bbe3caafc70348999ca5e4526fd649618)) ### [7.14.3](https://github.com/crimx/ext-saladict/compare/v7.14.2...v7.14.3) (2020-07-12) ### Bug Fixes * **panel:** fix machine source text fade color ([b36deb6](https://github.com/crimx/ext-saladict/commit/b36deb68df02254da2577cad4b9eb65722e5155a)) ### [7.14.2](https://github.com/crimx/ext-saladict/compare/v7.14.1...v7.14.2) (2020-07-12) ### Bug Fixes * **panel:** move saladict-theme down under darkMode ([52d8fb2](https://github.com/crimx/ext-saladict/commit/52d8fb20dd9d53a83b0c1f807102220977aa9e0d)) ### [7.14.1](https://github.com/crimx/ext-saladict/compare/v7.14.0...v7.14.1) (2020-07-12) ### Bug Fixes * **options:** update antd typings ([2879c4f](https://github.com/crimx/ext-saladict/commit/2879c4fe9e1ef6a5397ac807ce94a2d188d61d30)) * fixed incorrect options merging ([8e062dc](https://github.com/crimx/ext-saladict/commit/8e062dc7dc87642da282bcceeabbaf86d58770f4)) * switch default slInitial back to collapse ([524223c](https://github.com/crimx/ext-saladict/commit/524223c4a2cfafd5a0a8d0a2eb6fa494926e5099)) ### Build System * add sass globals to storybook ([f44ce5a](https://github.com/crimx/ext-saladict/commit/f44ce5a6bd54b2f3346d63934151db6aa76789d0)) ## [7.14.0](https://github.com/crimx/ext-saladict/compare/v7.13.4...v7.14.0) (2020-07-10) ### Features * **background:** add background permission ([9d41e09](https://github.com/crimx/ext-saladict/commit/9d41e0980e17dffbcce2804a66b850f9e33c147e)), closes [#892](https://github.com/crimx/ext-saladict/issues/892) * **menus:** add saladict standlone panel ([0d4a732](https://github.com/crimx/ext-saladict/commit/0d4a73278a523405b1ab1d28ade9d514826ecd4a)), closes [#864](https://github.com/crimx/ext-saladict/issues/864) * **panel:** add dict item catalog ([f07ea25](https://github.com/crimx/ext-saladict/commit/f07ea25751fcb746363e09b3804da03520b35249)) ### Bug Fixes * **components:** fix select padding in firefox ([dd366d3](https://github.com/crimx/ext-saladict/commit/dd366d396e8ca63298aeb70a575dfaad858f75b7)) * **components:** typo ([0229cc2](https://github.com/crimx/ext-saladict/commit/0229cc28124567cbe6a79c71d474c935da094756)) * **dicts:** update tts for tencent and caiyun ([afdac41](https://github.com/crimx/ext-saladict/commit/afdac41d4099aef1efa6021c60b741e3ad6fda22)) * **options:** update sortable list on store changes ([075aef7](https://github.com/crimx/ext-saladict/commit/075aef764d8327f78288e0d6d183ae51f9f82f48)) * **panel:** correct panel history ([1b2781f](https://github.com/crimx/ext-saladict/commit/1b2781f48e875f07536be14a733e93a901db5958)), closes [#881](https://github.com/crimx/ext-saladict/issues/881) * **panel:** fix catalog scrolling ([ded090d](https://github.com/crimx/ext-saladict/commit/ded090d81b7d2bd66b0c12806a1c3fa457257328)) ### Tests * **panel:** update dict item stories ([507c638](https://github.com/crimx/ext-saladict/commit/507c638ba34a9b903cbee06b36056ede15c66a2d)) * update stories ([49b2ad2](https://github.com/crimx/ext-saladict/commit/49b2ad2eb68e0b39117b224ad1c05264b8ed678f)) * **dicts:** log runtime messages ([5c23327](https://github.com/crimx/ext-saladict/commit/5c23327db3f6b44f25487b3cc0866ab38741adc9)) ### [7.13.4](https://github.com/crimx/ext-saladict/compare/v7.13.3...v7.13.4) (2020-06-23) ### Bug Fixes * **options:** entry incorrect initial form state ([f009670](https://github.com/crimx/ext-saladict/commit/f0096707ba14425e3c4a60a9548ddad3adf8f3e0)), closes [#865](https://github.com/crimx/ext-saladict/issues/865) ### [7.13.3](https://github.com/crimx/ext-saladict/compare/v7.13.2...v7.13.3) (2020-06-20) ### Bug Fixes * **dicts:** add macmillan american ([0c63217](https://github.com/crimx/ext-saladict/commit/0c632178733dd32cbd0a5aaab2aad31207a237b1)), closes [#837](https://github.com/crimx/ext-saladict/issues/837) * **dicts:** update cnki params ([952702c](https://github.com/crimx/ext-saladict/commit/952702c57946c7058291a062163511594b7c5d57)), closes [#852](https://github.com/crimx/ext-saladict/issues/852) ### Build System * upgrade deps ([549983a](https://github.com/crimx/ext-saladict/commit/549983ac77d58de9471c8cf01a29ff95b4616e8f)) ### [7.13.2](https://github.com/crimx/ext-saladict/compare/v7.13.1...v7.13.2) (2020-06-04) ### Bug Fixes * **sync-services:** fix anki returning random order of field names ([227089c](https://github.com/crimx/ext-saladict/commit/227089c8f713b32bafd512a74c5423ea0a7b5673)) ### [7.13.1](https://github.com/crimx/ext-saladict/compare/v7.13.0...v7.13.1) (2020-06-02) ### Bug Fixes * **ankiconnect:** compatible with anki localization ([f1ce54c](https://github.com/crimx/ext-saladict/commit/f1ce54c732c4a3ed04b8b881c1df3d4810c65f62)) * remove unused ([f0d203c](https://github.com/crimx/ext-saladict/commit/f0d203c84f107762d7b753593851d4ffb417c310)) ## [7.13.0](https://github.com/crimx/ext-saladict/compare/v7.12.1...v7.13.0) (2020-06-01) ### Features * **panel:** add option for panel size and position memo ([bce3bfb](https://github.com/crimx/ext-saladict/commit/bce3bfbaeee386529232d261025844b187ca43e8)), closes [#812](https://github.com/crimx/ext-saladict/issues/812) * **sync-services:** add ankiconnect ([cd12702](https://github.com/crimx/ext-saladict/commit/cd127027a66909903f91c1f17ed7173428a0ecfd)) ### Bug Fixes * **dicts:** remove horizontal scroll ([7ebf09d](https://github.com/crimx/ext-saladict/commit/7ebf09d224e06df08750cdbde6f6cd31034875c3)), closes [#818](https://github.com/crimx/ext-saladict/issues/818) * **dicts:** remove lexico associated translation ([81d2cc7](https://github.com/crimx/ext-saladict/commit/81d2cc7a811a249b4ef7350fe9f31e95d1a7ce78)), closes [#818](https://github.com/crimx/ext-saladict/issues/818) * **i18n:** make loader singleton ([6539fc7](https://github.com/crimx/ext-saladict/commit/6539fc78debda645c4d3524ac42c146ee20e4d00)) * **options:** add key to react component ([09a7d32](https://github.com/crimx/ext-saladict/commit/09a7d3277943aafae090e172e5046bca72181ee0)) * **panel:** open standalone panel anyway ([88259b0](https://github.com/crimx/ext-saladict/commit/88259b0893967a0e993d1c8102043378cd27c9c7)), closes [#832](https://github.com/crimx/ext-saladict/issues/832) * **panel:** update fav icon after saving words ([c803998](https://github.com/crimx/ext-saladict/commit/c803998132dbc71f78d4a29730784e9446035f58)) * **sync-services:** add version on request ([f84f359](https://github.com/crimx/ext-saladict/commit/f84f3590c577b43e67ced19382565692b6cdc67c)) * **word-editor:** translate context when word editor shows up ([95bf129](https://github.com/crimx/ext-saladict/commit/95bf129dbfc97a9992c62e84cf7be27ce294eecc)) * stop playing audio on panel close ([97cabf4](https://github.com/crimx/ext-saladict/commit/97cabf49e7aca7754edde247003fbcb4ea42dd59)), closes [#824](https://github.com/crimx/ext-saladict/issues/824) * **wordpage:** dark mode ([5921673](https://github.com/crimx/ext-saladict/commit/59216735ab2f88e9bdc9f6b8adae6e4cb4e7d93c)) ### Tests * **sync-services:** add Anki Connect ([1fb55e8](https://github.com/crimx/ext-saladict/commit/1fb55e83b58354e8449ed0b6353e591f4c47e779)) * **sync-services:** update webdav to new architecture ([d98c16c](https://github.com/crimx/ext-saladict/commit/d98c16cdfca16a5f2c0df4a7ed78e75b4441c8cd)) ### [7.12.1](https://github.com/crimx/ext-saladict/compare/v7.12.0...v7.12.1) (2020-05-17) ### Bug Fixes * **dicts:** update googledict style ([52e66df](https://github.com/crimx/ext-saladict/commit/52e66dfe282b74bc21a7bec4e91bf8a66a34f0cf)) * **macmillan:** add styles on labels ([768ba78](https://github.com/crimx/ext-saladict/commit/768ba7851d7e22517a3d4a23dad135c103f229ab)), closes [#803](https://github.com/crimx/ext-saladict/issues/803) ## [7.12.0](https://github.com/crimx/ext-saladict/compare/v7.11.2...v7.12.0) (2020-05-15) ### Features * **command:** add shortcut for adding notebook ([524dd6c](https://github.com/crimx/ext-saladict/commit/524dd6c7a250c415a865f1587c79b09bef7cbc7c)), closes [#785](https://github.com/crimx/ext-saladict/issues/785) * **pdf:** open pdf viewer in standalone panel ([07f8c71](https://github.com/crimx/ext-saladict/commit/07f8c71195d1c0cf41f0592115921d226bebef07)) * **selection:** add altKey for search modes ([fdc2ba5](https://github.com/crimx/ext-saladict/commit/fdc2ba56ad63d278668633c507a1e0c3a11070eb)), closes [#729](https://github.com/crimx/ext-saladict/issues/729) ### Bug Fixes * respect qsFocus option ([2a9cf06](https://github.com/crimx/ext-saladict/commit/2a9cf062a29aea60aa15f473ba3b5a543f9dea49)), closes [#784](https://github.com/crimx/ext-saladict/issues/784) * upgrade neutrino-webextension ([5c1c48d](https://github.com/crimx/ext-saladict/commit/5c1c48d0b58a22ad172196e3106e00383b133eaa)), closes [#790](https://github.com/crimx/ext-saladict/issues/790) ### Tests * **dicts:** update macmillan ([70150fd](https://github.com/crimx/ext-saladict/commit/70150fd41a34de85c24418932ba33d4cb8ce84d8)) * **pdf:** update pdf tests ([6890c6d](https://github.com/crimx/ext-saladict/commit/6890c6d030415fa53adb741e7cfc0650fe43e044)) ### [7.11.2](https://github.com/crimx/ext-saladict/compare/v7.11.1...v7.11.2) (2020-05-06) ### Bug Fixes * **wordeditor:** incorrect z-index ([3701084](https://github.com/crimx/ext-saladict/commit/3701084299cc3902a2e55e9e14d472fba9bcdf27)), closes [#780](https://github.com/crimx/ext-saladict/issues/780) * **wordpage:** refresh table on word changes ([5520feb](https://github.com/crimx/ext-saladict/commit/5520feb1b28eb73e689f5881cc07d9855c11fac9)), closes [#780](https://github.com/crimx/ext-saladict/issues/780) ### [7.11.1](https://github.com/crimx/ext-saladict/compare/v7.11.0...v7.11.1) (2020-05-05) ### Bug Fixes * **firefox:** add franc to dynamic chunks ([61580b1](https://github.com/crimx/ext-saladict/commit/61580b1217bd293885b854ad061d55b082b5be2b)), closes [#778](https://github.com/crimx/ext-saladict/issues/778) * **wordeditor:** fix z-index on internal page ([5c80ebb](https://github.com/crimx/ext-saladict/commit/5c80ebb186c782c0c4747fca3de97e035a334cb6)) ### Tests * update check-update ([6582938](https://github.com/crimx/ext-saladict/commit/6582938c814a92b5ab36a8ae20b4ebdf6a77cc98)) ## [7.11.0](https://github.com/crimx/ext-saladict/compare/v7.10.4...v7.11.0) (2020-05-01) ### Features * **dicts:** add jikipedia ([046b850](https://github.com/crimx/ext-saladict/commit/046b850c83516c43022773bdd2ab6cacbb7696fa)) * fix buggy axios ([9eb8172](https://github.com/crimx/ext-saladict/commit/9eb817242365961cd940bd5e54547b601678c7ce)) * **panel:** add sticky folding ([7b2c352](https://github.com/crimx/ext-saladict/commit/7b2c3524b452925d126d6bd15770649a353e2068)), closes [#765](https://github.com/crimx/ext-saladict/issues/765) * **panel:** remember last standalone window position ([3d25428](https://github.com/crimx/ext-saladict/commit/3d254280e6c2a16a7bd5de99eace55090c04cc88)), closes [#766](https://github.com/crimx/ext-saladict/issues/766) * **profiles:** add shortcuts for top profiles ([de9ca07](https://github.com/crimx/ext-saladict/commit/de9ca077c23147859ebc648b3202faa5b25bca15)) * added option qsFocus ([51e59f9](https://github.com/crimx/ext-saladict/commit/51e59f91fb27ed3c942d2f9c4aa88e31f78eef84)), closes [#764](https://github.com/crimx/ext-saladict/issues/764) ### Bug Fixes * **badge:** remove badge text ([873b1c7](https://github.com/crimx/ext-saladict/commit/873b1c77d3655e4b10dcb389108aabf9c9e31b4c)), closes [#770](https://github.com/crimx/ext-saladict/issues/770) * **options:** prevent panel being opened accidentally ([a673c9f](https://github.com/crimx/ext-saladict/commit/a673c9f94f46d19121058b0f534a5aaa750d8453)), closes [#769](https://github.com/crimx/ext-saladict/issues/769) * **panel:** do not update search box text on selection ([b104405](https://github.com/crimx/ext-saladict/commit/b1044050d92ee4fb5a2f4bd594d2bd9ca44eca12)) * **pdf:** remove 'unsafe-eval' CSP ([eaea459](https://github.com/crimx/ext-saladict/commit/eaea459ae500cf84cea3f65e59c573d6816d222d)) ### Build System * fix script arguments ([df78f19](https://github.com/crimx/ext-saladict/commit/df78f199b71fd4b018903fd57e00c39928986c1c)) ### Tests * **background:** remove update check ([1f1b5ff](https://github.com/crimx/ext-saladict/commit/1f1b5ffa7ed8fb99773f645eb20555a00b12bacd)) * **storybook:** add path pattern ([61a883a](https://github.com/crimx/ext-saladict/commit/61a883a928aaa7c640194592ceb42a650e6d2647)) ### [7.10.4](https://github.com/crimx/ext-saladict/compare/v7.10.3...v7.10.4) (2020-04-27) ### [7.10.3](https://github.com/crimx/ext-saladict/compare/v7.10.2...v7.10.3) (2020-04-26) ### [7.10.2](https://github.com/crimx/ext-saladict/compare/v7.10.1...v7.10.2) (2020-04-26) ### Bug Fixes * **dicts:** cnki should respect options ([c78d2d7](https://github.com/crimx/ext-saladict/commit/c78d2d7ae41dc08d1c7a8a3900613392825c3527)), closes [#752](https://github.com/crimx/ext-saladict/issues/752) * **options:** smooth dark/bright transition ([5433cac](https://github.com/crimx/ext-saladict/commit/5433cac8f074e00a597e91c6804cf3fc8acf3bef)) * **options:** typo ([c66ed05](https://github.com/crimx/ext-saladict/commit/c66ed0586a1662d2a9c4611f8ef8e2c4c7099b60)) * **selection:** cancel instant capture on keyup ([c6dbaa7](https://github.com/crimx/ext-saladict/commit/c6dbaa7ef92c29827b6a35db7bc6824c8696843f)), closes [#756](https://github.com/crimx/ext-saladict/issues/756) ### [7.10.1](https://github.com/crimx/ext-saladict/compare/v7.10.0...v7.10.1) (2020-04-24) ### Bug Fixes * **wordpage:** firefox layout ([7165fda](https://github.com/crimx/ext-saladict/commit/7165fda9101b24a229e15a64396ef39ef1c7fb85)) ## [7.10.0](https://github.com/crimx/ext-saladict/compare/v7.9.3...v7.10.0) (2020-04-24) ### Features * add token settings ([df2924f](https://github.com/crimx/ext-saladict/commit/df2924f7f1a88ce50c21e6019ddf63c73f3a2ae1)) ### Bug Fixes * **context-menus:** encode selection text ([0da9e84](https://github.com/crimx/ext-saladict/commit/0da9e84fd2c5805d66af637f8977340408b2d21a)) * **context-menus:** load locale ([e1981b1](https://github.com/crimx/ext-saladict/commit/e1981b145680a7b2fbce86be5a93e50e0266beac)) * **googledict:** audio link ([407fa9b](https://github.com/crimx/ext-saladict/commit/407fa9b669800bde3ccc29b217e8515d810f022d)) * **i18n:** make ready changes every time ([2a03730](https://github.com/crimx/ext-saladict/commit/2a0373045d343c77782ca327bce3d6bdf40b7c0c)) * **i18n:** proper init language without reloading ([bca03cb](https://github.com/crimx/ext-saladict/commit/bca03cb2f3c0d3fb7258b594e69661512606877d)) * **options:** avoid stale values ([d98b53a](https://github.com/crimx/ext-saladict/commit/d98b53a7973edca8da7274f7fc774da532e30e5e)) * **options:** layout adjustment ([84496e6](https://github.com/crimx/ext-saladict/commit/84496e698cba7656ea0a46b8dd959fee00f1faaf)) * **options:** make data immutable ([fc967db](https://github.com/crimx/ext-saladict/commit/fc967db6cb3eed729c9bb3dfd1b3ba9545d0c917)) * **options:** only get values from item name ([fea31ab](https://github.com/crimx/ext-saladict/commit/fea31abd5ade22c9ff44a0a8115519a5586ff1c3)) * **options:** reduce re-rendering of the whole form ([02f61a9](https://github.com/crimx/ext-saladict/commit/02f61a9105b0adbf87a17b4170e6522bcc5de370)) * **options:** rerender error boundary on entry change ([7450057](https://github.com/crimx/ext-saladict/commit/7450057896ce10bd52f5c0602353e2532563d999)) * **options:** search words on options page ([4bfc211](https://github.com/crimx/ext-saladict/commit/4bfc211d26286d71b178ffcecf9145d7ec222b15)) * **panel:** correct standalone position on multi-screen ([f2f152f](https://github.com/crimx/ext-saladict/commit/f2f152ff604260e6e65a05b7dfea7d84f8d07e96)) * **panel:** disable external style reset on standalone panel ([c2f26be](https://github.com/crimx/ext-saladict/commit/c2f26bec4ae533b3f22610fe5ce7ad722e15d446)) * **panel:** hide external divs ([d584e31](https://github.com/crimx/ext-saladict/commit/d584e313e4cbf6370a8d8c397ce32f4fb9659976)), closes [#703](https://github.com/crimx/ext-saladict/issues/703) * **panel:** more robust dargging ([e5a876b](https://github.com/crimx/ext-saladict/commit/e5a876b5475e29538874834ed985cde21a7a5ae2)), closes [#747](https://github.com/crimx/ext-saladict/issues/747) * **panel:** mta font size ([0b261fd](https://github.com/crimx/ext-saladict/commit/0b261fd23d9ea9512ffc59d3adafdd14967b69c7)), closes [#721](https://github.com/crimx/ext-saladict/issues/721) * **panel:** profiles float box ([7b59fb3](https://github.com/crimx/ext-saladict/commit/7b59fb3246bca4c99acd8129ddb981c913affd56)) * **pdf:** update pdf script ([f419402](https://github.com/crimx/ext-saladict/commit/f419402bfe96e9759076f865c0ea813a8dc011d1)) * **selection:** instant selection ([85d43a0](https://github.com/crimx/ext-saladict/commit/85d43a0a03519985828362c99556719c86761f84)), closes [#742](https://github.com/crimx/ext-saladict/issues/742) * **sync-services:** ignore word addition from sync services ([685cd02](https://github.com/crimx/ext-saladict/commit/685cd02e09adb11bf508ef9ae3033d4d4591763c)), closes [#717](https://github.com/crimx/ext-saladict/issues/717) * content style origin ([230275d](https://github.com/crimx/ext-saladict/commit/230275d5db56e65de9570d27d62ceed1f0618a32)) ### Build System * better chunk naming for dict favicons ([d1f65fd](https://github.com/crimx/ext-saladict/commit/d1f65fde14b23992ce7a1969a0a3b778a4caef70)) * control split chunks ([858ea64](https://github.com/crimx/ext-saladict/commit/858ea644d19016218b2b4481fbd63e80e8345f65)) * fix dotenv ([07c7fc6](https://github.com/crimx/ext-saladict/commit/07c7fc6b78c1acc84607a0946775d2332f875ffb)) ### Tests * **panel:** update storybook ([657da12](https://github.com/crimx/ext-saladict/commit/657da12dd418a99a5d13f83158ed9f1d51fe3c33)) ### [7.9.3](https://github.com/crimx/ext-saladict/compare/v7.9.2...v7.9.3) (2020-03-19) ### Bug Fixes * **panel:** prevent ff flash ([#691](https://github.com/crimx/ext-saladict/issues/691)) ([d18df80](https://github.com/crimx/ext-saladict/commit/d18df80956e8423d808e9a8ac64458ddc73b3b22)) * **word-editor:** inner panel not showing up ([b8c6064](https://github.com/crimx/ext-saladict/commit/b8c606486d69fc00c1babe5d6569bcb61f26a801)), closes [#694](https://github.com/crimx/ext-saladict/issues/694) ### [7.9.2](https://github.com/crimx/ext-saladict/compare/v7.9.1...v7.9.2) (2020-03-14) ### Bug Fixes * **dicts:** wrong dict config ([551a0b3](https://github.com/crimx/ext-saladict/commit/551a0b30db29c14dd093d33f01c440d669846fc4)) ### [7.9.1](https://github.com/crimx/ext-saladict/compare/v7.9.0...v7.9.1) (2020-03-10) ### Bug Fixes * **dicts:** add fallback language for machine translate ([60b10da](https://github.com/crimx/ext-saladict/commit/60b10da2dbb890270965de7b24a6672ced4ce579)), closes [#674](https://github.com/crimx/ext-saladict/issues/674) * **dicts:** enhance cjk detection ([8311d9e](https://github.com/crimx/ext-saladict/commit/8311d9e30d01740930725cd9a83adfa5a92bf26e)) * **dicts:** remove caching async function ([03d7866](https://github.com/crimx/ext-saladict/commit/03d78669dacece312ba7bf2a5d8763d9b760730b)) ## [7.9.0](https://github.com/crimx/ext-saladict/compare/v7.8.0...v7.9.0) (2020-03-09) ### Features * **dicts:** add lexico ([a86fc7d](https://github.com/crimx/ext-saladict/commit/a86fc7db85f8646f6326b6e1dbbd235ce930c7d6)) * **dicts:** add renren ([b4dc38d](https://github.com/crimx/ext-saladict/commit/b4dc38da25e838f1c9869d66d6cb2b9ecfbf3fb5)) ### Bug Fixes * **dicts:** correct tts language ([76eb34d](https://github.com/crimx/ext-saladict/commit/76eb34d70fe802b3117a5c31a2e8f1d732f2f34e)), closes [#659](https://github.com/crimx/ext-saladict/issues/659) * **renren:** prevent detail click event being captured by panel ([921d102](https://github.com/crimx/ext-saladict/commit/921d102deac23ec19196a41512883d645c73ae13)) * **wordpage:** keyword matching ([b9a1a3e](https://github.com/crimx/ext-saladict/commit/b9a1a3e211a405595724a9e466ffc8d9a2c7ec1d)) ### Tests * fix bing fixtures ([a7731a2](https://github.com/crimx/ext-saladict/commit/a7731a22131dc358c3036b14d59b9a3d33344a53)) ## [7.8.0](https://github.com/crimx/ext-saladict/compare/v7.7.6...v7.8.0) (2020-02-13) ### Bug Fixes * remove extra clipboard search on command ([0b7166e](https://github.com/crimx/ext-saladict/commit/0b7166e)), closes [#647](https://github.com/crimx/ext-saladict/issues/647) * space escape ([2c31562](https://github.com/crimx/ext-saladict/commit/2c31562)), closes [#635](https://github.com/crimx/ext-saladict/issues/635) ### Features * add standalone word editor ([24d487a](https://github.com/crimx/ext-saladict/commit/24d487a)), closes [#608](https://github.com/crimx/ext-saladict/issues/608) ### [7.7.6](https://github.com/crimx/ext-saladict/compare/v7.7.5...v7.7.6) (2020-02-03) ### [7.7.5](https://github.com/crimx/ext-saladict/compare/v7.7.4...v7.7.5) (2020-02-03) ### [7.7.4](https://github.com/crimx/ext-saladict/compare/v7.7.3...v7.7.4) (2020-02-03) ### [7.7.3](https://github.com/crimx/ext-saladict/compare/v7.7.2...v7.7.3) (2020-02-02) ### [7.7.2](https://github.com/crimx/ext-saladict/compare/v7.7.1...v7.7.2) (2020-01-27) ### [7.7.1](https://github.com/crimx/ext-saladict/compare/v7.7.0...v7.7.1) (2020-01-24) ### Bug Fixes * pdf.js requires unsafe-eval csp ([533a66d](https://github.com/crimx/ext-saladict/commit/533a66d)), closes [#630](https://github.com/crimx/ext-saladict/issues/630) ## [7.7.0](https://github.com/crimx/ext-saladict/compare/v7.6.2...v7.7.0) (2020-01-24) ### Bug Fixes * **pdf:** match double quotes ([46060bd](https://github.com/crimx/ext-saladict/commit/46060bd)) ### Features * **options:** add privacy settings ([9408002](https://github.com/crimx/ext-saladict/commit/9408002)) ### [7.6.2](https://github.com/crimx/ext-saladict/compare/v7.6.1...v7.6.2) (2020-01-16) ### Tests * remove fixtures ([eca13a3](https://github.com/crimx/ext-saladict/commit/eca13a3)) ### [7.6.1](https://github.com/crimx/ext-saladict/compare/v7.6.0...v7.6.1) (2020-01-06) ### Bug Fixes * **background:** remove duplicated qs panel onclose response ([b9d209b](https://github.com/crimx/ext-saladict/commit/b9d209b)), closes [#618](https://github.com/crimx/ext-saladict/issues/618) * **selection:** respect qs panel selection settings ([4990479](https://github.com/crimx/ext-saladict/commit/4990479)) ## [7.6.0](https://github.com/crimx/ext-saladict/compare/v7.5.4...v7.6.0) (2019-12-29) ### Bug Fixes * **panel:** ignore snapshot if the panel was hidden ([ae9a538](https://github.com/crimx/ext-saladict/commit/ae9a538)) * **panel:** open word editor on wordpage ([dbb9b58](https://github.com/crimx/ext-saladict/commit/dbb9b58)), closes [#590](https://github.com/crimx/ext-saladict/issues/590) * **selection:** detect mouseup in panel ([989a9f6](https://github.com/crimx/ext-saladict/commit/989a9f6)) * remove invalid window state ([c258de2](https://github.com/crimx/ext-saladict/commit/c258de2)) * round window positions ([fa3d264](https://github.com/crimx/ext-saladict/commit/fa3d264)), closes [#607](https://github.com/crimx/ext-saladict/issues/607) ### Features * **content:** add picker for ctx translated results ([6c0c4b8](https://github.com/crimx/ext-saladict/commit/6c0c4b8)) * **menus:** add copu pdf url to clipboard ([cfe6d9d](https://github.com/crimx/ext-saladict/commit/cfe6d9d)), closes [#571](https://github.com/crimx/ext-saladict/issues/571) ### [7.5.4](https://github.com/crimx/ext-saladict/compare/v7.5.3...v7.5.4) (2019-12-11) ### Bug Fixes * dual screen windows management ([8196a6d](https://github.com/crimx/ext-saladict/commit/8196a6d)), closes [#587](https://github.com/crimx/ext-saladict/issues/587) ### [7.5.3](https://github.com/crimx/ext-saladict/compare/v7.5.2...v7.5.3) (2019-12-10) ### Bug Fixes * **dicts:** update moji ([fb528b1](https://github.com/crimx/ext-saladict/commit/fb528b1)) * self messaging server init order ([8473faa](https://github.com/crimx/ext-saladict/commit/8473faa)) * 自定义 css 对独立面板不生效 ([#579](https://github.com/crimx/ext-saladict/issues/579)) ([1db0c5a](https://github.com/crimx/ext-saladict/commit/1db0c5a)) ### Tests * remove opentranslate ([d655258](https://github.com/crimx/ext-saladict/commit/d655258)) * update api ([34535ea](https://github.com/crimx/ext-saladict/commit/34535ea)) * update webdav testing ([782f288](https://github.com/crimx/ext-saladict/commit/782f288)) ### [7.5.2](https://github.com/crimx/ext-saladict/compare/v7.5.1...v7.5.2) (2019-11-11) ### Bug Fixes * **sync:** webdav url ending ([5dc51a8](https://github.com/crimx/ext-saladict/commit/5dc51a8)), closes [#562](https://github.com/crimx/ext-saladict/issues/562) ### [7.5.1](https://github.com/crimx/ext-saladict/compare/v7.5.0...v7.5.1) (2019-11-04) ### Bug Fixes * translate context before editing ([5c92445](https://github.com/crimx/ext-saladict/commit/5c92445)), closes [#550](https://github.com/crimx/ext-saladict/issues/550) ## [7.5.0](https://github.com/crimx/ext-saladict/compare/v7.4.0...v7.5.0) (2019-11-03) ### Bug Fixes * update homepage url ([102ff19](https://github.com/crimx/ext-saladict/commit/102ff19)) * **config:** merge machine pronounce config ([1c37346](https://github.com/crimx/ext-saladict/commit/1c37346)), closes [#540](https://github.com/crimx/ext-saladict/issues/540) * **content:** prevent triggering page key events ([a74b255](https://github.com/crimx/ext-saladict/commit/a74b255)) * **panel:** reset text align ([3b38d19](https://github.com/crimx/ext-saladict/commit/3b38d19)), closes [#537](https://github.com/crimx/ext-saladict/issues/537) * missing pdf locale properties ([10e9636](https://github.com/crimx/ext-saladict/commit/10e9636)), closes [#548](https://github.com/crimx/ext-saladict/issues/548) ### Features * **content:** add bowl offset config ([3a1327f](https://github.com/crimx/ext-saladict/commit/3a1327f)), closes [#535](https://github.com/crimx/ext-saladict/issues/535) ## [7.4.0](https://github.com/crimx/ext-saladict/compare/v7.3.2...v7.4.0) (2019-10-24) ### Bug Fixes * **dicts:** fix custom tl ([1156c5e](https://github.com/crimx/ext-saladict/commit/1156c5e)) * **menus:** update all menus before selected Closes [#533](https://github.com/crimx/ext-saladict/issues/533) ([076e072](https://github.com/crimx/ext-saladict/commit/076e072)) * **selection:** get page info on selection ([972a9aa](https://github.com/crimx/ext-saladict/commit/972a9aa)), closes [#531](https://github.com/crimx/ext-saladict/issues/531) * **selection:** prevent mouseup being cancelled ([0485244](https://github.com/crimx/ext-saladict/commit/0485244)) ### Features * **dicts:** add mojidict ([2ec91ef](https://github.com/crimx/ext-saladict/commit/2ec91ef)) ### [7.3.2](https://github.com/crimx/ext-saladict/compare/v7.3.1...v7.3.2) (2019-10-19) ### Bug Fixes * ignore numbers ([352a84c](https://github.com/crimx/ext-saladict/commit/352a84c)) ### [7.3.1](https://github.com/crimx/ext-saladict/compare/v7.3.0...v7.3.1) (2019-10-18) ### Bug Fixes * **badges:** prevent stale values ([5f4c5d5](https://github.com/crimx/ext-saladict/commit/5f4c5d5)) ## [7.3.0](https://github.com/crimx/ext-saladict/compare/v7.2.2...v7.3.0) (2019-10-18) ### Features * **wordpage:** support replacing linebreaks ([c112de5](https://github.com/crimx/ext-saladict/commit/c112de5)) ### [7.2.2](https://github.com/crimx/ext-saladict/compare/v7.2.1...v7.2.2) (2019-10-15) ### [7.2.1](https://github.com/crimx/ext-saladict/compare/v7.2.0...v7.2.1) (2019-10-13) ### Bug Fixes * **selection:** in-panel selection ([507581a](https://github.com/crimx/ext-saladict/commit/507581a)), closes [#513](https://github.com/crimx/ext-saladict/issues/513) ## [7.2.0](https://github.com/crimx/ext-saladict/compare/v7.1.1...v7.2.0) (2019-10-12) ### Bug Fixes * **selection:** ignore anchor and button click on panel ([904445c](https://github.com/crimx/ext-saladict/commit/904445c)), closes [#512](https://github.com/crimx/ext-saladict/issues/512) ### Features * match all characters ([59c8183](https://github.com/crimx/ext-saladict/commit/59c8183)), closes [#434](https://github.com/crimx/ext-saladict/issues/434) ### [7.1.1](https://github.com/crimx/ext-saladict/compare/v7.1.0...v7.1.1) (2019-10-08) ### Bug Fixes * **panel:** save word without confirm ([#500](https://github.com/crimx/ext-saladict/issues/500)) ([3d77d97](https://github.com/crimx/ext-saladict/commit/3d77d97)) * **selection:** only detect left click ([fc3797d](https://github.com/crimx/ext-saladict/commit/fc3797d)), closes [#502](https://github.com/crimx/ext-saladict/issues/502) * **selection:** prevent unexpected in-panel selection ([3c057ce](https://github.com/crimx/ext-saladict/commit/3c057ce)), closes [#498](https://github.com/crimx/ext-saladict/issues/498) ## [7.1.0](https://github.com/crimx/ext-saladict/compare/v7.0.4...v7.1.0) (2019-10-03) ### Bug Fixes * **panel:** always focus mta box on expand ([57840eb](https://github.com/crimx/ext-saladict/commit/57840eb)) * **panel:** prevent page shortkeys when typing ([ba9a37a](https://github.com/crimx/ext-saladict/commit/ba9a37a)), closes [#490](https://github.com/crimx/ext-saladict/issues/490) ### Features * **panel:** add new shortcut for searching clipboard ([e5f279d](https://github.com/crimx/ext-saladict/commit/e5f279d)), closes [#485](https://github.com/crimx/ext-saladict/issues/485) * **panel:** add touch mode close [#492](https://github.com/crimx/ext-saladict/issues/492) ([c86e6e8](https://github.com/crimx/ext-saladict/commit/c86e6e8)) ### [7.0.4](https://github.com/crimx/ext-saladict/compare/v7.0.3...v7.0.4) (2019-10-01) ### Bug Fixes * **dicts:** hjdict switch buttons. close [#489](https://github.com/crimx/ext-saladict/issues/489) ([a2718d4](https://github.com/crimx/ext-saladict/commit/a2718d4)) * youdao translate ([02381da](https://github.com/crimx/ext-saladict/commit/02381da)) ### [7.0.3](https://github.com/crimx/ext-saladict/compare/v7.0.2...v7.0.3) (2019-09-30) ### Bug Fixes * **panel:** fix mta box init focus with clipboard content [#487](https://github.com/crimx/ext-saladict/issues/487) ([789270d](https://github.com/crimx/ext-saladict/commit/789270d)) * **qs:** selection on quick search panel ([3ede289](https://github.com/crimx/ext-saladict/commit/3ede289)), closes [#487](https://github.com/crimx/ext-saladict/issues/487) ### [7.0.2](https://github.com/crimx/ext-saladict/compare/v7.0.0...v7.0.2) (2019-09-30) ### Bug Fixes * **config:** number merging ([cb53388](https://github.com/crimx/ext-saladict/commit/cb53388)) * **config:** update quick search location ([70bd9be](https://github.com/crimx/ext-saladict/commit/70bd9be)), closes [#479](https://github.com/crimx/ext-saladict/issues/479) * **dicts:** include cambridge dphrase block ([29f7b3c](https://github.com/crimx/ext-saladict/commit/29f7b3c)), closes [#480](https://github.com/crimx/ext-saladict/issues/480) * **pdf:** inject panel on firefox close [#477](https://github.com/crimx/ext-saladict/issues/477) ([a7ac72b](https://github.com/crimx/ext-saladict/commit/a7ac72b)) * **popup:** correct popup width ([170fe72](https://github.com/crimx/ext-saladict/commit/170fe72)), closes [#481](https://github.com/crimx/ext-saladict/issues/481) * **sync:** update mkcol authorzation close [#475](https://github.com/crimx/ext-saladict/issues/475) ([2e433e5](https://github.com/crimx/ext-saladict/commit/2e433e5)) ### [7.0.1](https://github.com/crimx/ext-saladict/compare/v7.0.0...v7.0.1) (2019-09-30) ### Bug Fixes * **config:** number merging ([569f69c](https://github.com/crimx/ext-saladict/commit/569f69c)) * **config:** update quick search location ([34a6a07](https://github.com/crimx/ext-saladict/commit/34a6a07)), closes [#479](https://github.com/crimx/ext-saladict/issues/479) * **dicts:** include cambridge dphrase block ([807923f](https://github.com/crimx/ext-saladict/commit/807923f)), closes [#480](https://github.com/crimx/ext-saladict/issues/480) * **pdf:** inject panel on firefox close [#477](https://github.com/crimx/ext-saladict/issues/477) ([745bb75](https://github.com/crimx/ext-saladict/commit/745bb75)) * **popup:** correct popup width ([6f14ba2](https://github.com/crimx/ext-saladict/commit/6f14ba2)), closes [#481](https://github.com/crimx/ext-saladict/issues/481) * **sync:** update mkcol authorzation close [#475](https://github.com/crimx/ext-saladict/issues/475) ([ddd6b77](https://github.com/crimx/ext-saladict/commit/ddd6b77)) ## [7.0.0](https://github.com/crimx/ext-saladict/compare/v6.33.2...v7.0.0) (2019-09-29) ### Bug Fixes * **background:** show unsupported badge on internal tabs ([fe06a06](https://github.com/crimx/ext-saladict/commit/fe06a06)) * **content:** correct history index ([bb94e87](https://github.com/crimx/ext-saladict/commit/bb94e87)) * **dicts:** convert chs to chz on guoyu and liangan ([5e5a058](https://github.com/crimx/ext-saladict/commit/5e5a058)) * **dicts:** correct text color on dark mode ([7484469](https://github.com/crimx/ext-saladict/commit/7484469)) * **dicts:** fix tencent referer ([9c7b0de](https://github.com/crimx/ext-saladict/commit/9c7b0de)) * **dicts:** params encoding ([c689e69](https://github.com/crimx/ext-saladict/commit/c689e69)) * **dicts:** update sogou ([c0dffa1](https://github.com/crimx/ext-saladict/commit/c0dffa1)) * **dicts:** update sogou api ([04a1e74](https://github.com/crimx/ext-saladict/commit/04a1e74)) * **dicts:** update sogou api ([055f24e](https://github.com/crimx/ext-saladict/commit/055f24e)) * **dicts:** update tencent api ([f13039f](https://github.com/crimx/ext-saladict/commit/f13039f)) * **dicts:** url params encode ([3db28ff](https://github.com/crimx/ext-saladict/commit/3db28ff)) * **options:** disable selection outside panel on options page ([491a791](https://github.com/crimx/ext-saladict/commit/491a791)) * **options:** increase ant modal mask z-index ([337e92a](https://github.com/crimx/ext-saladict/commit/337e92a)) * **options:** z-index on tooltips ([c94e340](https://github.com/crimx/ext-saladict/commit/c94e340)) * **panel:** add to notebook on standalone panel ([c7b00e5](https://github.com/crimx/ext-saladict/commit/c7b00e5)) * **panel:** calc hight changes on expand ([f6f335e](https://github.com/crimx/ext-saladict/commit/f6f335e)) * **panel:** correct standalone css variables ([f0e087c](https://github.com/crimx/ext-saladict/commit/f0e087c)) * **panel:** fancy scrollbar on standalone panel ([4734ab8](https://github.com/crimx/ext-saladict/commit/4734ab8)) * **panel:** keep panel showing on options page ([d43ac1f](https://github.com/crimx/ext-saladict/commit/d43ac1f)) * **panel:** normal scrollbar width on firefox ([a0385a8](https://github.com/crimx/ext-saladict/commit/a0385a8)) * **panel:** remove Firefox button inner border ([00a069f](https://github.com/crimx/ext-saladict/commit/00a069f)) * **selection:** add page info in selection ([b64e85e](https://github.com/crimx/ext-saladict/commit/b64e85e)) * **selection:** check mouse target when anchor node is null ([1a2487f](https://github.com/crimx/ext-saladict/commit/1a2487f)) * **selection:** keep panel coords when pinned ([7648247](https://github.com/crimx/ext-saladict/commit/7648247)) * **selection:** skip extra selection change on Firefox ([754db43](https://github.com/crimx/ext-saladict/commit/754db43)) * firefox ext api ([b8efad0](https://github.com/crimx/ext-saladict/commit/b8efad0)) * lang check ([a8bfe92](https://github.com/crimx/ext-saladict/commit/a8bfe92)) * **selection:** text field selection ([a8628b6](https://github.com/crimx/ext-saladict/commit/a8628b6)) * remove buttons option on filrefox ([970b921](https://github.com/crimx/ext-saladict/commit/970b921)) * remove scrollbar color on firefox ([a00214c](https://github.com/crimx/ext-saladict/commit/a00214c)) * skip empty src for speaker ([65ff654](https://github.com/crimx/ext-saladict/commit/65ff654)) * sync service download ([af05e51](https://github.com/crimx/ext-saladict/commit/af05e51)) * **components:** add appear styles for shadow portal ([b84a8e8](https://github.com/crimx/ext-saladict/commit/b84a8e8)) * **content:** max panel height calculation ([de30946](https://github.com/crimx/ext-saladict/commit/de30946)) * **content:** search on bowl hover ([d7e126d](https://github.com/crimx/ext-saladict/commit/d7e126d)) * **dicts:** axios api ([dda444d](https://github.com/crimx/ext-saladict/commit/dda444d)) * **dicts:** encode uri component ([101ae50](https://github.com/crimx/ext-saladict/commit/101ae50)) * **dicts:** update new speaker classname ([ad19c84](https://github.com/crimx/ext-saladict/commit/ad19c84)) * **i18n:** sync init ([3aedb2d](https://github.com/crimx/ext-saladict/commit/3aedb2d)) * **manifest:** new assets path ([67e3421](https://github.com/crimx/ext-saladict/commit/67e3421)) * **options:** new quick search locations ([667dc13](https://github.com/crimx/ext-saladict/commit/667dc13)) * **panel:** add dict item key ([e85f949](https://github.com/crimx/ext-saladict/commit/e85f949)) * **panel:** env detection ([7817f54](https://github.com/crimx/ext-saladict/commit/7817f54)) * **panel:** firefox detect height change ([9016d81](https://github.com/crimx/ext-saladict/commit/9016d81)) * **panel:** fix sluggish scroll on Firefox ([d054f81](https://github.com/crimx/ext-saladict/commit/d054f81)) * **panel:** open options page when clicking icon ([6e2dc5e](https://github.com/crimx/ext-saladict/commit/6e2dc5e)) * **panel:** prevent textarea input event propagation ([36285ff](https://github.com/crimx/ext-saladict/commit/36285ff)) * **popup:** qrcode panel z-index ([0234943](https://github.com/crimx/ext-saladict/commit/0234943)) * **selection:** skip extra event after instant capture ([1a01ac5](https://github.com/crimx/ext-saladict/commit/1a01ac5)) * **storybook:** add width for panel wrapper ([276139c](https://github.com/crimx/ext-saladict/commit/276139c)) * **wordpage:** context translation ([3f01b81](https://github.com/crimx/ext-saladict/commit/3f01b81)) * context menus locale name ([2617939](https://github.com/crimx/ext-saladict/commit/2617939)) * correctly made payload and meta optional ([9ac6fb3](https://github.com/crimx/ext-saladict/commit/9ac6fb3)) * css type ([de9b809](https://github.com/crimx/ext-saladict/commit/de9b809)) * firefox bugs ([efab253](https://github.com/crimx/ext-saladict/commit/efab253)) * **panel:** fix menu bar shrinking ([2e0c8fc](https://github.com/crimx/ext-saladict/commit/2e0c8fc)) * **panel:** panel opcaity transition ([673ce82](https://github.com/crimx/ext-saladict/commit/673ce82)) * **panel:** typo ([9f7626d](https://github.com/crimx/ext-saladict/commit/9f7626d)) * dom purify parse innerHTML ([6af3120](https://github.com/crimx/ext-saladict/commit/6af3120)) * getFullLink supports other protocols ([6b08d5f](https://github.com/crimx/ext-saladict/commit/6b08d5f)) * locale format ([3439005](https://github.com/crimx/ext-saladict/commit/3439005)) * nested p tags ([fb69f55](https://github.com/crimx/ext-saladict/commit/fb69f55)) * prevent dict panel being closed ([9c3fd0b](https://github.com/crimx/ext-saladict/commit/9c3fd0b)) * relative url ([2a565a6](https://github.com/crimx/ext-saladict/commit/2a565a6)) * reove style global reset ([5d89ebd](https://github.com/crimx/ext-saladict/commit/5d89ebd)) * union hack ([d0d3cdd](https://github.com/crimx/ext-saladict/commit/d0d3cdd)) * **storybook:** disable storybook shortcuts ([6da3254](https://github.com/crimx/ext-saladict/commit/6da3254)) * **storybook:** prevent full rerender ([fe996dd](https://github.com/crimx/ext-saladict/commit/fe996dd)) * **storybook:** skip wrapper components ([cd370c9](https://github.com/crimx/ext-saladict/commit/cd370c9)) * update namespace ([9f1e253](https://github.com/crimx/ext-saladict/commit/9f1e253)) ### Build System * add shadow dom css support and storybook addons ([211986a](https://github.com/crimx/ext-saladict/commit/211986a)) * add storybook ([5e1e88e](https://github.com/crimx/ext-saladict/commit/5e1e88e)) * fix mjs type ([d76f6c7](https://github.com/crimx/ext-saladict/commit/d76f6c7)) * new pack script ([78ee6aa](https://github.com/crimx/ext-saladict/commit/78ee6aa)) * remove style loader on development ([e4bd588](https://github.com/crimx/ext-saladict/commit/e4bd588)) * rename jsonp function ([5d4941c](https://github.com/crimx/ext-saladict/commit/5d4941c)) * split webpack chunks ([ad90c96](https://github.com/crimx/ext-saladict/commit/ad90c96)) * update build system to neutrino and babel-ts ([b3b05c3](https://github.com/crimx/ext-saladict/commit/b3b05c3)) ### Features * **panel:** add fancy scrollbar ([4be6ac1](https://github.com/crimx/ext-saladict/commit/4be6ac1)) * **popup:** add options for opening standalone panel [#470](https://github.com/crimx/ext-saladict/issues/470) ([2f0be7e](https://github.com/crimx/ext-saladict/commit/2f0be7e)) * **profile:** add nihongo profile ([285b08b](https://github.com/crimx/ext-saladict/commit/285b08b)) * add dark mode ([a9c9407](https://github.com/crimx/ext-saladict/commit/a9c9407)) * add shadow portal ([3d3e025](https://github.com/crimx/ext-saladict/commit/3d3e025)) ### Tests * update browser api specs ([768ce07](https://github.com/crimx/ext-saladict/commit/768ce07)) * update mocks ([cefd766](https://github.com/crimx/ext-saladict/commit/cefd766)) * **dicts:** add bing mock requests ([641d9db](https://github.com/crimx/ext-saladict/commit/641d9db)) * **dicts:** remove mock text ([3d57c20](https://github.com/crimx/ext-saladict/commit/3d57c20)) * **dicts:** udapte googledict html ([c5c6b80](https://github.com/crimx/ext-saladict/commit/c5c6b80)) * **storybook:** update stories ([780fade](https://github.com/crimx/ext-saladict/commit/780fade)) * added jest ([99484c7](https://github.com/crimx/ext-saladict/commit/99484c7)) * clean old test ([074f058](https://github.com/crimx/ext-saladict/commit/074f058)) * refactor background ([938aeea](https://github.com/crimx/ext-saladict/commit/938aeea)) * **storybook:** add dictionaries stories ([0714aed](https://github.com/crimx/ext-saladict/commit/0714aed)) ### BREAKING CHANGES * No compatible with the old build system ### [6.33.7](https://github.com/crimx/ext-saladict/compare/v6.33.6...v6.33.7) (2019-09-13) ### [6.33.6](https://github.com/crimx/ext-saladict/compare/v6.33.5...v6.33.6) (2019-09-12) ### Bug Fixes * fit the outdated typings ([f019572](https://github.com/crimx/ext-saladict/commit/f019572)) * update dicts ([4492cd0](https://github.com/crimx/ext-saladict/commit/4492cd0)) ### [6.33.5](https://github.com/crimx/ext-saladict/compare/v6.33.4...v6.33.5) (2019-08-11) ### Bug Fixes * change the checksums of panel.css ([e2ed394](https://github.com/crimx/ext-saladict/commit/e2ed394)) ### [6.33.4](https://github.com/crimx/ext-saladict/compare/v6.33.3...v6.33.4) (2019-08-09) ### Bug Fixes * **manifest:** fix chrome 67 bug ([bca3b56](https://github.com/crimx/ext-saladict/commit/bca3b56)) ### [6.33.3](https://github.com/crimx/ext-saladict/compare/v6.33.2...v6.33.3) (2019-08-08) ### Bug Fixes * **manifest:** remvoe update url ([f83a485](https://github.com/crimx/ext-saladict/commit/f83a485)) ## [6.33.2](https://github.com/crimx/ext-saladict/compare/v6.33.1...v6.33.2) (2019-06-27) ## [6.33.1](https://github.com/crimx/ext-saladict/compare/v6.33.0...v6.33.1) (2019-06-15) ### Bug Fixes * **dicts:** https audio ([d8d569f](https://github.com/crimx/ext-saladict/commit/d8d569f)) # [6.33.0](https://github.com/crimx/ext-saladict/compare/v6.32.0...v6.33.0) (2019-06-12) ### Bug Fixes * **dicts:** baidu options mixed with google ([239527e](https://github.com/crimx/ext-saladict/commit/239527e)) * **selection:** context extraction ([ec7421d](https://github.com/crimx/ext-saladict/commit/ec7421d)) ### Features * **dicts:** add weblio ejje ([5b01e1e](https://github.com/crimx/ext-saladict/commit/5b01e1e)) # [6.32.0](https://github.com/crimx/ext-saladict/compare/v6.31.1...v6.32.0) (2019-05-31) ### Bug Fixes * same origin iframe ([39bacf9](https://github.com/crimx/ext-saladict/commit/39bacf9)), closes [#373](https://github.com/crimx/ext-saladict/issues/373) * **dicts:** update zdic ([ebef2ce](https://github.com/crimx/ext-saladict/commit/ebef2ce)) * **options:** styles ([50cc464](https://github.com/crimx/ext-saladict/commit/50cc464)) * assgin timeout ticket ([23f85c5](https://github.com/crimx/ext-saladict/commit/23f85c5)) * correct popup page id ([45db00b](https://github.com/crimx/ext-saladict/commit/45db00b)) * ignore esc key on standalone panel ([1970556](https://github.com/crimx/ext-saladict/commit/1970556)) ### Features * add audio control ([6df1682](https://github.com/crimx/ext-saladict/commit/6df1682)) * add soundtouch ([ca7e7f8](https://github.com/crimx/ext-saladict/commit/ca7e7f8)) ## [6.31.1](https://github.com/crimx/ext-saladict/compare/v6.31.0...v6.31.1) (2019-05-25) ### Bug Fixes * **dicts:** multiline result ([b17e915](https://github.com/crimx/ext-saladict/commit/b17e915)) # [6.31.0](https://github.com/crimx/ext-saladict/compare/v6.30.0...v6.31.0) (2019-05-24) ### Bug Fixes * **dicts:** caiyun options ([3efae72](https://github.com/crimx/ext-saladict/commit/3efae72)) * update typings ([a3074e0](https://github.com/crimx/ext-saladict/commit/a3074e0)) * **options:** use short title to prevent overflow ([52fb802](https://github.com/crimx/ext-saladict/commit/52fb802)) ### Features * **dicts:** add caiyun ([92ad971](https://github.com/crimx/ext-saladict/commit/92ad971)) * **dicts:** add tencent translate ([46657ea](https://github.com/crimx/ext-saladict/commit/46657ea)) # [6.30.0](https://github.com/crimx/ext-saladict/compare/v6.29.0...v6.30.0) (2019-05-11) ### Bug Fixes * **sync:** replace settimeout with alarms ([05f1260](https://github.com/crimx/ext-saladict/commit/05f1260)), closes [#361](https://github.com/crimx/ext-saladict/issues/361) * check empty word fields ([60f8066](https://github.com/crimx/ext-saladict/commit/60f8066)), closes [#363](https://github.com/crimx/ext-saladict/issues/363) * typo ([8b0f3ff](https://github.com/crimx/ext-saladict/commit/8b0f3ff)) * **dicts:** get correct lang list on consecutive searches ([4ad4dcf](https://github.com/crimx/ext-saladict/commit/4ad4dcf)), closes [#360](https://github.com/crimx/ext-saladict/issues/360) ### Features * **dicts:** add jukuu ([a4775fd](https://github.com/crimx/ext-saladict/commit/a4775fd)) # [6.29.0](https://github.com/crimx/ext-saladict/compare/v6.28.1...v6.29.0) (2019-05-02) ### Bug Fixes * **panel:** correct history forward btn ([ee1d4f6](https://github.com/crimx/ext-saladict/commit/ee1d4f6)), closes [#349](https://github.com/crimx/ext-saladict/issues/349) * add z-index to google page translate ([f59cc57](https://github.com/crimx/ext-saladict/commit/f59cc57)) ### Features * **dicts:** add cnki ([2743cac](https://github.com/crimx/ext-saladict/commit/2743cac)), closes [#336](https://github.com/crimx/ext-saladict/issues/336) * add comp EntryBox ([fdd71dd](https://github.com/crimx/ext-saladict/commit/fdd71dd)) ## [6.28.1](https://github.com/crimx/ext-saladict/compare/v6.28.0...v6.28.1) (2019-04-17) ### Bug Fixes * add z-index to google page translate elements ([38b08e5](https://github.com/crimx/ext-saladict/commit/38b08e5)) # [6.28.0](https://github.com/crimx/ext-saladict/compare/v6.27.8...v6.28.0) (2019-04-17) ### Features * add standalone sidebar layout ([6f4c5b8](https://github.com/crimx/ext-saladict/commit/6f4c5b8)) ## [6.27.8](https://github.com/crimx/ext-saladict/compare/v6.27.7...v6.27.8) (2019-03-31) ### Bug Fixes * **options:** correct import and export options ([cbf2921](https://github.com/crimx/ext-saladict/commit/cbf2921)) ## [6.27.7](https://github.com/crimx/ext-saladict/compare/v6.27.6...v6.27.7) (2019-03-27) ### Bug Fixes * **panel:** proper update dict styles ([264c731](https://github.com/crimx/ext-saladict/commit/264c731)), closes [#331](https://github.com/crimx/ext-saladict/issues/331) ## [6.27.6](https://github.com/crimx/ext-saladict/compare/v6.27.5...v6.27.6) (2019-03-27) ### Bug Fixes * **panel:** correct dict style update ([81a1d08](https://github.com/crimx/ext-saladict/commit/81a1d08)) ## [6.27.5](https://github.com/crimx/ext-saladict/compare/v6.27.4...v6.27.5) (2019-03-24) ### Bug Fixes * **sync:** only sync on notebook changes ([90d4183](https://github.com/crimx/ext-saladict/commit/90d4183)) ## [6.27.4](https://github.com/crimx/ext-saladict/compare/v6.27.3...v6.27.4) (2019-03-23) ### Bug Fixes * **panel:** frame head typos ([94a055f](https://github.com/crimx/ext-saladict/commit/94a055f)) * **sync:** proper trun off shanbay ([af93ba0](https://github.com/crimx/ext-saladict/commit/af93ba0)) ## [6.27.3](https://github.com/crimx/ext-saladict/compare/v6.27.2...v6.27.3) (2019-03-19) ### Bug Fixes * **dicts:** update sogou token ([570dc90](https://github.com/crimx/ext-saladict/commit/570dc90)) ## [6.27.2](https://github.com/crimx/ext-saladict/compare/v6.27.1...v6.27.2) (2019-03-18) ### Bug Fixes * fix google translate ([d8715d1](https://github.com/crimx/ext-saladict/commit/d8715d1)) * **dicts:** disable passive wheel events on lastest Chrome ([9e8c3f0](https://github.com/crimx/ext-saladict/commit/9e8c3f0)) ## [6.27.1](https://github.com/crimx/ext-saladict/compare/v6.27.0...v6.27.1) (2019-03-17) ### Bug Fixes * **manifest:** firefox incognito mode ([58b946f](https://github.com/crimx/ext-saladict/commit/58b946f)) # [6.27.0](https://github.com/crimx/ext-saladict/compare/v6.26.0...v6.27.0) (2019-03-17) ### Bug Fixes * compress data ([3795836](https://github.com/crimx/ext-saladict/commit/3795836)) * **dicts:** fix shanbay typing warning ([99caa99](https://github.com/crimx/ext-saladict/commit/99caa99)) * **dicts:** prevent in-panel search ([f88b960](https://github.com/crimx/ext-saladict/commit/f88b960)) * **dicts:** remove float elements ([143b258](https://github.com/crimx/ext-saladict/commit/143b258)) * **dicts:** typings ([bafe61c](https://github.com/crimx/ext-saladict/commit/bafe61c)) * **manifest:** load pdf viewer under incognito mode ([5d57b25](https://github.com/crimx/ext-saladict/commit/5d57b25)) * **menus:** prevent items being removed in incognito mode ([a380980](https://github.com/crimx/ext-saladict/commit/a380980)) * **panel:** disable fav icon on options page ([c616149](https://github.com/crimx/ext-saladict/commit/c616149)) * typings ([7f382a2](https://github.com/crimx/ext-saladict/commit/7f382a2)) * **panel:** center panel vertically when word editor shows up ([c31b5fa](https://github.com/crimx/ext-saladict/commit/c31b5fa)), closes [#315](https://github.com/crimx/ext-saladict/issues/315) * **panel:** max z-index for dict panel ([51b60d5](https://github.com/crimx/ext-saladict/commit/51b60d5)), closes [#316](https://github.com/crimx/ext-saladict/issues/316) * **sync:** duration ([4785a71](https://github.com/crimx/ext-saladict/commit/4785a71)) ### Features * **dicts:** add shanbay dictionary ([95ee0d5](https://github.com/crimx/ext-saladict/commit/95ee0d5)) * **panel:** add custom css ([4c58886](https://github.com/crimx/ext-saladict/commit/4c58886)) * **sync:** add shanbay ([a7389d5](https://github.com/crimx/ext-saladict/commit/a7389d5)) ### Performance Improvements * cache lang checks ([5e3034e](https://github.com/crimx/ext-saladict/commit/5e3034e)) # [6.26.0](https://github.com/crimx/ext-saladict/compare/v6.25.1...v6.26.0) (2019-03-09) ### Bug Fixes * **dicts:** add mp3 playing and fixing styles ([aefd2b1](https://github.com/crimx/ext-saladict/commit/aefd2b1)) * **options:** change wording close [#314](https://github.com/crimx/ext-saladict/issues/314) ([17aa162](https://github.com/crimx/ext-saladict/commit/17aa162)) * **selection:** match frames ([c923ce8](https://github.com/crimx/ext-saladict/commit/c923ce8)) ### Features * **menus:** google page translation ([a023074](https://github.com/crimx/ext-saladict/commit/a023074)) ### Performance Improvements * **panel:** prevent flickering when switching profiles ([c0bc97f](https://github.com/crimx/ext-saladict/commit/c0bc97f)) ## [6.25.1](https://github.com/crimx/ext-saladict/compare/v6.25.0...v6.25.1) (2019-03-04) ### Bug Fixes * firefox style error ([8256e63](https://github.com/crimx/ext-saladict/commit/8256e63)) * **dicts:** omit cookies ([3b6d1a8](https://github.com/crimx/ext-saladict/commit/3b6d1a8)), closes [#312](https://github.com/crimx/ext-saladict/issues/312) * correct lang selection ([86add32](https://github.com/crimx/ext-saladict/commit/86add32)) # [6.25.0](https://github.com/crimx/ext-saladict/compare/v6.24.4...v6.25.0) (2019-03-02) ### Bug Fixes * type error ([75d93d9](https://github.com/crimx/ext-saladict/commit/75d93d9)) * **dicts:** update hjdict korean page ([66c7341](https://github.com/crimx/ext-saladict/commit/66c7341)) * **options:** popup options ([5701525](https://github.com/crimx/ext-saladict/commit/5701525)) * **options:** styling ([dca805f](https://github.com/crimx/ext-saladict/commit/dca805f)) * **options:** wording ([087102a](https://github.com/crimx/ext-saladict/commit/087102a)) * **panel:** prevent drag event losing ([05dbaec](https://github.com/crimx/ext-saladict/commit/05dbaec)) * better korean rendering ([e13b51e](https://github.com/crimx/ext-saladict/commit/e13b51e)) ### Features * **dicts:** add dict naver ([cef45b4](https://github.com/crimx/ext-saladict/commit/cef45b4)) ### Performance Improvements * **panel:** faster style loading ([e2757af](https://github.com/crimx/ext-saladict/commit/e2757af)) * **panel:** remove extra update for auto-pasting ([2f7182b](https://github.com/crimx/ext-saladict/commit/2f7182b)) ## [6.24.4](https://github.com/crimx/ext-saladict/compare/v6.24.3...v6.24.4) (2019-02-17) ### Bug Fixes * **dicts:** update sogou token ([82b18cf](https://github.com/crimx/ext-saladict/commit/82b18cf)) ## [6.24.3](https://github.com/crimx/ext-saladict/compare/v6.24.2...v6.24.3) (2019-02-13) ### Bug Fixes * csp ([7d8790d](https://github.com/crimx/ext-saladict/commit/7d8790d)) * fix analytics ([adebbd3](https://github.com/crimx/ext-saladict/commit/adebbd3)) ## [6.24.2](https://github.com/crimx/ext-saladict/compare/v6.24.1...v6.24.2) (2019-02-13) ### Bug Fixes * **panel:** fix changing page title ([1fe0acc](https://github.com/crimx/ext-saladict/commit/1fe0acc)) * **panel:** fix fav icon ([5f0433f](https://github.com/crimx/ext-saladict/commit/5f0433f)) ## [6.24.1](https://github.com/crimx/ext-saladict/compare/v6.24.0...v6.24.1) (2019-02-13) ### Bug Fixes * **background:** proper init ([9babdef](https://github.com/crimx/ext-saladict/commit/9babdef)) # [6.24.0](https://github.com/crimx/ext-saladict/compare/v6.23.1...v6.24.0) (2019-02-12) ### Bug Fixes * **selection:** fixed [#296](https://github.com/crimx/ext-saladict/issues/296) ([1f9b6a6](https://github.com/crimx/ext-saladict/commit/1f9b6a6)) * fix typo ([129e863](https://github.com/crimx/ext-saladict/commit/129e863)) * **options:** add syncConfig ([51a9e57](https://github.com/crimx/ext-saladict/commit/51a9e57)) ### Features * **config:** add baidu to ctxTrans ([a2c1fda](https://github.com/crimx/ext-saladict/commit/a2c1fda)) * **dicts:** add baidu ([a9fecee](https://github.com/crimx/ext-saladict/commit/a9fecee)) * add analytics ([48582bd](https://github.com/crimx/ext-saladict/commit/48582bd)) ## [6.23.1](https://github.com/crimx/ext-saladict/compare/v6.23.0...v6.23.1) (2019-01-28) ### Bug Fixes * **options:** fix reset related errors ([55654e0](https://github.com/crimx/ext-saladict/commit/55654e0)) * **options:** wording ([8a47354](https://github.com/crimx/ext-saladict/commit/8a47354)) * **panel:** correct init selection ([b4a13eb](https://github.com/crimx/ext-saladict/commit/b4a13eb)) # [6.23.0](https://github.com/crimx/ext-saladict/compare/v6.22.8...v6.23.0) (2019-01-24) ### Bug Fixes * **panel:** open notebook on right click ([0099024](https://github.com/crimx/ext-saladict/commit/0099024)) * close [#289](https://github.com/crimx/ext-saladict/issues/289) ([1615794](https://github.com/crimx/ext-saladict/commit/1615794)) * **options:** add description ([deca4cb](https://github.com/crimx/ext-saladict/commit/deca4cb)) * **options:** add valuePropName for switch ([8574a30](https://github.com/crimx/ext-saladict/commit/8574a30)) * **options:** close modal ([b241d8b](https://github.com/crimx/ext-saladict/commit/b241d8b)) * **options:** fix holding toggling ([5f7cdfe](https://github.com/crimx/ext-saladict/commit/5f7cdfe)) * **options:** get profile id list on init ([114ccf0](https://github.com/crimx/ext-saladict/commit/114ccf0)) * **options:** keep modal hide animation ([18ce805](https://github.com/crimx/ext-saladict/commit/18ce805)) * **options:** remove unused ([0c6ea6d](https://github.com/crimx/ext-saladict/commit/0c6ea6d)) * **popup:** fix popup flickering ([90b7d72](https://github.com/crimx/ext-saladict/commit/90b7d72)) * **selection:** extract sentence head ([d5649e0](https://github.com/crimx/ext-saladict/commit/d5649e0)), closes [#287](https://github.com/crimx/ext-saladict/issues/287) * disable warning on dev ([2abc24a](https://github.com/crimx/ext-saladict/commit/2abc24a)) * fix config typing ([d164efb](https://github.com/crimx/ext-saladict/commit/d164efb)) * fix type error ([3db0b88](https://github.com/crimx/ext-saladict/commit/3db0b88)) * **options:** update active profile name on init ([83cadf3](https://github.com/crimx/ext-saladict/commit/83cadf3)) * remove activeProfileID when reset ([bbd5f01](https://github.com/crimx/ext-saladict/commit/bbd5f01)) * **options:** replace p elements with lis ([ed42ccb](https://github.com/crimx/ext-saladict/commit/ed42ccb)) * **profiles:** fix addActiveProfileListener ([2c67642](https://github.com/crimx/ext-saladict/commit/2c67642)) * langcode comparison ([4dade9b](https://github.com/crimx/ext-saladict/commit/4dade9b)) ### Features * **content:** add salad bowl clicking ([e6834af](https://github.com/crimx/ext-saladict/commit/e6834af)) * **popup:** add browser action behaviors ([6672a7a](https://github.com/crimx/ext-saladict/commit/6672a7a)), closes [#280](https://github.com/crimx/ext-saladict/issues/280) * add context translate engines config ([52e390b](https://github.com/crimx/ext-saladict/commit/52e390b)) ## [6.22.8](https://github.com/crimx/ext-saladict/compare/v6.22.7...v6.22.8) (2019-01-07) ### Bug Fixes * blacklist stackedit.io ([775298d](https://github.com/crimx/ext-saladict/commit/775298d)), closes [#277](https://github.com/crimx/ext-saladict/issues/277) * encode uri ([6098e34](https://github.com/crimx/ext-saladict/commit/6098e34)) * ignore &[#8203](https://github.com/crimx/ext-saladict/issues/8203); ([156275b](https://github.com/crimx/ext-saladict/commit/156275b)), closes [#274](https://github.com/crimx/ext-saladict/issues/274) ### Performance Improvements * faster matching sentence head ([3fa2fb6](https://github.com/crimx/ext-saladict/commit/3fa2fb6)), closes [#274](https://github.com/crimx/ext-saladict/issues/274) ## [6.22.7](https://github.com/crimx/ext-saladict/compare/v6.22.6...v6.22.7) (2018-12-31) ### Bug Fixes * better pdf context menu ([e7ada83](https://github.com/crimx/ext-saladict/commit/e7ada83)) * keep empty selected dicts ([acf3fb3](https://github.com/crimx/ext-saladict/commit/acf3fb3)) ## [6.22.6](https://github.com/crimx/ext-saladict/compare/v6.22.5...v6.22.6) (2018-12-23) ### Bug Fixes * fix pdf fetching ([c79fc89](https://github.com/crimx/ext-saladict/commit/c79fc89)) ## [6.22.5](https://github.com/crimx/ext-saladict/compare/v6.22.4...v6.22.5) (2018-12-21) ### Bug Fixes * fix google dict image src error ([682ae16](https://github.com/crimx/ext-saladict/commit/682ae16)) ## [6.22.4](https://github.com/crimx/ext-saladict/compare/v6.22.3...v6.22.4) (2018-12-21) ## [6.22.3](https://github.com/crimx/ext-saladict/compare/v6.22.2...v6.22.3) (2018-12-20) ### Bug Fixes * **dicts:** extract cambridge plurals ([2124c17](https://github.com/crimx/ext-saladict/commit/2124c17)) * **dicts:** update cambridge parser Closes [#266](https://github.com/crimx/ext-saladict/issues/266) ([b996c87](https://github.com/crimx/ext-saladict/commit/b996c87)) ## [6.22.2](https://github.com/crimx/ext-saladict/compare/v6.22.1...v6.22.2) (2018-12-14) ### Bug Fixes * **dicts:** update sogou api ([22f4d13](https://github.com/crimx/ext-saladict/commit/22f4d13)) ## [6.22.1](https://github.com/crimx/ext-saladict/compare/v6.22.0...v6.22.1) (2018-12-10) ### Bug Fixes * **dicts:** fix hjdict cookies ([b89618f](https://github.com/crimx/ext-saladict/commit/b89618f)) * **dicts:** use xhr to avoid Origin header ([36ffb4c](https://github.com/crimx/ext-saladict/commit/36ffb4c)), closes [#259](https://github.com/crimx/ext-saladict/issues/259) # [6.22.0](https://github.com/crimx/ext-saladict/compare/v6.21.2...v6.22.0) (2018-12-04) ### Bug Fixes * generate styles for all selected dicts ([1de0599](https://github.com/crimx/ext-saladict/commit/1de0599)) * highlight window and tab is exist ([840c085](https://github.com/crimx/ext-saladict/commit/840c085)), closes [#251](https://github.com/crimx/ext-saladict/issues/251) * **dicts:** add id when searching ([246c9af](https://github.com/crimx/ext-saladict/commit/246c9af)) * inject panel to every page on install ([f108f2e](https://github.com/crimx/ext-saladict/commit/f108f2e)) * **manifest:** make dicts styles web accessible ([2bc0dff](https://github.com/crimx/ext-saladict/commit/2bc0dff)) ### Features * **dicts:** add dict Hjdict ([76b8210](https://github.com/crimx/ext-saladict/commit/76b8210)), closes [#252](https://github.com/crimx/ext-saladict/issues/252) * **helpers:** add lang check ([5375f67](https://github.com/crimx/ext-saladict/commit/5375f67)) ### Performance Improvements * **dicts:** only load necessary styles ([e15fbdd](https://github.com/crimx/ext-saladict/commit/e15fbdd)) ## [6.21.2](https://github.com/crimx/ext-saladict/compare/v6.21.1...v6.21.2) (2018-11-28) ### Bug Fixes * **dicts:** encode sogou ([cd2cbc5](https://github.com/crimx/ext-saladict/commit/cd2cbc5)) ## [6.21.1](https://github.com/crimx/ext-saladict/compare/v6.21.0...v6.21.1) (2018-11-26) ### Bug Fixes * merge config ([791b7df](https://github.com/crimx/ext-saladict/commit/791b7df)) # [6.21.0](https://github.com/crimx/ext-saladict/compare/v6.20.2...v6.21.0) (2018-11-25) ### Bug Fixes * download exported wordpage files on Firefox ([aaba271](https://github.com/crimx/ext-saladict/commit/aaba271)) * **dicts:** sogou api ([bdd0bb5](https://github.com/crimx/ext-saladict/commit/bdd0bb5)) * **helpers:** remove Chs chars from Korean matching ([1daed3c](https://github.com/crimx/ext-saladict/commit/1daed3c)), closes [#249](https://github.com/crimx/ext-saladict/issues/249) * remove unused ([151bfb5](https://github.com/crimx/ext-saladict/commit/151bfb5)) ### Features * **dict:** add dict wikipedia ([603c558](https://github.com/crimx/ext-saladict/commit/603c558)) ## [6.20.2](https://github.com/crimx/ext-saladict/compare/v6.20.1...v6.20.2) (2018-11-09) ### Bug Fixes * **content:** exit after deleting word ([af34a37](https://github.com/crimx/ext-saladict/commit/af34a37)), closes [#245](https://github.com/crimx/ext-saladict/issues/245) ## [6.20.1](https://github.com/crimx/ext-saladict/compare/v6.20.0...v6.20.1) (2018-11-03) ### Bug Fixes * suggests panel ([8f4e13d](https://github.com/crimx/ext-saladict/commit/8f4e13d)) # [6.20.0](https://github.com/crimx/ext-saladict/compare/v6.19.0...v6.20.0) (2018-11-03) ### Bug Fixes * **content:** search context when jumping from popup to wordpage ([c1703ef](https://github.com/crimx/ext-saladict/commit/c1703ef)) * fix trans component ([0711058](https://github.com/crimx/ext-saladict/commit/0711058)) * **dicts:** remove trimming ([44f1fc4](https://github.com/crimx/ext-saladict/commit/44f1fc4)) * **panel:** fix suggests panel logic ([dff562d](https://github.com/crimx/ext-saladict/commit/dff562d)) ### Features * **dicts:** add options to remove linebreaks on PDF ([8b72b69](https://github.com/crimx/ext-saladict/commit/8b72b69)) * add search suggests ([f1f458b](https://github.com/crimx/ext-saladict/commit/f1f458b)) # [6.19.0](https://github.com/crimx/ext-saladict/compare/v6.18.1...v6.19.0) (2018-11-01) ### Bug Fixes * **configs:** new value could be empty when deleting ([5a5fad5](https://github.com/crimx/ext-saladict/commit/5a5fad5)) * **dicts:** trim text ([5dac1e7](https://github.com/crimx/ext-saladict/commit/5dac1e7)) * **helpers:** ignore irrelevant events ([72aa11a](https://github.com/crimx/ext-saladict/commit/72aa11a)) * **sync:** correct interval repeat ([0c7b607](https://github.com/crimx/ext-saladict/commit/0c7b607)) * **wordpage:** reset selected rows on full fetch ([9f0f42e](https://github.com/crimx/ext-saladict/commit/9f0f42e)) ### Features * **background:** add badge text ([8477f65](https://github.com/crimx/ext-saladict/commit/8477f65)) * **helpers:** add webdav sync service ([64df7c3](https://github.com/crimx/ext-saladict/commit/64df7c3)) * **sync:** add sync options ([73e4ce6](https://github.com/crimx/ext-saladict/commit/73e4ce6)) * add shift for instant search ([20a942a](https://github.com/crimx/ext-saladict/commit/20a942a)), closes [#232](https://github.com/crimx/ext-saladict/issues/232) ## [6.18.1](https://github.com/crimx/ext-saladict/compare/v6.18.0...v6.18.1) (2018-10-17) # [6.18.0](https://github.com/crimx/ext-saladict/compare/v6.17.1...v6.18.0) (2018-10-16) ### Bug Fixes * **content:** fix triple ctrl switch ([d1dcdeb](https://github.com/crimx/ext-saladict/commit/d1dcdeb)), closes [#222](https://github.com/crimx/ext-saladict/issues/222) ### Features * **content:** add more keys for holding mode ([bda8c07](https://github.com/crimx/ext-saladict/commit/bda8c07)), closes [#221](https://github.com/crimx/ext-saladict/issues/221) ## [6.17.1](https://github.com/crimx/ext-saladict/compare/v6.17.0...v6.17.1) (2018-10-12) ### Bug Fixes * **panel:** fix panel not showing if animation is off ([a533f59](https://github.com/crimx/ext-saladict/commit/a533f59)) * **panel:** fix search text loading on standalone panel ([828e315](https://github.com/crimx/ext-saladict/commit/828e315)) # [6.17.0](https://github.com/crimx/ext-saladict/compare/v6.16.1...v6.17.0) (2018-10-11) ### Bug Fixes * **background:** fix typing ([8cc0760](https://github.com/crimx/ext-saladict/commit/8cc0760)) * **dicts:** update google tk ([c36d15d](https://github.com/crimx/ext-saladict/commit/c36d15d)), closes [#212](https://github.com/crimx/ext-saladict/issues/212) * **locales:** typo ([4934e7c](https://github.com/crimx/ext-saladict/commit/4934e7c)) * **panel:** only load on top frame ([c9a8bf7](https://github.com/crimx/ext-saladict/commit/c9a8bf7)), closes [#214](https://github.com/crimx/ext-saladict/issues/214) * **panel:** prevent context menu on right click ([dd2b5ce](https://github.com/crimx/ext-saladict/commit/dd2b5ce)) * **selection:** input and textarea selection on Firefox ([dfa95f7](https://github.com/crimx/ext-saladict/commit/dfa95f7)) ### Features * **command:** add command for quick search panel ([dc34810](https://github.com/crimx/ext-saladict/commit/dc34810)) * **menu:** add google cn page translate ([f549fdc](https://github.com/crimx/ext-saladict/commit/f549fdc)) * **panel:** add quick search standalone panel ([2ff9fa2](https://github.com/crimx/ext-saladict/commit/2ff9fa2)) ## [6.16.1](https://github.com/crimx/ext-saladict/compare/v6.16.0...v6.16.1) (2018-10-02) ### Bug Fixes * **options:** support firefox export ([322ca3c](https://github.com/crimx/ext-saladict/commit/322ca3c)), closes [#210](https://github.com/crimx/ext-saladict/issues/210) # [6.16.0](https://github.com/crimx/ext-saladict/compare/v6.15.4...v6.16.0) (2018-10-01) ### Features * **dicts:** chs to chz ([c0cf11e](https://github.com/crimx/ext-saladict/commit/c0cf11e)) ## [6.15.4](https://github.com/crimx/ext-saladict/compare/v6.15.3...v6.15.4) (2018-09-29) ### Bug Fixes * **panel:** config not updating on init ([361c8ca](https://github.com/crimx/ext-saladict/commit/361c8ca)), closes [#209](https://github.com/crimx/ext-saladict/issues/209) * **panel:** not selecting when panel is called by triple ctrl ([b34b84d](https://github.com/crimx/ext-saladict/commit/b34b84d)), closes [#193](https://github.com/crimx/ext-saladict/issues/193) ## [6.15.3](https://github.com/crimx/ext-saladict/compare/v6.15.2...v6.15.3) (2018-09-23) ### Bug Fixes * **content:** fix salad bowl tomato on Firefox ([552f1ab](https://github.com/crimx/ext-saladict/commit/552f1ab)) ## [6.15.2](https://github.com/crimx/ext-saladict/compare/v6.15.1...v6.15.2) (2018-09-23) ### Bug Fixes * **panel:** dict info could be undefined ([388edc0](https://github.com/crimx/ext-saladict/commit/388edc0)) ## [6.15.1](https://github.com/crimx/ext-saladict/compare/v6.15.0...v6.15.1) (2018-09-23) ### Bug Fixes * **panel:** fix init config state mismatch ([06d7d5a](https://github.com/crimx/ext-saladict/commit/06d7d5a)) * **selection:** fix lang check for instant capture ([90aeb1b](https://github.com/crimx/ext-saladict/commit/90aeb1b)) # [6.15.0](https://github.com/crimx/ext-saladict/compare/v6.14.0...v6.15.0) (2018-09-16) ### Bug Fixes * **panel:** fix line breaking in English ([4b76760](https://github.com/crimx/ext-saladict/commit/4b76760)) * **panel:** fix profile panel shows on hover ([4b96472](https://github.com/crimx/ext-saladict/commit/4b96472)) * **popup:** fix body width ([5212b6a](https://github.com/crimx/ext-saladict/commit/5212b6a)) ### Features * **dicts:** add lang selection for machine translations ([739e5ea](https://github.com/crimx/ext-saladict/commit/739e5ea)) * **panel:** enable searchText on dict result ([85ec153](https://github.com/crimx/ext-saladict/commit/85ec153)) # [6.14.0](https://github.com/crimx/ext-saladict/compare/v6.13.4...v6.14.0) (2018-09-11) ### Bug Fixes * fix typings ([5678833](https://github.com/crimx/ext-saladict/commit/5678833)) ### Features * **components:** add dict weblio [#156](https://github.com/crimx/ext-saladict/issues/156) ([86bd514](https://github.com/crimx/ext-saladict/commit/86bd514)) ## [6.13.4](https://github.com/crimx/ext-saladict/compare/v6.13.3...v6.13.4) (2018-09-06) ### Bug Fixes * **panel:** fix focus losing on triple ctrl ([ff7a67b](https://github.com/crimx/ext-saladict/commit/ff7a67b)), closes [#193](https://github.com/crimx/ext-saladict/issues/193) ## [6.13.3](https://github.com/crimx/ext-saladict/compare/v6.13.2...v6.13.3) (2018-09-04) ### Bug Fixes * **config:** fix quota bytes limit ([cfb7268](https://github.com/crimx/ext-saladict/commit/cfb7268)) * **panel:** fix mta box toggling on update ([9f9983f](https://github.com/crimx/ext-saladict/commit/9f9983f)) ## [6.13.2](https://github.com/crimx/ext-saladict/compare/v6.13.1...v6.13.2) (2018-09-03) ### Bug Fixes * **panel:** fix search box update on selection ([7f3a270](https://github.com/crimx/ext-saladict/commit/7f3a270)), closes [#197](https://github.com/crimx/ext-saladict/issues/197) ## [6.13.1](https://github.com/crimx/ext-saladict/compare/v6.13.0...v6.13.1) (2018-09-02) ### Bug Fixes * **content:** fix popup init ([fc3cc85](https://github.com/crimx/ext-saladict/commit/fc3cc85)) # [6.13.0](https://github.com/crimx/ext-saladict/compare/v6.12.1...v6.13.0) (2018-09-02) ### Bug Fixes * **config:** add met merge config ([74ae476](https://github.com/crimx/ext-saladict/commit/74ae476)) * **configs:** fix config not updating on init ([7d54aa0](https://github.com/crimx/ext-saladict/commit/7d54aa0)) * **dicts:** fix etymonline ([c5aeca2](https://github.com/crimx/ext-saladict/commit/c5aeca2)) * **helpers:** prevent profiles blow up ([a5b7d2f](https://github.com/crimx/ext-saladict/commit/a5b7d2f)) * **panel:** fix mta search box search text ([244a45c](https://github.com/crimx/ext-saladict/commit/244a45c)) * **panel:** fix typings ([f312ffe](https://github.com/crimx/ext-saladict/commit/f312ffe)) * **panel:** safety check ([23e06db](https://github.com/crimx/ext-saladict/commit/23e06db)) ### Features * **options:** add options for toggling multiline search box ([df4e241](https://github.com/crimx/ext-saladict/commit/df4e241)) * **panel:** add multiline search box ([7370fc5](https://github.com/crimx/ext-saladict/commit/7370fc5)) ## [6.12.1](https://github.com/crimx/ext-saladict/compare/v6.12.0...v6.12.1) (2018-09-01) ### Bug Fixes * **config:** fix quota bytes per item exceeds ([2a64195](https://github.com/crimx/ext-saladict/commit/2a64195)) ### Features * **options:** add profile adding ([22fe8e7](https://github.com/crimx/ext-saladict/commit/22fe8e7)) # [6.12.0](https://github.com/crimx/ext-saladict/compare/v6.11.0...v6.12.0) (2018-08-31) ### Bug Fixes * **content:** correct config and selection listener order ([876b691](https://github.com/crimx/ext-saladict/commit/876b691)) * **content:** fix blank after new config ([ddc4953](https://github.com/crimx/ext-saladict/commit/ddc4953)) * **content:** hide config profile panel on options page ([a60ee25](https://github.com/crimx/ext-saladict/commit/a60ee25)) * **dicts:** fix camberidge audio ([f4c48b2](https://github.com/crimx/ext-saladict/commit/f4c48b2)), closes [#192](https://github.com/crimx/ext-saladict/issues/192) * **helpers:** fix listener interface ([41133da](https://github.com/crimx/ext-saladict/commit/41133da)) * **panel:** add search box delay ([645797c](https://github.com/crimx/ext-saladict/commit/645797c)) * **panel:** fix search box selection delay ([a3ffe05](https://github.com/crimx/ext-saladict/commit/a3ffe05)) * **selection:** fix context matching line ending ([da80def](https://github.com/crimx/ext-saladict/commit/da80def)) ### Features * **config:** support multi-configs ([5d7660b](https://github.com/crimx/ext-saladict/commit/5d7660b)) * **content:** add never show button for word editor ([ef77e7c](https://github.com/crimx/ext-saladict/commit/ef77e7c)) * **content:** add UI for switching profile ([3507b45](https://github.com/crimx/ext-saladict/commit/3507b45)) * **helpers:** add config id list stream ([398e3fd](https://github.com/crimx/ext-saladict/commit/398e3fd)) * **options:** add config profile settings ([3825d74](https://github.com/crimx/ext-saladict/commit/3825d74)) * **options:** add profile operations ([c7a622c](https://github.com/crimx/ext-saladict/commit/c7a622c)) # [6.11.0](https://github.com/crimx/ext-saladict/compare/v6.10.2...v6.11.0) (2018-08-28) ### Bug Fixes * **content:** fix typo ([ff6140a](https://github.com/crimx/ext-saladict/commit/ff6140a)) * **dicts:** fix typings ([2ea57d3](https://github.com/crimx/ext-saladict/commit/2ea57d3)) ### Features * **content:** auto-fill translation field ([efe95d2](https://github.com/crimx/ext-saladict/commit/efe95d2)) * **dicts:** add sogou translation ([3e11231](https://github.com/crimx/ext-saladict/commit/3e11231)) * **options:** add word of the day for options page ([251c119](https://github.com/crimx/ext-saladict/commit/251c119)) ## [6.10.2](https://github.com/crimx/ext-saladict/compare/v6.10.1...v6.10.2) (2018-08-27) ### Bug Fixes * **panel:** better search box focus ([fc16541](https://github.com/crimx/ext-saladict/commit/fc16541)), closes [#182](https://github.com/crimx/ext-saladict/issues/182) * **panel:** fix typing ([4c706a7](https://github.com/crimx/ext-saladict/commit/4c706a7)) * **panel:** improve smoothness when panel shows up ([a3ee454](https://github.com/crimx/ext-saladict/commit/a3ee454)) ## [6.10.1](https://github.com/crimx/ext-saladict/compare/v6.10.0...v6.10.1) (2018-08-27) ### Bug Fixes * **dicts:** fix longman styles ([4dc74c4](https://github.com/crimx/ext-saladict/commit/4dc74c4)) * **locales:** typo ([f86f77e](https://github.com/crimx/ext-saladict/commit/f86f77e)) # [6.10.0](https://github.com/crimx/ext-saladict/compare/v6.9.0...v6.10.0) (2018-08-27) ### Features * **background:** add pfd black and white list ([750a745](https://github.com/crimx/ext-saladict/commit/750a745)) * **dicts:** add google tl option ([489bdd9](https://github.com/crimx/ext-saladict/commit/489bdd9)) * **options:** dict options support select ([dcf6357](https://github.com/crimx/ext-saladict/commit/dcf6357)) * **panel:** add wordEditor deleteCards ([47f8f1b](https://github.com/crimx/ext-saladict/commit/47f8f1b)) * **panel:** improve fav word process ([b632a28](https://github.com/crimx/ext-saladict/commit/b632a28)) * **wordpage:** add page export/delete ([6ceb21a](https://github.com/crimx/ext-saladict/commit/6ceb21a)) * **wordpage:** add word count ([6667f53](https://github.com/crimx/ext-saladict/commit/6667f53)) ### Performance Improvements * **panel:** regression. remove search delay ([c65e53c](https://github.com/crimx/ext-saladict/commit/c65e53c)) # [6.9.0](https://github.com/crimx/ext-saladict/compare/v6.8.3...v6.9.0) (2018-08-04) ### Bug Fixes * **panel:** patch internal panel css ([befe9ce](https://github.com/crimx/ext-saladict/commit/befe9ce)) ### Features * **dicts:** bing sentence highlight ([ed1b7c4](https://github.com/crimx/ext-saladict/commit/ed1b7c4)) ## [6.8.3](https://github.com/crimx/ext-saladict/compare/v6.8.2...v6.8.3) (2018-07-27) ### Bug Fixes * **dicts:** fix audio link ([8b7e140](https://github.com/crimx/ext-saladict/commit/8b7e140)), closes [#175](https://github.com/crimx/ext-saladict/issues/175) ## [6.8.2](https://github.com/crimx/ext-saladict/compare/v6.8.1...v6.8.2) (2018-07-26) ### Bug Fixes * **panel:** triple ctrl auto search ([00a1381](https://github.com/crimx/ext-saladict/commit/00a1381)), closes [#174](https://github.com/crimx/ext-saladict/issues/174) ## [6.8.1](https://github.com/crimx/ext-saladict/compare/v6.8.0...v6.8.1) (2018-07-26) ### Bug Fixes * **helpers:** remove chs chars from korean [#173](https://github.com/crimx/ext-saladict/issues/173) ([38f1dce](https://github.com/crimx/ext-saladict/commit/38f1dce)) # [6.8.0](https://github.com/crimx/ext-saladict/compare/v6.7.0...v6.8.0) (2018-07-26) ### Bug Fixes * **selection:** correct word count ([6a28e3d](https://github.com/crimx/ext-saladict/commit/6a28e3d)), closes [#172](https://github.com/crimx/ext-saladict/issues/172) ### Features * **panel:** open links in new tabs ([d40cf2a](https://github.com/crimx/ext-saladict/commit/d40cf2a)) # [6.7.0](https://github.com/crimx/ext-saladict/compare/v6.6.0...v6.7.0) (2018-07-26) ### Bug Fixes * **dicts:** encode url ([0c0cd20](https://github.com/crimx/ext-saladict/commit/0c0cd20)), closes [#170](https://github.com/crimx/ext-saladict/issues/170) * **options:** auto-search on options page ([39ed461](https://github.com/crimx/ext-saladict/commit/39ed461)) * **panel:** reset internal style ([3119084](https://github.com/crimx/ext-saladict/commit/3119084)) * **selection:** better ctrl detection ([9f0c6b6](https://github.com/crimx/ext-saladict/commit/9f0c6b6)), closes [#168](https://github.com/crimx/ext-saladict/issues/168) * **selection:** better language detection ([c1f24e2](https://github.com/crimx/ext-saladict/commit/c1f24e2)) ### Features * **dicts:** add google tts ([5af7145](https://github.com/crimx/ext-saladict/commit/5af7145)) * **options:** add minor language options ([f269d0f](https://github.com/crimx/ext-saladict/commit/f269d0f)) * **selection:** minor lang selection ([8948f3c](https://github.com/crimx/ext-saladict/commit/8948f3c)) # [6.6.0](https://github.com/crimx/ext-saladict/compare/v6.5.1...v6.6.0) (2018-07-19) ### Bug Fixes * **content:** fix new selection interface ([2bb8ada](https://github.com/crimx/ext-saladict/commit/2bb8ada)) * **selection:** ignore right click [#166](https://github.com/crimx/ext-saladict/issues/166) ([ee5b794](https://github.com/crimx/ext-saladict/commit/ee5b794)) * **selection:** ignore triple ctrl when panel is visible [#162](https://github.com/crimx/ext-saladict/issues/162) ([e0e3208](https://github.com/crimx/ext-saladict/commit/e0e3208)) ### Features * **selection:** add selection inside dict panel [#165](https://github.com/crimx/ext-saladict/issues/165) ([f8da4be](https://github.com/crimx/ext-saladict/commit/f8da4be)) * **selection:** support selection on internal pages ([226be86](https://github.com/crimx/ext-saladict/commit/226be86)) ### Performance Improvements * **dicts:** improve google translate stability ([edd5fa9](https://github.com/crimx/ext-saladict/commit/edd5fa9)) # [6.5.1](https://github.com/crimx/ext-saladict/compare/v6.1.0...v6.5.1) (2018-07-10) ### Bug Fixes * **panel:** support span links ([32b4a3a](https://github.com/crimx/ext-saladict/commit/32b4a3a)) # [6.5.0](https://github.com/crimx/ext-saladict/compare/v6.4.1...v6.5.0) (2018-07-08) ### Bug Fixes * **options:** missing files breaks CI build ([ae35a49](https://github.com/crimx/ext-saladict/commit/ae35a49)) * update ci ([ba3108e](https://github.com/crimx/ext-saladict/commit/ba3108e)) * update ci ([97e5201](https://github.com/crimx/ext-saladict/commit/97e5201)) * update ci ([7dd28a8](https://github.com/crimx/ext-saladict/commit/7dd28a8)) * **popup:** fix preloading selection on popup page ([5912183](https://github.com/crimx/ext-saladict/commit/5912183)) ### Features * **$browser:** longman dictionary's exmaples add speaker ([8601078](https://github.com/crimx/ext-saladict/commit/8601078)) * **$browser:** longman max level is 3 ([c9a4a80](https://github.com/crimx/ext-saladict/commit/c9a4a80)) * **context:** add context menus saladict search [#152](https://github.com/crimx/ext-saladict/issues/152) ([6125ca8](https://github.com/crimx/ext-saladict/commit/6125ca8)) * **dicts:** add google dict ([355740c](https://github.com/crimx/ext-saladict/commit/355740c)), closes [#145](https://github.com/crimx/ext-saladict/issues/145) * **options:** add balck-white list [#155](https://github.com/crimx/ext-saladict/issues/155) ([a2c8d13](https://github.com/crimx/ext-saladict/commit/a2c8d13)) * **selection:** support Monaco editor ([edd0012](https://github.com/crimx/ext-saladict/commit/edd0012)) ## [6.4.1](https://github.com/crimx/ext-saladict/compare/v6.4.0...v6.4.1) (2018-06-28) ### Bug Fixes * **content:** fix dynamic document.body [#150](https://github.com/crimx/ext-saladict/issues/150) ([27f2787](https://github.com/crimx/ext-saladict/commit/27f2787)) * **manifest:** fix browser global conflict [#148](https://github.com/crimx/ext-saladict/issues/148) ([ca0d8a1](https://github.com/crimx/ext-saladict/commit/ca0d8a1)) # [6.4.0](https://github.com/crimx/ext-saladict/compare/v6.3.2...v6.4.0) (2018-06-17) ### Bug Fixes * **background:** regression. mistakenly added new code ([f974c61](https://github.com/crimx/ext-saladict/commit/f974c61)) * **content:** prevent selection detection on word editor ([8cc86a8](https://github.com/crimx/ext-saladict/commit/8cc86a8)) * **content:** regression: use position ([b5d75d8](https://github.com/crimx/ext-saladict/commit/b5d75d8)) * **panel:** fix Firefox popup page delay bug ([c5a4d6d](https://github.com/crimx/ext-saladict/commit/c5a4d6d)) * **panel:** iframe occasionally flickering ([e89cd03](https://github.com/crimx/ext-saladict/commit/e89cd03)) * **selection:** range could be null ([3cc2ec2](https://github.com/crimx/ext-saladict/commit/3cc2ec2)) * **selection:** update context matching [#144](https://github.com/crimx/ext-saladict/issues/144) ([fa20ab7](https://github.com/crimx/ext-saladict/commit/fa20ab7)) ### Features * **background:** add page translations [#146](https://github.com/crimx/ext-saladict/issues/146) ([c5d6225](https://github.com/crimx/ext-saladict/commit/c5d6225)) * **background:** add shortcut for instant capture ([bc46a2f](https://github.com/crimx/ext-saladict/commit/bc46a2f)) * **content:** add query panel state ([c92a7d0](https://github.com/crimx/ext-saladict/commit/c92a7d0)) * **content:** broadcast store state ([d0a356f](https://github.com/crimx/ext-saladict/commit/d0a356f)) * **options:** add instant capture ([71955a4](https://github.com/crimx/ext-saladict/commit/71955a4)) * **popup:** add instant capture toggle ([32dcfdc](https://github.com/crimx/ext-saladict/commit/32dcfdc)) * **selection:** add cursor instant capture [#14](https://github.com/crimx/ext-saladict/issues/14) ([ef37346](https://github.com/crimx/ext-saladict/commit/ef37346)) ## [6.3.2](https://github.com/crimx/ext-saladict/compare/v6.3.1...v6.3.2) (2018-06-13) ### Bug Fixes * **popup:** qrcode hiding ([eec0d02](https://github.com/crimx/ext-saladict/commit/eec0d02)) ## [6.3.1](https://github.com/crimx/ext-saladict/compare/v6.3.0...v6.3.1) (2018-06-13) ### Bug Fixes * **config:** increase default word count ([a8d98c4](https://github.com/crimx/ext-saladict/commit/a8d98c4)) * **config:** lang code auto update ([cffa171](https://github.com/crimx/ext-saladict/commit/cffa171)) * **popup:** fix id ([830386a](https://github.com/crimx/ext-saladict/commit/830386a)) # [6.3.0](https://github.com/crimx/ext-saladict/compare/v6.2.2...v6.3.0) (2018-06-12) ### Bug Fixes * **panel:** reset dict height ([f359205](https://github.com/crimx/ext-saladict/commit/f359205)) ### Features * **background:** add shortcuts [#141](https://github.com/crimx/ext-saladict/issues/141) ([76a35a2](https://github.com/crimx/ext-saladict/commit/76a35a2)) * **content:** add search history incognito mode ([8168c12](https://github.com/crimx/ext-saladict/commit/8168c12)) * **popup:** add active toggle ([3f1d115](https://github.com/crimx/ext-saladict/commit/3f1d115)), closes [#140](https://github.com/crimx/ext-saladict/issues/140) ### Performance Improvements * **panel:** better animation ([8777470](https://github.com/crimx/ext-saladict/commit/8777470)) ## [6.2.2](https://github.com/crimx/ext-saladict/compare/v6.2.1...v6.2.2) (2018-06-08) ### Bug Fixes * **panel:** fix missing calculation on hiding [#135](https://github.com/crimx/ext-saladict/issues/135) ([bb5823e](https://github.com/crimx/ext-saladict/commit/bb5823e)) ## [6.2.1](https://github.com/crimx/ext-saladict/compare/v6.2.0...v6.2.1) (2018-06-08) ### Features * **config:** add sogou [#134](https://github.com/crimx/ext-saladict/issues/134) ([8833a69](https://github.com/crimx/ext-saladict/commit/8833a69)) # [6.2.0](https://github.com/crimx/ext-saladict/compare/v6.1.3...v6.2.0) (2018-06-07) ### Bug Fixes * **content:** animation toggle on word editor ([a1c7efd](https://github.com/crimx/ext-saladict/commit/a1c7efd)) * **dicts:** hide sharing ([f1bd672](https://github.com/crimx/ext-saladict/commit/f1bd672)) ### Features * **dicts:** add google options ([9b4790d](https://github.com/crimx/ext-saladict/commit/9b4790d)) * **panel:** add double click search [#115](https://github.com/crimx/ext-saladict/issues/115) ([313ff16](https://github.com/crimx/ext-saladict/commit/313ff16)) * **panel:** selection word count [#129](https://github.com/crimx/ext-saladict/issues/129) ([a4eb1a1](https://github.com/crimx/ext-saladict/commit/a4eb1a1)) ### Performance Improvements * **content:** increase responsiveness ([b95f2ae](https://github.com/crimx/ext-saladict/commit/b95f2ae)) ## [6.1.3](https://github.com/crimx/ext-saladict/compare/v6.1.2...v6.1.3) (2018-06-06) ### Bug Fixes * **panel:** remove panel visibility delay [#132](https://github.com/crimx/ext-saladict/issues/132) ([305de64](https://github.com/crimx/ext-saladict/commit/305de64)) ## [6.1.2](https://github.com/crimx/ext-saladict/compare/v6.1.1...v6.1.2) (2018-06-06) ### Bug Fixes * **panel:** fix null pointer ([dc3a41e](https://github.com/crimx/ext-saladict/commit/dc3a41e)), closes [#130](https://github.com/crimx/ext-saladict/issues/130) ## [6.1.1](https://github.com/crimx/ext-saladict/compare/v6.1.0...v6.1.1) (2018-06-05) ### Bug Fixes * **panel:** fix right click ([d06be31](https://github.com/crimx/ext-saladict/commit/d06be31)) # [6.1.0](https://github.com/crimx/ext-saladict/compare/v6.0.0...v6.1.0) (2018-06-05) ### Bug Fixes * **panel:** fix auto focus [#124](https://github.com/crimx/ext-saladict/issues/124) ([4d03bda](https://github.com/crimx/ext-saladict/commit/4d03bda)) * **panel:** fix entering in options page and popup page ([298bbb1](https://github.com/crimx/ext-saladict/commit/298bbb1)) * **panel:** fix height calc ([148cd56](https://github.com/crimx/ext-saladict/commit/148cd56)) * **panel:** fix iframe flickering in Chrome [#124](https://github.com/crimx/ext-saladict/issues/124) [#113](https://github.com/crimx/ext-saladict/issues/113) [#119](https://github.com/crimx/ext-saladict/issues/119) ([922d8d4](https://github.com/crimx/ext-saladict/commit/922d8d4)) * **panel:** popup preload [#124](https://github.com/crimx/ext-saladict/issues/124) ([69aa7a8](https://github.com/crimx/ext-saladict/commit/69aa7a8)) * **panel:** remove animation ([55ef5fa](https://github.com/crimx/ext-saladict/commit/55ef5fa)), closes [#123](https://github.com/crimx/ext-saladict/issues/123) * **selection:** always update last selection text ([c6aca15](https://github.com/crimx/ext-saladict/commit/c6aca15)) * **selection:** ctrl key detection [#124](https://github.com/crimx/ext-saladict/issues/124) [#122](https://github.com/crimx/ext-saladict/issues/122) [#114](https://github.com/crimx/ext-saladict/issues/114) ([a5893d2](https://github.com/crimx/ext-saladict/commit/a5893d2)) ### Features * **components:** explain export ([c6022c3](https://github.com/crimx/ext-saladict/commit/c6022c3)) * **config:** add toggle for word editor [#118](https://github.com/crimx/ext-saladict/issues/118) ([eb95680](https://github.com/crimx/ext-saladict/commit/eb95680)) * **content:** explain translations ([c6d9d50](https://github.com/crimx/ext-saladict/commit/c6d9d50)), closes [#117](https://github.com/crimx/ext-saladict/issues/117) # [6.0.0](https://github.com/crimx/ext-saladict/compare/v5.31.7...v6.0.0) (2018-05-30) ### Bug Fixes * **assets:** assets to static ([8dc5092](https://github.com/crimx/ext-saladict/commit/8dc5092)) * **background:** content script cannot catch rejections from bg ([29667a5](https://github.com/crimx/ext-saladict/commit/29667a5)) * **background:** context menus i18n init event ([25dbeef](https://github.com/crimx/ext-saladict/commit/25dbeef)) * **background:** fix typo ([6d7e25c](https://github.com/crimx/ext-saladict/commit/6d7e25c)) * **browser:** fix diffrenent removeListener api ([40c4721](https://github.com/crimx/ext-saladict/commit/40c4721)) * **browser:** fix webext polyfills ([ce11f10](https://github.com/crimx/ext-saladict/commit/ce11f10)) * **build:** fix fake env ([a1bf3ae](https://github.com/crimx/ext-saladict/commit/a1bf3ae)) * **components:** better keys for star rates ([6b50750](https://github.com/crimx/ext-saladict/commit/6b50750)) * **components:** fix Speaker svg dimension ([7d48654](https://github.com/crimx/ext-saladict/commit/7d48654)) * **components:** fix StarRates style ([1f65959](https://github.com/crimx/ext-saladict/commit/1f65959)) * **components:** stop setState when unmounted ([0d86b56](https://github.com/crimx/ext-saladict/commit/0d86b56)) * **config:** more test friendly ([a8e194f](https://github.com/crimx/ext-saladict/commit/a8e194f)) * **config:** replace the empty sting ([56937a9](https://github.com/crimx/ext-saladict/commit/56937a9)) * **config:** update config ([635608b](https://github.com/crimx/ext-saladict/commit/635608b)) * **content:** close on save & filter self ([e92a2f6](https://github.com/crimx/ext-saladict/commit/e92a2f6)) * **content:** delay injecting css ([a12e070](https://github.com/crimx/ext-saladict/commit/a12e070)) * **content:** fix close panel when pinned ([4771637](https://github.com/crimx/ext-saladict/commit/4771637)) * **content:** fix dict item height restore on search ([d5fbc78](https://github.com/crimx/ext-saladict/commit/d5fbc78)) * **content:** fix firefox !important bug ([73c162a](https://github.com/crimx/ext-saladict/commit/73c162a)) * **content:** fix firefox svg animation ([76f17cb](https://github.com/crimx/ext-saladict/commit/76f17cb)) * **content:** fix iframe flickering in Chrome ([9bab2af](https://github.com/crimx/ext-saladict/commit/9bab2af)) * **content:** fix init position since now use translate ([9ffb1ef](https://github.com/crimx/ext-saladict/commit/9ffb1ef)) * **content:** fix inject css ([4a82878](https://github.com/crimx/ext-saladict/commit/4a82878)) * **content:** fix long press ctrl ([e2613fc](https://github.com/crimx/ext-saladict/commit/e2613fc)) * **content:** fix mask button disabled on fold ([35769e9](https://github.com/crimx/ext-saladict/commit/35769e9)) * **content:** fix new config refresh bug ([3f44afc](https://github.com/crimx/ext-saladict/commit/3f44afc)) * **content:** get the first config before listening ([2a6fdf6](https://github.com/crimx/ext-saladict/commit/2a6fdf6)) * **database:** ignore case ([72f6abe](https://github.com/crimx/ext-saladict/commit/72f6abe)) * **dicts:** add space after basic ([8bbfb08](https://github.com/crimx/ext-saladict/commit/8bbfb08)) * **dicts:** bypass etymonline referer checking ([a105eec](https://github.com/crimx/ext-saladict/commit/a105eec)) * **dicts:** change url ([4772fa9](https://github.com/crimx/ext-saladict/commit/4772fa9)) * **dicts:** fix bing audio ([b0b641f](https://github.com/crimx/ext-saladict/commit/b0b641f)) * **dicts:** fix bing audio language detection ([ad577c3](https://github.com/crimx/ext-saladict/commit/ad577c3)) * **dicts:** fix bing phsym key ([d248a6d](https://github.com/crimx/ext-saladict/commit/d248a6d)) * **dicts:** fix cambridge link ([68b0a4d](https://github.com/crimx/ext-saladict/commit/68b0a4d)) * **dicts:** fix COBUILD page link ([7a6f45e](https://github.com/crimx/ext-saladict/commit/7a6f45e)) * **dicts:** fix img style ([130158a](https://github.com/crimx/ext-saladict/commit/130158a)) * **dicts:** fix lang code ([b6532cd](https://github.com/crimx/ext-saladict/commit/b6532cd)) * **dicts:** fix locales ([c39151e](https://github.com/crimx/ext-saladict/commit/c39151e)) * **dicts:** fix macmillan style ([64aee9b](https://github.com/crimx/ext-saladict/commit/64aee9b)) * **dicts:** fix p margin ([1561888](https://github.com/crimx/ext-saladict/commit/1561888)) * **dicts:** get correct href ([cd887e1](https://github.com/crimx/ext-saladict/commit/cd887e1)) * **dicts:** remove logging ([b19dab8](https://github.com/crimx/ext-saladict/commit/b19dab8)) * **dicts:** use innerHTML ([dee2afd](https://github.com/crimx/ext-saladict/commit/dee2afd)) * **dicts:** use lower case ([f66e99a](https://github.com/crimx/ext-saladict/commit/f66e99a)) * **helper:** rxjs6 fromEventPattern inconsistency ([7c1594d](https://github.com/crimx/ext-saladict/commit/7c1594d)) * **helpers:** always merge config ([49e01ff](https://github.com/crimx/ext-saladict/commit/49e01ff)) * **helpers:** fix wrong deletion ([a55c46d](https://github.com/crimx/ext-saladict/commit/a55c46d)) * **helpers:** get first config ([d8bb3fa](https://github.com/crimx/ext-saladict/commit/d8bb3fa)) * **helpers:** get initial config ([a221bc7](https://github.com/crimx/ext-saladict/commit/a221bc7)) * **helpers:** handle annoying msg errors from webext polyfill ([272f1eb](https://github.com/crimx/ext-saladict/commit/272f1eb)) * **helpers:** props added to window should be optional ([1f12b26](https://github.com/crimx/ext-saladict/commit/1f12b26)) * **locales:** add from Saladict ([b3b215e](https://github.com/crimx/ext-saladict/commit/b3b215e)) * **locales:** add missing locales ([fc500ab](https://github.com/crimx/ext-saladict/commit/fc500ab)) * **locales:** fix browser ui locale naming ([e57e61c](https://github.com/crimx/ext-saladict/commit/e57e61c)) * **locales:** fix wording ([2d7486e](https://github.com/crimx/ext-saladict/commit/2d7486e)) * **locales:** use standard lang code ([7a901ec](https://github.com/crimx/ext-saladict/commit/7a901ec)) * **manifest:** declare wordeditor as web accessible resources ([f9984f5](https://github.com/crimx/ext-saladict/commit/f9984f5)) * **manifest:** fix manifest ([685ded1](https://github.com/crimx/ext-saladict/commit/685ded1)) * **menus:** fix rxjs path ([07fc3a9](https://github.com/crimx/ext-saladict/commit/07fc3a9)) * **options:** add key for v-for ([becfe1d](https://github.com/crimx/ext-saladict/commit/becfe1d)) * **options:** add max width ([45a1532](https://github.com/crimx/ext-saladict/commit/45a1532)) * **options:** fix auto search ([cd87b86](https://github.com/crimx/ext-saladict/commit/cd87b86)) * **options:** fix decimal bug ([835e1ca](https://github.com/crimx/ext-saladict/commit/835e1ca)) * **options:** fix language code ([d86b5c0](https://github.com/crimx/ext-saladict/commit/d86b5c0)) * **options:** fix modal scroll bar flickering ([9294de4](https://github.com/crimx/ext-saladict/commit/9294de4)) * **options:** fix style ([2417ec4](https://github.com/crimx/ext-saladict/commit/2417ec4)) * **options:** search text when options page is opened ([c359b70](https://github.com/crimx/ext-saladict/commit/c359b70)) * **package:** update normalize.css to version 8.0.0 ([1dbc42f](https://github.com/crimx/ext-saladict/commit/1dbc42f)) * **panel:** blur input on drag start ([1759053](https://github.com/crimx/ext-saladict/commit/1759053)) * **panel:** calculate margin height ([33d1a0a](https://github.com/crimx/ext-saladict/commit/33d1a0a)) * **panel:** can't unfold a dict when the panel fisrt popup ([41e9498](https://github.com/crimx/ext-saladict/commit/41e9498)) * **panel:** close panel and word editer on esc ([d133d6e](https://github.com/crimx/ext-saladict/commit/d133d6e)) * **panel:** debounce animation end ([0cd7e32](https://github.com/crimx/ext-saladict/commit/0cd7e32)) * **panel:** fix data inconsistency ([1b956aa](https://github.com/crimx/ext-saladict/commit/1b956aa)) * **panel:** fix firame flickering ([81aebc7](https://github.com/crimx/ext-saladict/commit/81aebc7)) * **panel:** fix icon bleeds ([9ce942a](https://github.com/crimx/ext-saladict/commit/9ce942a)) * **panel:** fix panel init height ([799cc4c](https://github.com/crimx/ext-saladict/commit/799cc4c)) * **panel:** fix panel init on options page ([be4815d](https://github.com/crimx/ext-saladict/commit/be4815d)) * **panel:** fix preload text ([79732b8](https://github.com/crimx/ext-saladict/commit/79732b8)) * **panel:** fix search box should follow history ([5cca229](https://github.com/crimx/ext-saladict/commit/5cca229)) * **panel:** fix styles ([557fdff](https://github.com/crimx/ext-saladict/commit/557fdff)) * **panel:** height recalculation on show full ([6f0077e](https://github.com/crimx/ext-saladict/commit/6f0077e)) * **panel:** hide dicts when the selection lang does not match ([b099b5f](https://github.com/crimx/ext-saladict/commit/b099b5f)) * **panel:** keep dict height unchanged when there is nothing ([47ef11b](https://github.com/crimx/ext-saladict/commit/47ef11b)) * **panel:** only set immediate to x and y when dragging ([ce0e252](https://github.com/crimx/ext-saladict/commit/ce0e252)) * **panel:** panel should listen to config on options page ([c751d44](https://github.com/crimx/ext-saladict/commit/c751d44)) * **panel:** recalc body height when expanding menus ([b6a5714](https://github.com/crimx/ext-saladict/commit/b6a5714)) * **panel:** record search text on options page ([86eec63](https://github.com/crimx/ext-saladict/commit/86eec63)) * **panel:** remove animation on popup page ([d298201](https://github.com/crimx/ext-saladict/commit/d298201)) * **panel:** replace same selection ([5270b8e](https://github.com/crimx/ext-saladict/commit/5270b8e)) * **panel:** stop following cursor when pinned ([c1d04c1](https://github.com/crimx/ext-saladict/commit/c1d04c1)) * **panel:** stop searching when selection lang doesn't match ([e506edd](https://github.com/crimx/ext-saladict/commit/e506edd)) * **panel:** try whatever I can to stop iframe flickering ([949d03e](https://github.com/crimx/ext-saladict/commit/949d03e)) * **panel:** tweak styles ([14437ef](https://github.com/crimx/ext-saladict/commit/14437ef)) * **popup:** add to notebook on popup page ([f27dc2f](https://github.com/crimx/ext-saladict/commit/f27dc2f)) * **popup:** remove white spaces ([c1f22e2](https://github.com/crimx/ext-saladict/commit/c1f22e2)) * **sass:** add global ([73aaffa](https://github.com/crimx/ext-saladict/commit/73aaffa)) * **selection:** compress whitespaces in selection ([d43eaa1](https://github.com/crimx/ext-saladict/commit/d43eaa1)) * **selection:** fix className breaking on svg elements ([f932de3](https://github.com/crimx/ext-saladict/commit/f932de3)) * **selection:** fix editor detection ([5d5c11a](https://github.com/crimx/ext-saladict/commit/5d5c11a)) * **selection:** fix ignoring same selection rule for double click ([a587c22](https://github.com/crimx/ext-saladict/commit/a587c22)) * **selection:** fix undefined detection ([2ad9269](https://github.com/crimx/ext-saladict/commit/2ad9269)) * **selection:** get target on mousedown ([248578d](https://github.com/crimx/ext-saladict/commit/248578d)) * **selection:** track same selection ([bbdbcbf](https://github.com/crimx/ext-saladict/commit/bbdbcbf)) * **static:** fix [#99](https://github.com/crimx/ext-saladict/issues/99) fanyi.youdao bypass http request ([fe84550](https://github.com/crimx/ext-saladict/commit/fe84550)) * **static:** use browser instead of chrome ([3709e3f](https://github.com/crimx/ext-saladict/commit/3709e3f)) * **types:** fix typings ([f242fd3](https://github.com/crimx/ext-saladict/commit/f242fd3)) ### Features * **background:** add auto pronounce ([20a8a33](https://github.com/crimx/ext-saladict/commit/20a8a33)) * **background:** add search result typing ([76dc07b](https://github.com/crimx/ext-saladict/commit/76dc07b)) * **background:** context menus with i18n ([9e66f55](https://github.com/crimx/ext-saladict/commit/9e66f55)) * **background:** timeout searching ([d2bd4d4](https://github.com/crimx/ext-saladict/commit/d2bd4d4)) * **build:** add devbuild flag ([a5f2af0](https://github.com/crimx/ext-saladict/commit/a5f2af0)) * **component:** add PortalFrame ([cbe2ddd](https://github.com/crimx/ext-saladict/commit/cbe2ddd)) * **components:** add word-phrase filter for WordPage ([29cf96a](https://github.com/crimx/ext-saladict/commit/29cf96a)), closes [#103](https://github.com/crimx/ext-saladict/issues/103) * **components:** add WordPage ([0727db1](https://github.com/crimx/ext-saladict/commit/0727db1)) * **components:** add wordpage search text ([f4a61fd](https://github.com/crimx/ext-saladict/commit/f4a61fd)) * **components:** change Speaker to render nothing when no src ([5a50165](https://github.com/crimx/ext-saladict/commit/5a50165)) * **content:** add animation switch ([8c2939e](https://github.com/crimx/ext-saladict/commit/8c2939e)) * **content:** add component DictItem ([c600671](https://github.com/crimx/ext-saladict/commit/c600671)) * **content:** add component SaladBowl ([4200c19](https://github.com/crimx/ext-saladict/commit/4200c19)) * **content:** add content script entry ([a07519d](https://github.com/crimx/ext-saladict/commit/a07519d)) * **content:** add dict panel ([9a68f2c](https://github.com/crimx/ext-saladict/commit/9a68f2c)) * **content:** add i18n for menu bar ([b1a8c9e](https://github.com/crimx/ext-saladict/commit/b1a8c9e)) * **content:** add menu bar ([bf6b74f](https://github.com/crimx/ext-saladict/commit/bf6b74f)) * **content:** add menu bar search history ([5f33191](https://github.com/crimx/ext-saladict/commit/5f33191)) * **content:** add mouseevent on bowl ([44a37bd](https://github.com/crimx/ext-saladict/commit/44a37bd)) * **content:** add notebook ui logic ([dfd18ac](https://github.com/crimx/ext-saladict/commit/dfd18ac)) * **content:** add onhold ([6b0844e](https://github.com/crimx/ext-saladict/commit/6b0844e)) * **content:** add panel closing ([8a20f74](https://github.com/crimx/ext-saladict/commit/8a20f74)) * **content:** add panel dragging ([9682afc](https://github.com/crimx/ext-saladict/commit/9682afc)) * **content:** add redux store ([956d2af](https://github.com/crimx/ext-saladict/commit/956d2af)) * **content:** add related words ([25c6cf7](https://github.com/crimx/ext-saladict/commit/25c6cf7)) * **content:** add relationships between bowl and panel ([bd24060](https://github.com/crimx/ext-saladict/commit/bd24060)) * **content:** add saladbow redux container ([6699707](https://github.com/crimx/ext-saladict/commit/6699707)) * **content:** add search box text update ([2dc6bbc](https://github.com/crimx/ext-saladict/commit/2dc6bbc)) * **content:** add store dictionaries ([56ce6dc](https://github.com/crimx/ext-saladict/commit/56ce6dc)) * **content:** add store search text ([ff5c751](https://github.com/crimx/ext-saladict/commit/ff5c751)) * **content:** add triple ctrl ([69ae1c4](https://github.com/crimx/ext-saladict/commit/69ae1c4)) * **content:** add word editor ([9415cf0](https://github.com/crimx/ext-saladict/commit/9415cf0)) * **content:** connect word editor to main component ([38a566c](https://github.com/crimx/ext-saladict/commit/38a566c)) * **content:** delay mouse on bowl ([0aa1b90](https://github.com/crimx/ext-saladict/commit/0aa1b90)) * **content:** finish word editor feature ([c77978c](https://github.com/crimx/ext-saladict/commit/c77978c)) * **content:** intergrate content script into options page ([5c946f1](https://github.com/crimx/ext-saladict/commit/5c946f1)) * **content:** listen to edit word event ([9188f21](https://github.com/crimx/ext-saladict/commit/9188f21)) * **content:** move search logic together ([f7a1294](https://github.com/crimx/ext-saladict/commit/f7a1294)) * **content:** notify parents of hight changing ([1d90c45](https://github.com/crimx/ext-saladict/commit/1d90c45)) * **content:** setup files ([90c41c3](https://github.com/crimx/ext-saladict/commit/90c41c3)) * **content:** support important styling ([d150e45](https://github.com/crimx/ext-saladict/commit/d150e45)) * **content:** supprot max height ([deecdcc](https://github.com/crimx/ext-saladict/commit/deecdcc)) * **content:** sync panel height with dict item height ([a0d215a](https://github.com/crimx/ext-saladict/commit/a0d215a)) * **context-menus:** add youglish ([cd39f22](https://github.com/crimx/ext-saladict/commit/cd39f22)) * **dicts:** add %h hyphen joining ([311ae79](https://github.com/crimx/ext-saladict/commit/311ae79)) * **dicts:** add cambridge ([be329bd](https://github.com/crimx/ext-saladict/commit/be329bd)) * **dicts:** add helper ([09f929f](https://github.com/crimx/ext-saladict/commit/09f929f)) * **dicts:** add helpers ([999362c](https://github.com/crimx/ext-saladict/commit/999362c)) * **dicts:** add helpers ([428cd08](https://github.com/crimx/ext-saladict/commit/428cd08)) * **dicts:** add Longman ([90688b6](https://github.com/crimx/ext-saladict/commit/90688b6)) * **dicts:** add oald ([65b7327](https://github.com/crimx/ext-saladict/commit/65b7327)) * **dicts:** add webster learners dict ([6e0f1ee](https://github.com/crimx/ext-saladict/commit/6e0f1ee)) * **dicts:** add youdao ([57ec2b3](https://github.com/crimx/ext-saladict/commit/57ec2b3)) * **dicts:** fix longman style ([95f172e](https://github.com/crimx/ext-saladict/commit/95f172e)) * **dicts:** more robust google engine ([701d1d4](https://github.com/crimx/ext-saladict/commit/701d1d4)) * **helpers:** let openURL support ext based url ([e89eb54](https://github.com/crimx/ext-saladict/commit/e89eb54)) * **history:** add history entry ([750a51a](https://github.com/crimx/ext-saladict/commit/750a51a)) * **i18n:** loader accepts callback ([c9077b8](https://github.com/crimx/ext-saladict/commit/c9077b8)) * **locale:** add back and next ([97d7094](https://github.com/crimx/ext-saladict/commit/97d7094)) * **locales:** add locales ([10e9a2e](https://github.com/crimx/ext-saladict/commit/10e9a2e)) * **locales:** update options locales ([5fbc0d1](https://github.com/crimx/ext-saladict/commit/5fbc0d1)) * **manifest:** support dynamically generated iframes ([94ff5d4](https://github.com/crimx/ext-saladict/commit/94ff5d4)), closes [#106](https://github.com/crimx/ext-saladict/issues/106) * **notebook:** add notebook entry ([b24c7db](https://github.com/crimx/ext-saladict/commit/b24c7db)) * **options:** add Acknowledgement ([578891a](https://github.com/crimx/ext-saladict/commit/578891a)) * **options:** add animation option ([4da7b31](https://github.com/crimx/ext-saladict/commit/4da7b31)) * **options:** add config import and export ([21d32d1](https://github.com/crimx/ext-saladict/commit/21d32d1)) * **options:** add displaying supported languages ([6bb345e](https://github.com/crimx/ext-saladict/commit/6bb345e)) * **options:** smart searching ([451860f](https://github.com/crimx/ext-saladict/commit/451860f)) * **options:** update options ([817a314](https://github.com/crimx/ext-saladict/commit/817a314)) * **options:** update options to the latest config ([6901f84](https://github.com/crimx/ext-saladict/commit/6901f84)) * **panel:** add error boundary for dict ([56e957d](https://github.com/crimx/ext-saladict/commit/56e957d)) * **panel:** add touch support ([2e856b4](https://github.com/crimx/ext-saladict/commit/2e856b4)) * **panel:** disable buttons in popup page ([2b80ea4](https://github.com/crimx/ext-saladict/commit/2b80ea4)) * **panel:** get all dict styles ([a80d0ac](https://github.com/crimx/ext-saladict/commit/a80d0ac)) * **panel:** integrate panel with popup page ([fddbc38](https://github.com/crimx/ext-saladict/commit/fddbc38)) * **panel:** open exteranl link ([9aaf70d](https://github.com/crimx/ext-saladict/commit/9aaf70d)) * **panel:** open url base on lang code ([efb5187](https://github.com/crimx/ext-saladict/commit/efb5187)) * **panel:** sticky header! pure css! ([e9490ac](https://github.com/crimx/ext-saladict/commit/e9490ac)) * **popup:** add temporary disabling dict panel ([7710e3c](https://github.com/crimx/ext-saladict/commit/7710e3c)) * **scss:** add scss globals ([af6e6df](https://github.com/crimx/ext-saladict/commit/af6e6df)) * **selection:** add double click detection ([1299bec](https://github.com/crimx/ext-saladict/commit/1299bec)) * **selection:** add get empty selection info ([9d01d9c](https://github.com/crimx/ext-saladict/commit/9d01d9c)) * **selection:** add noTypeField ([f395f8c](https://github.com/crimx/ext-saladict/commit/f395f8c)) * **selection:** detect esc key in all frames ([1e6ecc5](https://github.com/crimx/ext-saladict/commit/1e6ecc5)) * **test:** add snapshot testing ([0be395c](https://github.com/crimx/ext-saladict/commit/0be395c)) * **wordbook:** add database for words ([46c4327](https://github.com/crimx/ext-saladict/commit/46c4327)) ### Performance Improvements * **config:** refactor to get ride of cloneDeep ([d986b53](https://github.com/crimx/ext-saladict/commit/d986b53)) * **helpers:** use DOMParser which is 6 time faster ([157a929](https://github.com/crimx/ext-saladict/commit/157a929)) * **message:** change message type to typescript enum ([7682a1d](https://github.com/crimx/ext-saladict/commit/7682a1d)) # Changelog [Unreleased] [5.31.7] - 2017-12-18 ### Added - 使用 webRequest 拦截 PDF 请求 ### Changed - 钉住时快速查询不移动窗口 - 设置页面增加反馈链接 ### Fixed - 第二次打开浏览器右键菜单不显示 - 必应词典相关单词可点击 [5.30.0] - 2017-12-08 ### Changed - 可同时选择多个划词模式 - 工具栏“选项”按钮改为词典目录 ### Fixed - 修复词典标题点击跳转 [5.29.3] - 2017-11-29 ### Added - 单词记录同时保存上下文和来源 - 可编辑单词记录,可添加翻译和注释笔记 - 可自定义导出模板 ### Changed - 使用无限容量权限 ### Fixed - 编辑完后卡片响应 - 查词框输入后马上点添加生词出现不匹配 [5.28.1] - 2017-11-26 ### Added - 增加生词本 ### Changed - 可配置预加载内容(剪贴板或页面选中词)与自动开始查词,快捷查词可设置出现的位置 ### Fixed - 重构代码,减少耦合 [5.27.3] - 2017-11-23 ### Added - 增加有道分级网页翻译2.0(支持 HTTPS) - 增加自动发音 - 增加查词历史记录 - 面板钉住时支持多种查词模式 - 必应词典无结果时增加相关词语 - 词带内部双击查词,点击单词链接也能直接查词 - 对抓取页面筛选节点以增强安全性 - 自身页面通信增加 page id 以解决冲突问题 ### Changed - 查看页面二维码移到地址栏旁的图标中 - Chrome 最低版本支持提升为 55 以提升性能与减少大小 - 重构代码以分散复杂度 - 二维码生成改用 vue-qriously 更轻盈 ### Fixed - 修复打开 PDF 时弹出框查词自动粘贴失效 - 修复 howjsay 相关词语获取 - 修复查词滚动错误 [5.19.1] - 2017-11-15 ### Added - 可配置双击时长 ### Fixed - 默认不显示词典以避免闪现 [5.18.5] - 2017-11-13 ### Added - 增加汉典 - 可配置词典只在某种语言下显示 ### Fixed - 修复繁体词典不能查简体字问题 - 修复默认收起的词典不能隐藏 - 更新 vuedraggable 修复拖动问题 - 延迟音频播放避免误触 - 每次查词滚动到顶端 [5.16.1] - 2017-10-28 ### Added - 添加 PDF 支持 ### Fixed - 修复通知框点击 [5.15.21] - 2017-10-26 ### Changed - 全不选时右键菜单隐藏 ### Fixed - 更新时才弹出通知 - 重构 event page,顶层只保留监听,加快加载速度 - 去掉 require.context,webpack 会自动生成路径 [5.15.19] - 2017-10-11 ### Changed - 重构事件监听 - 重构 chrome api wrap ### Fixed - 点击发音 - 自动恢复 dom 挂载 - 更新 etymonline 词典 [5.15.14] - 2017-09-05 ### Changed - 弹出查词框时自动选中所有剪贴板内容 - 查词结构导出图片样式调整 [5.15.12] - 2017-09-02 ### Fixed - 修复 ctrl/⌘ 模式时切换窗口的问题 - 麦克米伦标题修复 ### Changed - 关闭自动查词 [5.15.9] - 2017-08-23 ### Fixed - 更新必应词典 - 修复拖动抖动问题 - 样式修补 ### Changed - 第一次安装时打开设置页面 [5.15.4] - 2017-08-10 ### Fixed - 词典样式 - 麦克米伦检测问题 [5.15.2] - 2017-08-06 ### Added - Macmillan 词典 - 海词词频分级 - 彩蛋 ### Fixed - 拖动问题 - 其它小修正 [5.12.8] - 2017-07-31 ### Added - 增加 Longman Business 词典 ### Changed - 只对剪贴板单个单词自动查词,多个单词会自动粘贴,但不开始查找,需要再按一下回车 - 使用懒加载性能大幅度优化,提取公共模块体积减少 - 更紧凑的架构设计,添加词典更简单 ### Fixed - 修复 Bing 发音问题 ## [5.11.23] - 2017-07-13 ### Added - 增加两岸词典与国语辞典 - 增加点击图标弹出查词面板 - 查词结果可以导出图片,在绿色工具栏上可以看到 ### Changed - 二维码功能移到工具栏上 ### Fixed - i18n 带 fallback - svg 属性迁就 html2canvas - 设置页面开始连查两遍的问题 - 通过 `:root:root:root:root:root` 进一步增加元素权值 - 改为插到 body 末尾 ## [5.7.20] - 2017-05-21 ### Added - 添加词源词典 - 右键添加有道词典、海词词典和金山词霸 ### Security - 增强稳定性 ## [5.5.14] - 2017-05-15 ### Changed - 词典可默认不展开 ## [5.5.12] - 2017-05-15 ### Added - 增加右键谷歌网页翻译 - 增加双语例句 ## [5.3.9] - 2017-05-03 ### Added - 添加重置按钮 - 增加 Howjsay 发音 ### Fixed - 降低查词图标敏感度 ## [5.1.6] - 2017-04-06 ### Added - 增加双击查词 ### Fixed - 减少动画加快显示 - 修复无法关闭 - 修复设置时高度不更新 ## [5.0.0] - 2017-04-04 ### Changed - 全新重写,全面优化,性能大幅度提高。 - 词典可以增删排序。 - 新增多个词典。 - 右键支持更多词典搜索。 - 保留了置顶与拖动功能。 - 更好用的配置界面。 - 更多变化使用中发现吧。 ## [4.1.1] - 2015-12-27 ### Changed - 在必应词典和 Urban Dictionary 基础上增加 Vocabulary.com 海词统计和 Howjsay ,释义发音更详细。 - 右键查词,选词后右键可直达牛津词典、韦氏词典、词源、谷歌翻译等等。 - 新增三种划词模式,适合各种强迫症。 - 连续按三次ctrl还可以直接查词,随时查词,无需再另开词典占内存啦。 - 词典界面可以拖动,还可以固定在网页上,看论文利器啊。 - 延迟响应时间,不容易误按,手残党福利。 - 保留了显示当前页面二维码功能(设置界面,鼠标悬停在 “Saladict”标题上)。 - 更多功能慢慢发现吧;D ## 3.0.1 ### Changed - 增加了划译开关 - 增加了 urban 词典的例子 - 增加了必应搜索图标 - 搜索图标右击可以变成翻译搜索 - 修复了几处错误并加速了结果显示 [Unreleased]: https://github.com/crimx/ext-saladict/compare/v5.31.7...HEAD [5.31.7]: https://github.com/crimx/ext-saladict/compare/v5.30.0...v5.31.7 [5.30.0]: https://github.com/crimx/ext-saladict/compare/v5.28.3...v5.30.0 [5.29.3]: https://github.com/crimx/ext-saladict/compare/v5.28.1...v5.29.3 [5.28.1]: https://github.com/crimx/ext-saladict/compare/v5.27.3...v5.28.1 [5.27.3]: https://github.com/crimx/ext-saladict/compare/v5.19.1...v5.27.3 [5.19.1]: https://github.com/crimx/ext-saladict/compare/v5.18.5...v5.19.1 [5.18.5]: https://github.com/crimx/ext-saladict/compare/v5.16.1...v5.18.5 [5.16.1]: https://github.com/crimx/ext-saladict/compare/v5.15.21...v5.16.1 [5.15.21]: https://github.com/crimx/ext-saladict/compare/v5.15.19...v5.15.21 [5.15.19]: https://github.com/crimx/ext-saladict/compare/v5.15.14...v5.15.19 [5.15.14]: https://github.com/crimx/ext-saladict/compare/v5.15.12...v5.15.14 [5.15.12]: https://github.com/crimx/ext-saladict/compare/v5.15.9...v5.15.12 [5.15.9]: https://github.com/crimx/ext-saladict/compare/v5.15.4...v5.15.9 [5.15.4]: https://github.com/crimx/ext-saladict/compare/v5.15.2...v5.15.4 [5.15.2]: https://github.com/crimx/ext-saladict/compare/v5.12.8...v5.15.2 [5.12.8]: https://github.com/crimx/ext-saladict/compare/v5.11.23...v5.12.8 [5.11.23]: https://github.com/crimx/ext-saladict/compare/v5.7.20...v5.11.23 [5.7.20]: https://github.com/crimx/ext-saladict/compare/v5.5.14...v5.7.20 [5.5.14]: https://github.com/crimx/ext-saladict/compare/v5.5.12...v5.5.14 [5.5.12]: https://github.com/crimx/ext-saladict/compare/v5.3.9...v5.5.12 [5.3.9]: https://github.com/crimx/ext-saladict/compare/v5.1.6...v5.3.9 [5.1.6]: https://github.com/crimx/ext-saladict/compare/v5.0.0...v5.1.6 [5.0.0]: https://github.com/crimx/ext-saladict/compare/v4.1.1...v5.0.0 [4.1.1]: https://github.com/crimx/ext-saladict/tree/v4.1.1 ================================================ FILE: CONTRIBUTING-zh.md ================================================ # 沙拉查词贡献指南 :+1::tada: 首先,感谢你愿意抽时间为这个项目作贡献! :tada::+1: ## 贡献前注意 :warning: 除非是小的修复,在动手前建议新开一个 WIP(施工中)issue 或 PR 阐述你要做的东西以及将要如何实现,以保证大家达成一致认识,而不白白浪费互相的时间与精力。 - 先阅读 [如何开始](#如何开始). - 遵循[代码格式](#代码格式)以及 [commit 格式](#commit格式). - 提交前先本地跑[测试](#测试)以及[构建](#构建)。也可以交给 CI 处理。 ## 如何开始 ```bash git clone git@github.com:crimx/ext-saladict.git cd ext-saladict yarn install yarn pdf ``` 在项目根添加 `.env` 文件,参考 `.env.example` 格式(可留空如果你不需要这些词典)。 ## 修改 UI 运行 `yarn fixtures` 下载测试文件(下载完成以后不必再运行)。 运行 `yarn storybook` 查看所有 UI 组件。 运行 `yarn start --wextentry [entry id]` 查看特定入口。项目会运行在 Webpack 开发服务器下的虚拟扩展环境中。 ## 测试 运行 `yarn test`。支持所有 Jest [参数](https://jestjs.io/docs/en/cli)。 ## 构建 运行 `yarn build`。 参数: - `--debug`: 取消压缩代码并输出源码映射(map)文件。 - `--analyze`: 显示打包分析图。 ## 如何添加词典 出于安全性和可维护性,沙拉查词不提供热添加词典的功能,所有的词典添加必须向本项目提交 PR 合并。如果词典使用了未公开接口请另起项目发布到 NPM 再引用进来。 1. 在 [`src/components/dictionaries/`](./src/components/dictionaries/) 下以词典 id 新建一个目录。 1. 可参考已有的词典如[必应](./src/components/dictionaries/bing),复制文件到新建的目录中。 1. 把图标换成该词典的 LOGO。 1. 编辑 `config.ts` 修改词典默认设置。参见 `DictItem` 类型查看选项含义。在 [app config](./src/app-config/dicts.ts) 中注册词典让 TypeScript 生成正确的类型。词典 **必须** 遵循字母表顺序。 1. 更新 `_locales.json` 添加多语言的词典名字。如果词典有自定义选项请一并添加多语言的名字。 1. `engine.ts` **必须** `export` 至少两个函数: 1. `getSrcPage` 函数。当用户点击词典标题时计算出相应的链接。 1. `search` 函数。负责获取、解析和返回词典结果,可参考类型了解细节。 - 从网页中解析信息 **必须** 使用 [../helpers.ts](./components/dictionaries/helpers.ts) 中的辅助方法以保证数据干净。 - 如果词典支持自动发音: 1. 在 [`config.autopron`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/app-config/index.ts#L202-L223) 中注册 id。 2. 在返回的结果中附带 [`audio`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/typings/server.ts#L5-L9) 域。 1. 其它 `export` 的方法可以在 `View.tsx` 中通过 `'DICT_ENGINE_METHOD'` 通信通道调用。类型细节见 `src/typings/message`。也可以在项目中搜索 `DICT_ENGINE_METHOD` 查看例子。通信 **必须** 通过 `'@/_helpers/browser-api'` 的 `message` 而不是原生的 `sendMessage` 方法. 1. 词典结果最终会传到 `View.tsx` 中的 React 组件中。该组件 **应该** 只负责渲染结果而不带复杂逻辑。 1. `_style.scss` 中的选择器 **应该** 遵循类似 [ECSS](http://ecss.io/chapter5.html#anatomy-of-the-ecss-naming-convention) 的命名方式。 ### 热更新开发词典 UI 组件 为了方便在 Storybook 中开发组件我们需要拦截词典引擎的网络请求返回本地文件。 1. 新建 `fixtures.js` 在 `test/specs/components/dictionaries/[dictID]` 下。 - 格式可参考其它词典。 - 每个请求可以提供页面链接或者 axios 设置(见 `mojidict` 词典)。如果后面的请求依赖前面请求的结果,可以通过参数获得。 1. 运行 `yarn fixtures` 下载结果。 1. 编辑 `test/specs/components/dictionaries/[dictID]/request.mock.ts`。它会在开发时拦截词典请求并返回下载好的结果。 1. 运行 `yarn storybook`。 ### 添加测试 1. 添加 `[dictID]/engine.spec.ts` 测试引擎。 ## 代码格式 本项目遵循 [Standard](https://standardjs.com) 的 TypeScript 变种格式。运行 `yarn lint` 可检查。 如果使用 VSCode 等 IDE 请确保 *eslint* 和 *prettier* 插件已安装。或者构建的时候也会进行 TypeScript 完整检查。 ## Commit 格式 本项目遵循 [conventional](https://conventionalcommits.org/) commit 格式。 你可以在 commit 时运行 `yarn commit` 按指示选择。或者在 VSCode 中使用 [VSCode Conventional Commits](https://github.com/vivaxy/vscode-conventional-commits) 插件。 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Saladict :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: ## How to Contribute :warning: Unless it is a small hot fix, before you write any code and get your hands dirty, please open an issue or make a WIP pull request to elaborate what you are trying to do and how you are going to implement it. Just to make sure we are on the same page and nobody's time and effort are wasted. - Read [How to get started](#how-to-get-started). - Follow [code style](#code-style) and [commit style](#commit-style). - Before submit, run [test](#testing) and [build](#building) locally. Or leave it to CI. ## How to get started ```bash git clone git@github.com:crimx/ext-saladict.git cd ext-saladict yarn install yarn pdf ``` Add a `.env` file following the `.env.example` format(leave empty if you don't use these dictionaries). ## UI Tweaking Run `yarn fixtures` to download fixtures(only need to run once). Run `yarn storybook` to view all the components. Run `yarn start --wextentry [entry id]` to view a certain entry with WDS in a fake WebExtension environment. ## Testing Run `yarn test` to run Jest. Supports all the Jest [options](https://jestjs.io/docs/en/cli). ## Building Run `yarn build` to start a full build. Toggle: - `--debug`: Remove compression and generate sourcemaps. - `--analyze`: Show detailed Webpack bundle analyzer. ## How to add a dictionary For safety and maintainability reason, Saladict will not support adding dictionaries on the fly. All dictionaries must be merged to this project via pull requests. If dictionary implementation makes use of private API please move it to an independent project, release on NPM, then import it to Saladict. 1. Create a directory at [`src/components/dictionaries/`](./src/components/dictionaries/), with the name of the dict ID. 1. Use any existing dictionary as guidance, e.g. [Bing](./src/components/dictionaries/bing). Copy files to the new directory. 1. Replace the favicon with a new LOGO. 1. Edit `config.ts` to change default options. See the `DictItem` type and explanation for more details. Register the dictionary in [app config](./src/app-config/dicts.ts) so that TypeScript generates the correct typings. Dict ID **MUST** follow alphabetical order. 1. Update `_locales.json` with the new dictionary name. Add locales for options, if any. 1. `engine.ts` **MUST** export at least two functions: 1. `getSrcPage` function which is responsible for generating source page url base on search text and app config. Source page url is opened when user clicks the dictionary title. 1. `search` function which is responsible for fetching, parsing and returning dictionary results. See the typings for more detail. - Extracting information from a webpage **MUST** use helper functions in [../helpers.ts](./components/dictionaries/helpers.ts) for data cleansing. - If the dictionary supports pronunciation: 1. Register the ID at [`config.autopron`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/app-config/index.ts#L202-L223). 1. Include an [`audio`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/typings/server.ts#L5-L9) field in the object which search engine returns. 1. Other exported functions can be called from `View.tsx` via `'DICT_ENGINE_METHOD'` message channel. See `src/typings/message` for typing details and search `DICT_ENGINE_METHOD` project-wise for examples. Messages **MUST** be sent via `message` from `'@/_helpers/browser-api'` instead of the native `sendMessage` function. 1. Search result will ultimately be passed to a React PureComponent in `View.tsx`, which **SHOULD** be a dumb component that renders the result accordingly. 1. Selectors in `_style.scss` **SHOULD** follow [ECSS](http://ecss.io/chapter5.html#anatomy-of-the-ecss-naming-convention)-ish naming convention. ### Develop the dictionary UI live To develop the component in Storybook we need to intercept http requests from dictionary engines and replace with the downloaded results. 1. Add `fixtures.js` at `test/specs/components/dictionaries/[dictID]`. - See other dictionaries for example. - You can offer url or axios config (See `mojidict` dictionary). All results from previous requests will be passed to the next request as array. 1. Run `yarn fixtures` to download fixtures. 1. Edit `test/specs/components/dictionaries/[dictID]/request.mock.ts`. It will intercept requests and return the downloaded fixtures. 1. Run `yarn storybook`. ### Add Testing 1. Add `[dictID]/engine.spec.ts` to test the engine. ## Code Style This project follows the TypeScript variation of [Standard](https://standardjs.com) JavaScript code style. If you are using IDEs like VSCode, make sure *eslint* and *prettier* plugins are installed. Or you can just run [building command](#building) to perform a TypeScript full check. ## Commit Style This project follows [conventional](https://conventionalcommits.org/) commit style. You can run `yarn commit` and follow the instructions, or use [VSCode Conventional Commits](https://github.com/vivaxy/vscode-conventional-commits) extension in VSCode. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 CRIMX 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. ================================================ FILE: README-zh.md ================================================ # 沙拉查词 Saladict [![Version](https://img.shields.io/github/release/crimx/ext-saladict.svg?label=version)](https://github.com/crimx/ext-saladict/releases) [![Chrome Web Store](https://badgen.net/chrome-web-store/users/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en) [![Chrome Web Store](https://badgen.net/chrome-web-store/stars/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en) [![Mozilla Add-on](https://badgen.net/amo/users/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/) [![Mozilla Add-on](https://badgen.net/amo/stars/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/) [![Build Status](https://travis-ci.com/crimx/ext-saladict.svg)](https://travis-ci.com/crimx/ext-saladict) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?maxAge=2592000)](http://commitizen.github.io/cz-cli/) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg?maxAge=2592000)](https://conventionalcommits.org) [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?maxAge=2592000)](https://standardjs.com/) [![License](https://img.shields.io/github/license/crimx/ext-saladict.svg?colorB=44cc11?maxAge=2592000)](https://github.com/crimx/ext-saladict/blob/dev/LICENSE) [【官网】](https://www.crimx.com/ext-saladict/)Chrome/Firefox 浏览器插件,网页划词翻译。

沙拉查词 7 为完全重写的版本。增加了更多细腻的动效与流畅的交互,更快速更稳定更多自定义设置。 ## 下载 见[下载页面](https://saladict.crimx.com/download.html)。 ## 改动日志 [CHANGELOG.md](./CHANGELOG.md) ## 从源码构建 ```bash git clone git@github.com:crimx/ext-saladict.git cd ext-saladict yarn install yarn pdf ``` 在项目根添加 `.env` 文件,参考 `.env.example` 格式(可留空如果你不需要这些词典)。 ```bash yarn build ``` 在 `build/` 目录下可查看针对各个浏览器打包好的扩展包。 ## 开发 见[项目贡献指南](./CONTRIBUTING-zh.md)。 ## 如何向本项目贡献代码 见[项目贡献指南](./CONTRIBUTING-zh.md)。 ## 声明 声明:沙拉查词作为自由开源的浏览器辅助插件,仅供学习交流,任何人均可免费获取产品与源码。如果认为你的合法权益收到侵犯请马上联系[作者](https://github.com/crimx)。 沙拉查词项目为 [MIT](https://github.com/crimx/ext-saladict/blob/dev/LICENSE) 许可,你可以随意使用源码,但必须附带该许可与版权声明。请勿用于任何违法犯罪行为,沙拉查词强烈谴责并会尽可能配合追究责任。对于照搬源码二次发布的套壳项目沙拉查词有责任对平台和用户发出相应的举报和提醒。 ## 更多截图

================================================ FILE: README.md ================================================ # Saladict 沙拉查词 [![Version](https://img.shields.io/github/release/crimx/ext-saladict.svg?label=version)](https://github.com/crimx/ext-saladict/releases) [![Chrome Web Store](https://badgen.net/chrome-web-store/users/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en) [![Chrome Web Store](https://badgen.net/chrome-web-store/stars/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en) [![Mozilla Add-on](https://badgen.net/amo/users/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/) [![Mozilla Add-on](https://badgen.net/amo/stars/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?maxAge=2592000)](http://commitizen.github.io/cz-cli/) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg?maxAge=2592000)](https://conventionalcommits.org) [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?maxAge=2592000)](https://standardjs.com/) [![License](https://img.shields.io/github/license/crimx/ext-saladict.svg?colorB=44cc11?maxAge=2592000)](https://github.com/crimx/ext-saladict/blob/dev/LICENSE) Chrome/Firefox WebExtension. Feature-rich inline translator with PDF support. [【中文说明】](./README-zh.md)Chrome/Firefox 浏览器插件,网页划词翻译。

## Downloads - [Chrome Web Store](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en) - [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/ext-saladict/) - [Microsoft Edge Addons](https://microsoftedge.microsoft.com/addons/detail/idghocbbahafpfhjnfhpbfbmpegphmmp)(Uploaded by @rumosky) - See [releases](https://github.com/crimx/ext-saladict/releases) for more. Saladict 7 is a complete rewrite with sophisticated interaction and buttery smooth experience. Built for speed, stability and customization. ## Change Log [CHANGELOG.md](./CHANGELOG.md) ## build from source ```bash git clone git@github.com:crimx/ext-saladict.git cd ext-saladict yarn install yarn pdf ``` Add a `.env` file following the `.env.example` format(leave empty if you don't use these dictionaries). ```bash yarn build ``` Artifacts can be found in `build/`. ## Development See the [contributing guide](./CONTRIBUTING.md). ## How can I contribute? [CONTRIBUTING.md](./CONTRIBUTING.md) ## Notice Saladict is a free and open-sourced project for study purpose only. Anyone can obtain a copy of Saladict free of charge. If you believe your legal rights have been violated please contact the [author](https://github.com/crimx) immediately. Saladict is licensed under [MIT](https://github.com/crimx/ext-saladict/blob/dev/LICENSE). You can use the source code freely as long as including a copy of license and copyright notice of Saladict. DO NOT use Saladict for any illegal or criminal activity. Saladict strongly condemns this behavior and will cooperate to the fullest extent possible in holding it accountable. As for copy-and-paste clone products Saladict has the responsibility to send corresponding reports and warnings to platforms and users. ## More screenshots:

================================================ FILE: assets/content.css ================================================ .saladict-div, .saladict-div > .saladict-external, .saladict-div > .saladict-panel { display: block !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important; border: none !important; outline: none !important; } ================================================ FILE: assets/fanyi.youdao.2.0/all-packed.css ================================================ html{_background:url(null) fixed;}.forbid-select{-moz-user-select:none;-khtml-user-select:none;user-select:none;}.OUTFOX_JTR_BAR{width:100%;margin:0;padding:0;border:none;position:fixed;z-index:2147483646;top:0;left:0;height:50px;_position:absolute;_top:expression((body.scrollTop+documentElement.scrollTop)+'px');}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP{color:#429d3b;height:38px;width:200px;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/ydd_tip.png") left top no-repeat;overflow:hidden;text-align:left;margin:0 auto;display:none;position:relative;top:-31px;z-index:1;}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP div{margin-left:5px;color:#429d3b;height:38px;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/ydd_tip.png") right top no-repeat;padding:3px 8px 3px 3px;}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP .update-date{margin-left:20px;}#OUTFOX_JTR_BAR_UPDATE_SHADE{background-color:#818181;height:50px;left:0;position:absolute;top:0;width:100%;opacity:.75;filter:alpha(opacity = 75);display:none;}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP_CONTENT_CLOSE{width:10px;height:10px;position:absolute;right:5px;top:5px;cursor:pointer;}.OUTFOX_JTR_BAR_CLOSE{display:block;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") -35px -69px no-repeat;width:17px;height:13px;position:absolute;top:1px;right:1px;cursor:pointer;}.OUTFOX_JTR_BAR_CLOSE:hover{background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") -56px -69px no-repeat;}#OUTFOX_JTR_BAR_BODY{margin:0;padding:0;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp-repeat-x.png") 0 -92px repeat-x;font-size:12px;height:50px;overflow:hidden;color:#6F6F6F;text-align:center;border:0;}#OUTFOX_JTR_BAR_BODY #wrapper{*width:960px;padding:0 10px;max-width:960px;margin:0 auto;text-align:left;}#OUTFOX_JTR_BAR_BODY .OUTFOX_BAR_TOTAL_NUM{font-family:Arial;font-size:18px;margin:0 5px;}#OUTFOX_JTR_BAR_BODY #headerLogo{background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/logo.png") no-repeat;text-indent:-999em;display:block;width:141px;height:41px;float:left;margin-top:7px;font-size:0;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/header_logo.png");_margin-top:17px;}#OUTFOX_JTR_BAR_BODY #sliderLabel{float:left;margin:22px 0 0 20px;}#OUTFOX_JTR_BAR_BODY #sliderWrapper{float:left;margin-top:6px;position:relative;left:15px;width:269px;}#OUTFOX_JTR_BAR_BODY #levelLabel{position:relative;height:20px;}#OUTFOX_JTR_BAR_BODY #level-0{right:10px;}#OUTFOX_JTR_BAR_BODY #level-1{left:149px;}#OUTFOX_JTR_BAR_BODY #level-2{left:83px;}#OUTFOX_JTR_BAR_BODY #level-3{left:17px;}#OUTFOX_JTR_BAR_BODY #levelLabel label{position:absolute;cursor:pointer;color:#4E86CC;}#OUTFOX_JTR_BAR_BODY.disable #levelLabel label{color:#ccc;cursor:default;}#OUTFOX_JTR_BAR_BODY #levelLabel .active{color:#707070;cursor:default;font-weight:bold;}#OUTFOX_JTR_BAR_BODY #status{float:left;margin:20px 10px 0 20px;_margin-top:22px;}#OUTFOX_JTR_BAR_BODY .statistic #status{margin-top:16px;}#OUTFOX_JTR_BAR_BODY #switchWrapper{float:right;margin-top:16px;}#OUTFOX_JTR_BAR_BODY #feedback{margin:18px 0 0 15px;float:right;text-align:right;}#OUTFOX_JTR_BAR_BODY #feedback a{color:#4E86CC;text-decoration:none;}#OUTFOX_JTR_BAR_BODY #feedback #fb{color:#FF8D3D;}#OUTFOX_JTR_BAR_BODY #feedback a:hover{text-decoration:underline;}#OUTFOX_JTR_BAR_BODY a:link{color:#4E86CC;text-decoration:none;}#OUTFOX_JTR_BAR_BODY #switch{color:#4D86CC;background-image:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/switch_button.png");display:block;width:70px;height:23px;cursor:pointer;line-height:23px;text-align:center;text-decoration:none;}#OUTFOX_JTR_BAR_BODY #switch:hover{background-image:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/switch_button_hover.png");}.OUTFOX_JTR_TRANSTIP_WRAPPER{z-index:2147483640;max-width:300px;*width:300px;}.OUTFOX_JTR_TRANSTIP_WRAPPER p{padding:10px;margin:0;}.OUTFOX_JTR_TRANSTIP_ADVISE_TOGGLE{display:block;cursor:pointer;}.OUTFOX_JTR_TRANSTIP_ADVISE_THANK_TIP .OUTFOX_JTR_TRANSTIP_ADVISE_TOGGLE{display:none;}.OUTFOX_JTR_TRANSTIP_ADVISE_THANK{display:none;}.OUTFOX_JTR_TRANSTIP_ADVISE_THANK_TIP .OUTFOX_JTR_TRANSTIP_ADVISE_THANK{display:inline;}.expand .OUTFOX_JTR_TRANSTIP_ADVISE_TEXT{width:250px;height:50px;margin:10px 0 0;resize:none;}.OUTFOX_JTR_NANCI_BAR{position:absolute;text-decoration:none;overflow:hidden;background:#fff;border:1px solid #AFCEF5;padding-left:3px;z-index:2147483641;-webkit-box-shadow:2px 2px 2px #ccc;-moz-box-shadow:2px 2px 2px #ccc;box-shadow:2px 2px 2px #ccc;}.OUTFOX_JTR_NANCI_BAR a{cursor:pointer;text-decoration:none;text-indent:-999em;width:15px;height:15px;display:inline-block;}.OUTFOX_JTR_NANCI_CTRL_DETAIL_BG{_width:15px;_height:15px;_zoom:1;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/icon_detail.png");}.OUTFOX_JTR_NANCI_CTRL_CLOSE_BG{_width:15px;_height:15px;_zoom:1;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/icon_delete.png");}.OUTFOX_JTR_NANCI_CTRL_DETAIL{_position:absolute;_margin-left:-8px;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") 0 -68px no-repeat;_background-image:none;}.OUTFOX_JTR_NANCI_CTRL_CLOSE{_position:absolute;_margin-left:-6px;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") -13px -70px no-repeat;_background-image:none;}.OUTFOX_JTR_TRANSTIP_ORIGIN .OUTFOX_JTR_TRANSTIP_ADVISE{padding-bottom:14px;}.OUTFOX_JTR_TRANSTIP_ADVISE_TEXT,.OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{display:none;}.expand .OUTFOX_JTR_TRANSTIP_ADVISE_TEXT,.expand .OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{display:block;}.finish .OUTFOX_JTR_TRANSTIP_ADVISE_TEXT,.finish .OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{display:block;visibility:hidden;}div.expand a.OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{background-image:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/trans_tip_submit_bg.png");width:96px;height:23px;line-height:23px;text-align:center;cursor:pointer;float:right;margin-top:10px;outline:none;text-decoration:none;}div.expand a.OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT:hover{background-image:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/trans_tip_submit_bg_hover.png");}.OUTFOX_NANCI_TIPS{white-space:nowrap;}.OUTFOX_JTR_NANCI_CTRL_WORD{color:#000;margin-right:5px;}#OUTFOX_JTR_BAR_BODY #failed{line-height:50px;}#OUTFOX_BAR_WRAPPER iframe.OUTFOX_JTR_BAR_HIDE{display:none;}*html .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-bottom,*html .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-top{width:300px;}*html .OUTFOX_JTR_TRANSTIP_WRAPPER{width:300px;border-width:0 1px;border-color:#d9d9d9;border-style:solid;}#yddWrapper{z-index:2147483640;max-width:280px;display:none;}.ydd-container *{padding:0;margin:0;font-size:12px;color:#2A2A2A;}.ydd-container{display:block;position:relative;width:100%;height:100%;font-size:12px;text-align:left;opacity:.95;*filter:alpha(opacity = 95);*background-color:#FFF;}.ydd-container div{display:block;float:none;}.ydd-container a{color:#4E86CC;}.ydd-container a:link{text-decoration:none;}.ydd-container a:hover{text-decoration:underline;}.ydd-body-wrapper{position:relative;padding:0 5px 0 6px;*background-color:transparent;}.ydd-body{padding:0 15px 10px;overflow:hidden;background-color:#fff;*background-color:transparent;}.ydd-lb{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAoCAYAAADdaosOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAAANklEQVQoke3LsQ0AIAgF0YOEwsbZnPQPxSK6ALRWXPtyAA4EsIAt6WTmdZpKMLMa2mNgYOAbPAs0Bm/LtPJAAAAAAElFTkSuQmCC);*background-image:none;background-repeat:repeat-y;background-position:left 0;width:6px;height:100%;position:absolute;left:0;top:0;}.ydd-rb{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAoCAYAAADdaosOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAAANElEQVQoke3JoREAIAwEwQuDwKDov7AvJhMaCBrzJ2/JzJJUwAE2sIA5IoKuN7TXYDB8hwuIjQgBYxgxJgAAAABJRU5ErkJggg==);*background-image:none;background-repeat:repeat-y;background-position:right 0;width:6px;height:100%;position:absolute;right:0;top:0;}.ydd-top-wrapper{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAUCAYAAAC07qxWAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAxMC8xNS8xMMHSDz0AAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzVxteM2AAAA2ElEQVQokcWQMY6DMBRE37etSAsFot8bUFPlAjlDjrIHyFFyAK7henuKPQAIEAhHeBsnIsFC0TY70sguxuOZEe8970AAE+4+kMiJBg7hwSufoIEPQL0ItsKqqk55nvfWWrfnKHVde+cczrmfYRiuZVlegFvgcqc0TeO11izLQtd1tG37XRTFEXBrsRrHkb7vmaaJJEnIsqyw1n6F/PfsKBFBRPDeM88zxhjSND0H0aOkUUptxtVaf67dAIzIpuCj6HqBqGNky/jXEdddxye8L9wp80fH/8v4C8bURyFzKYfYAAAAAElFTkSuQmCC);*background-image:none;background-repeat:no-repeat;background-position:right 0;padding-right:10px;height:20px;}.ydd-top{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVAAAAAUCAYAAADbVmUXAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAZFJREFUeNrs2ktKw1AYhuFzSaIQLGRkRqEjR67A7Th1Irq0LqILElpDk3jS5nJ6/C0WYgn2fSAE21ILhZePpLppGtXSWiuB+CAAzNBf9ar58R8KAdXCB9IEFcDMRRMHsxEC2pwKqB9K6QCAubqZeHVKx1FE/YD2gTRdLE0QT8MKBTBj6cTr0z9q7zy8LgxoH03rnY0QUwCYm2zi5Vl3R9U9V4UR9a8Z6CCa0Wq1eiqK4jmO40djzD3fD4B/HtDePpxuYH66864bmd9uJvkL1HrxjNfr9Uue5+9pmqokSZS1lq8HwLUEVPVtdOet6+NWiqi4QNvlmWXZ62KxUFVVqc1mM7wZAFwT177bboXuwp97SgE1y+XyzcXTlmU5Pqm59Ang+rTt6yL6cfhzXKFiQKMoejDGEE0AOKxQK43JMKD7O+7uBXdtQAEAQx9PBvQIAQWAYYGq3wR0+J0nAQWA8wLKAgWAQF3XQzgJKACc6ayAcgceAMYeskAB4BILlIACAAEFgMsGlGugADD2UArolwADAA7gcFvj3gN0AAAAAElFTkSuQmCC);*background-image:none;background-repeat:no-repeat;background-position:left 0;height:20px;*width:240px;}.ydd-bottom-wrapper{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAeCAYAAAAVdY8wAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAAArElEQVQ4jeWUsQ3CMBBFX4AJEDukSJ2K/SgYhI3oU2SBFCnC4X8UOGCSECgoEJx0siw/ve9z4azrOmdQVVWR5/kGOAEGnFfuI26y3gclfdj4n9E/Ncz3RnvsWTA98FfgvDGEUANKrSNQEm3bHiLYw4/R7o6ZHcuy3AEhtS4kIQkzq5um2RdFsQXOEbzdMwPWcdNHhcR2j+b6bfgA9hTqQUum8ydNBiwHbze1cgH57aT/yHsyngAAAABJRU5ErkJggg==);*background-image:none;background-repeat:no-repeat;background-position:right 0;padding-right:10px;height:30px;}.ydd-bottom{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAU8AAAAeCAYAAACsaJwUAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAABXklEQVR4nO3dPY7TUBQF4PNshxSp0qVgB5Qp0rMMNkFHwYIowyqoUqJkJaTEpsAWJmQG6RWTjPJ90pOVn+JWR+e6eSVJk6RN0iVZ7Pf799vt9utmswkA13VP/TAMw0vOAfCqCE+AClfDcxiG9H3/0rMAvBqaJ0AF4QlQwdoOUEHzBKggPAEqWNsBKmieABU0T4AKmidABeEJUMHaDlBB8wSoIDwBKljbASpongAVpvAcxpOmaYZEeAI8p8sYmqNBeAL8X5uk5PclcCVJs16vf+x2u49N09x2MoA7VvInPKcbNN+cTqfvy+Xy7U0nA7hjl+88+yQ/z+fzl67rPmmfANeV/N0+m4z3tx+Px2+LxeJdKeWW8wHcpTJ7TuHZjqc7HA6fV6vVh7ZtrfAAM/PwvGygU4hOn8vs/wAPbf7Oc9LPvuvzb3AKUODhXQZheeLkyhPgYV0LwufCUnACJPkFXHpxDgJO4nYAAAAASUVORK5CYII=);*background-image:none;background-repeat:no-repeat;background-position:left 0;height:30px;_line-height:24px;line-height:24px;padding:0 15px;*width:210px;}.ydd-key-title{font-size:13px;font-weight:bold;}.ydd-phonetic{font-family:"lucida sans unicode",arial,sans-serif;color:#999;}.ydd-base-trans .ydd-tabs{display:none;}.ydd-trans-wrapper{margin:5px 0;}.ydd-trans-wrapper a{color:#999;}.ydd-tabs{background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/swipe_hr.png") 0 9px no-repeat;margin:5px 0;}.ydd-tab{color:#707070;background:#fff;padding-right:5px;}.ydd-voice{margin:0 5px;}.ydd-no-result{margin-top:7px;}html* .ydd-bg-top{background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp-repeat-x.png") 0 0 repeat-x;width:280px;height:40px;position:absolute;z-index:-1;top:0;left:0;}html* .ydd-bottom-wrapper{background:url(chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp-repeat-x.png) 0 -46px repeat-x;}html* #yddWrapper{width:280px;border-width:0 1px;border-color:#d9d9d9;border-style:solid;}html* .OUTFOX_JTR_TRANSTIP_WRAPPER #yddWrapper,html* .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-bottom,html* .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-top{width:300px;}html* .ydd-title{line-height:14px;}.slider-container{height:9px;width:269px;margin:0;cursor:pointer;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") 0 -56px no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/slider_bg.png");}.disable .slider-container{cursor:default;}#sliderBackground{width:0;overflow:hidden;}.slider-background{height:100%;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") 0 -23px no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/slider_bar.png");}.disable .slider-background{background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") 0 -39px no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/slider_bar_gray.png");}.slider{display:block;height:20px;width:75px;top:14px;position:absolute;background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") 0 0 no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/slider.png");cursor:pointer;}.slider:hover{background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") -157px 0 no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/slider_hover.png");}.disable .slider,.disable .slider:hover{background:url("chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png") -78px 0 no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = "http://shared.ydstatic.com/jtr/v1/images/slider_gray.png");} ================================================ FILE: assets/fanyi.youdao.2.0/conn.html ================================================ ================================================ FILE: assets/fanyi.youdao.2.0/conn.js ================================================ if (!this.JSON) { this.JSON = {}; } /** * JSON 解析库 */ (function() { function f(n) { return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function(key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function(key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta = {'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},rep; function quote(string) { escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function(a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { var i,k,v,length,mind = gap,partial,value = holder[key]; if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } if (typeof rep === 'function') { value = rep.call(holder, key, value); } switch (typeof value) {case'string':return quote(value);case'number':return isFinite(value) ? String(value) : 'null';case'boolean':case'null':return String(value);case'object':if (!value) { return'null'; } gap += indent;partial = [];if (Object.prototype.toString.apply(value) === '[object Array]') { length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { k = rep[i]; if (typeof k === 'string') { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { for (k in value) { if (Object.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}';gap = mind;return v; } } if (typeof JSON.stringify !== 'function') { JSON.stringify = function(value, replacer, space) { var i; gap = ''; indent = ''; if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } } else if (typeof space === 'string') { indent = space; } rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } return str('', {'':value}); }; } if (typeof JSON.parse !== 'function') { JSON.parse = function(text, reviver) { var j; function walk(holder, key) { var k,v,value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function(a) { return'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { j = eval('(' + text + ')'); return typeof reviver === 'function' ? walk({'':j}, '') : j; } throw new SyntaxError('JSON.parse'); }; } }()); window.onload = function() { var BACKFLAG = 'dataBack'; /** * 事件绑定 * @param object 要绑定事件的对象 * @param eventName 事件名称 * @param callback 事件处理函数 */ var bind = function(object, eventName, callback) { if (!callback) { return; } if (object.addEventListener) { object.addEventListener(eventName, callback, false); } else if (object.attachEvent) { object.attachEvent('on' + eventName, callback); } else { object['on' + eventName] = callback; } return this; }; /** * 判断对象是否函数 * @param obj 待检查对象 */ var isFunction = function(obj) { return !!(Object.prototype.toString.call(obj) === "[object Function]"); }; /** * 跨域通信响应对象 */ var Response = { /** * 响应请求信息 * @param callback */ onMessage:function(callback) { if (!isFunction(callback)) { callback = function() { }; } bind(window, 'message', function(eve) { callback(eve); }); }, /** * 向另一个域发送请求 * @param responseData */ sendMessage : function(responseData) { parent.postMessage(JSON.stringify(responseData), '*'); } }; /** * 本地存储。 所有本地存储相关数据存储到 youdao 的域下,这样才能做到用户设置与域无关。 * @param key 键 * @param value 值 */ var storage = function(key, value) { /** * html5 中的本地存储方式 * @param key * @param value */ var html5LocalStorage = function(key, value) { var store = window.localStorage; if (value === undefined) { return store.getItem(key); } if (key !== undefined && value !== undefined) { store.setItem(key, value); return value; } }; /** * IE 本地存储方式 userData * @param key * @param value */ var userdata = function(key, value) { var store = document.documentElement; store.addBehavior("#default#userData"); if (value === undefined) { store.load("fanyiweb2"); return store.getAttribute(key); } if (key !== undefined && value !== undefined) { store.setAttribute(key, value); store.save("fanyiweb2"); return value; } }; if (!!window.localStorage) { return html5LocalStorage(key, value); } if (!!document.documentElement.addBehavior) { return userdata(key, value); } }; /** * 创建 Ajax 对象 */ function createXMLHttpObject() { var XHRFactory = [ function () { return new XMLHttpRequest(); }, function () { return new ActiveXObject('Msxml2.XMLHTTP'); }, function () { return new ActiveXObject('Msxml3.XMLHTTP'); }, function () { return new ActiveXObject('Microsoft.XMLHTTP'); } ]; var xhr = false; for (var i = 0; i < XHRFactory.length; i++) { try { xhr = XHRFactory[i](); } catch(e) { continue; } break; } return xhr; } /** * 处理跨域请求 */ var handleMessage = function() { /** * 将 request 中的 data 转为对象 * @param request */ var initData = function(request) { var dataArray = request.data,data = {}; if (typeof dataArray === 'string') { dataArray = dataArray.split('&'); } for (var i = 0; i < dataArray.length; i++) { var d = dataArray[i].split('='); data[d[0]] = d[1]; } return data; }; /** * 所有请求的处理函数,请求的处理函数与 request.handler 属性值应保持一致 */ var handlers = { /** * 获取翻译的查询结果 * @param request 请求数据 */ translate : function(request) { browser.runtime.sendMessage({ type: 'YOUDAO_TRANSLATE_AJAX', payload: request }) .then(response => { Response.sendMessage({ ...response, 'handler': BACKFLAG, }) }) // var xhr = createXMLHttpObject(); // xhr.onreadystatechange = function(event) { // if (xhr.readyState == 4) { // var data = xhr.status == 200 ? xhr.responseText : null; // Response.sendMessage({ // 'handler': BACKFLAG, // 'response': data, // 'index': request.index // }); // } // }; // xhr.open(request.type, request.url, true); // if (request.type === 'POST') { // xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // xhr.send(request.data); // } else { // xhr.send(null); // } }, /** * 本地存储 * @param request 请求信息 */ localStorage:function(request) { var data = initData(request); var result = decodeURIComponent(storage(data.key, data.value)); Response.sendMessage({ 'handler': BACKFLAG, 'response': result, 'index': request.index }); } }; return function(request) { if (!!!handlers[request.handler]) { throw new Error('类别为 ' + request.handler + ' 跨域请求处理函数不存在!'); } handlers[request.handler](request); }; }(); /** * 注册消息处理机制 */ Response.onMessage(function(eve) { handleMessage(JSON.parse(eve.data)); }); Response.sendMessage({handler:'transferStationReady'}); } ================================================ FILE: assets/fanyi.youdao.2.0/main.js ================================================ /* eslint-disable */ ;(function () { var JSONDAO, TR; if(this.JSON&&this.JSON.stringify.toString().indexOf("[native code]")!==-1){JSONDAO=this.JSON}else{JSONDAO={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i0){i(e.pop())}};if(!!window.postMessage){this.createMessageChannel(c,f,g,h)}else{this.createJTRAssist(c,h)}this.ajax=function(i){e.push(i);return this};return this},createMessageChannel:function(d,f,h,i){var e=this;var c=(function(){if(a.isDOM(a.query("#"+d))){throw new Error("Existed CDA iFrame element")}if(f&&h){var j=document.createElement("iframe");j.setAttribute("id",d);j.className=("OUTFOX_JTR_CONN");j.style.display="none";j.setAttribute("src",h);document.body.appendChild(j);return j.contentWindow}else{throw new Error("Empty domain is not allowed")}})();var g=(function(){var k=[];var l=0;var j={transferStationReady:function(){i(function(m){m.data=a.param(m.data);m.index=l++;k[m.index]={dataType:m.dataType,callback:m.callback};delete m.callback;c.postMessage(JSONDAO.stringify(m),'*');return e})},dataBack:function(n){if(!!n&&!!k[n.index]){var m=k[n.index];if(a.isFunction(m.callback)){m.callback(a.parseData(m.dataType,n.response))}delete k[n.index]}}};return function(n){var m=JSONDAO.parse(n.data);j[m.handler](JSONDAO.parse(n.data))}})();a.bind(window,"message",function(j){g.call(e,j)})},createJTRAssist:function(h,e){var k=this;var d="http://fanyi.youdao.com/web2/JTRAssist.swf?"+(+new Date());var j=function(){if(!!a.query("#"+h)){return}var l=document.createElement("div");if(a.browser.msie==="6.0"||a.browser.msie==="7.0"){l.innerHTML=''}else{l.innerHTML=''}document.body.appendChild(l)};var c="outfox_jtr_fproxy_callback_",f=0;var g=function(m){var l=c+(f++);window[l]=function(n){m.callback.call(k,a.parseData(m.dataType,decodeURI(n)))};a.query("#"+h).load(m.url,m.data,m.type||"POST",'window["'+l+'"]')};var i=function(n){var l=n.data.key,m=n.data.value;if(m===undefined){if(a.isFunction(n.callback)){n.callback(a.parseData(n.dataType,a.query("#"+h).getItem(l)))}}else{a.query("#"+h).setItem(l,m)}};j();window.JTRAssistIsReady=function(){e(function(l){switch(l.handler){case"translate":l.data=a.param(l.data);g(l);break;case"localStorage":i(l);break;default:throw new Error("Unsupported request type :"+l.handler)}return k})}}};b.prototype.init.prototype=b.prototype;a.CDA=b})(J);(function(a){TR=function(d,b,g,c){this._manager=c;this._reqSize=b.reqSize;this._onStatusChange=b.onStatusChange||function(){};this._url=b.url;this.conn=g;this._context=d;this._request=function(i,j,k){g.ajax({url:i,handler:"translate",type:"POST",data:j,callback:k,dataType:"json"})};var h=new a.Page(d);var f=[];var e=h.getMainArticle();if(e){f=a.getTextNodes(e.elem,TR.isInclude)}this.mainNodeLength=f.length||null;this._nodeIndex=[];f=f.concat(a.getTextNodes(d,function(i){return(i!==(e&&e.elem))&&TR.isInclude(i)}));a.each(f,function(i,j){this._nodeIndex.push(this._manager.addNode(j))},this);this.workingThread=0;this.guid=b.guid||a.guid()};TR.prototype={doTrans:function(){var d=++this.workingThread;var h={ue:a.getDocumentCharset()||null,data:null,relatedUrl:document.location.href,guid:this.guid,mainLength:this.mainNodeLength,requestId:a.guid()};var j=[];var f=0;for(var g=0;gf.start};a.each(e.data,function(f,g){if(g.length>0){this._bubbleSort(g,b);this._manager.tipsResults[f]=g;if(d===this.workingThread){this._manager.replaceTips(f,c)}}else{this._manager.tipsResults[f]=[]}},this)},_bubbleSort:function(c,f){for(var d=c.length-2;d>=0;d--){for(var b=0;b<=d;b++){if(f(c[b+1],c[b])){var e=c[b];c[b]=c[b+1];c[b+1]=e}}}return c}};TR.isInclude=function(b){return !(b.tagName==="SCRIPT"||b.tagName==="STYLE"||b.tagName==="PRE"||(b.className&&/OUTFOX_JTR_/.test(b.className)))};a.TR=TR})(J);if(!J||!J.bind){throw new Error("swipe extension need J.bind support")}(function(h){var g="http://fanyi.youdao.com",f=g+"/fsearch",b=g+"/translate";var j=function(k){return''};var c=function(k){return k.split&&k.split(" ").length||0};var e={isJapanese:function(k){return !Boolean(/[^\u0800-\u4e00]/.test(k))},isContainJapanese:function(k){var m=0;for(var l=0;l2},isKoera:function(k){for(i=0;i12592&&k.charCodeAt(i)<12687)||(k.charCodeAt(i)>=44032&&k.charCodeAt(i)<=55203))){return true}}return false},isContainKoera:function(k){var m=0;for(var l=0;l0},isChinese:function(k){return !Boolean(/[^\u4e00-\u9fa5]/.test(k))},isContainChinese:function(k){var m=0;for(var l=0;l5}};var a=function(k,l){return new a.fn.init(k,l)};a.fn=a.prototype={init:function(l,m){var k=this;this.wrapper=a.createFrameWrapper();this.conn=a.initConnection(m);this.context=l;h.bind(document.body,"click",function(o){var n=o||window.event;k.wrapper.style.display="none";k.wrapper.style.position="absolute";k.wrapper.innerHTML=""})},enableSwipe:function(){if(!this._swipeListener){var k=this;this._swipeListener=function(l){k._onSwipe.call(k,l)};h.bind(this.context,"mouseup",this._swipeListener)}},disableSwipe:function(){if(this._swipeListener){h.unbind(this.context,"mouseup",this._swipeListener);delete this._swipeListener}},_onSwipe:function(l){var k="",n="";var m={};if(window.getSelection){k=window.getSelection()}else{if(document.selection){k=document.selection.createRange()}}if(k.toString){n=k.toString()}else{if(k.text){n=k.text.toString()}}var o=h.textPos(l,{});n=h.trim(n);if(!a.validateSwipeWord(n)){return}this.swipeWord(n,o.x,o.y);if(this.onSwipeCallback){this.onSwipeCallback(n)}},swipeWord:function(p,k,q,o,n){var l=this;var m=null;this.wrapper.innerHTML="";if((!e.isContainChinese(p)&&c(p)>=3)||(e.isContainChinese(p)||e.isContainJapanese(p)&&p.length>4)){m="translate"}else{m="dict"}this.conn.request({action:m,word:p},function(r){l.wrapper.innerHTML="";l._onResponse.call(l,r);a.initWrapper(l.wrapper,k,q,o,n)})},_onResponse:function(l){var m=l.firstChild,k=null;if(!m){return}else{if(m.baseName&&m.baseName=="xml"){m=m.nextSibling}}switch(m.tagName){case"response":k=a.processXmlTransData(l);break;case"yodaodict":k=a.processXmlDictData(l);break;default:throw new Error("Incorrect xml data")}if(k){this.wrapper.appendChild(k);this.wrapper.style.display="block"}}};a.createFrameWrapper=function(){var k=document.createElement("div");k.id="yddWrapper";h.bind(k,"click",function(l){h.stopPropagation(l)});h.bind(k,"mouseup",function(l){h.stopPropagation(l)});document.body.appendChild(k);return k};a.validateSwipeWord=function(k){return !(k===""||k.length>2000)};a.initConnection=function(k){var n=null;var m=function(q){var p=null,r=null;if(q.action=="dict"){r={client:"JTRHelper",keyfrom:"JTRHelper.bookmark",q:q.word,pos:-1,doctype:"xml",xmlVersion:"3.2",dogVersion:"1.0",vendor:"jtr",le:"eng"};p=f}else{r={client:"JTRHelper",keyfrom:"JTRHelper.bookmark",i:q.word,doctype:"xml",xmlVersion:"1.1",dogVersion:"1.0"};p=b}return[p,r]};if(window.chrome&&window.chrome.extension&&window.chrome.extension.sendRequest){n={request:function(p,q){window.chrome.extension.sendRequest(p,function(r){if(r){q((new DOMParser()).parseFromString(r,"text/xml"))}})}};return n}else{if(k){n={request:function(q,r){var p=m(q);k.ajax({url:p[0],handler:"translate",data:p[1],callback:r,dataType:"xml",type:"POST"})}};return n}else{if(h.CDA){var l=null;try{l=h.CDA("_OUTFOX_JTR_SWIPE_CONN",g,CONN_FILE_PATH)}catch(o){throw new Error("Unable to get cross-domain ajax file.")}n={request:function(q,r){var p=m(q);l.ajax({url:p[0],handler:"translate",data:p[1],callback:r,dataType:"xml",type:"POST"})}};return n}else{throw new Error("Unable to initialize cross-domain connection port.")}}}};a.initWrapper=function(p,t,s,q,D){var n=0,v=0,B=50,l=h.scroll().top,z=h.scroll().left,u=p.clientHeight,C=p.clientWidth,w=h.getPageSize().window.height,F=h.getPageSize().window.width;q=q||0;D=D||0;if(s-u>=l+B){v=s-u}else{v=s+D}if(t+C<=F+z){n=t+q}else{n=F+z-C}var r=!!(h.css(document.body,"position")!=="static");var A=h.css(document.body,"marginLeft");var o=h.css(document.body,"marginRight");if(A==="auto"&&o==="auto"){var E=h.getPageSize().page.width;var k=parseInt(h.css(document.body,"width"));if(E>k){A=(E-k)/2}else{A=0}}A=r?parseInt(A):0;var m=r?parseInt(h.css(document.body,"marginTop")):0;h.css(p,{position:"absolute",left:(n-A)+"px",top:(v-m)+"px"})};a.processXmlTransData=function(r){var l=(r.getElementsByTagName("input")[0].childNodes[1]||r.getElementsByTagName("input")[0].childNodes[0]).nodeValue,q=(r.getElementsByTagName("translation")[0].childNodes[1]||r.getElementsByTagName("translation")[0].childNodes[0]).nodeValue,p=r.getElementsByTagName("response")[0].getAttribute("errorCode")-0,n=h.trim(q),m=h.trim(l);if((e.isContainChinese(m)||e.isContainJapanese(m)||e.isContainKoera(m))&&m.length>15){m=m.substring(0,8)+" ..."}else{if(m.length>25){m=m.substring(0,15)+" ..."}}if(m==n){return null}var o="http://fanyi.youdao.com/translate?i="+encodeURIComponent(l)+"&keyfrom=chrome";var k='
{input} 详细››
{trans}
';return h.formatTemplate(k,{searchURL:o,input:a.escapeHTML(m),trans:a.escapeHTML(q)})};a.processXmlDictData=function(D){var l=null,n=null,q=[],z=[],x="",v="",F="",r="",w=0;var t=function(H){try{return D.getElementsByTagName(H)[0].firstChild.nodeValue}catch(G){return""}};x=t("return-phrase");v=t("dictcn-speach");F=t("lang");r=t("phonetic-symbol");if((n=D.getElementsByTagName("translation"))&&n.length>0){for(w=0;w0){for(w=0;w
{title} {phonetic} {speechHTML} 详细››
基本翻译
{baseTransHTML}
网络释义
{webTransHTML}
';if((e.isContainChinese(E)||e.isContainJapanese(E)||e.isContainKoera(E))&&E.length>15){E=E.substring(0,10)+"..."}else{if(E.length>25){E=E.substring(0,15)+" ..."}}if(q.length+z.length>0&&v){A=''+j("http://dict.youdao.com/speech?audio="+v,"test","CLICK","dictcn_speech")+""}for(w=0;w'+q[w]+""}for(w=0;w'+z[w].key+": "+z[w].value+""}u=h.formatTemplate(s,{phonetic:r?"["+r+"]":"",title:E,searchURL:B,speechHTML:A,baseTransHTML:o,webTransHTML:m});var k=h.query(".ydd-middle",u)[0];n=h.query(".ydd-base-trans",k)[0];l=h.query(".ydd-web-trans",k)[0];if(q.length+z.length===0){k.innerHTML='

没有英汉互译结果

'}try{if(q.length===0){k.removeChild(n)}else{if(z.length===0){k.removeChild(l)}}}catch(y){}return u};var d={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};a.escapeHTML=function(k){return String(k).replace(/[&<>"'\/]/g,function(l){return d[l]})};a.fn.init.prototype=a.fn;h.Swipe=a})(J);(function(a){var b=function(d,c,f){var e=this;if(!a.isDOM(d)){throw Error("Invalid slider container element")}this.container=d;if(!a.isDOM(c)){throw Error("Invalid slider controller block element")}this.controller=c;for(var g=this.container;g;g=g.parentNode){if(g.nodeType===9){this.document=g;break}}if(!g){throw Error("Can't find parent Document element of container, container dom node should insert to document first")}if(a.isDOM(f.bar)){this.bar=f.bar}else{this.bar=null}this.range=Number(f.max-f.min);if(!(this.range&&this.range>0)){throw Error("range must greater than 0")}if(a.isFunction(f.callback)){this.callback=function(h){f.callback.call(e,h)}}this.borderFix=Number(f.borderFix)||0;this.mousemove=function(h){e._mousemove(h)};this.mouseup=function(h){e._mouseup(h)};this.mousedown=function(h){e._mousedown(h)}};b.prototype={enable:function(){a.bind(this.container,"mousedown",this.mousedown)},disable:function(){a.unbind(this.container,"mousedown",this.mousedown)},to:function(e,d){var c=this,f=null;tempFunc=function(){if(!c.container.offsetHeight||!c.container.clientWidth){f=setTimeout(tempFunc,200);return}var g=c.container.clientWidth-c.controller.clientWidth-2*c.borderFix;pos=e/c.range*g;c._valueChange(pos/g*c.range,pos)};tempFunc()},_mousemove:function(d){var c=d||window.event;this._moveHandler(c,false)},_mouseup:function(d){var c=d||window.event;this._moveHandler(c,true);a.unbind(this.document,"mouseup",this.mouseup);a.unbind(this.document,"mousemove",this.mousemove);this.container.style.cursor="pointer"},_mousedown:function(d){var c=d||window.event;this._moveHandler(c,false);a.bind(this.document,"mouseup",this.mouseup);a.bind(this.document,"mousemove",this.mousemove);if(c.preventDefault){c.preventDefault()}c.returnValue=false},_moveHandler:function(e,i){var f=e.clientX-1/2*this.controller.clientWidth-a.findPos(this.container).x-this.borderFix,h=this.container.clientWidth-this.controller.clientWidth-2*this.borderFix,g=h/this.range,c=g/2,d=f%g,j=0;if(f<0){f=0;d=0}else{if(f>h){f=h;d=0}}if(i&&dg-c){j=f-d+g}else{j=f}}this._valueChange(j/h*this.range,j)},_valueChange:function(c,d){this.callback(c);this.controller.style.left=d+this.borderFix+"px";if(this.bar){this.bar.style.width=d+this.controller.clientWidth/2+"px"}}};a.Slider=b})(J);(function(a){var b=function(){this.nodes=[];this.originals={};this.transResults={};this.tipsResults={}};b.prototype={addNode:function(e,c){var d=0;if(!c){for(;d=d){c++}}},this);return c}};a.NodeManager=b})(J);(function(a){var b=function(){this.map={};this.dataMap={}};b.prototype={getId:function(c){var d=null;a.each(this.map,function(f,e){if(e===c){d=f;return false}});if(d===null){d=a.guid();this.map[d]=c}return d},data:function(g,c,f){var h=this.getId(g);if(arguments.length===3){if(!this.dataMap[h]){this.dataMap[h]={}}this.dataMap[h][c]=f;return f}else{var e=null;try{e=this.dataMap[h][c]}catch(d){e=undefined}return e}}};a.Cache=b})(J);(function(a){var b=function(d){this.contentDocument=d;this.cache=new a.Cache()};b.prototype={IGNORE_TAGS:["HTML","HEAD","META","TITLE","SCRIPT","STYLE","LINK","IMG","FORM","INPUT","BUTTON","TEXTAREA","SELECT","OPTION","LABEL","IFRAME","UL","OL","LI","DD","DT","A","OBJECT","PARAM","EMBED","NOSCRIPT","EM","B","STRONG","I","INS","BR","HR","PRE","H1","H2","H3","H4","H5","CITE"],getMainArticle:function(){return null;var g=null,e="";if(!!location){e=location.hostname}if(/\b(google|facebook|twitter)\b/i.test(e)){return null}var d=this._getAllArticle();if(!(d&&d.length)){return null}d.sort(function(i,h){return !!(h.weight-i.weight)});for(var f=2;f>0;f--){g=d[0];d.splice(0,1);break}return g},_getAllArticle:function(){var f=this.contentDocument.getElementsByTagName("*"),e=[];for(var d=0,h=f.length>100?100:f.length;d300&&d.offsetHeight>200}};var c=function(d){this.elem=d;this._texts=this._getAllTexts();this.weight=this.calcWeight()};c.prototype={IGNORE_TAGS:["A","DD","DT","OL","OPTION","PRE","SCRIPT","STYLE","UL","IFRAME"],MINOR_REGEXP:/comment|combx|disqus|foot|header|menu|rss|shoutbox|sidebar|sponsor/i,MAJOR_REGEXP:/article|entry|post|body|column|main|content/i,TINY_REGEXP:/comment/i,BLANK_REGEXP:/\S/i,_getAllTexts:function(){var g=[],d=a.getTextNodes(this.elem);for(var h=0,f=d.length;h20){continue}for(var f=j.parentNode;f&&f!=this.elem;f=f.parentNode){h-=0.1}d+=Math.pow(g*h,1.25)}return d},calcContentWeight:function(){var d=1;for(var e=this.elem;e;e=e.parentNode){var f=e.id+" "+e.className;if(this.MAJOR_REGEXP.test(f)){d+=0.4}if(this.MINOR_REGEXP.test(f)){d-=0.8}}return d},calcWeight:function(){return this.calcStructWeight()*this.calcContentWeight()},_checkTagName:function(d){return a.indexOf(this.IGNORE_TAGS,d.tagName)==-1},_checkLength:function(d){return Boolean(this.BLANK_REGEXP.test(d.nodeValue))},_checkMinorContent:function(d){return Boolean(this.TINY_REGEXP.test(d.id+" "+d.className))}};a.Page=b})(J);(function(a){var b={runCount:0,swipe:true,mode:"TIPS",level:1};var c={0:["TIPS",3],1:["TIPS",2],2:["TIPS",1],3:["TRANS","0"],4:["NONE","NONE"]};a.TR.UI=function(e,f){var d=this;this.initLogger(f.logURL);this.log({action:"start"});this.guid=a.guid();this.context=e;this.conn=a.CDA("OUTFOX_JTR_CDA",f.domain,f.connFilePath);this.update=f.update;this.updateTipMsg=f.updateTipMsg;this.updateDate=f.updateDate;this.manager=new a.NodeManager();this.barHeight=50;this.permissionDenied="由于该网页存在安全性限制, 无法加载有道网页翻译2.0";this.translator=new a.TR(e,{reqSize:f.reqSize,onStatusChange:function(){d._trStatusChangeCallback.apply(d,arguments)},url:{textTrans:f.transURL,tips:f.tipsURL},guid:this.guid},this.conn,this.manager);this.queue={TRANS:{0:{currentThread:-1}},TIPS:{1:{currentThread:-1},2:{currentThread:-1},3:{currentThread:-1}},NONE:{NONE:{}}};this.mode=null;this.level=null;this.initFrame(f.cssURL,function(){var h="";var g=this;if(location){h=location.href}this.movePage(g.barHeight);this.frame.body.innerHTML='
';this.initTipContent();this.initBarClose();this.initSwitch();this.initSlider();this.initLabel();this.initTipsCtrl();this.initTransTip();this.initSwipe();var i=function(j){g.loadSetting(j);g.enable();g.writeSettings({runCount:g.settings.runCount+1});a.each(c,function(k,l){if(g.mode===l[0]&&g.level===l[1]){g.slider.to(k)}})};this.conn.ajax({handler:"localStorage",data:{key:"settings"},dataType:"json",callback:function(j){i(j)}})})};a.TR.UI.prototype={positionElementInViewPort:function(k){var n=k.tip;var x=k.target;var l=!!(a.css(document.body,"position")!=="static");var r=a.css(document.body,"marginLeft");var h=a.css(document.body,"marginRight");if(r==="auto"&&h==="auto"){var w=a.getPageSize().page.width;var d=parseInt(a.css(document.body,"width"));if(w>d){r=(w-d)/2}else{r=0}}r=l?parseInt(r):0;var f=l?parseInt(a.css(document.body,"marginTop")):0;var j=a.findPos(x),p=0,g=0,e=a.scroll().top,s=a.scroll().left,t=j.x,m=j.y,i=x.offsetHeight,o=n.clientHeight,u=n.clientWidth,q=a.getPageSize().window.height,v=a.getPageSize().window.width;if(m-o>=e+this.barHeight){p=m-o}else{p=m+i}if(t+u<=v+s){g=t}else{g=v+s-u}a.css(k.tip,{position:"absolute",top:(p-f)+"px",left:(g-r)+"px"})},disable:function(){this.changeMode("NONE","NONE");this.slider.disable();this.frame.body.className="disable";this.disabled=true;this.updateStatus();this.switchElem.innerHTML="重新翻译"},enable:function(){this.changeMode(this.settings.mode,this.settings.level);this.slider.enable();a.removeClass(this.frame.body,"disable");a.addClass(this.frame.body,"enable");this.disabled=false;this.updateStatus();this.switchElem.innerHTML="取消翻译"},_trStatusChangeCallback:function(e){if(!e.id||!e.action||!e.level){return}var d=this.queue[e.action][e.level];if(d.currentThread<=e.id){d.currentThread=e.id;d.status=e.status;d.data=e.data||null;if(e.action===this.mode&&e.level===this.level){this.updateStatus()}}},updateStatus:function(){var f=this.queue[this.mode][this.level];a.removeClass(this.statusElem.parentNode,"statistic");if(f.status==="busy"&&f.data){this.switchElem.style.visibility="hidden";var d=parseInt(f.data[0]*100/f.data[1],10);if(this.mode==="TRANS"){this.statusElem.innerHTML="正在翻译 "+d+"% ..."}else{this.statusElem.innerHTML="正在分析 "+d+"% ..."}this.statusElem.className="busy"}else{if(f.status==="finish"){this.switchElem.style.visibility="inherit";if(this.mode==="TRANS"){this.statusElem.innerHTML="翻译完成"}else{var e=this.manager.countTips(this.level);if(e!==0){a.addClass(this.statusElem.parentNode,"statistic");this.statusElem.innerHTML='共注释'+e+"个难词"}else{this.statusElem.innerHTML="恭喜您!该网页上没有难词~"}}this.statusElem.className="finish"}else{this.switchElem.style.visibility="inherit";this.statusElem.innerHTML="翻译助手已关闭";this.statusElem.className="finish"}}},initLogger:function(d){this.logURL=d;this._logImgCache=[]},log:function(e){/*if(this.logURL){e.relatedURL=document.location.href;e.guid=this.guid;var d=new Image();d.src=this.logURL+"?"+a.param(e)+"&"+(new Date()).getTime();this._logImgCache[this._logImgCache.length]=d}*/},initSwipe:function(){var d=this;this.swipe=a.Swipe(this.context,this.conn);this.swipe.onSwipeCallback=function(e){d.log({action:"swipeWord",word:e})}},movePage:function(d){if(a.browser.msie){var f=a.css(this.context,"paddingTop");try{f=parseInt(f)}catch(e){f=0}this.context.style.cssText+=";padding-top:"+(d+f)+"px !important;"}else{var g=a.css(this.context,"marginTop");try{g=parseInt(g)}catch(e){g=0}if(a.css(this.context,"position")==="static"){a.css(this.context,{position:"relative"})}this.context.style.cssText+=";margin-top:"+(d+g)+"px !important;"}},initFrame:function(f,i){var d=this;var h=document.createElement("div");h.id="OUTFOX_BAR_WRAPPER";this.context.appendChild(h);this.wrapper=h;function g(k){k.innerHTML='';var j=a.query("#OUTFOX_JTR_BAR");j.setAttribute("frameBorder",0);if(a.browser.msie&&document.domain!=window.location.hostname){j.src="javascript:void(document.write(\" ` ) await fs.writeFile(viewerPath, file) } function cleanInit() { pdfDirs.forEach(name => { shell.rm('-rf', path.join(publicPDFRoot, name)) }) } async function exists(path) { try { await fs.access(path) } catch (e) { shell.echo(path + ' not exist') shell.exit(1) } } function exec(command, errorMsg) { const execResult = shell.exec(command) if (execResult.code !== 0) { if (errorMsg) { shell.echo(errorMsg) } shell.echo(execResult.stdout) shell.echo(execResult.stderr) shell.exit(1) } } async function cloneFiles() { for (const pdfFile of pdfFiles) { const targetPath = path.join(publicPDFRoot, pdfFile) await fs.ensureFile(targetPath) await fs.copy(path.join(__dirname, repoRoot, pdfFile), targetPath) } const restPdfDirs = pdfDirs.filter(name => name !== 'web/locale') for (const pdfDir of restPdfDirs) { const targetPath = path.join(publicPDFRoot, pdfDir) await fs.ensureDir(targetPath) await fs.copy(path.join(__dirname, repoRoot, pdfDir), targetPath) } // copy locale.properties await fs.ensureDir(path.join(publicPDFRoot, 'web/locale')) await fs.copy( path.join(__dirname, repoRoot, 'web/locale/locale.properties'), path.join(publicPDFRoot, 'web/locale/locale.properties') ) const locales = ( await fs.readdir(path.join(__dirname, repoRoot, 'web/locale')) ).filter( name => name.startsWith('en') || name.startsWith('zh') || /^(ja|ko|uk)$/.test(name) ) for (const locale of locales) { const targetPath = path.join(publicPDFRoot, 'web/locale', locale) await fs.ensureDir(targetPath) await fs.copy( path.join(__dirname, repoRoot, 'web/locale', locale), targetPath ) } } ================================================ FILE: scripts/setup-env.js ================================================ const fs = require('fs-extra') const path = require('path') main().catch(swallow) // set-up local testing env async function main() { fs.ensureDir(path.join(__dirname, '../build')) const depsPath = path.join(__dirname, '../deps') const destPath = path.join(__dirname, '../node_modules') if (await isDirectory(depsPath)) { const depsFiles = [] const rawDepsFiles = await fs.readdir(depsPath) for (const name of rawDepsFiles) { if (name.startsWith('@')) { const nsFiles = await fs.readdir(path.join(depsPath, name)) for (const nsName of nsFiles) { depsFiles.push(path.join(name, nsName)) } } else { depsFiles.push(name) } } await Promise.all( depsFiles.map(async name => { const destPkgPath = path.join(destPath, name) await fs.remove(destPkgPath).catch(swallow) await fs.ensureDir(destPkgPath).catch(swallow) await fs.copy(path.join(depsPath, name), destPkgPath).catch(swallow) }) ) } } async function isDirectory(dirPath) { const dirStat = await fs.stat(dirPath).catch(swallow) return Boolean(dirStat && dirStat.isDirectory()) } function swallow() { return null } ================================================ FILE: scripts/start.js ================================================ 'use strict' // Do this as the first thing so that any code reading it knows the right env. process.env.BABEL_ENV = 'development' process.env.NODE_ENV = 'development' const argv = require('minimist')(process.argv.slice(2)) if (argv.debug) { process.env.DEBUG_MODE = true } // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will // terminate the Node.js process with a non-zero exit code. process.on('unhandledRejection', err => { throw err }) // Ensure environment variables are read. require('../config/env') const fs = require('fs') const chalk = require('chalk') const webpack = require('webpack') const WebpackDevServer = require('webpack-dev-server') const clearConsole = require('react-dev-utils/clearConsole') const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles') const { choosePort, createCompiler, prepareProxy, prepareUrls, } = require('react-dev-utils/WebpackDevServerUtils') const openBrowser = require('react-dev-utils/openBrowser') const paths = require('../config/paths') const config = require('../config/webpack.config.dev') const createDevServerConfig = require('../config/webpackDevServer.config') const useYarn = fs.existsSync(paths.yarnLockFile) const isInteractive = process.stdout.isTTY // Tools like Cloud9 rely on this. const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000 const HOST = process.env.HOST || '0.0.0.0' // We attempt to use the default port but if it is busy, we offer the user to // run on a different port. `detect()` Promise resolves to the next free port. choosePort(HOST, DEFAULT_PORT) .then(port => { if (port == null) { // We have not found a port. return } const protocol = process.env.HTTPS === 'true' ? 'https' : 'http' const appName = require(paths.appPackageJson).name const urls = prepareUrls(protocol, HOST, port) // Create a webpack compiler that is configured with custom messages. const compiler = createCompiler(webpack, config, appName, urls, useYarn) // Load proxy config const proxySetting = require(paths.appPackageJson).proxy const proxyConfig = prepareProxy(proxySetting, paths.appPublic) // Serve webpack assets generated by the compiler over a web sever. const serverConfig = createDevServerConfig( proxyConfig, urls.lanUrlForConfig ) const devServer = new WebpackDevServer(compiler, serverConfig) // Launch WebpackDevServer. devServer.listen(port, HOST, err => { if (err) { return console.log(err) } if (isInteractive) { clearConsole() } console.log(chalk.cyan('Starting the development server...\n')) openBrowser(urls.localUrlForBrowser) }) ;['SIGINT', 'SIGTERM'].forEach(function(sig) { process.on(sig, function() { devServer.close() process.exit() }) }) }) .catch(err => { if (err && err.message) { console.log(err.message) } process.exit(1) }) ================================================ FILE: scripts/style-extractor.js ================================================ /* eslint-disable @typescript-eslint/no-unused-vars */ const postcss = require('postcss') const fs = require('fs') const path = require('path') /** * Get all ids and class names under a root node. * Use in console. * @param {HTMLElement} root */ function getIdsAndClassNames(root) { const result = new Set() _fn(root) return Array.from(result) .map(x => `'${x}',`) .join('\n') function _fn(node) { if (!node) { return } if (typeof node.className === 'string') { node.className .split(/\s+/) .filter(Boolean) .reduce((r, n) => r.add('.' + n), result) } if (node.id) { result.add('#' + node.id) } Array.from(node.children).forEach(_fn) } } /** * Get relevant styles if the selector contains the keywords.. * @param {string[]} attrs - i.e. ["head", "red"] * @param {string} from - css path * @param {string} to - css path */ function getStylesByAttrs(attrs, from, to) { let result = '' const lattrs = attrs.map(name => name.toLocaleLowerCase()) fs.readFile(from, (err, source) => { if (err) { console.error(err) process.exit(1) } const root = postcss.parse(source, { from, to }) root.walkRules(rule => { const selector = rule.selector.toLowerCase() if (lattrs.some(attr => selector.includes(attr))) { result += rule.toString() + '\n\n' rule.remove() } }) root.walkAtRules(rule => { result += rule.toString() + '\n\n' }) fs.writeFile(to, result, () => true) }) } ================================================ FILE: scripts/test.js ================================================ 'use strict' // Do this as the first thing so that any code reading it knows the right env. process.env.BABEL_ENV = 'test' process.env.NODE_ENV = 'test' process.env.PUBLIC_URL = '' const rawArgv = process.argv.slice(2) const argv = require('minimist')(rawArgv) if (argv.debug) { process.env.DEBUG_MODE = true } // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will // terminate the Node.js process with a non-zero exit code. process.on('unhandledRejection', err => { throw err }) // Ensure environment variables are read. require('../config/env') const jest = require('jest') // Watch unless on CI or in coverage mode // if (!process.env.CI && !argv.coverage) { // rawArgv.push('--watch') // } if (process.env.CI) { rawArgv.push('--no-watchman', '--runInBand', '--no-cache') } jest.run(rawArgv) ================================================ FILE: src/_helpers/__mocks__/browser-api.ts ================================================ /** * @file Wraps some of the extension apis */ import { Observable, fromEventPattern } from 'rxjs' import { map } from 'rxjs/operators' import { Message } from '@/typings/message' /* --------------------------------------- *\ * #Types \* --------------------------------------- */ export type StorageArea = 'all' | 'local' | 'sync' export type StorageListenerCb = ( changes: browser.storage.StorageChange, areaName: string ) => void type onMessageEvent = ( message: Message, sender: browser.runtime.MessageSender, sendResponse: Function ) => Promise | boolean | void /* --------------------------------------- *\ * #Globals \* --------------------------------------- */ const noop = () => { /* do nothing */ } // share the listener so that it can be manipulated manually declare global { interface Window { __messageListeners__: Map< onMessageEvent, Map > __messageSelfListeners__: Map< onMessageEvent, Map > __storageListeners__: Map> } } /** * key: {function} user's callback function * values: {Map} listeners, key: message type, values: generated or user's callback functions */ window.__messageListeners__ = window.__messageListeners__ || new Map() /** * For self page messaging * key: {function} user's callback function * values: {Map} listeners, key: message type, values: generated or user's callback functions */ window.__messageSelfListeners__ = window.__messageSelfListeners__ || new Map() /** * key: {function} user's callback function * values: {Map} listeners, key: message type, values: generated or user's callback functions */ window.__storageListeners__ = window.__storageListeners__ || new Map() const messageListeners = window.__messageListeners__ const messageSelfListeners = window.__messageSelfListeners__ const storageListeners = window.__storageListeners__ /* --------------------------------------- *\ * #Exports \* --------------------------------------- */ export const storage = { sync: { clear: _storageClear(), remove: _storageRemove(), get: _storageGet(), set: _storageSet(), /** Only for sync area */ addListener: _storageAddListener('sync'), /** Only for sync area */ removeListener: _storageRemoveListener('sync'), createStream: noop, dispatch: _dispatchStorageEvent('sync') }, local: { clear: _storageClear(), remove: _storageRemove(), get: _storageGet(), set: _storageSet(), /** Only for local area */ addListener: _storageAddListener('local'), /** Only for local area */ removeListener: _storageRemoveListener('local'), createStream: noop, dispatch: _dispatchStorageEvent('local') }, /** Clear all area */ clear: _storageClear(), addListener: _storageAddListener('all'), removeListener: _storageRemoveListener('all'), createStream: noop as ReturnType, dispatch: dispatchStorageEvent } storage.sync.createStream = _storageCreateStream('sync') storage.local.createStream = _storageCreateStream('local') storage.createStream = _storageCreateStream('all') /** * Wraps in-app runtime.sendMessage and tabs.sendMessage * Does not warp cross extension messaging! */ export const message = { send: _messageSend(false), addListener: _messageAddListener(false), removeListener: _messageRemoveListener(false), createStream: noop, dispatch: _dispatchMessageEvent(false), self: { initClient: jest.fn(() => Promise.resolve()), initServer: jest.fn(noop), send: _messageSend(true), addListener: _messageAddListener(true), removeListener: _messageRemoveListener(true), createStream: noop, dispatch: _dispatchMessageEvent(true) } } message.createStream = _messageCreateStream(false) message.self.createStream = _messageCreateStream(true) /** * Open a url on new tab or highlight a existing tab if already opened */ export const openUrl = jest.fn(() => Promise.resolve()) export default { openUrl, storage, message } /* --------------------------------------- *\ * #Storage \* --------------------------------------- */ function _storageClear() { return jest.fn(storageClear) function storageClear(): Promise { return Promise.resolve() } } function _storageRemove() { return jest.fn(storageRemove) function storageRemove(keys: string | string[]): Promise { return Promise.resolve() } } function _storageGet() { return jest.fn(storageGet) function storageGet(key?: string | string[] | null): Promise function storageGet(key: T | any): Promise function storageGet(...args): Promise { return Promise.resolve() as any } } function _storageSet() { return jest.fn(storageSet) function storageSet(keys: any): Promise { return Promise.resolve() as any } } function _storageAddListener(area: string) { return jest.fn(storageAddListener) function storageAddListener(cb: StorageListenerCb): void function storageAddListener(key: string, cb: StorageListenerCb): void function storageAddListener(...args): void { let key: string let cb: StorageListenerCb if (typeof args[0] === 'function') { key = '' cb = args[0] } else if (typeof args[0] === 'string' && typeof args[1] === 'function') { key = args[0] cb = args[1] } else { throw new Error('wrong arguments type') } let listeners = storageListeners.get(cb) if (!listeners) { listeners = new Map() storageListeners.set(cb, listeners) } const listenerKey = area + key let listener = listeners.get(listenerKey) if (!listener) { listener = (changes, areaName) => { if ((area === 'all' || areaName === area) && (!key || changes[key])) { cb(changes, areaName) } } listeners.set(listenerKey, listener) } } } function _storageRemoveListener(area: string) { return jest.fn(storageRemoveListener) function storageRemoveListener(key: string, cb: StorageListenerCb): void function storageRemoveListener(cb: StorageListenerCb): void function storageRemoveListener(...args): void { let key: string let cb: StorageListenerCb if (typeof args[0] === 'function') { key = '' cb = args[0] } else if (typeof args[0] === 'string' && typeof args[1] === 'function') { key = args[0] cb = args[1] } else { throw new Error('wrong arguments type') } const listeners = storageListeners.get(cb) if (listeners) { if (key) { // remove 'cb' listeners with 'key' under 'storageArea' const listenerKey = area + key const listener = listeners.get(listenerKey) if (listener) { listeners.delete(listenerKey) if (listeners.size <= 0) { storageListeners.delete(cb) } } } else { // remove all 'cb' listeners under 'storageArea' storageListeners.delete(cb) } } } } function _storageCreateStream(area: string) { return jest.fn(storageCreateStream) function storageCreateStream(key: string) { const obj = area === 'all' ? storage : storage[area] return fromEventPattern( handler => obj.addListener(key, handler as StorageListenerCb), handler => obj.removeListener(key, handler as StorageListenerCb) ).pipe(map((args: any) => (Array.isArray(args) ? args[0][key] : args[key]))) } } interface DispatchStorageEventOptions { /** message key */ key?: string newValue?: any oldValue?: any } interface DispatchStorageEventOptionsGeneral extends DispatchStorageEventOptions { area?: StorageArea | '' } function _dispatchStorageEvent(area: 'sync' | 'local') { const _fn = dispatchStorageEvent return function dispatchStorageEvent(options: DispatchStorageEventOptions) { return _fn(Object.assign(options, { area })) } } export function dispatchStorageEvent( options: DispatchStorageEventOptionsGeneral ): void { storageListeners.forEach(m => { m.forEach((cb, key) => { if (!options.key || options.key === key) { if (!options.area || options.area === 'all') { cb({ newValue: options.newValue, oldValue: options.oldValue }, 'sync') cb( { newValue: options.newValue, oldValue: options.oldValue }, 'local' ) } else { cb( { newValue: options.newValue, oldValue: options.oldValue }, options.area ) } } }) }) } /* --------------------------------------- *\ * #Message \* --------------------------------------- */ function _messageSend(self: boolean) { return jest.fn(self ? messageSendSelf : messageSend) function messageSend(tabId: number, message: Message): Promise function messageSend(message: Message): Promise function messageSend(...args): Promise { return Promise.resolve() } function messageSendSelf(message: Message): Promise { return Promise.resolve() } } function _messageAddListener(self: boolean) { return jest.fn( messageAddListener as any ) function messageAddListener( messageType: Message['type'], cb: onMessageEvent ): void function messageAddListener(cb: onMessageEvent): void function messageAddListener(...args): void { const allListeners = self ? messageSelfListeners : messageListeners const messageType = args.length === 1 ? undefined : args[0] const cb = args.length === 1 ? args[0] : args[1] let listeners = allListeners.get(cb) if (!listeners) { listeners = new Map() allListeners.set(cb, listeners) } let listener = listeners.get(messageType || '__DEFAULT_MSGTYPE__') if (!listener) { listener = ((message, sender, sendResponse) => { if (message && (self ? window.pageId === 'PAGE_INFO' : !'PAGE_INFO')) { if (messageType == null || message.type === messageType) { return cb(message, sender, sendResponse) } } }) as onMessageEvent listeners.set(messageType, listener) } } } function _messageRemoveListener(self: boolean) { return jest.fn( messageRemoveListener as any ) function messageRemoveListener( messageType: Message['type'], cb: onMessageEvent ): void function messageRemoveListener(cb: onMessageEvent): void function messageRemoveListener(...args): void { const allListeners = self ? messageSelfListeners : messageListeners const messageType = args.length === 1 ? undefined : args[0] const cb = args.length === 1 ? args[0] : args[1] const listeners = allListeners.get(cb) if (listeners) { if (messageType) { const listener = listeners.get(messageType) if (listener) { listeners.delete(messageType) if (listeners.size <= 0) { allListeners.delete(cb) } } } else { // delete all cb related callbacks allListeners.delete(cb) } } } } function _messageCreateStream(self: boolean) { return jest.fn(messageCreateStream) function messageCreateStream( messageType?: Message['type'] ): Observable { const obj = self ? message.self : message const pattern$ = messageType ? fromEventPattern( handler => obj.addListener(messageType, handler as onMessageEvent), handler => obj.removeListener(messageType, handler as onMessageEvent) ) : fromEventPattern( handler => obj.addListener(handler as onMessageEvent), handler => obj.removeListener(handler as onMessageEvent) ) return pattern$.pipe(map(args => (Array.isArray(args) ? args[0] : args))) } } interface DispatchMessageEventOptions { message: Message sender?: browser.runtime.MessageSender sendResponse?: Function } interface DispatchMessageEventOptionsGeneral extends DispatchMessageEventOptions { self?: boolean } function _dispatchMessageEvent(self: boolean) { const _fn = dispatchMessageEvent return function dispatchMessageEvent(options: DispatchMessageEventOptions) { return _fn(Object.assign(options, { self })) } } export function dispatchMessageEvent( options: DispatchMessageEventOptionsGeneral ) { const listeners = options.self ? messageSelfListeners : messageListeners listeners.forEach(m => { m.forEach((cb, type) => { if (options.message.type === type) { cb(options.message, options.sender || {}, options.sendResponse || noop) } }) }) } ================================================ FILE: src/_helpers/__mocks__/config-manager.ts ================================================ import { AppConfig, getDefaultConfig } from '@/app-config' import { Observable, fromEventPattern, of, concat } from 'rxjs' import { map } from 'rxjs/operators' const listeners = new Set<(changed: AppConfigChanged) => void>() export interface AppConfigChanged { newConfig: AppConfig oldConfig?: AppConfig } export const initConfig = jest.fn(() => Promise.resolve()) export const resetConfig = jest.fn(() => Promise.resolve()) export const getConfig = jest.fn(() => Promise.resolve(getDefaultConfig())) export const updateConfig = jest.fn((config: AppConfig) => Promise.resolve()) export const addConfigListener = jest.fn( (cb: (changed: AppConfigChanged) => void) => { listeners.add(cb) } ) /** * Get AppConfig and create a stream listening config changing */ export const createConfigStream = jest.fn( (): Observable => { return concat( of(getDefaultConfig()), fromEventPattern(handler => addConfigListener(handler) ).pipe(map(args => (Array.isArray(args) ? args[0] : args).newConfig)) ) } ) export function dispatchConfigChangedEvent( newConfig: AppConfig, oldConfig?: AppConfig ) { listeners.forEach(cb => cb({ newConfig, oldConfig })) } ================================================ FILE: src/_helpers/__mocks__/selection.ts ================================================ export interface SelectionMock { hasSelection: jest.Mock getSelectionText: jest.Mock getSelectionSentence: jest.Mock getSelectionInfo: jest.Mock getDefaultSelectionInfo: jest.Mock } module.exports = jest.genMockFromModule('../selection') ================================================ FILE: src/_helpers/analytics/events.ts ================================================ export type GAEventBase = { category: string action: string label?: string value?: string } type GAEventFactory = T export type GAEvent = GAEventFactory< | { category: 'Page_Translate' action: 'Open_Google' | 'Open_Youdao' | 'Open_Caiyun' label: | 'From_Browser_Action' | 'From_Context_Menus' | 'From_Browser_Shortcut' } | { category: 'PDF_Viewer' action: 'Open_PDF_Viewer' label: | 'From_Browser_Action' | 'From_Context_Menus' | 'From_Browser_Shortcut' } > ================================================ FILE: src/_helpers/analytics/index.ts ================================================ import UAParser from 'ua-parser-js' import axios from 'axios' import uuid from 'uuid/v4' import { message, storage } from '@/_helpers/browser-api' import { genUniqueKey } from '@/_helpers/uniqueKey' import { GAEvent, GAEventBase } from './events' import { isBackgroundPage } from '../saladict' export type GAParams = { [key: string]: string } export async function reportPageView(page: string): Promise { const ua = new UAParser() const browser = ua.getBrowser() const os = ua.getOS() try { await requestGA({ t: 'pageview', // required by pageview dp: page, // Dimensions cd1: browser.name || 'None', cd2: (browser.version || '0.0') .split('.') .slice(0, 3) .join('.'), cd3: os.name || 'None', cd4: os.version || '0.0', // Document Encoding de: 'UTF-8', // Document location URL dl: document.location.href, // Screen Colors sd: screen.colorDepth + '-bit', // Screen Resolution sr: screen.width + 'x' + screen.height, // User Language ul: 'zh-cn' }) } catch (error) { if (!process.env.DEBUG) { console.error('Report pageview error', error) } } } export async function reportEvent(event: GAEvent) { const params: GAParams = { t: 'event', ec: event.category, ea: event.action } if ((event as GAEventBase).label != null) { params.el = (event as GAEventBase).label! } if ((event as GAEventBase).value != null) { params.ev = (event as GAEventBase).value! } try { await requestGA(params) } catch (error) { if (!process.env.DEBUG) { console.error('Report event error', error) } } } async function requestGA(extraParams: GAParams) { if (!isBackgroundPage()) { return message.send({ type: 'REQUEST_GA', payload: extraParams }) } if ( process.env.DEBUG || process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development' ) { console.log('requestGA', extraParams) return } let cid = (await storage.sync.get<{ gacid: string }>('gacid')).gacid if (!cid) { cid = uuid() storage.sync.set({ gacid: cid }) } return axios({ url: 'https://www.google-analytics.com/collect', method: 'post', headers: { 'content-type': 'text/plain;charset=UTF-8' }, data: new URLSearchParams({ // required v: '1', tid: 'UA-49163616-4', cid, // Cache Buster z: genUniqueKey(), ...extraParams }) }) } export function setupRequestGAListener() { message.addListener('REQUEST_GA', ({ payload }) => { requestGA(payload) }) } ================================================ FILE: src/_helpers/browser-api.ts ================================================ /** * @file Wraps some of the extension apis */ import { Observable, fromEventPattern } from 'rxjs' import { map, filter } from 'rxjs/operators' import { Message, MessageResponse, MsgType } from '@/typings/message' import { Mutable } from '@/typings/helpers' /* --------------------------------------- *\ * #Types \* --------------------------------------- */ export type StorageArea = 'all' | 'local' | 'sync' export type StorageChange = { oldValue?: T newValue?: T } export type StorageUpdate = { oldValue?: T newValue: T } export type StorageListenerCb = ( changes: { [field in K]: StorageChange }, areaName: string ) => void type onMessageEvent = ( message: T & { __pageId__?: string }, sender: browser.runtime.MessageSender ) => Promise | boolean | void /* --------------------------------------- *\ * #Globals \* --------------------------------------- */ const noop = () => { /* do nothing */ } /** * key: {function} user's callback function * values: {Map} listeners, key: message type, values: generated or user's callback functions */ const messageListeners: WeakMap< Function, Map > = new WeakMap() /** * For self page messaging * key: {function} user's callback function * values: {Map} listeners, key: message type, values: generated or user's callback functions */ const messageSelfListeners: WeakMap< Function, Map > = new WeakMap() /** * key: {function} user's callback function * values: {Map} listeners, key: message type, values: generated or user's callback functions */ const storageListeners: WeakMap< StorageListenerCb, Map > = new WeakMap() /* --------------------------------------- *\ * #Exports \* --------------------------------------- */ export const storage = { sync: { clear: storageClear, remove: storageRemove, get: storageGet, set: storageSet, /** Only for sync area */ addListener: storageAddListener, /** Only for sync area */ removeListener: storageRemoveListener, createStream: storageCreateStream, get __storageArea__(): 'sync' { return 'sync' } }, local: { clear: storageClear, remove: storageRemove, get: storageGet, set: storageSet, /** Only for local area */ addListener: storageAddListener, /** Only for local area */ removeListener: storageRemoveListener, createStream: storageCreateStream, get __storageArea__(): 'local' { return 'local' } }, /** Clear all area */ clear: storageClear, addListener: storageAddListener, removeListener: storageRemoveListener, createStream: storageCreateStream, get __storageArea__(): 'all' { return 'all' } } as const /** * Wraps in-app runtime.sendMessage and tabs.sendMessage * Does not warp cross extension messaging! */ export const message = { send: messageSend, addListener: messageAddListener, removeListener: messageRemoveListener, createStream: messageCreateStream, get __self__(): false { return false }, self: { initClient, initServer, send: messageSendSelf, addListener: messageAddListener, removeListener: messageRemoveListener, createStream: messageCreateStream, get __self__(): true { return true } } } as const export interface OpenUrlOptions { url: string /** use browser.runtime.getURL? */ self?: boolean /** focus the new tab? default true */ active?: boolean /** ignore existing url? default true */ unique?: boolean } /** * Open a url on new tab or highlight a existing tab if already opened */ export async function openUrl(url: string, self?: boolean): Promise export async function openUrl(options: OpenUrlOptions): Promise export async function openUrl( optionsOrUrl: string | OpenUrlOptions, self?: boolean ): Promise { const options: OpenUrlOptions = typeof optionsOrUrl === 'string' ? { url: optionsOrUrl, self } : optionsOrUrl const unique = options.unique !== false const url = options.self ? browser.runtime.getURL(options.url) : options.url if (unique) { const tabs = await browser.tabs.query({ url }).catch(() => []) if (tabs.length > 0) { const { index, windowId } = tabs[0] if (typeof browser.tabs['highlight'] === 'function') { // Only Chrome supports tab.highlight for now await browser.tabs['highlight']({ tabs: index, windowId }) } await browser.windows.update(windowId!, { focused: true }) return } } await browser.tabs.create({ url, active: options.active !== false }) } /* --------------------------------------- *\ * #Storage \* --------------------------------------- */ type StorageThisTwo = typeof storage.sync | typeof storage.local type StorageThisThree = StorageThisTwo | typeof storage function storageClear(): Promise function storageClear(this: StorageThisThree): Promise { return this.__storageArea__ === 'all' ? Promise.all([ browser.storage.local.clear(), browser.storage.sync.clear() ]).then(noop) : browser.storage[this.__storageArea__].clear() } function storageRemove(keys: string | string[]): Promise function storageRemove( this: StorageThisTwo, keys: string | string[] ): Promise { return browser.storage[this.__storageArea__].remove(keys) } function storageGet( key?: string | string[] | null ): Promise> function storageGet(key: T | any): Promise> function storageGet(this: StorageThisTwo, ...args) { return browser.storage[this.__storageArea__].get(...args) as Promise< Partial > } function storageSet(keys: any): Promise function storageSet(this: StorageThisTwo, keys: any): Promise { return browser.storage[this.__storageArea__].set(keys) } function storageAddListener(cb: StorageListenerCb): void function storageAddListener( key: K, cb: StorageListenerCb ): void function storageAddListener(this: StorageThisThree, ...args): void { let key: string let cb: StorageListenerCb if (typeof args[0] === 'function') { key = '' cb = args[0] } else if (typeof args[0] === 'string' && typeof args[1] === 'function') { key = args[0] cb = args[1] } else { throw new Error('wrong arguments type') } let listeners = storageListeners.get(cb) if (!listeners) { listeners = new Map() storageListeners.set(cb, listeners) } const listenerKey = this.__storageArea__ + key let listener = listeners.get(listenerKey) if (!listener) { listener = (changes, areaName) => { if ( (this.__storageArea__ === 'all' || areaName === this.__storageArea__) && (!key || key in changes) ) { cb(changes, areaName) } } listeners.set(listenerKey, listener) } return browser.storage.onChanged.addListener(listener) } function storageRemoveListener(key: string, cb: StorageListenerCb): void function storageRemoveListener(cb: StorageListenerCb): void function storageRemoveListener(this: StorageThisThree, ...args): void { let key: string let cb: StorageListenerCb if (typeof args[0] === 'function') { key = '' cb = args[0] } else if (typeof args[0] === 'string' && typeof args[1] === 'function') { key = args[0] cb = args[1] } else { throw new Error('wrong arguments type') } const listeners = storageListeners.get(cb) if (listeners) { if (key) { // remove 'cb' listeners with 'key' under 'storageArea' const listenerKey = this.__storageArea__ + key const listener = listeners.get(listenerKey) if (listener) { browser.storage.onChanged.removeListener(listener) listeners.delete(listenerKey) if (listeners.size <= 0) { storageListeners.delete(cb) } return } } else { // remove all 'cb' listeners under 'storageArea' listeners.forEach(listener => { browser.storage.onChanged.removeListener(listener) }) storageListeners.delete(cb) return } } browser.storage.onChanged.removeListener(cb) } function storageCreateStream(key: string): Observable> function storageCreateStream( this: StorageThisThree, key: string ): Observable> { if (!key) { throw new Error('Missing key') } return fromEventPattern>( handler => this.addListener(key, handler as StorageListenerCb), handler => this.removeListener(key, handler as StorageListenerCb) ).pipe( filter(args => Object.prototype.hasOwnProperty.call( Array.isArray(args) ? args[0] : args, key ) ), map(args => (Array.isArray(args) ? args[0][key] : args[key])) ) } /* --------------------------------------- *\ * #Message \* --------------------------------------- */ type MessageThis = typeof message | typeof message.self function messageSend>( message: Message ): Promise function messageSend>( tabId: number, message: Message ): Promise function messageSend( ...args: [Message] | [number, Message] ): Promise { let callContext: Error if (process.env.DEBUG) { callContext = new Error('Message Call Context') } return (args.length === 1 ? browser.runtime.sendMessage(args[0]) : browser.tabs.sendMessage(args[0], args[1]) ).catch(err => { if (process.env.DEBUG) { console.warn(err.message, ...args, callContext) } }) } async function messageSendSelf( message: Message ): Promise : R> { let callContext: Error if (process.env.DEBUG) { callContext = new Error('Message Call Context') } if (window.pageId === undefined) { await initClient() } return browser.runtime .sendMessage( Object.assign({}, message, { __pageId__: window.pageId, type: `[[${message.type}]]` }) ) .catch(err => { if (process.env.DEBUG) { console.warn(err.message, message, callContext) } }) } function messageAddListener( messageType: T, cb: onMessageEvent> ): void function messageAddListener( cb: onMessageEvent ): void function messageAddListener( this: MessageThis, ...args: [T, onMessageEvent>] | [onMessageEvent] ): void { if (window.pageId === undefined) { initClient() } const allListeners = this.__self__ ? messageSelfListeners : messageListeners const messageType = args.length === 1 ? undefined : args[0] const cb = args.length === 1 ? args[0] : args[1] let listeners = allListeners.get(cb) if (!listeners) { listeners = new Map() allListeners.set(cb, listeners) } let listener = listeners.get(messageType || '__DEFAULT_MSGTYPE__') if (!listener) { listener = ((message, sender) => { if ( message && (this.__self__ ? window.pageId === message.__pageId__ : !message.__pageId__) ) { if (messageType == null || message.type === messageType) { return cb(message as Message & { __pageId__?: string }, sender) } } }) as onMessageEvent listeners.set(messageType || '__DEFAULT_MSGTYPE__', listener) } // object is handled return browser.runtime.onMessage.addListener(listener as any) } function messageRemoveListener( messageType: Message['type'], cb: onMessageEvent ): void function messageRemoveListener(cb: onMessageEvent): void function messageRemoveListener( this: MessageThis, ...args: [Message['type'], onMessageEvent] | [onMessageEvent] ): void { const allListeners = this.__self__ ? messageSelfListeners : messageListeners const messageType = args.length === 1 ? undefined : args[0] const cb = args.length === 1 ? args[0] : args[1] const listeners = allListeners.get(cb) if (listeners) { if (messageType) { const listener = listeners.get(messageType) if (listener) { // @ts-ignore browser.runtime.onMessage.removeListener(listener) listeners.delete(messageType) if (listeners.size <= 0) { allListeners.delete(cb) } return } } else { // delete all cb related callbacks listeners.forEach(listener => // @ts-ignore browser.runtime.onMessage.removeListener(listener) ) allListeners.delete(cb) return } } // @ts-ignore browser.runtime.onMessage.removeListener(cb) } function messageCreateStream( messageType?: T ): Observable> function messageCreateStream( this: MessageThis, messageType?: T ): Observable> { const pattern$ = messageType ? fromEventPattern>( handler => this.addListener(messageType, handler), handler => this.removeListener(messageType, handler) ) : fromEventPattern>( handler => this.addListener(handler), handler => this.removeListener(handler) ) // Arguments could be an array if there are multiple values emitted. return pattern$.pipe(map(args => (Array.isArray(args) ? args[0] : args))) } /** * Deploy page script for self-messaging * This method is called on the first sendMessage */ function initClient(): Promise { if (window.pageId === undefined) { return message .send<'PAGE_INFO'>({ type: 'PAGE_INFO' }) .then(({ pageId, faviconURL, pageTitle, pageURL }) => { window.pageId = pageId window.faviconURL = faviconURL if (pageTitle) { window.pageTitle = pageTitle } if (pageURL) { window.pageURL = pageURL } return pageId }) } else { return Promise.resolve(window.pageId) } } /** * Deploy background proxy for self-messaging * This method should be invoked in background script */ function initServer(): void { window.pageId = 'background page' const selfMsgTester = /^\[\[(.+)\]\]$/ browser.runtime.onMessage.addListener( (message: object, sender: browser.runtime.MessageSender) => { if (!message || !message['type']) { return } if ((message as Message).type === 'PAGE_INFO') { return Promise.resolve(_getPageInfo(sender)) } const selfMsg = selfMsgTester.exec((message as Message).type) if (selfMsg) { ;(message as Mutable).type = selfMsg[1] as MsgType const tabId = sender.tab && sender.tab.id if (tabId) { return messageSend(tabId, message as Message) } else { return messageSend(message as Message) } } } ) } function _getPageInfo(sender: browser.runtime.MessageSender) { const result = { pageId: '' as string | number, faviconURL: '', pageTitle: '', pageURL: '' } const tab = sender.tab if (tab) { result.pageId = tab.id || '' if (tab.favIconUrl) { result.faviconURL = tab.favIconUrl } if (tab.url) { result.pageURL = tab.url } if (tab.title) { result.pageTitle = tab.title } } else { // FRAGILE: Assume only browser action page is tabless result.pageId = 'popup' if (sender.url && !sender.url.startsWith('http')) { result.faviconURL = 'https://saladict.crimx.com/favicon.ico' } } return result } ================================================ FILE: src/_helpers/check-update.ts ================================================ export interface ReleaseData { version: string data: string[] } /** * 3 major newer * 2 minor newer * 1 patch newer * 0 same version * -1 patch older * -2 minor older * -3 major older */ export type VersionDiff = number export type ReleaseResponse = { diff: VersionDiff data?: ReleaseData } export async function checkUpdate( compareVersion?: string, data?: ReleaseData ): Promise { if (!data) { try { const isZh = window.appConfig.langCode.startsWith('zh') const response = await fetch( `https://saladict.crimx.com/releases/${isZh ? 'chs' : 'eng'}.json` ) data = await response.json() } catch (e) { console.error(e) } } if (!data) { return { diff: 0 } } if (!compareVersion) { return { diff: 3, data } } const prev = compareVersion.split('.').map(Number) const curr = data.version .slice(1) .split('.') .map(Number) for (let i = 0; i < 3; i++) { if (curr[i] > prev[i]) { return { diff: 3 - i, data } } if (curr[i] < prev[i]) { return { diff: i - 3, data } } } return { diff: 0, data } } ================================================ FILE: src/_helpers/chs-to-chz.ts ================================================ const charMap = new Map([ ['与', '與'], ['丒', '囟'], ['专', '專'], ['丗', '卅'], ['业', '業'], ['丛', '叢'], ['东', '東'], ['丝', '絲'], ['両', '兩'], ['丢', '丟'], ['两', '兩'], ['严', '嚴'], ['丧', '喪'], ['个', '個'], ['丬', '爿'], ['丯', '丰'], ['临', '臨'], ['丶', '⼂'], ['为', '為'], ['丽', '麗'], ['举', '舉'], ['义', '義'], ['乌', '烏'], ['乐', '樂'], ['乔', '喬'], ['习', '習'], ['乡', '鄉'], ['书', '書'], ['买', '買'], ['乱', '亂'], ['亀', '龜'], ['亁', '乾'], ['争', '爭'], ['亏', '虧'], ['亘', '亙'], ['亚', '亞'], ['产', '產'], ['亩', '畝'], ['亲', '親'], ['亵', '褻'], ['亸', '嚲'], ['亻', '人'], ['亿', '億'], ['仅', '僅'], ['从', '從'], ['仑', '崙'], ['仓', '倉'], ['仪', '儀'], ['们', '們'], ['仮', '假'], ['众', '眾'], ['会', '會'], ['伛', '傴'], ['伞', '傘'], ['伟', '偉'], ['传', '傳'], ['伤', '傷'], ['伥', '倀'], ['伦', '倫'], ['伧', '傖'], ['伪', '偽'], ['伫', '佇'], ['体', '體'], ['佥', '僉'], ['侠', '俠'], ['侣', '侶'], ['侥', '僥'], ['侦', '偵'], ['侧', '側'], ['侨', '僑'], ['侩', '儈'], ['侪', '儕'], ['侬', '儂'], ['俣', '俁'], ['俦', '儔'], ['俨', '儼'], ['俩', '倆'], ['俪', '儷'], ['俭', '儉'], ['债', '債'], ['倾', '傾'], ['偬', '傯'], ['偻', '僂'], ['偾', '僨'], ['偿', '償'], ['傥', '儻'], ['傧', '儐'], ['储', '儲'], ['傩', '儺'], ['兎', '兔'], ['兑', '兌'], ['兖', '兗'], ['兪', '俞'], ['兰', '蘭'], ['关', '關'], ['兴', '興'], ['兹', '茲'], ['养', '養'], ['兽', '獸'], ['兾', '糞'], ['兿', '藝'], ['冁', '囅'], ['内', '內'], ['円', '丹'], ['冈', '岡'], ['册', '冊'], ['写', '寫'], ['军', '軍'], ['农', '農'], ['冝', '宜'], ['冦', '寇'], ['冧', '霖'], ['冨', '富'], ['冩', '寫'], ['冮', '江'], ['冯', '馮'], ['冲', '沖'], ['决', '決'], ['况', '況'], ['冸', '泮'], ['冺', '泯'], ['冻', '凍'], ['冿', '津'], ['净', '淨'], ['凁', '涑'], ['凂', '浼'], ['凃', '涂'], ['凄', '淒'], ['凉', '涼'], ['减', '減'], ['凑', '湊'], ['凒', '溰'], ['凓', '溧'], ['凕', '溟'], ['凖', '準'], ['凙', '澤'], ['凛', '凜'], ['凟', '瀆'], ['凤', '鳳'], ['凥', '尻'], ['処', '處'], ['凨', '云'], ['凫', '鳧'], ['凬', '凰'], ['凭', '憑'], ['凮', '鳳'], ['凯', '凱'], ['凴', '憑'], ['击', '擊'], ['凼', '窞'], ['凾', '亟'], ['凿', '鑿'], ['刄', '刃'], ['刅', '刃'], ['刋', '刊'], ['刍', '芻'], ['刘', '劉'], ['则', '則'], ['刚', '剛'], ['创', '創'], ['删', '刪'], ['刦', '劫'], ['刧', '劫'], ['别', '別'], ['刭', '剄'], ['刴', '剁'], ['刹', '剎'], ['刼', '劫'], ['刽', '劊'], ['刿', '劌'], ['剀', '剴'], ['剂', '劑'], ['剐', '剮'], ['剑', '劍'], ['剥', '剝'], ['剧', '劇'], ['剰', '剩'], ['劎', '劍'], ['劒', '劍'], ['劔', '劍'], ['劝', '勸'], ['办', '辦'], ['务', '務'], ['劢', '勱'], ['动', '動'], ['励', '勵'], ['劲', '勁'], ['劳', '勞'], ['労', '勞'], ['劵', '卷'], ['効', '效'], ['劽', '裂'], ['势', '勢'], ['勅', '敕'], ['勋', '勛'], ['勐', '猛'], ['勚', '勩'], ['勠', '戮'], ['勥', '強'], ['勧', '勸'], ['匀', '勻'], ['匦', '匭'], ['匮', '匱'], ['区', '區'], ['医', '醫'], ['华', '華'], ['协', '協'], ['单', '單'], ['卖', '賣'], ['単', '單'], ['卙', '斟'], ['卛', '攣'], ['卟', '嚇'], ['卢', '盧'], ['卤', '鹵'], ['卥', '囟'], ['卧', '臥'], ['卫', '衛'], ['却', '卻'], ['卺', '巹'], ['厅', '廳'], ['历', '歷'], ['厉', '厲'], ['压', '壓'], ['厌', '厭'], ['厕', '廁'], ['厛', '廳'], ['厠', '廁'], ['厢', '廂'], ['厣', '厴'], ['厦', '廈'], ['厨', '廚'], ['厩', '廄'], ['厮', '廝'], ['厰', '廠'], ['厳', '嚴'], ['厶', '⼛'], ['县', '縣'], ['叁', '參'], ['叄', '參'], ['叆', '靉'], ['叇', '靆'], ['双', '雙'], ['収', '收'], ['叏', '發'], ['叐', '發'], ['发', '發'], ['变', '變'], ['叙', '敘'], ['叠', '疊'], ['叧', '另'], ['叶', '葉'], ['号', '號'], ['叹', '嘆'], ['叽', '嘰'], ['吓', '嚇'], ['吕', '呂'], ['吖', '嗄'], ['吗', '嗎'], ['吣', '唚'], ['吨', '噸'], ['启', '啟'], ['吴', '吳'], ['吿', '告'], ['呋', '咐'], ['呐', '吶'], ['呑', '吞'], ['呒', '嘸'], ['呓', '囈'], ['呕', '嘔'], ['呖', '嚦'], ['呗', '唄'], ['员', '員'], ['呙', '咼'], ['呛', '嗆'], ['呜', '嗚'], ['呪', '咒'], ['咏', '詠'], ['咙', '嚨'], ['咛', '嚀'], ['咝', '吱'], ['咣', '光'], ['咤', '吒'], ['哌', '呱'], ['响', '響'], ['哐', '匡'], ['哑', '啞'], ['哒', '噠'], ['哓', '嘵'], ['哔', '嗶'], ['哕', '噦'], ['哗', '嘩'], ['哙', '噲'], ['哜', '嚌'], ['哝', '噥'], ['哟', '喲'], ['唝', '嗊'], ['唠', '嘮'], ['唡', '啢'], ['唢', '嗩'], ['唣', '嗦'], ['唤', '喚'], ['唿', '呼'], ['啧', '嘖'], ['啬', '嗇'], ['啭', '囀'], ['啰', '囉'], ['啴', '嘽'], ['啸', '嘯'], ['喷', '噴'], ['喹', '奎'], ['喽', '嘍'], ['喾', '嚳'], ['嗪', '唚'], ['嗫', '囁'], ['嗬', '呵'], ['嗳', '噯'], ['嗵', '通'], ['嘘', '噓'], ['嘞', '咧'], ['嘠', '嘎'], ['嘣', '迸'], ['嘤', '嚶'], ['嘨', '嘯'], ['嘭', '膨'], ['嘱', '囑'], ['嘷', '嚎'], ['噜', '嚕'], ['噻', '塞'], ['噼', '劈'], ['嚔', '涕'], ['嚢', '囊'], ['嚣', '囂'], ['嚯', '謔'], ['团', '團'], ['园', '園'], ['囱', '囪'], ['围', '圍'], ['囵', '圇'], ['国', '國'], ['图', '圖'], ['圆', '圓'], ['圣', '聖'], ['圹', '壙'], ['场', '場'], ['块', '塊'], ['坚', '堅'], ['坛', '壇'], ['坜', '壢'], ['坝', '壩'], ['坞', '塢'], ['坟', '墳'], ['坠', '墜'], ['垄', '壟'], ['垅', '壟'], ['垆', '壚'], ['垒', '壘'], ['垦', '墾'], ['垧', '坰'], ['垩', '堊'], ['垫', '墊'], ['垲', '塏'], ['垴', '瑙'], ['埘', '塒'], ['埚', '堝'], ['堑', '塹'], ['堕', '墮'], ['塡', '填'], ['塬', '原'], ['墙', '牆'], ['壮', '壯'], ['声', '聲'], ['壳', '殼'], ['壶', '壺'], ['壸', '壼'], ['夂', '⼢'], ['处', '處'], ['备', '備'], ['夊', '⼢'], ['够', '夠'], ['头', '頭'], ['夹', '夾'], ['夺', '奪'], ['奁', '奩'], ['奂', '奐'], ['奋', '奮'], ['奖', '獎'], ['奥', '奧'], ['妆', '妝'], ['妇', '婦'], ['妈', '媽'], ['妩', '嫵'], ['妪', '嫗'], ['妫', '媯'], ['姗', '姍'], ['姹', '奼'], ['娄', '婁'], ['娅', '婭'], ['娆', '嬈'], ['娇', '嬌'], ['娈', '孌'], ['娱', '娛'], ['娲', '媧'], ['娴', '嫻'], ['婳', '嫿'], ['婴', '嬰'], ['婵', '嬋'], ['婶', '嬸'], ['媪', '媼'], ['嫒', '嬡'], ['嫔', '嬪'], ['嫱', '嬙'], ['嬷', '嬤'], ['孙', '孫'], ['学', '學'], ['孪', '孿'], ['孶', '孳'], ['宝', '寶'], ['实', '實'], ['宠', '寵'], ['审', '審'], ['宪', '憲'], ['宫', '宮'], ['宽', '寬'], ['宾', '賓'], ['寝', '寢'], ['对', '對'], ['寻', '尋'], ['导', '導'], ['対', '對'], ['寿', '壽'], ['専', '專'], ['尅', '剋'], ['将', '將'], ['尓', '爾'], ['尔', '爾'], ['尘', '塵'], ['尝', '嘗'], ['尧', '堯'], ['尴', '尷'], ['尽', '盡'], ['层', '層'], ['屃', '屭'], ['屉', '屜'], ['届', '屆'], ['屛', '屏'], ['属', '屬'], ['屡', '屢'], ['屦', '屨'], ['屿', '嶼'], ['岁', '歲'], ['岂', '豈'], ['岖', '嶇'], ['岗', '崗'], ['岘', '峴'], ['岙', '嶴'], ['岚', '嵐'], ['岛', '島'], ['岭', '嶺'], ['岿', '巋'], ['峄', '嶧'], ['峡', '峽'], ['峣', '嶢'], ['峤', '嶠'], ['峥', '崢'], ['峦', '巒'], ['峯', '峰'], ['崂', '嶗'], ['崃', '崍'], ['崄', '嶮'], ['崭', '嶄'], ['崾', '要'], ['嵘', '嶸'], ['嵚', '嶔'], ['嵝', '嶁'], ['巄', '巃'], ['巅', '巔'], ['巌', '巖'], ['巓', '巔'], ['巩', '鞏'], ['币', '幣'], ['帅', '帥'], ['师', '師'], ['帏', '幃'], ['帐', '帳'], ['帜', '幟'], ['带', '帶'], ['帧', '幀'], ['帮', '幫'], ['帯', '帶'], ['帱', '幬'], ['帻', '幘'], ['帼', '幗'], ['幂', '冪'], ['幇', '幫'], ['幚', '幫'], ['幞', '襆'], ['幷', '并'], ['广', '廣'], ['庁', '廳'], ['広', '麼'], ['庄', '莊'], ['庅', '麼'], ['庆', '慶'], ['庐', '廬'], ['庑', '廡'], ['库', '庫'], ['应', '應'], ['庙', '廟'], ['庞', '龐'], ['废', '廢'], ['庼', '廎'], ['廏', '廄'], ['廐', '廄'], ['廪', '廩'], ['廴', '⼵'], ['廵', '巡'], ['开', '開'], ['异', '異'], ['弃', '棄'], ['弑', '弒'], ['张', '張'], ['弥', '彌'], ['弯', '彎'], ['弹', '彈'], ['强', '強'], ['归', '歸'], ['当', '當'], ['录', '錄'], ['彚', '彙'], ['彛', '羿'], ['彜', '羿'], ['彟', '獲'], ['彠', '獲'], ['彡', '⼺'], ['彦', '彥'], ['彻', '徹'], ['径', '徑'], ['徕', '徠'], ['徸', '德'], ['忄', '心'], ['忆', '憶'], ['忏', '懺'], ['忧', '憂'], ['忾', '愾'], ['怀', '懷'], ['态', '態'], ['怂', '慫'], ['怃', '憮'], ['怅', '悵'], ['怆', '愴'], ['怜', '憐'], ['总', '總'], ['怼', '懟'], ['怿', '懌'], ['恋', '戀'], ['恒', '恆'], ['恳', '懇'], ['恶', '惡'], ['恸', '慟'], ['恹', '懨'], ['恺', '愷'], ['恻', '惻'], ['恼', '惱'], ['恽', '惲'], ['悦', '悅'], ['悫', '愨'], ['悬', '懸'], ['悭', '慳'], ['悯', '憫'], ['惊', '驚'], ['惧', '懼'], ['惨', '慘'], ['惩', '懲'], ['惫', '憊'], ['惬', '愜'], ['惭', '慚'], ['惮', '憚'], ['惯', '慣'], ['惽', '惛'], ['愠', '慍'], ['愤', '憤'], ['愦', '憒'], ['慑', '懾'], ['慭', '憖'], ['懑', '懣'], ['懒', '懶'], ['懔', '懍'], ['懴', '懺'], ['戅', '戇'], ['戆', '戇'], ['戋', '戔'], ['戏', '戲'], ['戗', '戧'], ['战', '戰'], ['戝', '敗'], ['戦', '戰'], ['戬', '戩'], ['戯', '戲'], ['戱', '戲'], ['户', '戶'], ['戸', '戶'], ['扌', '手'], ['执', '執'], ['扩', '擴'], ['扪', '捫'], ['扫', '掃'], ['扬', '揚'], ['扰', '擾'], ['抅', '拘'], ['抚', '撫'], ['抛', '拋'], ['抟', '摶'], ['抠', '摳'], ['抡', '掄'], ['抢', '搶'], ['护', '護'], ['报', '報'], ['担', '擔'], ['拟', '擬'], ['拢', '攏'], ['拣', '揀'], ['拥', '擁'], ['拦', '攔'], ['拧', '擰'], ['拨', '撥'], ['择', '擇'], ['挚', '摯'], ['挛', '攣'], ['挜', '掗'], ['挝', '撾'], ['挞', '撻'], ['挟', '挾'], ['挠', '撓'], ['挡', '擋'], ['挢', '撟'], ['挣', '掙'], ['挤', '擠'], ['挥', '揮'], ['挦', '撏'], ['捞', '撈'], ['损', '損'], ['捡', '撿'], ['换', '換'], ['捣', '搗'], ['掳', '擄'], ['掴', '摑'], ['掷', '擲'], ['掸', '撣'], ['掺', '摻'], ['掼', '摜'], ['揸', '喳'], ['揽', '攬'], ['揿', '撳'], ['搀', '攙'], ['搁', '擱'], ['搂', '摟'], ['搃', '摠'], ['搅', '攪'], ['携', '攜'], ['摄', '攝'], ['摅', '攄'], ['摆', '擺'], ['摇', '搖'], ['摈', '擯'], ['摊', '攤'], ['撃', '擊'], ['撄', '攖'], ['撑', '撐'], ['撪', '攆'], ['撵', '攆'], ['撷', '擷'], ['撹', '攪'], ['撺', '攛'], ['擕', '攜'], ['擞', '擻'], ['擡', '抬'], ['擥', '掔'], ['擧', '舉'], ['擪', '壓'], ['攒', '攢'], ['攵', '又'], ['敇', '敕'], ['敌', '敵'], ['敛', '斂'], ['敮', '歃'], ['数', '數'], ['斉', '齊'], ['斋', '齋'], ['斎', '齋'], ['斓', '斕'], ['斩', '斬'], ['断', '斷'], ['旧', '舊'], ['时', '時'], ['旷', '曠'], ['旸', '暘'], ['昙', '曇'], ['昼', '晝'], ['昽', '曨'], ['显', '顯'], ['晋', '晉'], ['晓', '曉'], ['晔', '曄'], ['晕', '暈'], ['晖', '暉'], ['暂', '暫'], ['暧', '曖'], ['术', '術'], ['杀', '殺'], ['杂', '雜'], ['权', '權'], ['条', '條'], ['来', '來'], ['杨', '楊'], ['极', '極'], ['枞', '樅'], ['枢', '樞'], ['枣', '棗'], ['枥', '櫪'], ['枧', '見'], ['枨', '棖'], ['枪', '槍'], ['枫', '楓'], ['枭', '梟'], ['柠', '檸'], ['柽', '檉'], ['栀', '梔'], ['栅', '柵'], ['标', '標'], ['栈', '棧'], ['栉', '櫛'], ['栊', '櫳'], ['栋', '棟'], ['栌', '櫨'], ['栎', '櫟'], ['栏', '欄'], ['树', '樹'], ['样', '樣'], ['栾', '欒'], ['桊', '棬'], ['桠', '椏'], ['桡', '橈'], ['桢', '楨'], ['档', '檔'], ['桤', '榿'], ['桥', '橋'], ['桦', '樺'], ['桧', '檜'], ['桨', '槳'], ['桩', '樁'], ['梦', '夢'], ['梼', '檮'], ['梾', '棶'], ['检', '檢'], ['棂', '欞'], ['椁', '槨'], ['椟', '櫝'], ['椠', '槧'], ['椭', '橢'], ['楼', '樓'], ['楽', '樂'], ['榄', '欖'], ['榇', '櫬'], ['榈', '櫚'], ['榉', '櫸'], ['榘', '矩'], ['槚', '檟'], ['槛', '檻'], ['槟', '檳'], ['槠', '櫧'], ['横', '橫'], ['樯', '檣'], ['樱', '櫻'], ['橥', '櫫'], ['橱', '櫥'], ['橹', '櫓'], ['橼', '櫞'], ['檪', '櫟'], ['檫', '察'], ['欢', '歡'], ['欤', '歟'], ['欧', '歐'], ['歳', '歲'], ['歴', '曆'], ['歺', '歲'], ['歼', '殲'], ['殁', '歿'], ['殇', '殤'], ['残', '殘'], ['殒', '殞'], ['殓', '殮'], ['殚', '殫'], ['殡', '殯'], ['殱', '殲'], ['殴', '毆'], ['毁', '毀'], ['毂', '轂'], ['毕', '畢'], ['毙', '斃'], ['毡', '氈'], ['毵', '毿'], ['毶', '鞠'], ['気', '氣'], ['氢', '氫'], ['氩', '氬'], ['氲', '氳'], ['氵', '水'], ['氽', '汆'], ['汇', '匯'], ['汉', '漢'], ['污', '汙'], ['汤', '湯'], ['汹', '洶'], ['沟', '溝'], ['没', '沒'], ['沣', '灃'], ['沤', '漚'], ['沥', '瀝'], ['沦', '淪'], ['沧', '滄'], ['沨', '渢'], ['沩', '溈'], ['沪', '滬'], ['沵', '濔'], ['泞', '濘'], ['泪', '淚'], ['泶', '澩'], ['泷', '瀧'], ['泸', '瀘'], ['泺', '濼'], ['泻', '瀉'], ['泼', '潑'], ['泽', '澤'], ['泾', '涇'], ['洁', '潔'], ['浃', '浹'], ['浅', '淺'], ['浆', '漿'], ['浇', '澆'], ['浈', '湞'], ['浊', '濁'], ['测', '測'], ['浍', '澮'], ['济', '濟'], ['浏', '瀏'], ['浐', '滻'], ['浑', '渾'], ['浒', '滸'], ['浓', '濃'], ['浔', '潯'], ['浕', '濜'], ['浜', '濱'], ['涙', '淚'], ['涛', '濤'], ['涝', '澇'], ['涞', '淶'], ['涟', '漣'], ['涡', '渦'], ['涣', '渙'], ['涤', '滌'], ['润', '潤'], ['涧', '澗'], ['涨', '漲'], ['涩', '澀'], ['淀', '澱'], ['渊', '淵'], ['渌', '淥'], ['渍', '漬'], ['渎', '瀆'], ['渐', '漸'], ['渑', '澠'], ['渔', '漁'], ['渖', '瀋'], ['渗', '滲'], ['温', '溫'], ['湼', '涅'], ['湾', '灣'], ['湿', '濕'], ['溃', '潰'], ['溅', '濺'], ['溆', '漵'], ['溇', '漊'], ['滙', '匯'], ['滚', '滾'], ['滝', '瀧'], ['滞', '滯'], ['滟', '灩'], ['滠', '灄'], ['满', '滿'], ['滢', '瀅'], ['滤', '濾'], ['滥', '濫'], ['滦', '灤'], ['滨', '濱'], ['滩', '灘'], ['滪', '澦'], ['漑', '溉'], ['潆', '瀠'], ['潇', '瀟'], ['潋', '瀲'], ['潍', '濰'], ['潜', '潛'], ['潴', '瀦'], ['澜', '瀾'], ['濑', '瀨'], ['濒', '瀕'], ['灎', '灩'], ['灏', '灝'], ['灔', '灩'], ['灜', '瀛'], ['灧', '灩'], ['灬', '火'], ['灭', '滅'], ['灯', '燈'], ['灵', '靈'], ['灾', '災'], ['灿', '燦'], ['炀', '煬'], ['炉', '爐'], ['炖', '燉'], ['炜', '煒'], ['炝', '熗'], ['点', '點'], ['炼', '煉'], ['炽', '熾'], ['烁', '爍'], ['烂', '爛'], ['烃', '烴'], ['烛', '燭'], ['烟', '煙'], ['烦', '煩'], ['烧', '燒'], ['烨', '燁'], ['烩', '燴'], ['烫', '燙'], ['烬', '燼'], ['热', '熱'], ['焕', '煥'], ['焖', '燜'], ['焘', '燾'], ['煅', '煆'], ['煳', '糊'], ['煺', '退'], ['熘', '溜'], ['爱', '愛'], ['爲', '為'], ['爷', '爺'], ['牍', '牘'], ['牜', '牛'], ['牦', '犛'], ['牵', '牽'], ['牺', '犧'], ['犊', '犢'], ['犟', '強'], ['犭', '犬'], ['状', '狀'], ['犷', '獷'], ['犸', '馬'], ['犹', '猶'], ['狈', '狽'], ['狍', '包'], ['狝', '獮'], ['狞', '獰'], ['独', '獨'], ['狭', '狹'], ['狮', '獅'], ['狯', '獪'], ['狰', '猙'], ['狱', '獄'], ['狲', '猻'], ['猃', '獫'], ['猎', '獵'], ['猕', '獼'], ['猡', '玀'], ['猪', '豬'], ['猫', '貓'], ['猬', '蝟'], ['献', '獻'], ['獭', '獺'], ['玑', '璣'], ['玙', '璵'], ['玚', '瑒'], ['玛', '瑪'], ['玮', '瑋'], ['环', '環'], ['现', '現'], ['玱', '瑲'], ['玺', '璽'], ['珏', '玨'], ['珐', '琺'], ['珑', '瓏'], ['珰', '璫'], ['珱', '瓔'], ['珲', '琿'], ['琏', '璉'], ['琐', '瑣'], ['琼', '瓊'], ['瑶', '瑤'], ['瑷', '璦'], ['璎', '瓔'], ['瓒', '瓚'], ['瓯', '甌'], ['産', '產'], ['电', '電'], ['画', '畫'], ['畅', '暢'], ['畲', '畬'], ['畳', '疊'], ['畴', '疇'], ['畵', '畫'], ['疎', '疏'], ['疖', '癤'], ['疗', '療'], ['疟', '瘧'], ['疠', '癘'], ['疡', '瘍'], ['疬', '癆'], ['疮', '瘡'], ['疯', '瘋'], ['疴', '痾'], ['痈', '癰'], ['痉', '痙'], ['痖', '啞'], ['痨', '癆'], ['痩', '瘦'], ['痪', '瘓'], ['痫', '癇'], ['痬', '瘍'], ['瘅', '癉'], ['瘆', '疹'], ['瘗', '瘞'], ['瘘', '瘺'], ['瘪', '癟'], ['瘫', '癱'], ['瘾', '癮'], ['瘿', '癭'], ['癀', '廣'], ['癍', '斑'], ['癎', '癇'], ['癞', '癩'], ['癣', '癬'], ['癫', '癲'], ['発', '發'], ['皑', '皚'], ['皱', '皺'], ['皲', '皸'], ['盏', '盞'], ['盐', '鹽'], ['监', '監'], ['盖', '蓋'], ['盗', '盜'], ['盘', '盤'], ['県', '縣'], ['眍', '區'], ['眞', '真'], ['眦', '眥'], ['眬', '矓'], ['着', '著'], ['睁', '睜'], ['睐', '睞'], ['睑', '瞼'], ['瞒', '瞞'], ['瞩', '矚'], ['矤', '病'], ['矫', '矯'], ['矶', '磯'], ['矾', '礬'], ['矿', '礦'], ['砀', '碭'], ['码', '碼'], ['砖', '磚'], ['砗', '硨'], ['砚', '硯'], ['砜', '風'], ['砺', '礪'], ['砻', '礱'], ['砾', '礫'], ['础', '礎'], ['硁', '硜'], ['硕', '碩'], ['硖', '硤'], ['硗', '磽'], ['硙', '磑'], ['硚', '礄'], ['硷', '鹼'], ['碍', '礙'], ['碛', '磧'], ['碜', '磣'], ['碱', '鹼'], ['碹', '宣'], ['磙', '袞'], ['礻', '示'], ['礼', '禮'], ['祎', '禕'], ['祢', '禰'], ['祯', '禎'], ['祷', '禱'], ['祸', '禍'], ['禀', '稟'], ['禄', '祿'], ['禅', '禪'], ['离', '離'], ['秃', '禿'], ['秆', '稈'], ['积', '積'], ['称', '稱'], ['秽', '穢'], ['秾', '穠'], ['税', '稅'], ['稣', '穌'], ['稳', '穩'], ['穑', '穡'], ['穷', '窮'], ['窃', '竊'], ['窍', '竅'], ['窑', '窯'], ['窜', '竄'], ['窝', '窩'], ['窥', '窺'], ['窦', '竇'], ['窭', '窶'], ['竖', '豎'], ['竜', '龍'], ['竞', '競'], ['笃', '篤'], ['笋', '筍'], ['笔', '筆'], ['笕', '筧'], ['笺', '箋'], ['笼', '籠'], ['笾', '籩'], ['筚', '篳'], ['筛', '篩'], ['筜', '簹'], ['筝', '箏'], ['筹', '籌'], ['签', '簽'], ['简', '簡'], ['箓', '籙'], ['箢', '宛'], ['箦', '簀'], ['箧', '篋'], ['箨', '籜'], ['箩', '籮'], ['箪', '簞'], ['箫', '簫'], ['篑', '簣'], ['篓', '簍'], ['篮', '籃'], ['篱', '籬'], ['簖', '籪'], ['籁', '籟'], ['籴', '糴'], ['类', '類'], ['籼', '秈'], ['粜', '糶'], ['粝', '糲'], ['粤', '粵'], ['粪', '糞'], ['粮', '糧'], ['糁', '糝'], ['糇', '餱'], ['糹', '糸'], ['紧', '緊'], ['絵', '繪'], ['絶', '絕'], ['絷', '縶'], ['綘', '健'], ['継', '繼'], ['続', '續'], ['緜', '綿'], ['縂', '總'], ['縄', '繩'], ['繋', '繫'], ['繍', '繡'], ['纟', '糸'], ['纠', '糾'], ['纡', '紆'], ['红', '紅'], ['纣', '紂'], ['纤', '纖'], ['纥', '紇'], ['约', '約'], ['级', '級'], ['纨', '紈'], ['纩', '纊'], ['纪', '紀'], ['纫', '紉'], ['纬', '緯'], ['纭', '紜'], ['纮', '紘'], ['纯', '純'], ['纰', '紕'], ['纱', '紗'], ['纲', '綱'], ['纳', '納'], ['纴', '紝'], ['纵', '縱'], ['纶', '綸'], ['纷', '紛'], ['纸', '紙'], ['纹', '紋'], ['纺', '紡'], ['纻', '紵'], ['纼', '紖'], ['纽', '紐'], ['纾', '紓'], ['线', '線'], ['绀', '紺'], ['绁', '紲'], ['绂', '紱'], ['练', '練'], ['组', '組'], ['绅', '紳'], ['细', '細'], ['织', '織'], ['终', '終'], ['绉', '縐'], ['绊', '絆'], ['绋', '紼'], ['绌', '絀'], ['绍', '紹'], ['绎', '繹'], ['经', '經'], ['绐', '紿'], ['绑', '綁'], ['绒', '絨'], ['结', '結'], ['绔', '褲'], ['绕', '繞'], ['绖', '絰'], ['绗', '絎'], ['绘', '繪'], ['给', '給'], ['绚', '絢'], ['绛', '絳'], ['络', '絡'], ['绝', '絕'], ['绞', '絞'], ['统', '統'], ['绠', '綆'], ['绡', '綃'], ['绢', '絹'], ['绣', '繡'], ['绤', '綌'], ['绥', '綏'], ['绦', '絛'], ['继', '繼'], ['绨', '綈'], ['绩', '績'], ['绪', '緒'], ['绫', '綾'], ['续', '續'], ['绮', '綺'], ['绯', '緋'], ['绰', '綽'], ['绱', '鞜'], ['绲', '緄'], ['绳', '繩'], ['维', '維'], ['绵', '綿'], ['绶', '綬'], ['绷', '繃'], ['绸', '綢'], ['绹', '綯'], ['绺', '綹'], ['绻', '綣'], ['综', '綜'], ['绽', '綻'], ['绾', '綰'], ['绿', '綠'], ['缀', '綴'], ['缁', '緇'], ['缂', '緙'], ['缃', '緗'], ['缄', '緘'], ['缅', '緬'], ['缆', '纜'], ['缇', '緹'], ['缈', '緲'], ['缉', '緝'], ['缊', '縕'], ['缋', '繢'], ['缌', '緦'], ['缍', '綞'], ['缎', '緞'], ['缏', '緶'], ['缐', '線'], ['缑', '緱'], ['缒', '縋'], ['缓', '緩'], ['缔', '締'], ['缕', '縷'], ['编', '編'], ['缗', '緡'], ['缘', '緣'], ['缙', '縉'], ['缚', '縛'], ['缛', '縟'], ['缜', '縝'], ['缝', '縫'], ['缞', '縗'], ['缟', '縞'], ['缠', '纏'], ['缡', '縭'], ['缢', '縊'], ['缣', '縑'], ['缤', '繽'], ['缥', '縹'], ['缦', '縵'], ['缧', '縲'], ['缨', '纓'], ['缩', '縮'], ['缪', '繆'], ['缫', '繅'], ['缬', '纈'], ['缭', '繚'], ['缮', '繕'], ['缯', '繒'], ['缰', '韁'], ['缱', '繾'], ['缲', '繰'], ['缳', '繯'], ['缴', '繳'], ['缵', '纘'], ['罂', '罌'], ['罗', '羅'], ['罚', '罰'], ['罢', '罷'], ['罴', '羆'], ['羁', '羈'], ['羗', '羌'], ['羟', '羥'], ['羡', '羨'], ['羣', '群'], ['羮', '羹'], ['翘', '翹'], ['翙', '翽'], ['翚', '翬'], ['耢', '勞'], ['耥', '尚'], ['耧', '耬'], ['耸', '聳'], ['耻', '恥'], ['聂', '聶'], ['聋', '聾'], ['职', '職'], ['聍', '聹'], ['联', '聯'], ['聩', '聵'], ['聪', '聰'], ['肀', '聿'], ['肃', '肅'], ['肠', '腸'], ['肤', '膚'], ['肷', '欠'], ['肾', '腎'], ['肿', '腫'], ['胀', '脹'], ['胁', '脅'], ['胆', '膽'], ['胧', '朧'], ['胨', '東'], ['胪', '臚'], ['胫', '脛'], ['胶', '膠'], ['脉', '脈'], ['脍', '膾'], ['脏', '髒'], ['脐', '臍'], ['脑', '腦'], ['脓', '膿'], ['脔', '臠'], ['脚', '腳'], ['脱', '脫'], ['脲', '反'], ['脶', '腡'], ['脸', '臉'], ['腭', '齶'], ['腻', '膩'], ['腽', '膃'], ['腾', '騰'], ['膑', '臏'], ['臓', '摹'], ['臜', '臢'], ['舆', '輿'], ['舣', '艤'], ['舰', '艦'], ['舱', '艙'], ['舻', '艫'], ['艰', '艱'], ['艹', '艸'], ['艺', '藝'], ['节', '節'], ['芈', '羋'], ['芗', '薌'], ['芜', '蕪'], ['芦', '蘆'], ['苁', '蓯'], ['苇', '葦'], ['苋', '莧'], ['苌', '萇'], ['苍', '蒼'], ['苎', '苧'], ['苏', '蘇'], ['苘', '萵'], ['茎', '莖'], ['茏', '蘢'], ['茑', '蔦'], ['茔', '塋'], ['茕', '煢'], ['茧', '繭'], ['荆', '荊'], ['荚', '莢'], ['荛', '蕘'], ['荜', '蓽'], ['荞', '蕎'], ['荟', '薈'], ['荠', '薺'], ['荡', '蕩'], ['荣', '榮'], ['荤', '葷'], ['荥', '滎'], ['荦', '犖'], ['荧', '熒'], ['荨', '蕁'], ['荩', '藎'], ['荪', '蓀'], ['荫', '蔭'], ['荬', '賣'], ['荭', '葒'], ['荮', '紂'], ['药', '藥'], ['莅', '蒞'], ['莱', '萊'], ['莲', '蓮'], ['莳', '蒔'], ['莴', '萵'], ['获', '獲'], ['莸', '蕕'], ['莹', '瑩'], ['莺', '鶯'], ['莼', '蓴'], ['菭', '恰'], ['萚', '蘀'], ['萝', '蘿'], ['萤', '螢'], ['营', '營'], ['萦', '縈'], ['萧', '蕭'], ['萨', '薩'], ['葱', '蔥'], ['蒇', '蕆'], ['蒉', '蕢'], ['蒋', '蔣'], ['蒌', '蔞'], ['蓝', '藍'], ['蓟', '薊'], ['蓠', '蘺'], ['蓦', '驀'], ['蔷', '薔'], ['蔹', '蘞'], ['蔺', '藺'], ['蔼', '藹'], ['蕲', '蘄'], ['蕴', '蘊'], ['薮', '藪'], ['藁', '槁'], ['藓', '蘚'], ['蘖', '蘗'], ['虏', '虜'], ['虑', '慮'], ['虚', '虛'], ['虬', '虯'], ['虮', '蟣'], ['虽', '雖'], ['虾', '蝦'], ['虿', '蠆'], ['蚀', '蝕'], ['蚁', '蟻'], ['蚂', '螞'], ['蚕', '蠶'], ['蚬', '蜆'], ['蛊', '蠱'], ['蛎', '蠣'], ['蛏', '蟶'], ['蛮', '蠻'], ['蛰', '蟄'], ['蛱', '蛺'], ['蛲', '蟯'], ['蛳', '螄'], ['蛴', '蠐'], ['蜕', '蛻'], ['蜖', '汀'], ['蜗', '蝸'], ['蝇', '蠅'], ['蝈', '蟈'], ['蝉', '蟬'], ['蝼', '螻'], ['蝾', '蠑'], ['蝿', '蠅'], ['螀', '螿'], ['螨', '顢'], ['蟏', '蠨'], ['蟮', '蟺'], ['蠎', '蟒'], ['衅', '釁'], ['衔', '銜'], ['衤', '衣'], ['补', '補'], ['衬', '襯'], ['衮', '袞'], ['袄', '襖'], ['袅', '裊'], ['袆', '褘'], ['袭', '襲'], ['袯', '襏'], ['袴', '褲'], ['装', '裝'], ['裆', '襠'], ['裈', '褌'], ['裢', '褳'], ['裣', '襝'], ['裤', '褲'], ['裥', '襉'], ['褛', '褸'], ['褴', '襤'], ['襕', '襴'], ['覇', '霸'], ['覚', '覺'], ['覧', '覽'], ['覩', '睹'], ['见', '見'], ['观', '觀'], ['规', '規'], ['觅', '覓'], ['视', '視'], ['觇', '覘'], ['览', '覽'], ['觉', '覺'], ['觊', '覬'], ['觋', '覡'], ['觌', '覿'], ['觎', '覦'], ['觏', '覯'], ['觐', '覲'], ['觑', '覷'], ['觗', '觝'], ['觞', '觴'], ['触', '觸'], ['觯', '觶'], ['訡', '吟'], ['詟', '讋'], ['詤', '謊'], ['誀', '浴'], ['誉', '譽'], ['誊', '謄'], ['説', '說'], ['読', '讀'], ['讁', '謫'], ['讠', '言'], ['计', '計'], ['订', '訂'], ['讣', '訃'], ['认', '認'], ['讥', '譏'], ['讦', '訐'], ['讧', '訌'], ['讨', '討'], ['让', '讓'], ['讪', '訕'], ['讫', '訖'], ['训', '訓'], ['议', '議'], ['讯', '訊'], ['记', '記'], ['讱', '訒'], ['讲', '講'], ['讳', '諱'], ['讴', '謳'], ['讵', '詎'], ['讶', '訝'], ['讷', '訥'], ['许', '許'], ['讹', '訛'], ['论', '論'], ['讼', '訟'], ['讽', '諷'], ['设', '設'], ['访', '訪'], ['诀', '訣'], ['证', '證'], ['诂', '詁'], ['诃', '訶'], ['评', '評'], ['诅', '詛'], ['识', '識'], ['诇', '詗'], ['诈', '詐'], ['诉', '訴'], ['诊', '診'], ['诋', '詆'], ['诌', '謅'], ['词', '詞'], ['诎', '詘'], ['诏', '詔'], ['诐', '詖'], ['译', '譯'], ['诒', '詒'], ['诓', '誆'], ['诔', '誄'], ['试', '試'], ['诖', '詿'], ['诗', '詩'], ['诘', '詰'], ['诙', '詼'], ['诚', '誠'], ['诛', '誅'], ['诜', '詵'], ['话', '話'], ['诞', '誕'], ['诟', '詬'], ['诠', '詮'], ['诡', '詭'], ['询', '詢'], ['诣', '詣'], ['诤', '諍'], ['该', '該'], ['详', '詳'], ['诧', '詫'], ['诨', '諢'], ['诩', '詡'], ['诪', '譸'], ['诫', '誡'], ['诬', '誣'], ['语', '語'], ['诮', '誚'], ['误', '誤'], ['诰', '誥'], ['诱', '誘'], ['诲', '誨'], ['诳', '誑'], ['说', '說'], ['诵', '誦'], ['诶', '誒'], ['请', '請'], ['诸', '諸'], ['诹', '諏'], ['诺', '諾'], ['读', '讀'], ['诼', '諑'], ['诽', '誹'], ['课', '課'], ['诿', '諉'], ['谀', '諛'], ['谁', '誰'], ['谂', '諗'], ['调', '調'], ['谄', '諂'], ['谅', '諒'], ['谆', '諄'], ['谇', '誶'], ['谈', '談'], ['谊', '誼'], ['谋', '謀'], ['谌', '諶'], ['谍', '諜'], ['谎', '謊'], ['谏', '諫'], ['谐', '諧'], ['谑', '謔'], ['谒', '謁'], ['谓', '謂'], ['谔', '諤'], ['谕', '諭'], ['谖', '諼'], ['谗', '讒'], ['谘', '諮'], ['谙', '諳'], ['谚', '諺'], ['谛', '諦'], ['谜', '謎'], ['谝', '諞'], ['谞', '住'], ['谟', '謨'], ['谠', '讜'], ['谡', '謖'], ['谢', '謝'], ['谣', '謠'], ['谤', '謗'], ['谥', '謚'], ['谦', '謙'], ['谧', '謐'], ['谨', '謹'], ['谩', '謾'], ['谪', '謫'], ['谫', '譾'], ['谬', '謬'], ['谭', '譚'], ['谮', '譖'], ['谯', '譙'], ['谰', '讕'], ['谱', '譜'], ['谲', '譎'], ['谳', '讞'], ['谴', '譴'], ['谵', '譫'], ['谶', '讖'], ['豮', '豶'], ['貭', '亍'], ['貮', '貳'], ['賍', '贓'], ['賎', '賤'], ['賖', '賒'], ['賘', '髒'], ['贋', '贗'], ['贘', '償'], ['贝', '貝'], ['贞', '貞'], ['负', '負'], ['贡', '貢'], ['财', '財'], ['责', '責'], ['贤', '賢'], ['败', '敗'], ['账', '賬'], ['货', '貨'], ['质', '質'], ['贩', '販'], ['贪', '貪'], ['贫', '貧'], ['贬', '貶'], ['购', '購'], ['贮', '貯'], ['贯', '貫'], ['贰', '貳'], ['贱', '賤'], ['贲', '賁'], ['贳', '貰'], ['贴', '貼'], ['贵', '貴'], ['贶', '貺'], ['贷', '貸'], ['贸', '貿'], ['费', '費'], ['贺', '賀'], ['贻', '貽'], ['贼', '賊'], ['贽', '贄'], ['贾', '賈'], ['贿', '賄'], ['赀', '貲'], ['赁', '賃'], ['赂', '賂'], ['赃', '贓'], ['资', '資'], ['赅', '賅'], ['赆', '贐'], ['赇', '賕'], ['赈', '賑'], ['赉', '賚'], ['赊', '賒'], ['赋', '賦'], ['赌', '賭'], ['赍', '齎'], ['赎', '贖'], ['赏', '賞'], ['赐', '賜'], ['赑', '贔'], ['赒', '賙'], ['赓', '賡'], ['赔', '賠'], ['赖', '賴'], ['赗', '賵'], ['赘', '贅'], ['赙', '賻'], ['赚', '賺'], ['赛', '賽'], ['赜', '賾'], ['赝', '贗'], ['赞', '贊'], ['赟', '贇'], ['赠', '贈'], ['赡', '贍'], ['赢', '贏'], ['赣', '贛'], ['赪', '赬'], ['赵', '趙'], ['趋', '趨'], ['趱', '趲'], ['趸', '躉'], ['跃', '躍'], ['跄', '蹌'], ['跞', '躒'], ['践', '踐'], ['跶', '躂'], ['跷', '蹺'], ['跸', '蹕'], ['跹', '躚'], ['跻', '躋'], ['踌', '躊'], ['踪', '蹤'], ['踬', '躓'], ['踯', '躑'], ['蹑', '躡'], ['蹒', '蹣'], ['蹰', '躕'], ['蹿', '躥'], ['躏', '躪'], ['躜', '躦'], ['躯', '軀'], ['车', '車'], ['轧', '軋'], ['轨', '軌'], ['轩', '軒'], ['轪', '軑'], ['轫', '軔'], ['转', '轉'], ['轭', '軛'], ['轮', '輪'], ['软', '軟'], ['轰', '轟'], ['轱', '古'], ['轲', '軻'], ['轳', '轤'], ['轴', '軸'], ['轵', '軹'], ['轶', '軼'], ['轷', '乎'], ['轸', '軫'], ['轹', '轢'], ['轺', '軺'], ['轻', '輕'], ['轼', '軾'], ['载', '載'], ['轾', '輊'], ['轿', '轎'], ['辀', '輈'], ['辁', '輇'], ['辂', '輅'], ['较', '較'], ['辄', '輒'], ['辅', '輔'], ['辆', '輛'], ['辇', '輦'], ['辈', '輩'], ['辉', '輝'], ['辊', '輥'], ['辋', '輞'], ['辌', '輬'], ['辍', '輟'], ['辎', '輜'], ['辏', '輳'], ['辐', '輻'], ['辑', '輯'], ['辒', '轀'], ['输', '輸'], ['辔', '轡'], ['辕', '轅'], ['辖', '轄'], ['辗', '輾'], ['辘', '轆'], ['辙', '轍'], ['辚', '轔'], ['辞', '辭'], ['辩', '辯'], ['辫', '辮'], ['辬', '辨'], ['边', '邊'], ['辽', '遼'], ['达', '達'], ['迁', '遷'], ['过', '過'], ['迈', '邁'], ['运', '運'], ['还', '還'], ['这', '這'], ['进', '進'], ['远', '遠'], ['违', '違'], ['连', '連'], ['迟', '遲'], ['迩', '邇'], ['迳', '逕'], ['迹', '跡'], ['选', '選'], ['逊', '遜'], ['递', '遞'], ['逦', '邐'], ['逻', '邏'], ['遗', '遺'], ['遥', '遙'], ['邓', '鄧'], ['邝', '鄺'], ['邬', '鄔'], ['邮', '郵'], ['邹', '鄒'], ['邺', '鄴'], ['邻', '鄰'], ['郄', '卻'], ['郏', '郟'], ['郐', '鄶'], ['郑', '鄭'], ['郓', '鄆'], ['郦', '酈'], ['郧', '鄖'], ['郷', '鄉'], ['郸', '鄲'], ['鄊', '鄉'], ['鄕', '鄉'], ['鄷', '酆'], ['酝', '醞'], ['酦', '醱'], ['酱', '醬'], ['酽', '釅'], ['酾', '釃'], ['酿', '釀'], ['释', '釋'], ['釡', '斧'], ['鉴', '鑒'], ['銮', '鑾'], ['錾', '鏨'], ['鎻', '鎖'], ['钅', '金'], ['钆', '釓'], ['钇', '釔'], ['针', '針'], ['钉', '釘'], ['钊', '釗'], ['钋', '釙'], ['钌', '釕'], ['钍', '釷'], ['钏', '釧'], ['钐', '釤'], ['钑', '鈒'], ['钒', '釩'], ['钓', '釣'], ['钔', '鍆'], ['钕', '釹'], ['钖', '鍚'], ['钗', '釵'], ['钘', '鈃'], ['钙', '鈣'], ['钚', '鈽'], ['钛', '鈦'], ['钜', '鉅'], ['钝', '鈍'], ['钞', '鈔'], ['钟', '鐘'], ['钠', '鈉'], ['钡', '鋇'], ['钢', '鋼'], ['钣', '鈑'], ['钤', '鈐'], ['钥', '鑰'], ['钦', '欽'], ['钧', '鈞'], ['钨', '鎢'], ['钩', '鉤'], ['钪', '鈧'], ['钫', '鈁'], ['钬', '鈥'], ['钮', '鈕'], ['钯', '鈀'], ['钰', '鈺'], ['钱', '錢'], ['钲', '鉦'], ['钳', '鉗'], ['钴', '鈷'], ['钵', '缽'], ['钶', '鈳'], ['钸', '鈽'], ['钹', '鈸'], ['钺', '鉞'], ['钻', '鑽'], ['钼', '鉬'], ['钽', '鉭'], ['钾', '鉀'], ['钿', '鈿'], ['铀', '鈾'], ['铁', '鐵'], ['铂', '鉑'], ['铃', '鈴'], ['铄', '鑠'], ['铅', '鉛'], ['铆', '鉚'], ['铈', '鈰'], ['铉', '鉉'], ['铊', '鉈'], ['铋', '鉍'], ['铌', '鈮'], ['铍', '鈹'], ['铎', '鐸'], ['铏', '鉶'], ['铐', '銬'], ['铑', '銠'], ['铒', '鉺'], ['铓', '鋩'], ['铔', '錏'], ['铕', '銪'], ['铖', '鋮'], ['铗', '鋏'], ['铘', '邪'], ['铙', '鐃'], ['铚', '銍'], ['铛', '鐺'], ['铜', '銅'], ['铝', '鋁'], ['铞', '吊'], ['铟', '銦'], ['铠', '鎧'], ['铡', '鍘'], ['铢', '銖'], ['铣', '銑'], ['铤', '鋌'], ['铥', '銩'], ['铦', '銛'], ['铧', '鏵'], ['铨', '銓'], ['铩', '鎩'], ['铪', '鉿'], ['铫', '銚'], ['铬', '鉻'], ['铭', '銘'], ['铮', '錚'], ['铯', '銫'], ['铰', '鉸'], ['铱', '銥'], ['铲', '鏟'], ['铳', '銃'], ['铴', '鐋'], ['铵', '銨'], ['银', '銀'], ['铷', '銣'], ['铸', '鑄'], ['铹', '鐒'], ['铺', '鋪'], ['铻', '鋙'], ['铼', '錸'], ['铽', '鋱'], ['链', '鏈'], ['铿', '鏗'], ['销', '銷'], ['锁', '鎖'], ['锂', '鋰'], ['锃', '呈'], ['锄', '鋤'], ['锅', '鍋'], ['锆', '鋯'], ['锇', '鋨'], ['锈', '鏽'], ['锉', '銼'], ['锊', '鋝'], ['锋', '鋒'], ['锌', '鋅'], ['锍', '琉'], ['锎', '鉲'], ['锏', '閒'], ['锐', '銳'], ['锑', '銻'], ['锒', '鋃'], ['锓', '鋟'], ['锔', '鋦'], ['锕', '錒'], ['锖', '錆'], ['锗', '鍺'], ['锘', '若'], ['错', '錯'], ['锚', '錨'], ['锛', '錛'], ['锜', '錡'], ['锝', '鎝'], ['锞', '錁'], ['锟', '錕'], ['锠', '琛'], ['锡', '錫'], ['锢', '錮'], ['锣', '鑼'], ['锤', '錘'], ['锥', '錐'], ['锦', '錦'], ['锧', '鑕'], ['锨', '杴'], ['锪', '忽'], ['锫', '培'], ['锬', '錟'], ['锭', '錠'], ['键', '鍵'], ['锯', '鋸'], ['锰', '錳'], ['锱', '錙'], ['锲', '鍥'], ['锴', '鍇'], ['锵', '鏘'], ['锶', '鍶'], ['锷', '鍔'], ['锸', '鍤'], ['锹', '鍬'], ['锺', '鍾'], ['锻', '鍛'], ['锼', '鎪'], ['锽', '鍠'], ['锾', '鍰'], ['锿', '鑀'], ['镀', '鍍'], ['镁', '鎂'], ['镂', '鏤'], ['镃', '鎡'], ['镄', '鐨'], ['镅', '鋂'], ['镆', '鏌'], ['镇', '鎮'], ['镈', '鎛'], ['镉', '鎘'], ['镊', '鑷'], ['镋', '钂'], ['镌', '鐫'], ['镍', '鎳'], ['镎', '拿'], ['镏', '鎦'], ['镐', '鎬'], ['镑', '鎊'], ['镒', '鎰'], ['镓', '鎵'], ['镔', '鑌'], ['镕', '鎔'], ['镖', '鏢'], ['镗', '鏜'], ['镘', '鏝'], ['镙', '鏍'], ['镛', '鏞'], ['镜', '鏡'], ['镝', '鏑'], ['镞', '鏃'], ['镟', '鏇'], ['镠', '鏐'], ['镡', '鐔'], ['镢', '钁'], ['镣', '鐐'], ['镤', '鏷'], ['镥', '魯'], ['镧', '鑭'], ['镨', '鐠'], ['镩', '串'], ['镪', '鏹'], ['镫', '鐙'], ['镬', '鑊'], ['镭', '鐳'], ['镮', '鐶'], ['镯', '鐲'], ['镰', '鐮'], ['镱', '鐿'], ['镲', '察'], ['镳', '鑣'], ['镴', '鑞'], ['镵', '鑱'], ['镶', '鑲'], ['长', '長'], ['閲', '閱'], ['门', '門'], ['闩', '閂'], ['闪', '閃'], ['闫', '閆'], ['闬', '閈'], ['闭', '閉'], ['问', '問'], ['闯', '闖'], ['闰', '閏'], ['闱', '闈'], ['闲', '閒'], ['闳', '閎'], ['间', '間'], ['闵', '閔'], ['闶', '閌'], ['闷', '悶'], ['闸', '閘'], ['闹', '鬧'], ['闺', '閨'], ['闻', '聞'], ['闼', '闥'], ['闽', '閩'], ['闾', '閭'], ['闿', '闓'], ['阀', '閥'], ['阁', '閣'], ['阂', '閡'], ['阃', '閫'], ['阄', '鬮'], ['阅', '閱'], ['阆', '閬'], ['阇', '闍'], ['阈', '閾'], ['阉', '閹'], ['阊', '閶'], ['阋', '鬩'], ['阌', '閿'], ['阍', '閽'], ['阎', '閻'], ['阏', '閼'], ['阐', '闡'], ['阑', '闌'], ['阒', '闃'], ['阓', '闠'], ['阔', '闊'], ['阕', '闋'], ['阖', '闔'], ['阗', '闐'], ['阘', '闒'], ['阙', '闕'], ['阚', '闞'], ['阛', '闤'], ['阝', '阜'], ['队', '隊'], ['阳', '陽'], ['阴', '陰'], ['阵', '陣'], ['阶', '階'], ['际', '際'], ['陆', '陸'], ['陇', '隴'], ['陈', '陳'], ['陉', '陘'], ['陕', '陝'], ['陧', '隉'], ['陨', '隕'], ['险', '險'], ['隂', '陰'], ['隌', '暗'], ['随', '隨'], ['隐', '隱'], ['隠', '隱'], ['隷', '隸'], ['隽', '雋'], ['难', '難'], ['雏', '雛'], ['雠', '讎'], ['雳', '靂'], ['雾', '霧'], ['霁', '霽'], ['霊', '靈'], ['霭', '靄'], ['靓', '靚'], ['静', '靜'], ['靥', '靨'], ['鞑', '韃'], ['鞒', '轎'], ['鞯', '韉'], ['鞲', '韝'], ['鞽', '轎'], ['韦', '韋'], ['韧', '韌'], ['韨', '韍'], ['韩', '韓'], ['韪', '韙'], ['韫', '韞'], ['韬', '韜'], ['韯', '籤'], ['韲', '齋'], ['韵', '韻'], ['顋', '腮'], ['顔', '顏'], ['顕', '顯'], ['页', '頁'], ['顶', '頂'], ['顷', '頃'], ['顸', '頇'], ['项', '項'], ['顺', '順'], ['须', '須'], ['顼', '頊'], ['顽', '頑'], ['顾', '顧'], ['顿', '頓'], ['颀', '頎'], ['颁', '頒'], ['颂', '頌'], ['颃', '頏'], ['预', '預'], ['颅', '顱'], ['领', '領'], ['颇', '頗'], ['颈', '頸'], ['颉', '頡'], ['颊', '頰'], ['颋', '頲'], ['颌', '頜'], ['颍', '潁'], ['颎', '熲'], ['颏', '頦'], ['颐', '頤'], ['频', '頻'], ['颓', '頹'], ['颔', '頷'], ['颕', '穎'], ['颖', '穎'], ['颗', '顆'], ['题', '題'], ['颙', '顒'], ['颚', '顎'], ['颛', '顓'], ['颜', '顏'], ['额', '額'], ['颞', '顳'], ['颟', '顢'], ['颠', '顛'], ['颡', '顙'], ['颢', '顥'], ['颣', '纇'], ['颤', '顫'], ['颥', '須'], ['颦', '顰'], ['颧', '顴'], ['颷', '飆'], ['风', '風'], ['飏', '颺'], ['飐', '颭'], ['飑', '颮'], ['飒', '颯'], ['飓', '颶'], ['飔', '颸'], ['飕', '颼'], ['飖', '颻'], ['飗', '飀'], ['飘', '飄'], ['飙', '飆'], ['飚', '飆'], ['飞', '飛'], ['飨', '饗'], ['飬', '養'], ['飮', '飲'], ['飱', '餐'], ['餍', '饜'], ['饣', '食'], ['饤', '飣'], ['饥', '飢'], ['饦', '飥'], ['饧', '餳'], ['饨', '飩'], ['饩', '餼'], ['饪', '飪'], ['饫', '飫'], ['饬', '飭'], ['饭', '飯'], ['饮', '飲'], ['饯', '餞'], ['饰', '飾'], ['饱', '飽'], ['饲', '飼'], ['饴', '飴'], ['饵', '餌'], ['饶', '饒'], ['饷', '餉'], ['饺', '餃'], ['饼', '餅'], ['饽', '餑'], ['饾', '餖'], ['饿', '餓'], ['馀', '餘'], ['馁', '餒'], ['馂', '餕'], ['馄', '餛'], ['馅', '餡'], ['馆', '館'], ['馇', '查'], ['馈', '饋'], ['馉', '稹'], ['馊', '餿'], ['馋', '饞'], ['馌', '饁'], ['馍', '饃'], ['馎', '餺'], ['馏', '餾'], ['馐', '饈'], ['馑', '饉'], ['馒', '饅'], ['馓', '饊'], ['馔', '饌'], ['馕', '囊'], ['马', '馬'], ['驭', '馭'], ['驮', '馱'], ['驯', '馴'], ['驰', '馳'], ['驱', '驅'], ['驲', '馹'], ['驳', '駁'], ['驴', '驢'], ['驵', '駔'], ['驶', '駛'], ['驷', '駟'], ['驸', '駙'], ['驹', '駒'], ['驺', '騶'], ['驻', '駐'], ['驼', '駝'], ['驽', '駑'], ['驾', '駕'], ['驿', '驛'], ['骀', '駘'], ['骁', '驍'], ['骂', '罵'], ['骃', '駰'], ['骄', '驕'], ['骅', '驊'], ['骆', '駱'], ['骇', '駭'], ['骈', '駢'], ['骊', '驪'], ['骋', '騁'], ['验', '驗'], ['骍', '騂'], ['骎', '駸'], ['骏', '駿'], ['骐', '騏'], ['骑', '騎'], ['骒', '騍'], ['骓', '騅'], ['骕', '驌'], ['骖', '驂'], ['骗', '騙'], ['骘', '騭'], ['骙', '騤'], ['骚', '騷'], ['骛', '騖'], ['骜', '驁'], ['骝', '騮'], ['骞', '騫'], ['骟', '騸'], ['骠', '驃'], ['骡', '騾'], ['骢', '驄'], ['骣', '驏'], ['骤', '驟'], ['骥', '驥'], ['骦', '驦'], ['骧', '驤'], ['髅', '髏'], ['髋', '髖'], ['髌', '髕'], ['鬓', '鬢'], ['魇', '魘'], ['魉', '魎'], ['鱼', '魚'], ['鱿', '魷'], ['鲀', '魨'], ['鲁', '魯'], ['鲂', '魴'], ['鲅', '鱍'], ['鲆', '平'], ['鲇', '占'], ['鲈', '鱸'], ['鲊', '鮓'], ['鲋', '鮒'], ['鲍', '鮑'], ['鲎', '鱟'], ['鲐', '鮐'], ['鲑', '鮭'], ['鲒', '鮚'], ['鲔', '鮪'], ['鲕', '鮞'], ['鲖', '鮦'], ['鲙', '鱠'], ['鲚', '鱭'], ['鲛', '鮫'], ['鲜', '鮮'], ['鲞', '鯗'], ['鲟', '鱘'], ['鲠', '鯁'], ['鲡', '鱺'], ['鲢', '鰱'], ['鲣', '鰹'], ['鲤', '鯉'], ['鲥', '鰣'], ['鲦', '鰷'], ['鲧', '鯀'], ['鲨', '鯊'], ['鲩', '鯇'], ['鲫', '鯽'], ['鲭', '鯖'], ['鲮', '鯪'], ['鲰', '鯫'], ['鲱', '鯡'], ['鲲', '鯤'], ['鲳', '鯧'], ['鲴', '固'], ['鲵', '鯢'], ['鲶', '鯰'], ['鲷', '鯛'], ['鲸', '鯨'], ['鲺', '虱'], ['鲻', '鯔'], ['鲼', '賁'], ['鲽', '鰈'], ['鲿', '鱨'], ['鳀', '鯷'], ['鳃', '鰓'], ['鳄', '鱷'], ['鳅', '鰍'], ['鳆', '鰒'], ['鳇', '鰉'], ['鳊', '扁'], ['鳋', '蚤'], ['鳌', '鰲'], ['鳍', '鰭'], ['鳏', '鰥'], ['鳐', '鰩'], ['鳒', '鰜'], ['鳔', '鰾'], ['鳕', '鱈'], ['鳖', '鱉'], ['鳗', '鰻'], ['鳘', '鱉'], ['鳙', '庸'], ['鳛', '鰼'], ['鳜', '鱖'], ['鳝', '鱔'], ['鳞', '鱗'], ['鳟', '鱒'], ['鳡', '鰲'], ['鳢', '鱧'], ['鳣', '鱣'], ['鸟', '鳥'], ['鸠', '鳩'], ['鸡', '雞'], ['鸢', '鳶'], ['鸣', '鳴'], ['鸤', '鳲'], ['鸥', '鷗'], ['鸦', '鴉'], ['鸧', '鶬'], ['鸨', '鴇'], ['鸩', '鴆'], ['鸪', '鴣'], ['鸬', '鸕'], ['鸭', '鴨'], ['鸮', '鴞'], ['鸯', '鴦'], ['鸰', '鴒'], ['鸱', '鴟'], ['鸲', '鴝'], ['鸳', '鴛'], ['鸵', '鴕'], ['鸶', '鷥'], ['鸷', '鷙'], ['鸹', '鴰'], ['鸺', '鵂'], ['鸼', '鵃'], ['鸽', '鴿'], ['鸾', '鸞'], ['鸿', '鴻'], ['鹁', '鵓'], ['鹂', '鸝'], ['鹃', '鵑'], ['鹄', '鵠'], ['鹅', '鵝'], ['鹆', '鵒'], ['鹇', '鷳'], ['鹈', '鵜'], ['鹉', '鵡'], ['鹊', '鵲'], ['鹋', '苗'], ['鹌', '鵪'], ['鹎', '鵯'], ['鹏', '鵬'], ['鹑', '鶉'], ['鹒', '鶊'], ['鹓', '鵷'], ['鹔', '鷫'], ['鹕', '鶘'], ['鹖', '鶡'], ['鹗', '鶚'], ['鹘', '鶻'], ['鹙', '鶖'], ['鹚', '鶿'], ['鹛', '眉'], ['鹜', '鶩'], ['鹝', '鷊'], ['鹞', '鷂'], ['鹠', '鶹'], ['鹡', '鶺'], ['鹢', '鷁'], ['鹣', '鶼'], ['鹤', '鶴'], ['鹥', '鷖'], ['鹦', '鸚'], ['鹧', '鷓'], ['鹨', '鷚'], ['鹩', '鷯'], ['鹪', '鷦'], ['鹫', '鷲'], ['鹬', '鷸'], ['鹭', '鷺'], ['鹯', '鸇'], ['鹰', '鷹'], ['鹱', '獲'], ['鹲', '鸏'], ['鹳', '鸛'], ['鹾', '鹺'], ['麦', '麥'], ['麸', '麩'], ['麹', '麴'], ['麺', '麵'], ['麽', '麼'], ['黄', '黃'], ['黉', '黌'], ['黒', '黑'], ['黙', '默'], ['黡', '黶'], ['黩', '黷'], ['黪', '黲'], ['黾', '黽'], ['鼋', '黿'], ['鼍', '鼉'], ['鼗', '鞀'], ['鼹', '鼴'], ['齄', '皻'], ['齐', '齊'], ['齑', '齏'], ['齿', '齒'], ['龀', '齔'], ['龁', '齕'], ['龂', '齗'], ['龃', '齟'], ['龄', '齡'], ['龅', '齙'], ['龆', '齠'], ['龇', '齜'], ['龈', '齦'], ['龉', '齬'], ['龊', '齪'], ['龋', '齲'], ['龌', '齷'], ['龙', '龍'], ['龚', '龔'], ['龛', '龕'], ['龟', '龜'] ]) export function chsToChz(text: string): string { if (!text) return '' let result = '' for (let i = 0; i < text.length; i++) { result += charMap.get(text[i]) || text[i] } return result } export default chsToChz ================================================ FILE: src/_helpers/config-manager.ts ================================================ import pako from 'pako' import { getDefaultConfig, AppConfig } from '@/app-config' import { mergeConfig } from '@/app-config/merge-config' import { storage } from './browser-api' import { Observable, from, concat, fromEventPattern } from 'rxjs' import { map } from 'rxjs/operators' export interface StorageChanged { newValue?: T oldValue?: T } export interface AppConfigChanged { newConfig: AppConfig oldConfig?: AppConfig } /** Compressed config data */ interface AppConfigCompressed { /** version */ v: 1 /** data */ d: string } function deflate(config: AppConfig): AppConfigCompressed { return { v: 1, d: pako.deflate(JSON.stringify(config), { to: 'string' }) } } function inflate(config: AppConfig | AppConfigCompressed): AppConfig function inflate(config: undefined): undefined function inflate( config?: AppConfig | AppConfigCompressed ): AppConfig | undefined function inflate( config?: AppConfig | AppConfigCompressed ): AppConfig | undefined { if (config && config['v'] === 1) { return JSON.parse( pako.inflate((config as AppConfigCompressed).d, { to: 'string' }) ) } return config as AppConfig } export async function initConfig(): Promise { let baseconfig = await getConfig() baseconfig = baseconfig && baseconfig.version ? mergeConfig(baseconfig) : getDefaultConfig() await updateConfig(baseconfig) return baseconfig } export async function resetConfig() { const baseconfig = getDefaultConfig() await updateConfig(baseconfig) return baseconfig } export async function getConfig(): Promise { const { baseconfig } = await storage.sync.get<{ baseconfig: AppConfig }>('baseconfig') return inflate(baseconfig || getDefaultConfig()) } export function updateConfig(baseconfig: AppConfig): Promise { if (process.env.DEBUG) { console.log(`Saved config`, baseconfig) } return storage.sync.set({ baseconfig: deflate(baseconfig) }) } /** * Listen to config changes */ export async function addConfigListener( cb: (changes: AppConfigChanged) => any ) { storage.sync.addListener(changes => { if (changes.baseconfig) { const { newValue, oldValue } = changes.baseconfig as StorageChanged< AppConfigCompressed > if (newValue) { cb({ newConfig: inflate(newValue), oldConfig: inflate(oldValue) }) } } }) } /** * Get config and create a stream listening to config change */ export function createConfigStream(): Observable { return concat( from(getConfig()), fromEventPattern<[AppConfigChanged] | AppConfigChanged>( addConfigListener ).pipe(map(args => (Array.isArray(args) ? args[0] : args).newConfig)) ) } ================================================ FILE: src/_helpers/dom.ts ================================================ /** * xhtml returns small case */ export function isTagName(node: Node, tagName: string): boolean { return ( ((node as HTMLElement).tagName || '').toLowerCase() === tagName.toLowerCase() ) } ================================================ FILE: src/_helpers/fetch-dom.ts ================================================ import DOMPurify from 'dompurify' import axios, { AxiosRequestConfig } from 'axios' export function fetchDOM( url: string, config: AxiosRequestConfig = {} ): Promise { return axios(url, { ...config, transformResponse: [data => data], responseType: 'text' }).then(({ data }) => DOMPurify.sanitize(data, { RETURN_DOM_FRAGMENT: true })) } /** about 6 time faster as it typically takes less than 5ms to parse a DOM */ export function fetchDirtyDOM( url: string, config: AxiosRequestConfig = {} ): Promise { return axios(url, { withCredentials: false, ...config, transformResponse: [data => data], responseType: 'document' }).then(({ data }) => process.env.NODE_ENV !== 'production' ? new DOMParser().parseFromString(data, 'text/html') : data ) } export function fetchPlainText( url: string, config: AxiosRequestConfig = {} ): Promise { return axios(url, { withCredentials: false, ...config, // axios bug https://github.com/axios/axios/issues/907 transformResponse: [data => data], responseType: 'text' }).then(({ data }) => data) } ================================================ FILE: src/_helpers/getSuggests.ts ================================================ import { first } from './promise-more' interface Suggest { entry: string explain: string } export function getSuggests(text: string): Promise { return first([getCiba(text), getYoudao(text)]).catch(() => []) } function getCiba(text: string): Promise { return fetch( 'http://dict-mobile.iciba.com/interface/index.php?c=word&m=getsuggest&nums=10&client=6&uid=0&is_need_mean=1&word=' + encodeURIComponent(text) ) .then(r => r.json()) .then(json => { if (json && Array.isArray(json.message)) { return json.message .filter(x => x && x.key) .map(x => ({ entry: x.key, explain: Array.isArray(x.means) && x.means.length > 0 ? x.means[0].part + ' ' + x.means[0].means.join(' ') : '' })) } if (process.env.DEBUG) { console.warn('fetch suggests failed', text, json) } throw new Error() }) } function getYoudao(text: string): Promise { return fetch( 'https://dict.youdao.com/suggest?doctype=json&le=en&ver=2.0&q=' + encodeURIComponent(text) ) .then(r => r.json()) .then(json => { if (json && json.data && Array.isArray(json.data.entries)) { return json.data.entries.filter(x => x && x.explain && x.entry) } if (process.env.DEBUG) { console.warn('fetch suggests failed', text, json) } throw new Error() }) } ================================================ FILE: src/_helpers/hooks.ts ================================================ import { useRef } from 'react' /** * Equivalent to useCallback(fn, []) */ export function useFixedCallback(fn: T): T { return useRef(fn).current } /** * Equivalent to useMemo(() => value, []) */ export function useFixedMemo(fn: () => T): T { const initedRef = useRef(false) const valueRef = useRef() if (!initedRef.current) { initedRef.current = true valueRef.current = fn() } return valueRef.current! } ================================================ FILE: src/_helpers/i18n.ts ================================================ import React, { useState, useLayoutEffect, FC, useContext, useRef, Fragment, PropsWithChildren } from 'react' import mapValues from 'lodash/mapValues' import i18n, { TFunction } from 'i18next' import { getConfig, addConfigListener } from '@/_helpers/config-manager' import zip from 'lodash/zip' export type LangCode = 'zh-CN' | 'zh-TW' | 'en' export type Namespace = | 'common' | 'content' | 'langcode' | 'menus' | 'options' | 'popup' | 'wordpage' | 'dicts' | 'sync' export interface RawLocale { 'zh-CN': string 'zh-TW': string en: string } export interface RawLocales { [message: string]: RawLocale } export interface RawDictLocales { name: RawLocale options?: RawLocales helps?: RawLocales } export interface DictLocales { name: string options?: { [message: string]: any } helps?: { [message: string]: any } } export async function i18nLoader(): Promise { if (i18n.language) { // singleton return i18n } const { langCode } = await getConfig() await i18n .use({ type: 'backend', init: () => {}, create: () => {}, read: async (lang: LangCode, ns: Namespace, cb: Function) => { try { if (ns === 'dicts') { const dictLocals = extractDictLocales(lang) cb(null, dictLocals) return dictLocals } if (ns === 'sync') { const syncLocales = extractSyncServiceLocales(lang) cb(null, syncLocales) return syncLocales } const { locale } = await import( /* webpackInclude: /_locales\/[^/]+\/[^/]+\.ts$/ */ /* webpackMode: "lazy" */ `@/_locales/${lang}/${ns}.ts` ) cb(null, locale) return locale } catch (err) { cb(err) } } }) .init({ lng: langCode, fallbackLng: false, whitelist: ['en', 'zh-CN', 'zh-TW'], debug: process.env.NODE_ENV === 'development', saveMissing: false, load: 'currentOnly', ns: 'common', defaultNS: 'common', interpolation: { escapeValue: false // not needed for react as it escapes by default } }) addConfigListener(({ newConfig }) => { if (i18n.language !== newConfig.langCode) { i18n.changeLanguage(newConfig.langCode) } }) return i18n } const defaultT: i18n.TFunction = () => '' export const I18nContext = React.createContext(undefined) if (process.env.DEBUG) { I18nContext.displayName = 'I18nContext' } export const I18nContextProvider: FC = ({ children }) => { const [lang, setLang] = useState(undefined) useLayoutEffect(() => { const setLangCallback = () => { setLang(i18n.language) } if (!i18n.language) { i18nLoader().then(() => { setLang(i18n.language) i18n.on('languageChanged', setLangCallback) }) } return () => { i18n.off('languageChanged', setLangCallback) } }, []) return React.createElement(I18nContext.Provider, { value: lang }, children) } export interface UseTranslateResult { /** * fixedT with the first namespace as default. * It is a wrapper of the original fixedT, which * keeps the same reference even after namespaces are loaded */ t: i18n.TFunction i18n: i18n.i18n /** * Are namespaces loaded? * false not ready * otherwise it is a non-zero positive number * that changes everytime when new namespaces are loaded. */ ready: false | number } /** * Tailored for this project. * The official `useTranslation` is too heavy. * @param namespaces will not monitor namespace changes. */ export function useTranslate( namespaces?: Namespace | Namespace[] ): UseTranslateResult { const ticketRef = useRef(0) const innerTRef = useRef(defaultT) // keep the exposed t function always the same const tRef = useRef((...args: Parameters) => innerTRef.current(...args) ) const lang = useContext(I18nContext) const genResult = (t: TFunction | null, ready: boolean) => { if (t) { innerTRef.current = t } if (ready) { ticketRef.current = (ticketRef.current + 1) % 100000 } const result: UseTranslateResult = { t: tRef.current, i18n, ready: ready ? ticketRef.current : false } return result } const [result, setResult] = useState(() => { if (!lang) { return genResult(defaultT, false) } if (!namespaces) { return genResult(i18n.t, true) } if ( Array.isArray(namespaces) ? namespaces.every(ns => i18n.hasResourceBundle(lang, ns)) : i18n.hasResourceBundle(lang, namespaces) ) { return genResult(i18n.getFixedT(lang, namespaces), true) } return genResult(defaultT, false) }) useLayoutEffect(() => { let isEffectRunning = true if (lang) { if (namespaces) { if ( Array.isArray(namespaces) ? namespaces.every(ns => i18n.hasResourceBundle(lang, ns)) : i18n.hasResourceBundle(lang, namespaces) ) { setResult(genResult(i18n.getFixedT(lang, namespaces), true)) } else { // keep the old t while marking not ready setResult(genResult(null, false)) i18n.loadNamespaces(namespaces).then(() => { if (isEffectRunning) { setResult(genResult(i18n.getFixedT(lang, namespaces), true)) } }) } } else { setResult(genResult(i18n.t, true)) } } return () => { isEffectRunning = false } }, [lang]) return result } /** * *

b

*

d

*
* ↓ * [ * "a", *

b

, * "c", *

d

, * "e" * ] */ export const Trans = React.memo>( ({ message, children }) => { if (!message) return null return React.createElement( Fragment, null, zip( message.split(/{[^}]*?}/), Array.isArray(children) ? children : [children] ) ) } ) function extractDictLocales(lang: LangCode) { const req = require.context( '@/components/dictionaries', true, /_locales\.(json|ts)$/ ) return req.keys().reduce<{ [id: string]: DictLocales }>((o, filename) => { const localeModule = req(filename) const json: RawDictLocales = localeModule.locales || localeModule const dictId = /([^/]+)\/_locales\.(json|ts)$/.exec(filename)![1] o[dictId] = { name: json.name[lang] } if (json.options) { o[dictId].options = mapValues(json.options, rawLocale => rawLocale[lang]) } if (json.helps) { o[dictId].helps = mapValues(json.helps, rawLocale => rawLocale[lang]) } return o }, {}) } function extractSyncServiceLocales(lang: LangCode) { const req = require.context( '@/background/sync-manager/services', true, /_locales\/.+\.ts$/ ) return req.keys().reduce<{ [id: string]: DictLocales }>((o, filename) => { const idMatch = new RegExp(`/([^/]+)/_locales/${lang}\\.ts$`).exec(filename) if (idMatch) { const localeModule = req(filename) o[idMatch[1]] = localeModule.locale || localeModule } return o }, {}) } ================================================ FILE: src/_helpers/injectSaladictInternal.ts ================================================ export async function injectDictPanel(tab: browser.tabs.Tab | undefined) { if (tab && tab.id) { const tabId = tab.id const manifest = browser.runtime.getManifest() if (manifest.content_scripts) { for (const script of manifest.content_scripts) { if (script.js) { for (const js of script.js) { await browser.tabs.executeScript(tabId, { file: js[0] === '/' ? js : `/${js}`, allFrames: script.all_frames, matchAboutBlank: script.match_about_blank, runAt: script.run_at }) } } if (script.css) { for (const css of script.css) { await browser.tabs.insertCSS(tabId, { file: css[0] === '/' ? css : `/${css}`, allFrames: script.all_frames, matchAboutBlank: script.match_about_blank, runAt: script.run_at }) } } } } } } ================================================ FILE: src/_helpers/integrity.ts ================================================ export const isExtTainted = browser.runtime.id !== atob('Y2Rvbm5tZmZrZGFvYWpma25vZWVlY21jaGlicG1rbWc=') && browser.runtime.id !== atob('c2FsYWRpY3RAY3JpbXguY29t') && browser.runtime.id !== atob('aWRnaG9jYmJhaGFmcGZoam5maHBiZmJtcGVncGhtbXA=') && /apple/i.test(navigator.vendor) ================================================ FILE: src/_helpers/lang-check.ts ================================================ import memoizeOne from 'memoize-one' const languages = [ 'chinese', 'english', 'japanese', 'korean', 'french', 'spanish', 'deutsch' ] as const type Languages = typeof languages[number] const matchers: { [key in Languages]: RegExp } = { chinese: /[\u4e00-\u9fa5]/, english: /[a-zA-Z]/, /** Hiragana & Katakana, no Chinese */ japanese: /[\u3041-\u3096\u30A0-\u30FF]/, /** Korean Hangul, no Chinese */ korean: /[\u3131-\u4dff\u9fa6-\uD79D]/, /** French, no English àâäèéêëîïôœùûüÿç */ french: /[\u00e0\u00e2\u00e4\u00e8\u00e9\u00ea\u00eb\u00ee\u00ef\u00f4\u0153\u00f9\u00fb\u00fc\u00ff\u00e7]/i, /** Spanish, no English áéíóúñü¡¿ */ spanish: /[\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc\u00a1\u00bf]/i, /** Deutsch, no English äöüÄÖÜß */ deutsch: /[\u00E4\u00F6\u00FC\u00C4\u00D6\u00DC\u00df]/i } export const isContainChinese = (text: string): boolean => matchers.chinese.test(text) export const isContainEnglish = (text: string): boolean => matchers.english.test(text) /** Hiragana & Katakana, no Chinese */ export const isContainJapanese = (text: string): boolean => matchers.japanese.test(text) /** Korean Hangul, no Chinese */ export const isContainKorean = (text: string): boolean => matchers.korean.test(text) /** French, no English àâäèéêëîïôœùûüÿç */ export const isContainFrench = (text: string): boolean => matchers.french.test(text) /** Deutsch, no English äöüÄÖÜß */ export const isContainDeutsch = (text: string): boolean => matchers.deutsch.test(text) /** Spanish, no English áéíóúñü¡¿ */ export const isContainSpanish = (text: string): boolean => matchers.spanish.test(text) const isContain: { [key in Languages]: (text: string) => boolean } = { chinese: memoizeOne(isContainChinese), english: memoizeOne(isContainEnglish), /** Hiragana & Katakana, no Chinese */ japanese: memoizeOne(isContainJapanese), /** Korean Hangul, no Chinese */ korean: memoizeOne(isContainKorean), /** French, no English àâäèéêëîïôœùûüÿç */ french: memoizeOne(isContainFrench), /** Spanish, no English áéíóúñü¡¿ */ spanish: memoizeOne(isContainSpanish), /** Deutsch, no English äöüÄÖÜß */ deutsch: memoizeOne(isContainDeutsch) } const matcherPunct = /[/[\]{}$^*+|?.\-~!@#%&()_='";:><,。?!,、;:“”﹃﹄「」﹁﹂‘’『』()—[]〔〕【】…-~·‧《》〈〉﹏_]/ const matchAllMeaningless = new RegExp(`^(\\d|\\s|${matcherPunct.source})+$`) const matcherCJK = new RegExp( `${matchers.chinese.source}|${matchers.japanese.source}|${matchers.korean.source}` ) export const countWords = memoizeOne((text: string): number => { return ( text .replace(new RegExp(matcherPunct, 'g'), ' ') .replace(new RegExp(matcherCJK, 'g'), ' x ') .match(/\S+/g) || '' ).length }) export type SupportedLangs = { [key in Languages | 'others' | 'matchAll']: boolean } export const supportedLangs: ReadonlyArray = [ ...languages, 'others', 'matchAll' ] export function checkSupportedLangs( langs: SupportedLangs, text: string ): boolean { if (!text) { return false } if (langs.matchAll) { if (matchAllMeaningless.test(text)) { return false } if (langs.others) { const checkedMatchers: RegExp[] = [/-|\.|\d|\s/] const uncheckedMatchers: RegExp[] = [] for (let i = languages.length - 1; i >= 0; i--) { const l = languages[i] if (langs[l]) { checkedMatchers.push(matchers[l]) } else { uncheckedMatchers.push(matchers[l]) } } for (let i = 0; i < text.length; i++) { // characters of latin languages may overlap if ( checkedMatchers.every(m => !m.test(text[i])) && uncheckedMatchers.some(m => m.test(text[i])) ) { return false } } return true } else { const checkedMatchers = languages .filter(l => langs[l]) .map(l => matchers[l]) checkedMatchers.push(/-|\.|\d|\s/) for (let i = text.length - 1; i >= 0; i--) { if (checkedMatchers.every(m => !m.test(text[i]))) { return false } } return true } } /* !langs.matchAll */ else { if (languages.some(l => langs[l] && isContain[l](text))) { return true } if (!langs.others || matchAllMeaningless.test(text)) { return false } const uncheckedMatchers = languages .filter(l => !langs[l]) .map(l => matchers[l]) uncheckedMatchers.push(new RegExp(`${matcherPunct.source}|\\d|\\s`)) for (let i = text.length - 1; i >= 0; i--) { if (uncheckedMatchers.every(m => !m.test(text[i]))) { return true } } return false } } ================================================ FILE: src/_helpers/matchPatternToRegExpStr.ts ================================================ export function matchPatternToRegExpStr(pattern: string): string { if (pattern === '') { return '^(?:http|https|file|ftp|app)://' } const schemeSegment = '(\\*|http|https|ws|wss|file|ftp)' const hostSegment = '(\\*|(?:\\*\\.)?(?:[^/*]+))?' const pathSegment = '(.*)' const matchPatternRegExp = new RegExp( `^${schemeSegment}://${hostSegment}/${pathSegment}$` ) const match = matchPatternRegExp.exec(pattern) if (!match) { return '' } let [, scheme, host, path] = match if (!host) { return '' } let regex = '^' if (scheme === '*') { regex += '(http|https)' } else { regex += scheme } regex += '://' if (host && host === '*') { regex += '[^/]+?' } else if (host) { if (host.match(/^\*\./)) { regex += '[^/]*?' host = host.substring(2) } regex += host.replace(/\./g, '\\.') } if (path) { if (path === '*') { regex += '(/.*)?' } else if (path.charAt(0) !== '/') { regex += '/' regex += path.replace(/\./g, '\\.').replace(/\*/g, '.*?') regex += '/?' } } return regex + '$' } ================================================ FILE: src/_helpers/observables.ts ================================================ import { filter, map, switchMap, delay, debounceTime, mergeMap, takeUntil, mapTo } from 'rxjs/operators' import { of, Observable, OperatorFunction, from } from 'rxjs' import { MouseEvent } from 'react' /** * Reusable Observable logics */ /** * Emits true on mouse enter and false on mouse leave. * Delay on mouse enter. * * Shadow DOM does not send mouseenter and mouseleave * cross the boundary which means React synthetic * event handler will not collect. * Here mouseover and mouseout are used to simulate. * * @param event$ mouseover and mouseout events */ export function hover( event$: Observable> ): Observable { return event$.pipe( filter( e => e.relatedTarget !== e.currentTarget && (!(e.relatedTarget instanceof Node) || !e.currentTarget.contains(e.relatedTarget)) ), map(e => e.type === 'mouseover') ) } /** * [[hover]] with delay on enter. */ export function hoverWithDelay( event$: Observable> ): Observable { return hover(event$).pipe( // delay enter but not leave switchMap(isEnter => of(isEnter).pipe(delay(isEnter ? 500 : 100))) ) } /** * Emits true is focus and false if blur. */ export function focusBlur(event$: Observable<{ type: string }>) { return event$.pipe( map(e => e.type !== 'blur'), debounceTime(100) ) } /** * * SwitchMap when value on specific key changes. */ export function switchMapBy( key: keyof T, mapFn: (val: T) => Observable | Promise ): OperatorFunction { return input$ => { return input$.pipe( mergeMap(val => from(mapFn(val)).pipe( takeUntil(input$.pipe(filter(input => input[key] === val[key]))) ) ) ) } } export function mapToTrue(input$: Observable) { return mapTo(true)(input$) } ================================================ FILE: src/_helpers/permission-manager.ts ================================================ import { AppConfig } from '@/app-config' import { isFirefox, isOpera, isSafari } from './saladict' export async function checkBackgroundPermission( config: AppConfig ): Promise { // Firefox, Opera and Safari does not support 'background' permission. if (isFirefox || isOpera || isSafari) return const backgroundPermissions: browser.permissions.AnyPermissions = { permissions: ['background'] } const hasBackground = await browser.permissions.contains( backgroundPermissions ) if (config.runInBg) { if (!hasBackground) { await browser.permissions.request( backgroundPermissions as browser.permissions.Permissions ) } } else { if (hasBackground) { try { await browser.permissions.remove( backgroundPermissions as browser.permissions.Permissions ) } catch (e) { // failed silently on remove console.error(e) } } } } ================================================ FILE: src/_helpers/profile-manager.ts ================================================ /** * Profiles are switchable profiles */ import pako from 'pako' import { getDefaultProfile, Profile, genProfilesStorage, ProfileIDList, ProfileID } from '@/app-config/profiles' import { mergeProfile } from '@/app-config/merge-profile' import { storage } from './browser-api' import { TFunction } from 'i18next' import { Observable, from, concat, fromEventPattern } from 'rxjs' import { map } from 'rxjs/operators' export interface StorageChanged { newValue: T oldValue?: T } export interface ProfileChanged { newProfile: Profile oldProfile?: Profile } /** Compressed profile data */ interface ProfileCompressed { /** version */ v: 1 /** data */ d: string } export function deflate(profile: Profile): ProfileCompressed { return { v: 1, d: pako.deflate(JSON.stringify(profile), { to: 'string' }) } } export function inflate(profile: Profile | ProfileCompressed): Profile export function inflate(profile: undefined): undefined export function inflate( profile?: Profile | ProfileCompressed ): Profile | undefined export function inflate( profile?: Profile | ProfileCompressed ): Profile | undefined { if (profile && profile['v'] === 1) { return JSON.parse( pako.inflate((profile as ProfileCompressed).d, { to: 'string' }) ) } return profile as Profile | undefined } export function getProfileName(name: string, t: TFunction): string { // default names const match = /^%%_(\S+)_%%$/.exec(name) if (match) { return t(`common:profile.${match[1]}`) || name } return name } export async function initProfiles(): Promise { let profiles: Profile[] = [] let profileIDList: ProfileIDList = [] let activeProfileID = '' const response = await storage.sync.get<{ profileIDList: ProfileIDList activeProfileID: string }>(['profileIDList', 'activeProfileID']) if (response.profileIDList) { profileIDList = response.profileIDList.filter(item => Boolean( item && typeof item.id === 'string' && typeof item.name === 'string' ) ) } if (response.activeProfileID) { activeProfileID = response.activeProfileID } if (profileIDList.length > 0) { // quota bytes limit for (const { id } of profileIDList) { const profile = await getProfile(id) profiles.push(profile ? mergeProfile(profile) : getDefaultProfile(id)) } } else { ;({ profileIDList, profiles } = genProfilesStorage()) activeProfileID = profileIDList[0].id } if (!activeProfileID) { activeProfileID = profileIDList[0].id } let activeProfile = profiles.find(({ id }) => id === activeProfileID) if (!activeProfile) { activeProfile = profiles[0] activeProfileID = activeProfile.id } await storage.sync.set({ profileIDList, activeProfileID }) // quota bytes per item limit for (const profile of profiles) { await updateProfile(profile) } return activeProfile } export async function resetAllProfiles() { const { profileIDList } = await storage.sync.get<{ profileIDList: ProfileIDList }>('profileIDList') if (profileIDList) { await storage.sync.remove([ ...profileIDList.map(({ id }) => id), 'profileIDList', 'activeProfileID', // legacy 'configProfileIDs', 'activeConfigID' ]) } return initProfiles() } export async function getProfile(id: string): Promise { return inflate((await storage.sync.get(id))[id]) } /** * Update profile */ export async function updateProfile(profile: Profile): Promise { if (process.env.DEBUG) { const profileIDList = await getProfileIDList() if (!profileIDList.find(item => item.id === profile.id)) { console.error(`Update Profile: profile ${profile.id} does not exist`) } else { console.log('Savedd Profile', profile) } } return storage.sync.set({ [profile.id]: deflate(profile) }) } export async function addProfile(profileID: ProfileID): Promise { const id = profileID.id const profileIDList = await getProfileIDList() if (process.env.DEBUG) { if (profileIDList.find(item => item.id === id) || (await getProfile(id))) { console.warn(`Add profile: profile ${id} exists`) } } return storage.sync.set({ profileIDList: [...profileIDList, profileID], [id]: deflate(getDefaultProfile(id)) }) } export async function removeProfile(id: string): Promise { const activeProfileID = await getActiveProfileID() let profileIDList = await getProfileIDList() if (process.env.DEBUG) { if ( !profileIDList.find(item => item.id === id) || !(await getProfile(id)) ) { console.warn(`Remove profile: profile ${id} does not exists`) } } profileIDList = profileIDList.filter(item => item.id !== id) if (activeProfileID === id) { await updateActiveProfileID(profileIDList[0].id) } await updateProfileIDList(profileIDList) return storage.sync.remove(id) } /** * Get the profile under the current mode */ export async function getActiveProfile(): Promise { const activeProfileID = await getActiveProfileID() if (activeProfileID) { const profile = await getProfile(activeProfileID) if (profile) { return profile } } return getDefaultProfile() } export async function getActiveProfileID(): Promise { return (await storage.sync.get('activeProfileID')).activeProfileID || '' } export function updateActiveProfileID(id: string): Promise { return storage.sync.set({ activeProfileID: id }) } /** * This is mainly for ordering */ export async function getProfileIDList(): Promise { return (await storage.sync.get('profileIDList')).profileIDList || [] } /** * This is mainly for ordering */ export function updateProfileIDList(list: ProfileIDList): Promise { return storage.sync.set({ profileIDList: list }) } export function addActiveProfileIDListener( cb: (changes: StorageChanged) => any ) { storage.sync.addListener('activeProfileID', ({ activeProfileID }) => { if (activeProfileID && activeProfileID.newValue) { cb(activeProfileID as StorageChanged) } }) } export function addProfileIDListListener( cb: (changes: StorageChanged) => any ) { storage.sync.addListener('profileIDList', ({ profileIDList }) => { if (profileIDList && profileIDList.newValue) { cb(profileIDList as StorageChanged) } }) } /** * Listen storage changes of the current profile */ export async function addActiveProfileListener( cb: (changes: ProfileChanged) => any ) { let activeID: string | undefined = await getActiveProfileID() storage.sync.addListener(changes => { // this id changed if (changes.activeProfileID) { const { newValue: newID, oldValue: oldID } = (changes as { activeProfileID: StorageChanged }).activeProfileID if (newID) { activeID = newID if (oldID) { storage.sync.get([oldID, newID]).then(obj => { if (obj[newID]) { cb({ newProfile: inflate(obj[newID]), oldProfile: inflate(obj[oldID]) }) return } }) } else { getProfile(newID).then(newProfile => { if (newProfile) { cb({ newProfile }) return } }) } } } // the active profile itself updated if (activeID && changes[activeID]) { const { newValue, oldValue } = changes[activeID] as StorageChanged< ProfileCompressed > if (newValue) { cb({ newProfile: inflate(newValue), oldProfile: inflate(oldValue) }) return } } }) } /** * Get active profile and create a stream listening to profile changing */ export function createProfileIDListStream(): Observable { return concat( from(getProfileIDList()), fromEventPattern< [StorageChanged] | StorageChanged >(addProfileIDListListener as any).pipe( map(args => (Array.isArray(args) ? args[0] : args).newValue) ) ) } /** * Get active profile and create a stream listening to profile changing */ export function createActiveProfileStream(): Observable { return concat( from(getActiveProfile()), fromEventPattern<[ProfileChanged] | ProfileChanged>( addActiveProfileListener as any ).pipe(map(args => (Array.isArray(args) ? args[0] : args).newProfile)) ) } ================================================ FILE: src/_helpers/promise-more.ts ================================================ /* eslint-disable prettier/prettier */ /** * Like Promise.all but always resolves. */ export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike, T10 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null, T8 | null, T9 | null, T10 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null, T8 | null, T9 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null, T8 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike]): Promise<[T1 | null, T2 | null, T3 | null]> export function reflect(iterable: [T1 | PromiseLike, T2 | PromiseLike]): Promise<[T1 | null, T2 | null]> export function reflect(iterable: ArrayLike>): Promise<(T | null)[]> export function reflect(iterable: ArrayLike) { const arr = Array.isArray(iterable) ? iterable : Array.from(iterable) return Promise.all(arr.map(p => Promise.resolve(p).catch(() => null))) } /** * Like Promise.all but only rejects when all are failed. */ export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike, T10 | PromiseLike]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike]): Promise<[T1, T2, T3, T4, T5, T6, T7]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike]): Promise<[T1, T2, T3, T4, T5, T6]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike]): Promise<[T1, T2, T3, T4, T5]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike]): Promise<[T1, T2, T3, T4]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike]): Promise<[T1, T2, T3]> export function any(iterable: [T1 | PromiseLike, T2 | PromiseLike]): Promise<[T1, T2]> export function any(iterable: ArrayLike>): Promise export function any(iterable: ArrayLike) { const arr = Array.isArray(iterable) ? iterable : Array.from(iterable) let rejectCount = 0 const promises: Promise[] = arr.map((p, i) => Promise.resolve(p).catch(e => { rejectCount++ return null }) ) return Promise.all(promises).then(resolutions => { if (rejectCount === resolutions.length) { return Promise.reject(new Error('All rejected')) } return Promise.resolve(resolutions) }) } /** * Returns the first resolved value as soon as it is resolved. * Fails when all are failed. */ export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike, T10 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike, T6 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike, T5 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike]): Promise export function first (iterable: [T1 | PromiseLike, T2 | PromiseLike]): Promise export function first (iterable: ArrayLike>): Promise export function first (iterable: ArrayLike) { const arr = Array.isArray(iterable) ? iterable : Array.from(iterable) let rejectCount = 0 return new Promise((resolve, reject) => arr.forEach(p => { Promise.resolve(p) .then(resolve) .catch(() => { if (++rejectCount === arr.length) { reject(new Error('All rejected')) } }) }) ) } /** * Like setTimeout but returns Promise. */ export function timer(delay?: number): Promise export function timer(delay: number, payload?: T): Promise export function timer(...args) { return new Promise(resolve => { setTimeout( () => (args.length > 1 ? resolve(args[1]) : resolve()), Number(args[0]) || 0 ) }) } /** * Timeouts a promise. * Rejects when timeout. */ export function timeout(pr: PromiseLike, delay = 0): Promise { return Promise.race([ pr, timer(delay).then(() => Promise.reject(new Error(`timeout ${delay}ms`))) ]) } export default { reflect, any, first, timer, timeout } ================================================ FILE: src/_helpers/record-manager.ts ================================================ /** * Abstracted layer for storing large amount of word records. */ import { message } from '@/_helpers/browser-api' export interface Word { /** primary key, milliseconds elapsed since the UNIX epoch */ date: number /** word text */ text: string /** the sentence where the text string is located */ context: string /** page title */ title: string /** page url */ url: string /** favicon url */ favicon: string /** translation */ trans: string /** custom note */ note: string } export type DBArea = 'notebook' | 'history' export function newWord(word?: Partial): Word { return word ? { date: word.date || Date.now(), text: word.text || '', context: word.context || '', title: word.title || '', url: word.url || '', favicon: word.favicon || '', trans: word.trans || '', note: word.note || '' } : { date: Date.now(), text: '', context: '', title: '', url: '', favicon: '', trans: '', note: '' } } export function isInNotebook(word: Word): Promise { return message .send<'IS_IN_NOTEBOOK'>({ type: 'IS_IN_NOTEBOOK', payload: word }) .catch(logError(false)) } export async function saveWord(area: DBArea, word: Word): Promise { await message.send({ type: 'SAVE_WORD', payload: { area, word } }) } export async function deleteWords( area: DBArea, dates?: number[] ): Promise { await message.send({ type: 'SYNC_SERVICE_DOWNLOAD' }) await message.send({ type: 'DELETE_WORDS', payload: { area, dates } }) } export function getWordsByText( area: DBArea, text: string ): Promise { return message.send<'GET_WORDS_BY_TEXT'>({ type: 'GET_WORDS_BY_TEXT', payload: { area, text } }) } export function getWords( area: DBArea, config: { itemsPerPage?: number pageNum?: number filters: { [field: string]: (string | number)[] | null | undefined } sortField?: string | number | (string | number)[] sortOrder?: 'ascend' | 'descend' | false | null searchText?: string } ) { return message.send<'GET_WORDS'>({ type: 'GET_WORDS', payload: { area, ...config } }) } function logError(valPassThrough: T): (x: any) => T { return err => { if (process.env.DEBUG) { console.error(err) } return valPassThrough } } ================================================ FILE: src/_helpers/saladict.ts ================================================ /** Pages with the Saladict extension domain */ export const isBackgroundPage = () => !!window.__SALADICT_BACKGROUND_PAGE__ export const isInternalPage = () => !!window.__SALADICT_INTERNAL_PAGE__ export const isOptionsPage = () => !!window.__SALADICT_OPTIONS_PAGE__ export const isPopupPage = () => !!window.__SALADICT_POPUP_PAGE__ export const isPDFPage = () => !!window.__SALADICT_PDF_PAGE__ export const isQuickSearchPage = () => !!window.__SALADICT_QUICK_SEARCH_PAGE__ /** Dict panel is in a standalone window */ export const isStandalonePage = () => isPopupPage() || isQuickSearchPage() /** do not record search history on these pages */ export const isNoSearchHistoryPage = () => isInternalPage() && !isStandalonePage() export const SALADICT_EXTERNAL = 'saladict-external' export const SALADICT_PANEL = 'saladict-panel' export const isFirefox = navigator.userAgent.includes('Firefox') export const isOpera = navigator.userAgent.includes('OPR') export const isSafari = /apple/i.test(navigator.vendor) /** * Is element in a Saladict external element */ export function isInSaladictExternal( element: Element | EventTarget | null ): boolean { if (!element) { return false } for (let el: Element | null = element as Element; el; el = el.parentElement) { if (el.classList && el.classList.contains(SALADICT_EXTERNAL)) { return true } } return false } /** * Is element in Saladict Dict Panel */ export function isInDictPanel(element: Node | EventTarget | null): boolean { if (!element) { return false } for (let el: Element | null = element as Element; el; el = el.parentElement) { if (el.classList && el.classList.contains(SALADICT_PANEL)) { return true } } return false } ================================================ FILE: src/_helpers/scrollbar-width.ts ================================================ import memoizeOne from 'memoize-one' export const getScrollbarWidth = memoizeOne(() => { if (typeof document === 'undefined') { return 0 } const strut = document.createElement('div') const strutStyle = strut.style strutStyle.position = 'fixed' strutStyle.left = '0' strutStyle.overflowY = 'scroll' strutStyle.visibility = 'hidden' document.body.appendChild(strut) const width = strut.getBoundingClientRect().right strut.remove() return width }) export default getScrollbarWidth ================================================ FILE: src/_helpers/storybook.tsx ================================================ import React, { FC, useState, useEffect } from 'react' import classNames from 'classnames' import root from 'react-shadow' import i18next from 'i18next' import { number, boolean } from '@storybook/addon-knobs' import SinonChrome from 'sinon-chrome' import { Message } from '@/typings/message' import { I18nContext, Namespace } from './i18n' interface StyleWrapProps { style: string } export const browser: typeof SinonChrome = window.browser as any export const StyleWrap: FC = props => { return (
{props.children}
) } /** * Workaround for {@link https://github.com/storybookjs/storybook/issues/729} */ export function withLocalStyle(style: object | string) { return function LocalStyle(fn) { return {fn()} } } export const I18nNS: FC<{ story: Function }> = props => { const [lang, setLang] = useState(i18next.language) useEffect(() => { const onLangChanged = (lang: string) => { setLang(lang) } i18next.on('languageChanged', onLangChanged) return () => i18next.off('languageChanged', onLangChanged) }, []) return ( {props.story()} ) } export function withi18nNS(ns: Namespace | Namespace[]) { // eslint-disable-next-line react/display-name return fn => { i18next.loadNamespaces(ns) return } } /** * Perform side effects and clean up when switching stroies * @param fn performs side effects and optionally returns a clean-up function */ export function withSideEffect(fn: React.EffectCallback) { const SideEffect: FC<{ story: Function }> = props => { useEffect(fn, []) return <>{props.story()} } // eslint-disable-next-line react/display-name return story => } export function mockRuntimeMessage(fn: (message: Message) => Promise) { return () => { browser.runtime.sendMessage.callsFake(fn) return () => { browser.runtime.sendMessage.callsFake(() => Promise.resolve()) } } } export interface WithSaladictPanelOptions { /** before the story component */ head?: React.ReactNode width?: number | string height?: number | string withAnimation?: boolean fontSize?: number | string color?: string backgroundColor?: string } /** * Fake salalict panel */ export function withSaladictPanel(options: WithSaladictPanelOptions) { return function SaladcitPanel(story: Function) { const width = options.width != null ? options.width : number('Panel Width', 450) const height = options.height != null ? options.height : number('Panel Height', window.innerHeight - 50) const darkMode = boolean('Dark Mode', false) const withAnimation = options.withAnimation != null ? options.withAnimation : boolean('Enable Animation', true) const fontSize = options.fontSize != null ? options.fontSize : number('Panel Font Size', 13) return (
e.stopPropagation()} > {options.head} {story({ width, height, fontSize, darkMode, withAnimation })}
) } } ================================================ FILE: src/_helpers/titlebar-offset.ts ================================================ /** * Extension API is inconsistent with the window top. * Sometimes the titlebar height is included, sometimes not. */ import { storage } from './browser-api' import { timer } from './promise-more' export interface TitlebarOffset { // main window title bar height main: number // panel window title bar height panel: number } export async function getTitlebarOffset(): Promise { return ( await storage.local.get<{ titlebarOffset?: TitlebarOffset }>( 'titlebarOffset' ) ).titlebarOffset } export function setTitlebarOffset(offset: TitlebarOffset): Promise { return storage.local.set({ titlebarOffset: offset }) } export async function calibrateTitlebarOffset(): Promise< TitlebarOffset | undefined > { try { const curWin = await browser.windows.getCurrent() if (curWin.id == null) return const mainWin = await browser.windows.create({ state: 'maximized' }) const panelWin = await browser.windows.create({ state: 'maximized', type: 'panel' }) if (mainWin?.id == null || panelWin?.id == null) return await browser.windows.update(curWin.id, { focused: true }) await timer(0) const main = (await browser.windows.get(mainWin.id)).top const panel = (await browser.windows.get(panelWin.id)).top browser.windows.remove(mainWin.id) browser.windows.remove(panelWin.id) if (main == null || panel == null) return return { main, panel } } catch (e) { if (process.env.DEBUG) { console.error(e) } } } ================================================ FILE: src/_helpers/translateCtx.ts ================================================ import { DictID, AppConfig } from '@/app-config' import { MachineTranslateResult } from '@/components/dictionaries/helpers' import { message } from './browser-api' import { isPDFPage } from './saladict' export type CtxTranslatorId = keyof AppConfig['ctxTrans'] export type CtxTranslateResults = { [id in CtxTranslatorId]: string } export interface FetchDictResultResponse { id: DictID result: MachineTranslateResult } /** * translate selection context with selected machine translatior * @param text search text * @param id machine translatior id */ export async function translateCtx( text: string, id: CtxTranslatorId ): Promise { try { const response = await message.send< 'FETCH_DICT_RESULT', FetchDictResultResponse >({ type: 'FETCH_DICT_RESULT', payload: { id, text, payload: { isPDF: isPDFPage() } } }) return ( (response && response.result && response.result.trans && response.result.trans.paragraphs.join('\n')) || '' ) } catch (e) { return '' } } /** * translate selection context with selected machine translatiors * @param text search text * @param ctxTrans machine translatiors */ export async function translateCtxs( text: string, ctxTrans: AppConfig['ctxTrans'] ): Promise { return ( await Promise.all( Object.keys(ctxTrans).map(async id => { let content = '' if (ctxTrans[id]) { try { content = await translateCtx(text, id as CtxTranslatorId) } catch (e) { console.warn(e) } } return { id, content } }) ) ).reduce((result, { id, content }) => { result[id] = content return result }, {} as CtxTranslateResults) } /** * get translator result from text */ export function parseCtxText(text: string): CtxTranslateResults { const matcher = /\[:: (\w+) ::\]\n([\s\S]+?)(?=(?:\[:: \w+ ::\])|(?:-{15}))/g let matchResult: RegExpExecArray | null const result = {} as CtxTranslateResults while ((matchResult = matcher.exec(text)) !== null) { result[matchResult[1] as CtxTranslatorId] = matchResult[2].replace( /\n+$/g, '' ) } return result } /** * Add Context translate result to text * @param text original text */ export function genCtxText( text: string, ctxTransResult: CtxTranslateResults ): string { const enginesWithResult = Object.keys(ctxTransResult).filter( id => ctxTransResult[id] ) if (enginesWithResult.length <= 0) { return text } const ctxResults = enginesWithResult .map(id => `[:: ${id} ::]\n` + ctxTransResult[id]) .join('\n\n') + `\n${''.padEnd(15, '-')}\n` if (!text) { return ctxResults } const matcher = /\[:: (\w+) ::\]\n([\s\S]+?)-{15}/ if (matcher.test(text)) { return text.replace(matcher, ctxResults) } return text + '\n\n' + ctxResults } ================================================ FILE: src/_helpers/uniqueKey.ts ================================================ /** * Generate a unique key */ export function genUniqueKey(): string { return ( Date.now() .toString() .slice(6) + Math.random() .toString() .slice(2, 8) ) } export function genUniqueKeyThunk() { return genUniqueKey } export function isGeneratedKey(key: unknown): boolean { return typeof key === 'string' && /^\d{13}$/.test(key) } ================================================ FILE: src/_helpers/wordoftheday.ts ================================================ import { fetchDirtyDOM } from '@/_helpers/fetch-dom' import { first } from '@/_helpers/promise-more' import { handleNoResult, getText } from '@/components/dictionaries/helpers' export async function getWordOfTheDay(): Promise { if (!process.env.DEBUG) { try { return await first([ getWebsterWordOfTheDay(), getDictionaryWordOfTheDay() ]) } catch (e) {} } return 'salad' } export async function getWebsterWordOfTheDay(): Promise { const doc = await fetchDirtyDOM( 'https://www.merriam-webster.com/word-of-the-day' ) const text = getText(doc, 'title') const matchResult = text.match(/Word of the Day: (.+) \| Merriam-Webster/) return (matchResult && matchResult[1]) || handleNoResult() } export async function getDictionaryWordOfTheDay(): Promise { const doc = await fetchDirtyDOM('https://www.dictionary.com/wordoftheday/') const text = getText(doc, 'title') const matchResult = text.match( /Get the Word of the Day - (.+) \| Dictionary\.com/ ) return (matchResult && matchResult[1]) || handleNoResult() } ================================================ FILE: src/_locales/en/background.ts ================================================ import { locale as _locale } from '../zh-CN/background' export const locale: typeof _locale = { app: { off: 'Saladict disabled. (Quick Search Panel is still available)', tempOff: 'Saladict disabled on current tab. (Quick Search Panel is still available)', unsupported: 'Embedded Saladict panel is unsupported for current tab. Use Standalone Saladict panel instead.' } } ================================================ FILE: src/_locales/en/common.ts ================================================ import { locale as _locale } from '../zh-CN/common' export const locale: typeof _locale = { add: 'Add', delete: 'Delete', save: 'Save', cancel: 'Cancel', edit: 'Edit', sort: 'Sort', rename: 'Rename', confirm: 'Confirm', changes_confirm: 'Changes not saved. Close anyway?', delete_confirm: 'Deleted item completely?', max: 'Max', min: 'Min', name: 'Name', none: 'None', enable: 'Enable', enabled: 'Enabled', disabled: 'Disabled', blacklist: 'Blacklist', whitelist: 'Whitelist', import: 'Import', export: 'Export', lang: { chinese: 'Chinese', chs: 'Chinese', deutsch: 'Deutsch', eng: 'English', english: 'English', french: 'French', japanese: 'Japanese', korean: 'Korean', matchAll: 'Match every character', minor: 'Minor', others: 'Others', spanish: 'Spanish' }, unit: { mins: 'minutes', ms: 'ms', s: 's', word: 'words' }, note: { word: 'Word', trans: 'Translation', note: 'Note', context: 'Context', contextCloze: 'Context Cloze', date: 'Date', srcTitle: 'Source Title', srcLink: 'Source Link', srcFavicon: 'Source Favicon' }, profile: { daily: 'Daily Mode', sentence: 'Sentence Mode', default: 'Default Mode', scholar: 'Scholar Mode', translation: 'Translation Mode', nihongo: 'Japanese Mode' } } ================================================ FILE: src/_locales/en/content.ts ================================================ import { locale as _locale } from '../zh-CN/content' export const locale: typeof _locale = { chooseLang: 'Choose another language', standalone: 'Saladict Standalone Panel', fetchLangList: 'Fetch full language list', transContext: 'Retranslate', neverShow: 'Stop showing', fromSaladict: 'From Saladict Panel', tip: { historyBack: 'Previous search history', historyNext: 'Next search history', searchText: 'Search text', openOptions: 'Open Options', addToNotebook: 'Add to Notebook. Right click to open Notebook', openNotebook: 'Open Notebook', openHistory: 'Open History', shareImg: 'Share as image', pinPanel: 'Pin the panel', closePanel: 'Close the panel', sidebar: 'Switch to sidebar mode. Right click to right side.', focusPanel: 'Panel gains focus when searching', unfocusPanel: 'Panel does not gain focus when searching' }, wordEditor: { title: 'Add to Notebook', wordCardsTitle: 'Other results from Notebook', deleteConfirm: 'Delete from Notebook?', closeConfirm: 'Changes will not be saved. Are you sure to close?', chooseCtxTitle: 'Pick translated results', ctxHelp: 'Keep the [:: xxx ::] and --------------- format if you want Saladict to handle translation selection and generate Anki table.' }, machineTrans: { switch: 'Switch Language', sl: 'Source Language', tl: 'Target Language', auto: 'Detect language', stext: 'Original', showSl: 'Show Source', copySrc: 'Copy Source', copyTrans: 'Copy Translation', login: 'Please provide {access token}.', dictAccount: 'access token' }, updateAnki: { title: 'Update to Anki', success: 'Successfully update word to Anki.', failed: 'Failed to update word to Anki.' } } ================================================ FILE: src/_locales/en/langcode.ts ================================================ import en from '@opentranslate/languages/locales/en.json' import { locale as _locale } from '../zh-CN/langcode' export const locale: typeof _locale = { ...en, default: 'Default', ne_NP: 'Nepali', ara: 'Arabic', 'bs-Latn': 'Bosnian', bul: 'Bulgarian', cht: 'Chinese (Traditional)', dan: 'Danish', est: 'Estonian', fin: 'Finnish', fra: 'French', iw: 'Hebrew', jp: 'Japanese', kor: 'Korean', kr: 'Korean', pt_BR: 'Brazilian', rom: 'Romanian', slo: 'Slovenian', spa: 'Spanish', swe: 'Swedish', tl: 'Tagalog (Filipino)', vie: 'Vietnamese', zh: 'Chinese (Simplified)', 'zh-CHS': 'Chinese (Simplified)', 'zh-CHT': 'Chinese (Traditional)' } ================================================ FILE: src/_locales/en/menus.ts ================================================ import { locale as _locale } from '../zh-CN/menus' export const locale: typeof _locale = { baidu_page_translate: 'Baidu Page Translate', baidu_search: 'Baidu Search', bing_dict: 'Bing Dict', bing_search: 'Bing Search', caiyuntrs: 'Lingocloud Page Translate', cambridge: 'Cambridge', copy_pdf_url: 'Copy PDF URL to Clipboard', dictcn: 'Dictcn', etymonline: 'Etymonline', google_cn_page_translate: 'Google cn Page Translate', google_page_translate: 'Google Page Translate', google_search: 'Google Search', google_translate: 'Google Translate', google_cn_translate: 'Google.cn Translate', guoyu: '國語辭典', history_title: 'Search History', iciba: 'iciba', liangan: '兩岸詞典', longman_business: 'Longman Business', manual_title: 'Manual', merriam_webster: 'Merriam Webster', microsoft_page_translate: 'Microsoft Page Translate', notebook_title: 'New Word List', notification_youdao_err: 'Youdao Page Translate 2.0 not responding.\nSaladict might not have permission to access this page.\nIgnore this message if Youdao panal is shown.', oxford: 'Oxford', page_permission_err: 'Saladict "{{name}}" does not have permission to access this page.', page_translations: 'Page Translations', saladict: 'Saladict', saladict_standalone: 'Saladict Standalone Panel', sogou: 'Sogou Translate', sogou_page_translate: 'Sogou Page Translate', termonline: 'Termonline', view_as_pdf: 'Open in PDF Viewer', youdao: 'Youdao', youdao_page_translate: 'Youdao Page Translate', youglish: 'YouGlish' } ================================================ FILE: src/_locales/en/options.ts ================================================ import { locale as _locale } from '../zh-CN/options' export const locale: typeof _locale = { title: 'Saladict Options', previewPanel: 'Preview Dict Panel', shortcuts: 'Set Shortcuts', msg_update_error: 'Unable to update', msg_updated: 'Successfully updated', msg_first_time_notice: 'First time notice', msg_err_permission: 'Unable to request "{{permission}}" permission.', unsave_confirm: 'Settings not saved. Sure to leave?', nativeSearch: 'search selected text outside of browser', firefox_shortcuts: 'Open about:addons, click the top right "gear" button, choose the last "Manage extension shortcuts".', tutorial: 'Tutorial', page_selection: 'Page Selection', nav: { General: 'General', Notebook: 'Notebook', Profiles: 'Profiles', DictPanel: 'Dict Panel', SearchModes: 'Search Modes', Dictionaries: 'Dictionaries', DictAuths: 'Access Tokens', Popup: 'Popup Panel', QuickSearch: 'Quick Search', Pronunciation: 'Pronunciation', PDF: 'PDF', ContextMenus: 'Context Menus', BlackWhiteList: 'Black/White List', ImportExport: 'Import/Export', Privacy: 'Privacy', Permissions: 'Permissions' }, config: { active: 'Enable Inline Translator', active_help: '"Quick Search" is still available even if Inline translation is turned off.', animation: 'Animation transitions', animation_help: 'Switch off animation transitions to reduce runtime cost.', runInBg: 'Keep in Background', runInBg_help: 'Keep the browser running in background after close so that global shortcuts still work.', darkMode: 'Dark Mode', langCode: 'App Language', editOnFav: 'Open WordEditor when saving', editOnFav_help: 'When turned off, new words will be added to notebook directly.', searchHistory: 'Keep search history', searchHistory_help: 'Your browsing history could be unintentionally revealed in Search history.', searchHistoryInco: 'Also in incognito mode', ctxTrans: 'Context Translate Engines', ctxTrans_help: 'Context sentence will be translated before being added to notebook.', searchSuggests: 'Search suggests', panelMaxHeightRatio: 'Panel max height ratio', panelWidth: 'Panel width', fontSize: 'Font size for search reasults', bowlOffsetX: 'Saladict icon Offset X', bowlOffsetY: 'Saladict icon Offset Y', panelCSS: 'Custom Dict Panel Styles', panelCSS_help: 'Custom CSS. For Dict Panel use .dictPanel-Root as root. For dictionaries use .dictRoot or .d-{id} as root', noTypeField: 'No selection on editable regions', noTypeField_help: 'If selection making in editable regions is banned, the extension will identify Input Boxes, TextAreas and other common text editors like CodeMirror, ACE and Monaco.', touchMode: 'Touch Mode', touchMode_help: 'Enable touch related selection', language: 'Selection Languages', language_help: 'Search when selection contains words in the chosen languages.', language_extra: 'Note that Japanese and Korean also include Chinese. French, Deutsch and Spanish also include English. If Chinese or English is cancelled while others are selected, only the exclusive parts of those languages are tested. E.g. kana characters in Japanese.', doubleClickDelay: 'Double Click Delay', mode: 'Normal Selection', panelMode: 'Inside Dict Panel', pinMode: 'When Panel is Pinned', qsPanelMode: 'When Standalone Panel is Opened', bowlHover: 'Icon Mouse Hover', bowlHover_help: 'Hover on the bowl icon to trigger searching instead of clicking.', autopron: { cn: { dict: 'Chinese Auto-Pronounce' }, en: { dict: 'English Auto-Pronounce', accent: 'Accent Preference' }, machine: { dict: 'Machine Auto-Pronounce', src: 'Machine Pronounce', src_help: 'Machine Translation Dictionary needs to be added and enabled on the list below to enable auto-pronunciation.', src_search: 'Read Source Text', src_trans: 'Read Translation Text' } }, pdfSniff: 'Enable PDF Sniffer', pdfSniff_help: 'If turned on, PDF links will be automatically captured.', pdfSniff_extra: 'It is recommended to {search selected text outside of browser} with your own favorite local reader.', pdfStandalone: 'Standalone Panel', pdfStandalone_help: 'Open PDF viewer in standalone panel.', baWidth: 'Width', baWidth_help: 'Browser Action Panel wdith. Dict Panel width will be used if a negative value is chosen.', baHeight: 'Height', baHeight_help: 'Browser Action Panel height.', baOpen: 'Browser Action', baOpen_help: 'When clicking the browser action icon in toolbar (next to the address bar). Items are same as Context Menus, which can be added or edited on the Context Menus config page.', tripleCtrl: 'Enable Ctrl Shortkey', tripleCtrl_help: 'Press {⌘ Command}(macOS) or {Ctrl}(Others) three times (or with browser shortkey) to summon the dictionary panel. ', defaultPinned: 'Pinned when shows up', qsLocation: 'Location', qsFocus: 'Focus when shows up', qsStandalone: 'Standalone', qsStandalone_help: 'Render dict panel in a standalone window. You can {search selected text outside of browser}.', qssaSidebar: 'Sidebar Layout', qssaSidebar_help: 'Rearrange windows to sidebar-like layout.', qssaHeight: 'Window Height', qssaPageSel: 'Selection Response', qssaPageSel_help: 'Response to page selection.', qssaRectMemo: 'Remember size and position', qssaRectMemo_help: 'Remember standalone panel size and position on close.', updateCheck: 'Check Update', updateCheck_help: 'Check update automatically.', analytics: 'Enable Google Analytics', analytics_help: 'Share anonymous device browser version information. Saladict author will offer prioritized support to popular devices and browsers.', opt: { reset: 'Reset Configs', reset_confirm: 'Reset to default settings. Confirm?', upload_error: 'Unable to save settings.', accent: { uk: 'UK', us: 'US' }, sel_blackwhitelist: 'Selection Black/White List', sel_blackwhitelist_help: 'Saladict will not react to selection in blacklisted pages.', pdf_blackwhitelist_help: 'Blacklisted PDF links will not jump to Saladict PDF Viewer.', contextMenus_description: 'Each context menus item can also be customized. Youdao and Google page translate are deprecated in favor of the official extensions.', contextMenus_edit: 'Edit Context Menus Items', contextMenus_url_rules: 'URL with %s in place of query.', baOpen: { popup_panel: 'Dict Panel', popup_fav: 'Add to Notebook', popup_options: 'Open Saladict Options', popup_standalone: 'Open Saladict Standalone Panel' }, openQsStandalone: 'Standalone Panel Options', pdfStandalone: { default: 'Never', always: 'Always', manual: 'Manual' } } }, matchPattern: { description: 'Specify URL as {URL Match Pattern} or {Regular Expression}. Empty fields will be removed.', url: 'URL Match Pattern', url_error: 'Incorrect URL Match Pattern.', regex: 'Regular Expression', regex_error: 'Incorrect Regular Expression.' }, searchMode: { icon: 'Show Icon', icon_help: 'A cute little icon pops up nearby the cursor.', direct: 'Direct Search', direct_help: 'Show dict panel directly.', double: 'Double Click', double_help: 'Show dict panel after double click selection.', holding: 'Hold a key', holding_help: 'After a selection is made, the selected key must be pressing when releasing mouse (Alt is "⌥ Option" on macOS. Meta key is "⌘ Command" on macOS and "⊞ Windows" for others.).', instant: 'Instant Capture', instant_help: 'Selection is automatically made near by the cursor.', instantDirect: 'Direct', instantKey: 'Key', instantKey_help: 'If "Direct" is chosen it is also recommeded setting browser shortkey to toggle Instant Capture. Otherwise browser text selection could be unable to perform.', instantDelay: 'Capture delay' }, profiles: { opt: { add_name: 'Add Profile Name', delete_confirm: 'Delete Profile "{{name}}". Confirm?', edit_name: 'Change Profile Name', help: 'Each profile represents an independent set of settings. Some of the settings (with {*} prefix) change according to profile. One may switch profiles by hovering on the menu icon on Dict Panel, or focus on the icon then hit {↓}.' } }, profile: { mtaAutoUnfold: 'Auto unfold multiline search box', waveform: 'Waveform Control', waveform_help: 'Display a button at the bottom of the Dict Panel for expanding the Waveform Control Panel which is only loaded after expansion.', stickyFold: 'Sticky Folding', stickyFold_help: 'Remembers manual dictionary folding/unfolding states when searching. Only last on the same page.', opt: { item_extra: 'This option may change base on "Profile".', mtaAutoUnfold: { always: 'Keep Unfolding', never: 'Never Unfold', once: 'Unfold Once', popup: 'Only On Browser Action', hide: 'Hide' }, dict_selected: 'Selected Dicts' } }, dict: { add: 'Add dicts', more_options: 'More Options', selectionLang: 'Selection Languages', selectionLang_help: 'Show this dictionary when selection contains words in the chosen languages.', defaultUnfold: 'Default Unfold', defaultUnfold_help: "If turned off, this dictionary won't start searching unless it's title bar is clicked.", selectionWC: 'Selection Word Count', selectionWC_help: 'Show this dictionary when selection word count meets the requirements. Set 999999 for unlimited words.', preferredHeight: 'Default Panel Height', preferredHeight_help: 'Maximum height on first appearance. Contents exceeding this height will be hidden. Set 999999 for unlimited height.', lang: { de: 'De', en: 'En', es: 'Es', fr: 'Fr', ja: 'Ja', kor: 'Kor', zhs: 'Zhs', zht: 'Zht' } }, syncService: { description: 'Sync settings.', start: 'Syncing. Do not close this page until finished.', finished: 'Syncing finished', success: 'Syncing success', failed: 'Syncing failed', close_confirm: 'Settings not saved. Close?', delete_confirm: 'Delete?', shanbay: { description: "Go to shanbay.com and log in first(must stay logged in). Note that it's a one-way sync(from Saladict to Shanbay). Only the new added words are synced. Words also need to be supported by Shanbay's database.", login: 'Will open shanbay.com. Please log in then come back and enable again.', sync_all: 'Upload all existing new words', sync_all_confirm: 'Too many new words in notebook. Saladict will upload in batches. Note that uploading too many words in short period would cause account banning which is unrecoverable. Confirm?', sync_last: 'Upload the last new word' }, eudic: { description: 'Before using Eudic to synchronize words, you must first create a default new word book on Eudic official website (my.eudic.net/home/index) (generally, it will be automatically generated and cannot be deleted after the first manual import). Pay attention not to synchronize frequently in a short time, which may cause temporary lock.', token: 'Authorization information', getToken: 'Get authorization', verify: 'Check authorization information', verified: 'Eudic authorization information checked successfully', enable_help: 'After opening, each new word added will be automatically synchronized to the Eudic default word book (salad to Eudic word book) in one direction, and only the new word itself will be synchronized (deleted out of synchronization)', token_help: 'Please confirm to set valid personal authorization information, otherwise the synchronization will fail. You can click the button at the bottom to check.', sync_all: 'Synchronize all new words', sync_help: 'Synchronize all existing new words in salad word book to the Eudic default word book (turn on the synchronization switch above at the same time and click save)', sync_all_confirm: 'Note that frequent synchronization in a short time may lead to lock temporarily. Are you sure to continue?' }, webdav: { description: 'Extension settings (including this) are synced via browser. New words notebook can be synced via WebDAV through settings here.', jianguo: 'See Jianguoyun for example', checking: 'Connecting...', exist_confirm: 'Saladict directory exists on server. Download it and merge with local data?', upload_confirm: 'Upload local data to Server right away?', verify: 'Verify server', verified: 'Successfully verified WebDAV server.', duration: 'Duration', duration_help: 'Data is guaranteed to be updated before upload. If you do not need real-time syncing across browsers, set a longer polling cycle to reduce CPU and memory footprint.', passwd: 'Password', url: 'Server Address', user: 'User Account' }, ankiconnect: { description: 'Please make sure Anki Connect plugin is installed and Anki is running. You can also update word to Anki in Word Editor.', checking: 'Checking...', deck_confirm: 'Deck "{{deck}}" does not exist in Anki. Generate a new deck?', deck_error: 'Unable to create deck "{{deck}}".', notetype_confirm: 'Note type "{{noteType}}" does not exist in Anki. Generate a new note type.', notetype_error: 'Unable to create note type "{{noteType}}".', upload_confirm: 'Sync local new words to Anki right away? Duplicated words (with same timestamp) will be skipped.', add_yourself: 'Please add it youself in Anki.', verify: 'Verify Anki Connect', verified: 'Successfully verified Anki Connect', enable_help: 'When enabled, each time a new word is added to Notebook it will also be ported to Anki automatically. Words that exist in Anki(with same "Date") can be force-updated in Word Editor.', host: 'Address', port: 'port', key: 'Key', key_help: 'Optional key can be added in Anki Connect config for identification.', deckName: 'Deck', deckName_help: 'If deck does not exist you can generate a default one automatically by clicking "Verify Anki Connect" below.', noteType: 'Note Type', noteType_help: 'Anki note type includes a set of fields and card type. If note type does not exist you can generate a default one automatically by clicking "Verify Anki Connect" below. DO NOT change field names when editing or adding card templates in Anki', tags: 'Tags', tags_help: 'Anki notes can include tags separated with commas.', escapeHTML: 'Escape HTML', escapeHTML_help: 'Escape HTML entities. Turn off if using HTML for manual layout.', syncServer: 'Sync Server', syncServer_help: 'Sync to server(e.g. AnkiWeb) after new words being added to local Anki.' } }, titlebarOffset: { title: 'Calibrate Titlebar Height', help: 'Different systems or browser settings may result in different titlebar height. Saladict will attempt to calibrate automatically. If you may adjust manually.', main: 'Normal', main_help: 'Normal windows may not have titlebar.', panel: 'Panel', panel_help: 'Saladict standalone quick search panel is a type of panel window.', calibrate: 'Auto-calibrate', calibrateSuccess: 'Calibration success', calibrateError: 'Calibration failed' }, headInfo: { acknowledgement: { title: 'Acknowledgement', yipanhuasheng: "for adding Merriam Webster's Dict, American Heritage Dict, Oxford Learner's Dict and Eudic Notebook sync service; and updating Urban Dict and Naver Dict", naver: 'for helping add Naver dict', shanbay: 'for adding Shanbay dict', trans_tw: 'for traditional Chinese translation', weblio: 'for helping add Weblio dict' }, contact_author: 'Contact Author', donate: 'Donate', instructions: 'Instructions', report_issue: 'Report Issue' }, form: { url_error: 'Incorrect URL.', number_error: 'Incorrect number.' }, preload: { title: 'Preload', auto: 'Auto search', auto_help: 'Search automatically when panel shows up.', clipboard: 'Clipboard', help: 'Preload content in search box when panel shows up.', selection: 'Page Selection' }, locations: { CENTER: 'Center', TOP: 'Top', RIGHT: 'Right', BOTTOM: 'Bottom', LEFT: 'Left', TOP_LEFT: 'Top Left', TOP_RIGHT: 'Top Right', BOTTOM_LEFT: 'Bottom Left', BOTTOM_RIGHT: 'Bottom Right' }, import_export_help: 'Configs are auto-synced via browser. Here you can also import/export manually. Backups are exported as plain text files. Please encrypt it yourself if needed.', import: { title: 'Import Configs', error: { title: 'Import Error', parse: 'Unable to parse backup. Incorrect format.', load: 'Unable to load backup. Browser cannot obtain the local file.', empty: 'No valid data found in the backup.' } }, export: { title: 'Export Configs', error: { title: 'Export Error', empty: 'No config to export.', parse: 'Unable to parse configs.' } }, dictAuth: { description: 'As the number of Saladict users grows, if you make heavily use of machine translation services it is recommended to register an account for better stability and accuracy. The account data will only be stored in the browser.', dictHelp: 'See the official website of {dict}.', manage: 'Manage Translator Accounts' }, third_party_privacy: 'Third Party Privacy', third_party_privacy_help: 'Saladict will not collect further information but search text and releated cookies will be sent to third party dictionary services(just like how you would search on their websites). If you do not want third party services to collect you data, remove the corresponding dictionaries at "Dictionaries" settings.', third_party_privacy_extra: 'Cannot be turned off as it is the core functionality of Saladict.', permissions: { success: 'Permission requested', cancel_success: 'Permission cancelled', failed: 'Permission request failed', cancelled: 'Permission request cancelled by user', missing: 'Missing permission "{{permission}}". Either grant it or disable related functions.', clipboardRead: 'Read Clipboard', clipboardRead_help: 'This permission is needed when clipboard preload is enable for popup panel or quick search panel.', clipboardWrite: 'Write Clipboard', clipboardWrite_help: 'This permission is needed when using titlebar menus to copy source/target text from machine translator.' }, unsupportedFeatures: { ff: 'Feature "{{feature}}" is not supported in Firefox.' } } ================================================ FILE: src/_locales/en/popup.ts ================================================ import { locale as _locale } from '../zh-CN/popup' export const locale: typeof _locale = { title: 'Saladict Browser Action Panel', app_active_title: 'Enable Inline Translator', app_temp_active_title: 'Temporary disabled to the page', instant_capture_pinned: ' (pinned) ', instant_capture_title: 'Enable Instant Capture', notebook_added: 'Added', notebook_empty: 'No selection found on the current page', notebook_error: 'Cannot add selected text to Notebook', page_no_response: 'Page no response', qrcode_title: 'Qrcode of the page' } ================================================ FILE: src/_locales/en/wordpage.ts ================================================ import { locale as _locale } from '../zh-CN/wordpage' export const locale: typeof _locale = { title: { history: 'Saladict Search History', notebook: 'Saladict Notebook' }, localonly: 'local only', column: { add: 'Add', date: 'Date', edit: 'Edit', note: 'Note', source: 'Source', trans: 'Translation', word: 'Word' }, delete: { title: 'Delete', all: 'Delete all', confirm: '. Confirm?', page: 'Delete page', selected: 'Delete selected' }, export: { title: 'Export', all: 'Export all', description: 'Describe the shape of each record: ', explain: 'How to export to ANKI and other tools', gencontent: 'Generated Content', linebreak: { default: 'Keep default linebreaks', n: 'replace linebreaks with \\n', br: 'replace linebreaks with
', p: 'replace linebreaks with

', space: 'replace linebreaks with space' }, page: 'Export page', placeholder: 'Placeholder', htmlescape: { title: 'Escape HTML characters in notes', text: 'Escape HTML' }, selected: 'Export selected' }, filterWord: { chs: 'Chinese', eng: 'English', word: 'Word', phrase: 'Phrase' }, wordCount: { selected: '{{count}} item selected', selected_plural: '{{count}} item selected', total: '{{count}} item total', total_plural: '{{count}} item total' } } ================================================ FILE: src/_locales/es/background.ts ================================================ import { locale as _locale } from '../zh-CN/background' export const locale: typeof _locale = { app: { off: 'Saladict desactivado. (El panel de búsqueda rápida sigue disponible)', tempOff: 'Saladict desactivado en la pestaña actual. (El panel de búsqueda rápida sigue disponible)', unsupported: 'El panel Saladict incrustado no es compatible con la pestaña actual. Utilice el panel Saladict independiente en su lugar.' } } ================================================ FILE: src/_locales/es/common.ts ================================================ import { locale as _locale } from '../zh-CN/common' export const locale: typeof _locale = { add: 'Añadir', delete: 'Eliminar', save: 'Guardar', cancel: 'CAncelar', edit: 'Editar', sort: 'Ordenar', rename: 'Renombrar', confirm: 'Confirmar', changes_confirm: 'Cambios no guardados. ¿Cerrar de todas formas?', delete_confirm: '¿Eliminar completamente el elemento?', max: 'Max', min: 'Min', name: 'Nombre', none: 'Ninguno', enable: 'Activar', enabled: 'Activado', disabled: 'Desactivado', blacklist: 'Lista negra', whitelist: 'Lista blanca', import: 'Importar', export: 'Exportar', lang: { chinese: 'Chino', chs: 'Chino', deutsch: 'Alemán', eng: 'Inglés', english: 'Inglés', french: 'Francés', japanese: 'Japonés', korean: 'Coreano', matchAll: 'Coincidir con todos los caracteres', minor: 'Menor', others: 'Otros', spanish: 'Español' }, unit: { mins: 'minutes', ms: 'ms', s: 's', word: 'words' }, note: { word: 'Word', trans: 'Translation', note: 'Note', context: 'Context', contextCloze: 'Context Cloze', date: 'Date', srcTitle: 'Source Title', srcLink: 'Source Link', srcFavicon: 'Source Favicon' }, profile: { daily: 'Daily Mode', sentence: 'Sentence Mode', default: 'Default Mode', scholar: 'Scholar Mode', translation: 'Translation Mode', nihongo: 'Japanese Mode' } } ================================================ FILE: src/_locales/es/content.ts ================================================ import { locale as _locale } from '../zh-CN/content' export const locale: typeof _locale = { chooseLang: 'elegir otro idioma', standalone: 'Panel de Saladict independiente', fetchLangList: 'Obtener la lista completa de idiomas', transContext: 'Retraducir', neverShow: 'Dejar de mostrar', fromSaladict: 'Desde el panel de Saladict', tip: { historyBack: 'Historial de búsqueda anterior', historyNext: 'Siguiente historial de búsqueda', searchText: 'Buscar texto', openOptions: 'Abrir opciones', addToNotebook: 'Agregar al cuaderno. Haga clic derecho para abrir el cuaderno', openNotebook: 'Abrir cuaderno', openHistory: 'Abrir historial', shareImg: 'Compartir como imagen', pinPanel: 'Fijar el panel', closePanel: 'Cerrar el panel', sidebar: 'Cambiar a modo barra lateral. Haga clic derecho para el lado derecho.', focusPanel: 'El panel gana foco al buscar', unfocusPanel: 'El panel no gana foco al buscar' }, wordEditor: { title: 'Agregar al cuaderno', wordCardsTitle: 'Otros resultados del cuaderno', deleteConfirm: '¿Eliminar del cuaderno?', closeConfirm: 'Los cambios no se guardarán. ¿Estás seguro de cerrar?', chooseCtxTitle: 'Elija los resultados traducidos', ctxHelp: 'Mantenga el formato [:: xxx ::] y --------------- si desea que Saladict maneje la selección de traducción y genere una tabla de Anki.' }, machineTrans: { switch: 'Cambiar idioma', sl: 'Idioma de origen', tl: 'Idioma de destino', auto: 'Detectar idioma', stext: 'Original', showSl: 'Mostrar fuente', copySrc: 'Copiar fuente', copyTrans: 'Copiar traducción', login: 'Proporcione {access token}.', dictAccount: 'access token' }, updateAnki: { title: 'Actualizar a Anki', success: 'Se actualizó correctamente la palabra a Anki.', failed: 'No se pudo actualizar la palabra a Anki.' } } ================================================ FILE: src/_locales/es/langcode.ts ================================================ import en from '@opentranslate/languages/locales/en.json' import { locale as _locale } from '../zh-CN/langcode' export const locale: typeof _locale = { ...en, default: 'Predeterminado', ara: 'Arabe', 'bs-Latn': 'Bosnio', bul: 'Búlgaro', cht: 'Chino (Tradicional)', dan: 'Danés', est: 'Estonio', fin: 'Finlandés', fra: 'Francés', iw: 'Hebreo', jp: 'Japonés', kor: 'Coreano', kr: 'Coreano', pt_BR: 'Brasileño', rom: 'Rumano', slo: 'Esloveno', spa: 'Español', swe: 'Sueco', tl: 'Tagalo (Filipino)', vie: 'Vietnamita', zh: 'Chino (Simplificado)', 'zh-CHS': 'Chino (Simplificado)', 'zh-CHT': 'Chino (Tradicional)' } ================================================ FILE: src/_locales/es/menus.ts ================================================ import { locale as _locale } from '../zh-CN/menus' export const locale: typeof _locale = { baidu_page_translate: 'Traductor web de baidu', baidu_search: 'Buscar en baidu', bing_dict: 'Bing diccionario', bing_search: 'Buscar en bing', caiyuntrs: 'Traductor de Lingocloud', cambridge: 'Cambridge', copy_pdf_url: 'Copiar URL de PDF al portapapeles', dictcn: 'Dictcn', etymonline: 'Etymonline', google_cn_page_translate: 'Traductor web de Google.cn', google_page_translate: 'Traductor de Google', google_search: 'Buscar en Google', google_translate: 'Traductor de Google', google_cn_translate: 'Traductor de Google.cn', guoyu: '國語辭典', history_title: 'Historial de búsqueda', iciba: 'iciba', liangan: '兩岸詞典', longman_business: 'Longman Business', manual_title: 'Manual', merriam_webster: 'Merriam Webster', microsoft_page_translate: 'Traductor web de Microsoft', notebook_title: 'Lista de palabras nuevas', notification_youdao_err: 'Youdao Page Translate 2.0 no responde.\nSaladict puede que no tenga permiso para acceder a esta página.\nIgnora este mensaje si el panel de Youdao se muestra.', oxford: 'Oxford', page_permission_err: 'Saladict "{{name}}" no tiene permiso para acceder a esta página.', page_translations: 'Traducciones de página', saladict: 'Saladict', saladict_standalone: 'Panel Saladict independiente', sogou: 'Traductor de Sogou', sogou_page_translate: 'Traductor web de Sogou', termonline: 'Termonline', view_as_pdf: 'Abrir en el visor de PDF', youdao: 'Youdao', youdao_page_translate: 'Traductor web de Youdao', youglish: 'YouGlish' } ================================================ FILE: src/_locales/es/options.ts ================================================ import { locale as _locale } from '../zh-CN/options' export const locale: typeof _locale = { title: 'Saladict Opciones', previewPanel: 'Panel de vista previa', shortcuts: 'Atajos de teclado', msg_update_error: 'Error al actualizar', msg_updated: 'Actualizado', msg_first_time_notice: '¡Bienvenido a Saladict!', msg_err_permission: 'No se ha podido solicitar el permiso "{{permission}}".', unsave_confirm: 'Hay cambios sin guardar. ¿Estás seguro de que quieres salir?', nativeSearch: 'Buscar con el motor de búsqueda nativo', firefox_shortcuts: 'Abra about:addons, haga clic en el botón superior derecho "engranaje", elija la última "Administrar accesos directos de extensión".', tutorial: 'Tutorial', page_selection: 'Selección de página', nav: { General: 'General', Notebook: 'Bloc de notas', Profiles: 'Perfiles', DictPanel: 'Panel de diccionario', SearchModes: 'Modos de búsqueda', Dictionaries: 'Diccionarios', DictAuths: 'Access Tokens', Popup: 'Popup Panel', QuickSearch: 'Busqueda rápida', Pronunciation: 'Pronunciación', PDF: 'PDF', ContextMenus: 'Menús de contexto', BlackWhiteList: 'Lista negra/Blanca', ImportExport: 'Importar/Exportar', Privacy: 'Privacidad', Permissions: 'Permisos', }, config: { active: 'Activar el traductor en línea', active_help: 'Si está desactivado, el traductor en línea no se mostrará en el panel de búsqueda rápida.', animation: 'Transiciones de animación', animation_help: 'Desactive las transiciones de animación para mejorar el rendimiento.', runInBg: 'Ejecutar en segundo plano', runInBg_help: 'Si está desactivado, Saladict se cerrará cuando se cierre la última ventana.', darkMode: ' Modo oscuro', langCode: 'Idioma de Saladict', editOnFav: 'Abrir WordEditor al guardar', editOnFav_help: 'Si está desactivado, WordEditor se abrirá cuando se agregue una palabra nueva.', searchHistory: 'Historial de búsqueda', searchHistory_help: 'Si está desactivado, el historial de búsqueda no se mostrará en el panel de búsqueda rápida.', searchHistoryInco: 'Incluir búsqueda de incógnito', ctxTrans: 'Traducir contexto', ctxTrans_help: 'Si está desactivado, el contexto no se mostrará en el panel de búsqueda rápida.', searchSuggests: 'Sugerencias de búsqueda', panelMaxHeightRatio: 'Altura máxima del panel', panelWidth: 'Ancho del panel', fontSize: 'Tamaño de fuente', bowlOffsetX: 'Desplazamiento X del icono de Saladict', bowlOffsetY: 'Desplazamiento Y del icono de Saladict', panelCSS: 'CSS personalizado', panelCSS_help: 'CSS personalizado. Para el panel de dictado, utilice .dictPanel-Root como raíz. Para diccionarios utilice .dictRoot o .d-{id} como raíz.', noTypeField: 'No hay selección en las regiones editables', noTypeField_help: 'Si la selección en regiones editables está prohibida, la extensión identificará los cuadros de entrada, las áreas de texto y otros editores de texto comunes como CodeMirror, ACE y Monaco.', touchMode: 'Modo táctil', touchMode_help: 'Activar la selección táctil', language: 'Selección de idiomas', language_help: 'Buscar cuando la selección contiene palabras en los idiomas elegidos.', language_extra: 'Tenga en cuenta que el japonés y el coreano también incluyen el chino. El francés, el alemán y el español también incluyen el inglés. Si se cancela el chino o el inglés mientras se seleccionan otros, sólo se comprueban las partes exclusivas de esos idiomas. Por ejemplo, los caracteres kana en japonés.', doubleClickDelay: 'Retraso de doble clic', mode: 'Seleccion normal', panelMode: 'Interior del panel Dict', pinMode: 'Cuando el panel está fijado', qsPanelMode: 'Cuando se abre el panel independiente', bowlHover: 'Icono al pasar el cursor por encima', bowlHover_help: 'Pase el ratón sobre el icono del cuenco para activar la búsqueda en lugar de hacer clic.', autopron: { cn: { dict: 'Autopronunciación en chino' }, en: { dict: 'Autopronunciación en inglés', accent: 'Preferencia de acento' }, machine: { dict: 'Autopronunciación de la máquina', src: 'Pronunciar a máquina', src_help: 'El diccionario de traducción automática debe añadirse y activarse en la siguiente lista para activar la pronunciación automática.', src_search: 'Leer texto original', src_trans: 'Leer el texto de la traducción' } }, pdfSniff: 'Activar PDF Sniffer', pdfSniff_help: 'Si está activada, los enlaces PDF se capturarán automáticamente.', pdfSniff_extra: 'Se recomienda {search selected text outside of browser} con su propio lector local favorito.', pdfStandalone: 'Panel independiente', pdfStandalone_help: 'Abrir visor de PDF en panel independiente.', baWidth: 'Anchura', baWidth_help: 'Navegador Acción Panel con. Si se elige un valor negativo, se utilizará la anchura del panel.', baHeight: 'Altura', baHeight_help: 'Navegador Acción Panel altura.', baOpen: 'Acción del navegador', baOpen_help: 'Al pulsar el icono de acción del navegador en la barra de herramientas (junto a la barra de direcciones). Los elementos son los mismos que los menús contextuales, que pueden añadirse o editarse en la página de configuración de menús contextuales.', tripleCtrl: 'Activar tecla corta Ctrl', tripleCtrl_help: 'Pulse {⌘ Command}(macOS) o {Ctrl}(Otros) tres veces (o con la tecla rápida del navegador) para acceder al panel del diccionario.', defaultPinned: 'Fijado cuando aparece', qsLocation: 'Ubicación', qsFocus: 'Concéntrese cuando aparezca', qsStandalone: 'Independiente', qsStandalone_help: 'Renderizar el panel de dictado en una ventana independiente. Puede {buscar texto seleccionado fuera del navegador}.', qssaSidebar: 'Barra lateral', qssaSidebar_help: 'Renderizar el panel de dictado en la barra lateral.', qssaHeight: 'Ventana altura', qssaPageSel: 'Selección de página', qssaPageSel_help: 'Seleccionar automáticamente el texto de la página.', qssaRectMemo: 'Recordar tamaño y posición', qssaRectMemo_help: 'Recuerde el tamaño y la posición del panel independiente al cerrar.', updateCheck: 'Comprobar actualizaciones', updateCheck_help: 'Compruebe si hay actualizaciones automáticamente.', analytics: 'Activar Google Analytics', analytics_help: 'Compartir información anónima sobre la versión del navegador del dispositivo. El autor de Saladict ofrecerá soporte prioritario a los dispositivos y navegadores más populares.', opt: { reset: 'Restablecer configuración', reset_confirm: '¿Estás seguro de que quieres restablecer la configuración?', upload_error: 'Error al cargar la configuración', accent: { uk: 'UK', us: 'US' }, sel_blackwhitelist: 'Lista negra y blanca de selección', sel_blackwhitelist_help: 'Saladict no reaccionará a la selección en páginas de la lista negra.', pdf_blackwhitelist_help: 'Los enlaces PDF de la lista negra no saltarán a Saladict PDF Viewer.', contextMenus_description: 'Cada elemento del menú contextual también se puede personalizar. Youdao y Google Traductor están obsoletos en favor de las extensiones oficiales.', contextMenus_edit: 'Editar elementos de menús contextuales', contextMenus_url_rules: 'URL con %s en lugar de query.', baOpen: { popup_panel: 'Panel de diccionario', popup_fav: 'Añadir al bloc de notas', popup_options: 'Abrir opciones de Saladict', popup_standalone: 'Panel independiente Open Saladict' }, openQsStandalone: 'Opciones de panel independiente', pdfStandalone: { default: 'Nunca', always: 'Siempre', manual: 'Manual' } } }, matchPattern: { description: 'Especifique URL como {URL Patrón de Coincidencia} o {Expresión Regular}. Se eliminarán los campos vacíos.', url: 'URL Patrón de Coincidencia', url_error: 'Patrón de coincidencia de URL incorrecto.', regex: 'Expresión regular', regex_error: 'Expresión regular incorrecta.' }, searchMode: { icon: 'Mostrar icono', icon_help: 'Aparecerá un bonito icono cerca del cursor.', direct: 'Busqueda directa', direct_help: 'Mostrar directamente el panel dict.', double: 'Doble clic', double_help: 'Mostrar panel de dict después de la selección de doble clic.', holding: 'Mantener pulsado', holding_help: 'Después de realizar una selección, la tecla seleccionada debe estar pulsada al soltar el ratón (Alt es "⌥ Opción" en macOS. Meta es "⌘ Comando" en macOS y "⊞ Windows" para los demás).', instant: 'Captura instantánea', instant_help: 'La selección se realiza automáticamente cerca del cursor.', instantDirect: 'Directo', instantKey: 'Tecla', instantKey_help: 'Si se elige "Directo", también se recomienda configurar la tecla de acceso directo del navegador para activar la Captura instantánea. De lo contrario, la selección de texto en el navegador podría ser imposible.', instantDelay: 'Retraso de captura' }, profiles: { opt: { add_name: 'Añadir nombre de perfil', delete_confirm: 'Eliminar Perfil "{{name}}". ¿Confirmar?', edit_name: 'Cambiar el nombre del perfil', help: 'Cada perfil representa un conjunto independiente de ajustes. Algunos de los ajustes (con el prefijo {*}) cambian según el perfil. Para cambiar de perfil, sitúe el cursor sobre el icono de menú del panel de dictado, o bien sitúe el cursor sobre el icono y pulse {↓}.' } }, profile: { mtaAutoUnfold: 'Despliegue automático del cuadro de búsqueda multilínea', waveform: 'Forma de onda', waveform_help: 'Muestra un botón en la parte inferior del panel de dictado para expandir el panel de control de forma de onda que sólo se carga después de la expansión.', stickyFold: 'Plegado adhesivo', stickyFold_help: 'Recuerda los estados de plegado/desplegado del diccionario manual al buscar. Sólo dura en la misma página.', opt: { item_extra: 'Esta opción puede cambiar en función del "Perfil".', mtaAutoUnfold: { always: 'Siempre Desplegar', never: 'Nunca Desplegar', once: 'Desplegar una vez', popup: 'Sólo en la acción del navegador', hide: 'Ocultar' }, dict_selected: 'Seleccionar diccionarios', } }, dict: { add: 'Añadir diccionarios', more_options: 'Mas opciones', selectionLang: 'Seleccionar idiomas', selectionLang_help: 'Muestra este diccionario cuando la selección contiene palabras en los idiomas elegidos.', defaultUnfold: 'Despliegue por defecto', defaultUnfold_help: "Si está desactivado, este diccionario no iniciará la búsqueda a menos que se haga clic en su barra de título.", selectionWC: 'Selección Número de palabras', selectionWC_help: 'Muestre este diccionario cuando el recuento de palabras de la selección cumpla los requisitos. Establezca 999999 para un número ilimitado de palabras.', preferredHeight: 'Altura predeterminada del panel', preferredHeight_help: 'Altura máxima en la primera aparición. Los contenidos que superen esta altura se ocultarán. Establezca 999999 para una altura ilimitada.', lang: { de: 'De', en: 'En', es: 'Es', fr: 'Fr', ja: 'Ja', kor: 'Kor', zhs: 'Zhs', zht: 'Zht' } }, syncService: { description: 'Ajustes de sincronización.', start: 'Sincronizando. No cierre esta página hasta que haya terminado.', finished: 'Sincronización finalizada', success: 'Sincronización correcta', failed: 'Sincronización fallida', close_confirm: 'No se ha guardado la configuración. ¿Cerrar?', delete_confirm: '¿Eliminar?', shanbay: { description: " Vaya a shanbay.com y conéctese primero(debe permanecer conectado). Tenga en cuenta que se trata de una sincronización unidireccional (de Saladict a Shanbay). Sólo se sincronizan las nuevas palabras añadidas. Las palabras también deben ser compatibles con la base de datos de Shanbay.", login: 'Se abrirá shanbay.com. Por favor, inicie sesión y luego volver y habilitar de nuevo.', sync_all: 'Cargar todas las palabras nuevas existentes', sync_all_confirm: 'Demasiadas palabras nuevas en el cuaderno. Saladict cargará por lotes. Ten en cuenta que si subes demasiadas palabras en un periodo corto de tiempo, tu cuenta será bloqueada y no se podrá recuperar. ¿Confirmar?', sync_last: 'Cargar la última palabra nueva' }, eudic: { description: 'Antes de utilizar Eudic para sincronizar palabras, primero debe crear un nuevo libro de palabras predeterminado en el sitio web oficial de Eudic (my.eudic.net/home/index) (por lo general, se generará automáticamente y no se podrá eliminar después de la primera importación manual). Preste atención a no sincronizar con frecuencia en poco tiempo, ya que podría provocar un bloqueo temporal.', token: 'Información sobre la autorización', getToken: 'Obtener autorización', verify: 'Comprobar la información de autorización', verified: 'Información de autorización Eudic comprobada correctamente', enable_help: 'Tras la apertura, cada nueva palabra añadida se sincronizará automáticamente con el libro de palabras predeterminado de Eudic (ensalada al libro de palabras de Eudic) en una dirección, y sólo se sincronizará la nueva palabra en sí (eliminada fuera de sincronización)', token_help: 'Por favor, confirme que la información de autorización personal es válida, de lo contrario la sincronización fallará. Puede hacer clic en el botón de la parte inferior para comprobarlo.', sync_all: 'Sincronizar todas las palabras nuevas', sync_help: 'Sincronice todas las palabras nuevas existentes en el libro de palabras de la ensalada con el libro de palabras predeterminado de Eudic (active el interruptor de sincronización anterior al mismo tiempo y haga clic en guardar).', sync_all_confirm: 'Tenga en cuenta que una sincronización frecuente en poco tiempo puede provocar un bloqueo temporal. ¿Está seguro de continuar?' }, webdav: { description: 'La configuración de las extensiones (incluida esta) se sincroniza a través del navegador. El cuaderno de nuevas palabras se puede sincronizar mediante WebDAV a través de la configuración aquí.', jianguo: 'Véase Jianguoyun, por ejemplo', checking: 'Conectando...', exist_confirm: 'El directorio Saladict existe en el servidor. ¿Descargarlo y fusionarlo con los datos locales?', upload_confirm: '¿Subir los datos locales al servidor de inmediato?', verify: 'Verificar servidor', verified: 'Verificado con éxito el servidor WebDAV.', duration: 'Duracion', duration_help: 'Se garantiza que los datos se actualizan antes de cargarlos. Si no necesita sincronización en tiempo real entre navegadores, establezca un ciclo de sondeo más largo para reducir el consumo de CPU y memoria.', passwd: 'Contraseña', url: 'Dirección del servidor', user: 'Usuario', }, ankiconnect: { description: 'Por favor, asegúrate de que el plugin Anki Connect está instalado y Anki se está ejecutando. También puede actualizar la palabra a Anki en el editor de Word.', checking: 'Verificando...', deck_confirm: 'El tablero "{{deck}}" no existe en Anki. ¿Generar un nuevo tablero?', deck_error: 'No se puede crear el tablero "{{deck}}".', notetype_confirm: 'El tipo de nota "{{noteType}}" no existe en Anki. Genera un nuevo tipo de nota.', notetype_error: 'No se puede crear el tipo de nota"{{noteType}}".', upload_confirm: '¿Sincronizar nuevas palabras locales a Anki inmediatamente? Las palabras duplicadas (con la misma marca de tiempo) se omitirán.', add_yourself: 'Por favor, añádelo tú mismo en Anki.', verify: 'Verificar Anki Connect', verified: 'Anki Connect verificado correctamente.', enable_help: 'Cuando está activada, cada vez que se añade una nueva palabra al Cuaderno, también se transfiere automáticamente a Anki. Las palabras que existen en Anki (con la misma "Fecha") pueden ser forzadas a actualizarse en el Editor de Palabras.', host: 'Dirección', port: 'Puerto', key: 'Clave', key_help: 'Se puede añadir una clave opcional en la configuración de Anki Connect para la identificación.', deckName: 'Tablero', deckName_help: 'Si el tablero no existe, puedes generar uno automáticamente haciendo clic en "Verificar Anki Connect" más abajo.', noteType: 'Tipo de nota', noteType_help: 'El tipo de nota Anki incluye un conjunto de campos y un tipo de tarjeta. Si el tipo de nota no existe puedes generar uno por defecto automáticamente haciendo clic en "Verificar Anki Connect" más abajo. NO cambie los nombres de los campos cuando edite o añada plantillas de tarjetas en Anki', tags: 'Etiquetas', tags_help: 'Anki notes can include tags separated with commas.', escapeHTML: 'Escapar HTML', escapeHTML_help: 'Escapar entidades HTML. Desactivar si se utiliza HTML para la maquetación manual.', syncServer: 'Sincronizar con el servidor', syncServer_help: 'Sincronización con el servidor (p.e. AnkiWeb) después de añadir nuevas palabras al Anki local.' } }, titlebarOffset: { title: 'Calibración de la altura de la barra de título', help: 'La altura de la barra de título puede variar según el sistema o la configuración del navegador. Saladict intentará calibrarla automáticamente. Si puede ajustar manualmente.', main: 'Normal', main_help: 'Las ventanas normales pueden no tener barra de título.', panel: 'Panel', panel_help: 'El panel de búsqueda rápida independiente de Saladict es un tipo de ventana de panel.', calibrate: 'Auto-calibrate', calibrateSuccess: 'Calibración correcta', calibrateError: 'Error de calibración' }, headInfo: { acknowledgement: { title: 'Reconocimiento', yipanhuasheng: "por añadir los diccionarios Merriam Webster's Dict, American Heritage Dict, Oxford Learner's Dict y el servicio de sincronización Eudic Notebook; y por actualizar los diccionarios Urban Dict y Naver Dict.", naver: 'por ayudar a añadir Naver dict', shanbay: 'por añadir Shanbay dict', trans_tw: 'por la traducción al chino tradicional', weblio: 'por ayudar a añadir Weblio dict' }, contact_author: 'Contactar al autor', donate: 'Donar', instructions: 'Instrucciones', report_issue: 'Informar de un problema', }, form: { url_error: 'URL incorrecta.', number_error: 'Numero incorrecto.' }, preload: { title: 'Precarga', auto: 'Búsqueda automática', auto_help: 'Búsqueda automática cuando aparece el panel.', clipboard: 'Clipboard', help: 'Precarga de contenido en el cuadro de búsqueda cuando aparece el panel.', selection: 'Selección de página' }, locations: { CENTER: 'Centrado', TOP: 'Arriba', RIGHT: 'Derecha', BOTTOM: 'Abajo', LEFT: 'Izquierda', TOP_LEFT: 'Arriba a la izquierda', TOP_RIGHT: 'Arriba a la derecha', BOTTOM_LEFT: 'Abajo a la izquierda', BOTTOM_RIGHT: 'Abajo a la derecha' }, import_export_help: 'Las configuraciones se sincronizan automáticamente a través del navegador. Aquí también puede importar/exportar manualmente. Las copias de seguridad se exportan como archivos de texto sin formato. Por favor, codifíquelos usted mismo si es necesario.', import: { title: 'Importar Configuraciones', error: { title: 'Error de importación', parse: 'No se ha podido analizar la copia de seguridad. Formato incorrecto.', load: 'No se puede cargar la copia de seguridad. El navegador no puede obtener el archivo local.', empty: 'No se han encontrado datos válidos en la copia de seguridad.' } }, export: { title: 'Exportar Configuraciones', error: { title: 'Error de exportación', empty: 'No hay configuración para exportar.', parse: 'No se pueden analizar las configuraciones.' } }, dictAuth: { description: 'A medida que crece el número de usuarios de Saladict, si hace un uso intensivo de los servicios de traducción automática se recomienda registrar una cuenta para mejorar la estabilidad y la precisión. Los datos de la cuenta sólo se almacenarán en el navegador.', dictHelp: 'Consulte el sitio web oficial de {dict}.', manage: 'Gestionar cuentas de traductor' }, third_party_privacy: 'Privacidad de terceros', third_party_privacy_help: 'Saladict no recopilará más información, pero el texto de la búsqueda y las cookies correspondientes se enviarán a servicios de diccionarios de terceros (igual que si buscara en sus sitios web). Si no desea que los servicios de terceros recopilen sus datos, elimine los diccionarios correspondientes en la configuración de "Diccionarios".', third_party_privacy_extra: 'No se puede desactivar, ya que es la funcionalidad principal de Saladict.', permissions: { success: 'Permiso solicitado', cancel_success: 'Permiso cancelado', failed: 'Solitud de permiso fallida', cancelled: 'Solicitud de permiso cancelada por el usuario', missing: 'Falta el permiso "{{permission}}". Concederlo o desactivar las funciones relacionadas.', clipboardRead: 'Leer portapapeles', clipboardRead_help: 'Este permiso es necesario cuando la precarga del portapapeles está activada para el panel emergente o el panel de búsqueda rápida.', clipboardWrite: 'Escribir en el portapapeles', clipboardWrite_help: 'Este permiso es necesario cuando se utilizan los menús de la barra de títulos para copiar texto de origen/destino del traductor automático.' }, unsupportedFeatures: { ff: 'La característica "{{feature}}" no es compatible con Firefox.' } } ================================================ FILE: src/_locales/es/popup.ts ================================================ import { locale as _locale } from '../zh-CN/popup' export const locale: typeof _locale = { title: 'Panel de acciones del navegador de Saladict', app_active_title: 'Activar el traductor en línea', app_temp_active_title: 'Desactivación temporal de la página', instant_capture_pinned: ' (fijado) ', instant_capture_title: 'Activar captura instantánea', notebook_added: 'Añadido', notebook_empty: 'No se ha encontrado ninguna selección en la página actual', notebook_error: 'No se puede añadir el texto seleccionado al bloc de notas', page_no_response: 'La pagina no responde', qrcode_title: 'Qrcode de la página' } ================================================ FILE: src/_locales/es/wordpage.ts ================================================ import { locale as _locale } from '../zh-CN/wordpage' export const locale: typeof _locale = { title: { history: 'Historial de búsqueda de Saladict', notebook: 'Bloc de notas Saladict' }, localonly: 'Solo local', column: { add: 'Añadir', date: 'Fecha', edit: 'Editar', note: 'Nota', source: 'Fuente', trans: 'Traducción', word: 'Palabra' }, delete: { title: 'Eliminar', all: 'Eliminar todo', confirm: '. ¿Desea eliminarlo?', page: 'Eliminar página', selected: 'Eliminar seleccionado' }, export: { title: 'Exportar', all: 'Exportar todo', description: 'Exportar a un archivo de texto', explain: 'Cómo exportar a ANKI y otras herramientas', gencontent: 'Generar contenido', linebreak: { default: 'Mantener los saltos de línea por defecto', n: 'sustituir los saltos de línea por \\n', br: 'sustituir los saltos de línea por
', p: 'sustituir los saltos de línea por

', space: 'sustituir los saltos de línea por espacios' }, page: 'Exportar página', placeholder: 'Marcador', htmlescape: { title: 'Caracteres HTML de escape en las notas', text: 'Escape HTML' }, selected: 'Exportar seleccionado', }, filterWord: { chs: 'Chino', eng: 'Inglés', word: 'Palabra', phrase: 'Frase', }, wordCount: { selected: '{{count}} elemento seleccionado', selected_plural: '{{count}} elemento seleccionado', total: '{{count}} elemento total', total_plural: '{{count}} elemento total' } } ================================================ FILE: src/_locales/manifest/en/messages.json ================================================ { "extension_name": { "description": "Extension name", "message": "Saladict - Pop-up Dictionary and Page Translator" }, "extension_short_name": { "description": "Extension short name", "message": "Saladict" }, "extension_description": { "description": "Description of extension", "message": "Saladict is an all-in-one professional pop-up dictionary and page translator which supports multiple search modes, page translations, new word notebook and PDF selection searching." }, "command_toggle_active": { "message": "Toggle inline translator" }, "command_toggle_instant": { "message": "Toggle instant capture" }, "command_open_quick_search": { "message": "Open or highlight standalone dict panel" }, "command_open_google": { "message": "Open Google Translate" }, "command_open_youdao": { "message": "Open Youdao Translate" }, "command_open_caiyun": { "message": "Open LingoCloud Translate" }, "command_open_pdf": { "message": "Open current PDF in Saladict" }, "command_search_clipboard": { "message": "Search clipboard content in Standalone Panel" }, "command_next_history": { "message": "Next Search History" }, "command_prev_history": { "message": "Previous Search History" }, "command_next_profile": { "message": "Next Profile" }, "command_prev_profile": { "message": "Previous Profile" }, "command_profile_1": { "message": "First Profile" }, "command_profile_2": { "message": "Second Profile" }, "command_profile_3": { "message": "Third Profile" }, "command_profile_4": { "message": "Fourth Profile" }, "command_profile_5": { "message": "Fifth Profile" }, "command_add_notebook": { "message": "Add to Notebook" } } ================================================ FILE: src/_locales/manifest/np/messages.json ================================================ { "extension_name": { "description": "Extension name", "message": "सलाडिक्ट - पप-अप शब्दकोश र पृष्ठ अनुवादक" }, "extension_short_name": { "description": "Extension short name", "message": "सलाडिक्ट" }, "extension_description": { "description": "Description of extension", "message": "सलाडिक्ट एक पेशेवर पप-अप शब्दकोश र पृष्ठ अनुवादक हो जसले बहु भाषा खोज , पृष्ठ अनुवाद, नयाँ शब्द नोटबुक र PDF खोजीलाई समर्थ छ।" }, "command_toggle_active": { "message": "इनलाइन अनुवादक टगल गर्नुहोस्" }, "command_toggle_instant": { "message": "तत्काल क्याप्चर टगल गर्नुहोस्" }, "command_open_quick_search": { "message": "स्ट्यान्डअलोन डिक्ट प्यानल खोल्नुहोस् वा हाइलाइट गर्नुहोस्" }, "command_open_google": { "message": "Google अनुवादक खोल्नुहोस्" }, "command_open_youdao": { "message": "Youdao अनुवाद खोल्नुहोस्" }, "command_open_caiyun": { "message": "LingoCloud अनुवाद खोल्नुहोस्" }, "command_open_pdf": { "message": "हालको PDF सलाडिक्टमा खोल्नुहोस् " }, "command_search_clipboard": { "message": "स्ट्यान्डअलोन प्यानलमा क्लिपबोर्ड सामग्री खोज्नुहोस्" }, "command_next_history": { "message": "अर्को खोज इतिहास" }, "command_prev_history": { "message": "अघिल्लो खोज इतिहास" }, "command_next_profile": { "message": "अर्को प्रोफाइल" }, "command_prev_profile": { "message": "अघिल्लो प्रोफाइल" }, "command_profile_1": { "message": "पहिलो प्रोफाइल" }, "command_profile_2": { "message": "दोस्रो प्रोफाइल" }, "command_profile_3": { "message": "तेस्रो प्रोफाइल" }, "command_profile_4": { "message": "चौथो प्रोफाइल" }, "command_profile_5": { "message": "पाँचौं प्रोफाइल" }, "command_add_notebook": { "message": "नोटबुकमा थप्नुहोस्" } } ================================================ FILE: src/_locales/manifest/zh_CN/messages.json ================================================ { "extension_name": { "description": "Extension name", "message": "沙拉查词-聚合词典划词翻译" }, "extension_short_name": { "description": "Extension short name", "message": "Saladict" }, "extension_description": { "description": "Description of extension", "message": "Saladict 沙拉查词是一款专业划词翻译扩展,为交叉阅读而生。大量权威词典涵盖中英日韩法德西语,支持复杂的划词操作、网页翻译、生词本与 PDF 浏览。" }, "command_toggle_active": { "message": "鼠标划词翻译开关" }, "command_toggle_instant": { "message": "鼠标悬浮取词开关" }, "command_open_quick_search": { "message": "打开独立词典窗口" }, "command_open_google": { "message": "对当前页启用谷歌翻译" }, "command_open_youdao": { "message": "对当前页启用有道翻译" }, "command_open_caiyun": { "message": "对当前页启用彩云小译" }, "command_open_pdf": { "message": "在 Saladict 中浏览此 PDF" }, "command_search_clipboard": { "message": "在独立窗口中搜索剪贴板内容" }, "command_next_history": { "message": "下一个临时查询历史" }, "command_prev_history": { "message": "上一个临时查询历史" }, "command_next_profile": { "message": "下一个情景模式" }, "command_prev_profile": { "message": "上一个情景模式" }, "command_profile_1": { "message": "第一个情景模式" }, "command_profile_2": { "message": "第二个情景模式" }, "command_profile_3": { "message": "第三个情景模式" }, "command_profile_4": { "message": "第四个情景模式" }, "command_profile_5": { "message": "第五个情景模式" }, "command_add_notebook": { "message": "加入生词本" } } ================================================ FILE: src/_locales/manifest/zh_TW/messages.json ================================================ { "extension_name": { "description": "Extension name", "message": "沙拉查詞-多字典滑鼠選字翻譯" }, "extension_short_name": { "description": "Extension short name", "message": "Saladict" }, "extension_description": { "description": "Description of extension", "message": "Saladict 沙拉查詞是一款專業滑鼠選字翻譯套件,為交叉閱讀而生。大量權威字典涵蓋中英日韓法德西語,支援複雜的選字操作、網頁翻譯、生字本与 PDF 瀏覽。" }, "command_toggle_active": { "message": "滑鼠選字翻譯開關" }, "command_toggle_instant": { "message": "滑鼠懸浮取詞開關" }, "command_open_quick_search": { "message": "開啟獨立詞典視窗" }, "command_open_google": { "message": "對此頁面使用 Google 翻譯" }, "command_open_youdao": { "message": "對此頁面使用有道翻譯" }, "command_open_caiyun": { "message": "對當前頁啟用彩雲小譯" }, "command_open_pdf": { "message": "在 Saladict 中瀏覽此 PDF" }, "command_search_clipboard": { "message": "在獨立視窗中搜尋剪貼簿內容" }, "command_next_history": { "message": "下一個臨時查詢歷史" }, "command_prev_history": { "message": "上一個臨時查詢歷史" }, "command_next_profile": { "message": "下一個情景模式" }, "command_prev_profile": { "message": "上一個情景模式" }, "command_profile_1": { "message": "第一個情景模式" }, "command_profile_2": { "message": "第二個情景模式" }, "command_profile_3": { "message": "第三個情景模式" }, "command_profile_4": { "message": "第四個情景模式" }, "command_profile_5": { "message": "第五個情景模式" }, "command_add_notebook": { "message": "加入生詞本" } } ================================================ FILE: src/_locales/ne/background.ts ================================================ import { locale as _locale } from '../zh-CN/background' export const locale: typeof _locale = { app: { off: 'सलाडिक्ट असक्षम। (द्रुत खोज प्यानल अझै उपलब्ध छ)', tempOff: 'हालको ट्याबमा सलाडिक्ट असक्षम पारियो। (द्रुत खोज प्यानल अझै उपलब्ध छ)', unsupported: 'हालको ट्याबका लागि एम्बेडेड सलाडिक प्यानल असमर्थित छ। यसको सट्टा स्ट्यान्डअलोन सलाडिक्ट प्यानल प्रयोग गर्नुहोस्।' } } ================================================ FILE: src/_locales/ne/common.ts ================================================ import { locale as _locale } from '../zh-CN/common' export const locale: typeof _locale = { add: 'थप्नुहोस्', delete: 'हटाउनुहोस्', save: 'बचत गर्नुहोस्', cancel: 'रद्द गर्नुहोस्', edit: 'सम्पादन', sort: 'क्रमबद्ध', rename: 'पुनःनामकरण', confirm: 'पुष्टि गर्नुहोस्', changes_confirm: 'परिवर्तनहरू बचत गरिएका छैनन्। जे भए पनि बन्द?', delete_confirm: 'वस्तु पूर्ण रूपमा मेटाउने', max: 'अधिकतम', min: 'न्यूनतम', name: 'नाम', none: 'खाली', enable: 'सक्षम गर्नुहोस्', enabled: 'सक्षम गरिएको', disabled: 'असक्षम गरिएको', blacklist: 'कालो सूची', whitelist: 'सेतो सूची', import: 'आयात गर्नुहोस्', export: 'निर्यात गर्नुहोस्', lang: { chinese: 'चिनियाँ', chs: 'चिनियाँ', deutsch: 'डेउस्च', eng: 'अंग्रेजी', english: 'अंग्रेजी', french: 'फ्रान्सेली', japanese: 'जापानी', korean: 'कोरियाली', matchAll: 'सबै अक्षर मिलाउनुहोस्', minor: 'झिनो', others: 'अन्य', spanish: 'स्पेनिस' }, unit: { mins: 'मिनेट', ms: 'मिसे', s: 'सेकेन्ड', word: 'शब्द' }, note: { word: 'शब्द', trans: 'अनुवाद', note: 'टिप्पणी', context: 'सन्दर्भ', contextCloze: 'सन्दर्भ क्लोज', date: 'मिति', srcTitle: 'स्रोत शीर्षक', srcLink: 'स्रोत लिंक', srcFavicon: 'स्रोत फेभिकन' }, profile: { daily: 'दैनिक मोड', sentence: 'वाक्य मोड', default: 'पूर्वनिर्धारित मोड', scholar: 'विद्वान मोड', translation: 'अनुवाद मोड', nihongo: 'जापानी मोड' } } ================================================ FILE: src/_locales/ne/content.ts ================================================ import { locale as _locale } from '../zh-CN/content' export const locale: typeof _locale = { chooseLang: 'अर्को भाषा छान्नुहोस्', standalone: 'सलाडिक्ट स्ट्यान्डअलोन प्यानल', fetchLangList: 'पूर्ण भाषा सूची तान्नुहोस्', transContext: 'पुनः अनुवाद गर्नुहोस्', neverShow: 'देखाउन रोक्नुहोस्', fromSaladict: 'सलाडिक्ट प्यानलबाट', tip: { historyBack: 'अघिल्लो खोज इतिहास', historyNext: 'पछिल्लो खोज इतिहास', searchText: 'पाठ खोज्नुहोस्', openOptions: 'विकल्प खोल्नुहोस्', addToNotebook: 'नोटबुकमा जोड्नुहोस्। नोटबुक खोल्न राइट क्लिक गर्नुहोस्', openNotebook: 'नोटबुक खोल्नुहोस्', openHistory: 'इतिहास खोल्नुहोस्', shareImg: 'तस्विर रूपमा साझा गर्नुहोस्', pinPanel: 'प्यानल पिन गर्नुहोस्', closePanel: 'प्यानल बन्द गर्नुहोस्', sidebar: 'साइडबार मोडमा स्विच गर्नुहोस्। दायाँ तर्फ दायाँ क्लिक गर्नुहोस्।', focusPanel: 'खोज गर्दा प्यानलले फोकस गर्ने', unfocusPanel: 'खोज गर्दा प्यानलले फोकस नगर्ने' }, wordEditor: { title: 'नोटबुकमा जोड्नुहोस्', wordCardsTitle: 'नोटबुकबाट अन्य परिणामहरू', deleteConfirm: 'नोटबुकबाट हटाउनुहोस्?', closeConfirm: 'परिवर्तनहरू सुरक्षित हुँदैनन्। के तपाईँ बन्द गर्न चाहानुहुन्छ?', chooseCtxTitle: 'अनुवाद गरिएको परिणामहरू छान्नुहोस्', ctxHelp: 'अनुवाद छान्न र एन्की तालिका उत्पन्न गर्न सलाडिकलाई निर्देशन गर्न यदि तपाईँले [:: xxx ::] र --------------- ढाँचा राख्न चाहानुहुन्छ भने।' }, machineTrans: { switch: 'भाषा बदल्नुहोस्', sl: 'स्रोत भाषा', tl: 'लक्षित भाषा', auto: 'भाषा पत्ता लगाउनुहोस्', stext: 'सक्कल', showSl: 'सोर्स देखाउनुहोस्', copySrc: 'स्रोत कपि गर्नुहोस्', copyTrans: 'अनुवाद कपि गर्नुहोस्', login: 'कृपया {एसेस टोकन्} प्रदान गर्नुहोस्।', dictAccount: 'एसेस टोकन्' }, updateAnki: { title: 'एन्कीमा अद्यावधिक गर्नुहोस्', success: 'शब्द एन्कीमा सफलतापूर्वक अद्यावधिक गरियो।', failed: 'शब्द एन्कीमा अद्यावधिक गर्न असफल भयो।' } } ================================================ FILE: src/_locales/ne/langcode.ts ================================================ import en from '@opentranslate/languages/locales/en.json' import { locale as _locale } from '../zh-CN/langcode' export const locale: typeof _locale = { ...en, default: 'पुर्वनिर्धारित', ne_NP: 'नेपाली', ara: 'अरबी', 'bs-Latn': 'बोस्नियाली (ल्याटिन)', bul: 'बुल्गेरियाली', cht: 'चिनियाँ (परम्परागत)', dan: 'डेनिस', est: 'इस्टोनियाली', fin: 'फिनिस', fra: 'फ्रान्सेली', iw: 'हिब्रु', jp: 'जापानी', kor: 'कोरियाली', kr: 'कोरियाली', pt_BR: 'पोर्तुगी (ब्राजिल)', rom: 'रोमानियाली', slo: 'स्लोभाकियाली', spa: 'स्पेनिस', swe: 'स्विडिस', tl: 'फिलिपिनी (तागालोग)', vie: 'भियतनामी', zh: 'चिनियाँ (सरलिकृत)', 'zh-CHS': 'चिनियाँ (सरलिकृत)', 'zh-CHT': 'चिनियाँ (परम्परागत)', } ================================================ FILE: src/_locales/ne/menus.ts ================================================ import { locale as _locale } from '../zh-CN/menus' export const locale: typeof _locale = { baidu_page_translate: 'बाइडु पृष्ठ अनुवाद', baidu_search: 'बाइडु खोजी', bing_dict: 'बिङ शब्दकोश', bing_search: 'बिङ खोजी', caiyuntrs: 'Lingocloud पृष्ठ अनुवाद', cambridge: 'क्याम्ब्रिज', copy_pdf_url: 'पीडीएफ यूआरएल क्लिपबोर्डमा प्रतिलिपि गर्नुहोस्', dictcn: 'Dictcn', etymonline: 'Etymonline', google_cn_page_translate: 'Google cn पृष्ठ अनुवाद', google_page_translate: 'Google पृष्ठ अनुवाद', google_search: 'Google खोजी', google_translate: 'Google अनुवाद', google_cn_translate: 'Google.cn अनुवाद', guoyu: '國語辭典', history_title: 'खोज इतिहास', iciba: 'iciba', liangan: '兩岸詞典', longman_business: 'Longman Business', manual_title: 'मैनुअल', merriam_webster: 'Merriam Webster', microsoft_page_translate: 'Microsoft पृष्ठ अनुवाद', notebook_title: 'नयाँ शब्द सूची', notification_youdao_err: 'यूडाओ पृष्ठ अनुवाद 2.0ले प्रतिक्रिया दिएन ।\nसलाडिक्ट यस पृष्ठमा पहुँच प्राप्त गर्न सक्दैन।\nयदि यूडाओ प्यानल देखाइएको छ भने यो सन्देश अवहेलना गर्नुहोस्।', oxford: 'अक्सफोर्ड', page_permission_err: 'सलाडिक्ट "{{name}}" यस पृष्ठमा पहुँच प्राप्त गर्न अनुमति छैन।', page_translations: 'पृष्ठ अनुवाद', saladict: 'सलाडिक्ट', saladict_standalone: 'सलाडिक्ट स्ट्यान्डअलोन प्यानल', sogou: 'सोगो अनुवाद', sogou_page_translate: 'सोगो पृष्ठ अनुवाद', termonline: 'Termonline', view_as_pdf: 'पीडीएफ भिउमा खोल्नुहोस्', youdao: 'यौडाओ', youdao_page_translate: 'यौडाओ पृष्ठ अनुवाद', youglish: 'यौग्लिश' } ================================================ FILE: src/_locales/ne/options.ts ================================================ import { locale as _locale } from '../zh-CN/options' export const locale: typeof _locale = { title: 'Saladict Options', previewPanel: 'Preview Dict Panel', shortcuts: 'Set Shortcuts', msg_update_error: 'Unable to update', msg_updated: 'Successfully updated', msg_first_time_notice: 'First time notice', msg_err_permission: 'Unable to request "{{permission}}" permission.', unsave_confirm: 'Settings not saved. Sure to leave?', nativeSearch: 'search selected text outside of browser', firefox_shortcuts: 'Open about:addons, click the top right "gear" button, choose the last "Manage extension shortcuts".', tutorial: 'Tutorial', page_selection: 'Page Selection', nav: { General: 'General', Notebook: 'Notebook', Profiles: 'Profiles', DictPanel: 'Dict Panel', SearchModes: 'Search Modes', Dictionaries: 'Dictionaries', DictAuths: 'Access Tokens', Popup: 'Popup Panel', QuickSearch: 'Quick Search', Pronunciation: 'Pronunciation', PDF: 'PDF', ContextMenus: 'Context Menus', BlackWhiteList: 'Black/White List', ImportExport: 'Import/Export', Privacy: 'Privacy', Permissions: 'Permissions' }, config: { active: 'Enable Inline Translator', active_help: '"Quick Search" is still available even if Inline translation is turned off.', animation: 'Animation transitions', animation_help: 'Switch off animation transitions to reduce runtime cost.', runInBg: 'Keep in Background', runInBg_help: 'Keep the browser running in background after close so that global shortcuts still work.', darkMode: 'Dark Mode', langCode: 'App Language', editOnFav: 'Open WordEditor when saving', editOnFav_help: 'When turned off, new words will be added to notebook directly.', searchHistory: 'Keep search history', searchHistory_help: 'Your browsing history could be unintentionally revealed in Search history.', searchHistoryInco: 'Also in incognito mode', ctxTrans: 'Context Translate Engines', ctxTrans_help: 'Context sentence will be translated before being added to notebook.', searchSuggests: 'Search suggests', panelMaxHeightRatio: 'Panel max height ratio', panelWidth: 'Panel width', fontSize: 'Font size for search reasults', bowlOffsetX: 'Saladict icon Offset X', bowlOffsetY: 'Saladict icon Offset Y', panelCSS: 'Custom Dict Panel Styles', panelCSS_help: 'Custom CSS. For Dict Panel use .dictPanel-Root as root. For dictionaries use .dictRoot or .d-{id} as root', noTypeField: 'No selection on editable regions', noTypeField_help: 'If selection making in editable regions is banned, the extension will identify Input Boxes, TextAreas and other common text editors like CodeMirror, ACE and Monaco.', touchMode: 'Touch Mode', touchMode_help: 'Enable touch related selection', language: 'Selection Languages', language_help: 'Search when selection contains words in the chosen languages.', language_extra: 'Note that Japanese and Korean also include Chinese. French, Deutsch and Spanish also include English. If Chinese or English is cancelled while others are selected, only the exclusive parts of those languages are tested. E.g. kana characters in Japanese.', doubleClickDelay: 'Double Click Delay', mode: 'Normal Selection', panelMode: 'Inside Dict Panel', pinMode: 'When Panel is Pinned', qsPanelMode: 'When Standalone Panel is Opened', bowlHover: 'Icon Mouse Hover', bowlHover_help: 'Hover on the bowl icon to trigger searching instead of clicking.', autopron: { cn: { dict: 'Chinese Auto-Pronounce' }, en: { dict: 'English Auto-Pronounce', accent: 'Accent Preference' }, machine: { dict: 'Machine Auto-Pronounce', src: 'Machine Pronounce', src_help: 'Machine Translation Dictionary needs to be added and enabled on the list below to enable auto-pronunciation.', src_search: 'Read Source Text', src_trans: 'Read Translation Text' } }, pdfSniff: 'Enable PDF Sniffer', pdfSniff_help: 'If turned on, PDF links will be automatically captured.', pdfSniff_extra: 'It is recommended to {search selected text outside of browser} with your own favorite local reader.', pdfStandalone: 'Standalone Panel', pdfStandalone_help: 'Open PDF viewer in standalone panel.', baWidth: 'Width', baWidth_help: 'Browser Action Panel wdith. Dict Panel width will be used if a negative value is chosen.', baHeight: 'Height', baHeight_help: 'Browser Action Panel height.', baOpen: 'Browser Action', baOpen_help: 'When clicking the browser action icon in toolbar (next to the address bar). Items are same as Context Menus, which can be added or edited on the Context Menus config page.', tripleCtrl: 'Enable Ctrl Shortkey', tripleCtrl_help: 'Press {⌘ Command}(macOS) or {Ctrl}(Others) three times (or with browser shortkey) to summon the dictionary panel. ', defaultPinned: 'Pinned when shows up', qsLocation: 'Location', qsFocus: 'Focus when shows up', qsStandalone: 'Standalone', qsStandalone_help: 'Render dict panel in a standalone window. You can {search selected text outside of browser}.', qssaSidebar: 'Sidebar Layout', qssaSidebar_help: 'Rearrange windows to sidebar-like layout.', qssaHeight: 'Window Height', qssaPageSel: 'Selection Response', qssaPageSel_help: 'Response to page selection.', qssaRectMemo: 'Remember size and position', qssaRectMemo_help: 'Remember standalone panel size and position on close.', updateCheck: 'Check Update', updateCheck_help: 'Check update automatically.', analytics: 'Enable Google Analytics', analytics_help: 'Share anonymous device browser version information. Saladict author will offer prioritized support to popular devices and browsers.', opt: { reset: 'Reset Configs', reset_confirm: 'Reset to default settings. Confirm?', upload_error: 'Unable to save settings.', accent: { uk: 'UK', us: 'US' }, sel_blackwhitelist: 'Selection Black/White List', sel_blackwhitelist_help: 'Saladict will not react to selection in blacklisted pages.', pdf_blackwhitelist_help: 'Blacklisted PDF links will not jump to Saladict PDF Viewer.', contextMenus_description: 'Each context menus item can also be customized. Youdao and Google page translate are deprecated in favor of the official extensions.', contextMenus_edit: 'Edit Context Menus Items', contextMenus_url_rules: 'URL with %s in place of query.', baOpen: { popup_panel: 'Dict Panel', popup_fav: 'Add to Notebook', popup_options: 'Open Saladict Options', popup_standalone: 'Open Saladict Standalone Panel' }, openQsStandalone: 'Standalone Panel Options', pdfStandalone: { default: 'Never', always: 'Always', manual: 'Manual' } } }, matchPattern: { description: 'Specify URL as {URL Match Pattern} or {Regular Expression}. Empty fields will be removed.', url: 'URL Match Pattern', url_error: 'Incorrect URL Match Pattern.', regex: 'Regular Expression', regex_error: 'Incorrect Regular Expression.' }, searchMode: { icon: 'Show Icon', icon_help: 'A cute little icon pops up nearby the cursor.', direct: 'Direct Search', direct_help: 'Show dict panel directly.', double: 'Double Click', double_help: 'Show dict panel after double click selection.', holding: 'Hold a key', holding_help: 'After a selection is made, the selected key must be pressing when releasing mouse (Alt is "⌥ Option" on macOS. Meta key is "⌘ Command" on macOS and "⊞ Windows" for others.).', instant: 'Instant Capture', instant_help: 'Selection is automatically made near by the cursor.', instantDirect: 'Direct', instantKey: 'Key', instantKey_help: 'If "Direct" is chosen it is also recommeded setting browser shortkey to toggle Instant Capture. Otherwise browser text selection could be unable to perform.', instantDelay: 'Capture delay' }, profiles: { opt: { add_name: 'Add Profile Name', delete_confirm: 'Delete Profile "{{name}}". Confirm?', edit_name: 'Change Profile Name', help: 'Each profile represents an independent set of settings. Some of the settings (with {*} prefix) change according to profile. One may switch profiles by hovering on the menu icon on Dict Panel, or focus on the icon then hit {↓}.' } }, profile: { mtaAutoUnfold: 'Auto unfold multiline search box', waveform: 'Waveform Control', waveform_help: 'Display a button at the bottom of the Dict Panel for expanding the Waveform Control Panel which is only loaded after expansion.', stickyFold: 'Sticky Folding', stickyFold_help: 'Remembers manual dictionary folding/unfolding states when searching. Only last on the same page.', opt: { item_extra: 'This option may change base on "Profile".', mtaAutoUnfold: { always: 'Keep Unfolding', never: 'Never Unfold', once: 'Unfold Once', popup: 'Only On Browser Action', hide: 'Hide' }, dict_selected: 'Selected Dicts' } }, dict: { add: 'Add dicts', more_options: 'More Options', selectionLang: 'Selection Languages', selectionLang_help: 'Show this dictionary when selection contains words in the chosen languages.', defaultUnfold: 'Default Unfold', defaultUnfold_help: "If turned off, this dictionary won't start searching unless it's title bar is clicked.", selectionWC: 'Selection Word Count', selectionWC_help: 'Show this dictionary when selection word count meets the requirements. Set 999999 for unlimited words.', preferredHeight: 'Default Panel Height', preferredHeight_help: 'Maximum height on first appearance. Contents exceeding this height will be hidden. Set 999999 for unlimited height.', lang: { de: 'De', en: 'En', es: 'Es', fr: 'Fr', ja: 'Ja', kor: 'Kor', zhs: 'Zhs', zht: 'Zht' } }, syncService: { description: 'Sync settings.', start: 'Syncing. Do not close this page until finished.', finished: 'Syncing finished', success: 'Syncing success', failed: 'Syncing failed', close_confirm: 'Settings not saved. Close?', delete_confirm: 'Delete?', shanbay: { description: "Go to shanbay.com and log in first(must stay logged in). Note that it's a one-way sync(from Saladict to Shanbay). Only the new added words are synced. Words also need to be supported by Shanbay's database.", login: 'Will open shanbay.com. Please log in then come back and enable again.', sync_all: 'Upload all existing new words', sync_all_confirm: 'Too many new words in notebook. Saladict will upload in batches. Note that uploading too many words in short period would cause account banning which is unrecoverable. Confirm?', sync_last: 'Upload the last new word' }, eudic: { description: 'Before using Eudic to synchronize words, you must first create a default new word book on Eudic official website (my.eudic.net/home/index) (generally, it will be automatically generated and cannot be deleted after the first manual import). Pay attention not to synchronize frequently in a short time, which may cause temporary lock.', token: 'Authorization information', getToken: 'Get authorization', verify: 'Check authorization information', verified: 'Eudic authorization information checked successfully', enable_help: 'After opening, each new word added will be automatically synchronized to the Eudic default word book (salad to Eudic word book) in one direction, and only the new word itself will be synchronized (deleted out of synchronization)', token_help: 'Please confirm to set valid personal authorization information, otherwise the synchronization will fail. You can click the button at the bottom to check.', sync_all: 'Synchronize all new words', sync_help: 'Synchronize all existing new words in salad word book to the Eudic default word book (turn on the synchronization switch above at the same time and click save)', sync_all_confirm: 'Note that frequent synchronization in a short time may lead to lock temporarily. Are you sure to continue?' }, webdav: { description: 'Extension settings (including this) are synced via browser. New words notebook can be synced via WebDAV through settings here.', jianguo: 'See Jianguoyun for example', checking: 'Connecting...', exist_confirm: 'Saladict directory exists on server. Download it and merge with local data?', upload_confirm: 'Upload local data to Server right away?', verify: 'Verify server', verified: 'Successfully verified WebDAV server.', duration: 'Duration', duration_help: 'Data is guaranteed to be updated before upload. If you do not need real-time syncing across browsers, set a longer polling cycle to reduce CPU and memory footprint.', passwd: 'Password', url: 'Server Address', user: 'User Account' }, ankiconnect: { description: 'Please make sure Anki Connect plugin is installed and Anki is running. You can also update word to Anki in Word Editor.', checking: 'Checking...', deck_confirm: 'Deck "{{deck}}" does not exist in Anki. Generate a new deck?', deck_error: 'Unable to create deck "{{deck}}".', notetype_confirm: 'Note type "{{noteType}}" does not exist in Anki. Generate a new note type.', notetype_error: 'Unable to create note type "{{noteType}}".', upload_confirm: 'Sync local new words to Anki right away? Duplicated words (with same timestamp) will be skipped.', add_yourself: 'Please add it youself in Anki.', verify: 'Verify Anki Connect', verified: 'Successfully verified Anki Connect', enable_help: 'When enabled, each time a new word is added to Notebook it will also be ported to Anki automatically. Words that exist in Anki(with same "Date") can be force-updated in Word Editor.', host: 'Address', port: 'port', key: 'Key', key_help: 'Optional key can be added in Anki Connect config for identification.', deckName: 'Deck', deckName_help: 'If deck does not exist you can generate a default one automatically by clicking "Verify Anki Connect" below.', noteType: 'Note Type', noteType_help: 'Anki note type includes a set of fields and card type. If note type does not exist you can generate a default one automatically by clicking "Verify Anki Connect" below. DO NOT change field names when editing or adding card templates in Anki', tags: 'Tags', tags_help: 'Anki notes can include tags separated with commas.', escapeHTML: 'Escape HTML', escapeHTML_help: 'Escape HTML entities. Turn off if using HTML for manual layout.', syncServer: 'Sync Server', syncServer_help: 'Sync to server(e.g. AnkiWeb) after new words being added to local Anki.' } }, titlebarOffset: { title: 'Calibrate Titlebar Height', help: 'Different systems or browser settings may result in different titlebar height. Saladict will attempt to calibrate automatically. If you may adjust manually.', main: 'Normal', main_help: 'Normal windows may not have titlebar.', panel: 'Panel', panel_help: 'Saladict standalone quick search panel is a type of panel window.', calibrate: 'Auto-calibrate', calibrateSuccess: 'Calibration success', calibrateError: 'Calibration failed' }, headInfo: { acknowledgement: { title: 'Acknowledgement', yipanhuasheng: "for adding Merriam Webster's Dict, American Heritage Dict, Oxford Learner's Dict and Eudic Notebook sync service; and updating Urban Dict and Naver Dict", naver: 'for helping add Naver dict', shanbay: 'for adding Shanbay dict', trans_tw: 'for traditional Chinese translation', weblio: 'for helping add Weblio dict' }, contact_author: 'Contact Author', donate: 'Donate', instructions: 'Instructions', report_issue: 'Report Issue' }, form: { url_error: 'Incorrect URL.', number_error: 'Incorrect number.' }, preload: { title: 'Preload', auto: 'Auto search', auto_help: 'Search automatically when panel shows up.', clipboard: 'Clipboard', help: 'Preload content in search box when panel shows up.', selection: 'Page Selection' }, locations: { CENTER: 'Center', TOP: 'Top', RIGHT: 'Right', BOTTOM: 'Bottom', LEFT: 'Left', TOP_LEFT: 'Top Left', TOP_RIGHT: 'Top Right', BOTTOM_LEFT: 'Bottom Left', BOTTOM_RIGHT: 'Bottom Right' }, import_export_help: 'Configs are auto-synced via browser. Here you can also import/export manually. Backups are exported as plain text files. Please encrypt it yourself if needed.', import: { title: 'Import Configs', error: { title: 'Import Error', parse: 'Unable to parse backup. Incorrect format.', load: 'Unable to load backup. Browser cannot obtain the local file.', empty: 'No valid data found in the backup.' } }, export: { title: 'Export Configs', error: { title: 'Export Error', empty: 'No config to export.', parse: 'Unable to parse configs.' } }, dictAuth: { description: 'As the number of Saladict users grows, if you make heavily use of machine translation services it is recommended to register an account for better stability and accuracy. The account data will only be stored in the browser.', dictHelp: 'See the official website of {dict}.', manage: 'Manage Translator Accounts' }, third_party_privacy: 'Third Party Privacy', third_party_privacy_help: 'Saladict will not collect further information but search text and releated cookies will be sent to third party dictionary services(just like how you would search on their websites). If you do not want third party services to collect you data, remove the corresponding dictionaries at "Dictionaries" settings.', third_party_privacy_extra: 'Cannot be turned off as it is the core functionality of Saladict.', permissions: { success: 'Permission requested', cancel_success: 'Permission cancelled', failed: 'Permission request failed', cancelled: 'Permission request cancelled by user', missing: 'Missing permission "{{permission}}". Either grant it or disable related functions.', clipboardRead: 'Read Clipboard', clipboardRead_help: 'This permission is needed when clipboard preload is enable for popup panel or quick search panel.', clipboardWrite: 'Write Clipboard', clipboardWrite_help: 'This permission is needed when using titlebar menus to copy source/target text from machine translator.' }, unsupportedFeatures: { ff: 'Feature "{{feature}}" is not supported in Firefox.' } } ================================================ FILE: src/_locales/ne/popup.ts ================================================ import { locale as _locale } from '../zh-CN/popup' export const locale: typeof _locale = { title: 'सलाडिक्ट ब्राउजर एक्सन प्यानल', app_active_title: 'ईनलाइन अनुवादक सक्षम गर्नुहोस्', app_temp_active_title: 'पृष्ठमा अस्थायी रूपमा असक्षम गरियो', instant_capture_pinned: ' (ताराङकित)', instant_capture_title: 'तत्काल क्याप्चर सक्षम गर्नुहोस्', notebook_added: 'थपियो', notebook_empty: 'हालको पृष्ठमा कुनै चयन फेला परेन', notebook_error: 'नोटबुकमा चयन गरिएको पाठ थप्न सकिएन', page_no_response: 'पृष्ठको कुनै प्रतिक्रिया छैन', qrcode_title: 'पृष्ठको क्युआर कोड' } ================================================ FILE: src/_locales/ne/wordpage.ts ================================================ import { locale as _locale } from '../zh-CN/wordpage' export const locale: typeof _locale = { title: { history: 'सलाडिक्ट खोज इतिहास', notebook: 'सलाडिक्ट नोटबुक', }, localonly: 'स्थानीयमा मात्र', column: { add: 'थप्नुहोस्', date: 'मिति', edit: 'सम्पादन', note: 'टिप्पणी', source: 'स्रोत', trans: 'अनुवाद', word: 'शब्द' }, delete: { title: 'मेटाउनुहोस्', all: 'सबै मेटाउनुहोस्', confirm: '. साच्चै ?', page: 'पृष्ठ मेटाउनुहोस्', selected: 'चयन गरिएको मेटाउनुहोस्' }, export: { title: 'निर्यात', all: 'सबै निर्यात गर्नुहोस्', description: 'हरेक रेकर्डको आकार बताउनुहोस्:', explain: 'एन्की र अन्य उपकरणमा कसरी निर्यात गर्ने', gencontent: 'निर्मित सामग्री', linebreak: { default: 'पूर्वनिर्धारित लाइनब्रेक राख्नुहोस्', n: 'लाइनब्रेकहरूलाई \\n संग स्थानान्तरण गर्नुहोस्', br: 'लाइनब्रेकहरूलाई
संग स्थानान्तरण गर्नुहोस्', p: 'लाइनब्रेकहरूलाई

संग स्थानान्तरण गर्नुहोस्', space: 'लाइनब्रेकहरूलाई स्पेस संग स्थानान्तरण गर्नुहोस्' }, page: 'पृष्ठ निर्यात गर्नुहोस्', placeholder: 'प्लेसहोल्डर', htmlescape: { title: 'टिप्पणीहरूमा HTML वर्णहरू ऐस्केप गर्नुहोस्', text: 'HTML ऐस्केप गर्नुहोस्' }, selected: 'चयन गरिएको निर्यात गर्नुहोस्' }, filterWord: { chs: 'चिनियाँ', eng: 'अंग्रेजी', word: 'शब्द', phrase: 'वाक्यांश' }, wordCount: { selected: '{{count}} बस्तु चयन गरिएको', selected_plural: '{{count}} बस्तुहरु चयन गरिएको', total: '{{count}} बस्तु जम्मा', total_plural: '{{count}} बस्तुहरु जम्मा' } } ================================================ FILE: src/_locales/zh-CN/background.ts ================================================ export const locale = { app: { off: '沙拉查词已关闭(快捷查词依然可用)', tempOff: '沙拉查词已对当前标签关闭(快捷查词依然可用)', unsupported: '内嵌查词面板不支持此类页面(独立窗口查词面板依然可用)' } } ================================================ FILE: src/_locales/zh-CN/common.ts ================================================ export const locale = { add: '添加', delete: '删除', save: '保存', cancel: '取消', edit: '编辑', sort: '排序', rename: '重命名', confirm: '确认', changes_confirm: '修改未保存。确认关闭?', delete_confirm: '确定完全删除该条目?', max: '最大', min: '最小', name: '名称', none: '无', enable: '开启', enabled: '已开启', disabled: '已关闭', blacklist: '黑名单', whitelist: '白名单', import: '导入', export: '导出', lang: { chinese: '中文', chs: '中文', deutsch: '德文', eng: '英文', english: '英文', french: '法文', japanese: '日文', korean: '韩文', minor: '其它语言', matchAll: '所有的字符都必须匹配', others: '其它字符', spanish: '西班牙文' }, unit: { mins: '分钟', ms: '毫秒', s: '秒', word: '个' }, note: { word: '单词', trans: '翻译', note: '笔记', context: '上下文', contextCloze: '上下文填空', date: '日期', srcTitle: '来源标题', srcLink: '来源链接', srcFavicon: '来源图标' }, profile: { daily: '日常模式', sentence: '句库模式', default: '默认模式', scholar: '学术模式', translation: '翻译模式', nihongo: '日语模式' } } ================================================ FILE: src/_locales/zh-CN/content.ts ================================================ export const locale = { chooseLang: '-选择其它语言-', standalone: '沙拉查词-独立查词窗口', fetchLangList: '获取全部语言列表', transContext: '重新翻译', neverShow: '不再弹出', fromSaladict: '来自沙拉查词面板', tip: { historyBack: '上一个查词记录', historyNext: '下一个查词记录', searchText: '查单词', openOptions: '打开设置', addToNotebook: '保存单词到生词本,右键打开生词本', openNotebook: '打开生词本', openHistory: '打开查词记录', shareImg: '以图片方式分享查词结果', pinPanel: '钉住查词面板', closePanel: '关闭查词面板', sidebar: '切换侧边栏模式,右键切换右侧', focusPanel: '查词时面板获取焦点', unfocusPanel: '查词时面板不获取焦点' }, wordEditor: { title: '保存到生词本', wordCardsTitle: '生词本其它记录', deleteConfirm: '从单词本中移除?', closeConfirm: '记录尚未保存,确认关闭?', chooseCtxTitle: '选择翻译结果', ctxHelp: '如需兼容选择翻译结果以及 Anki 生成表格请保持 [:: xxx ::] 和 --------------- 格式。' }, machineTrans: { switch: '更改语言', sl: '来源语言', tl: '目标语言', auto: '自动检测', stext: '原文', showSl: '显示原文', copySrc: '复制原文', copyTrans: '复制译文', login: '请登录{词典帐号}以使用。', dictAccount: '词典帐号' }, updateAnki: { title: '更新到 Anki', success: '更新到 Anki 成功。', failed: '更新单词到 Anki 失败。' } } ================================================ FILE: src/_locales/zh-CN/langcode.ts ================================================ import zhCN from '@opentranslate/languages/locales/zh-CN.json' export const locale = { ...zhCN, default: '随扩展语言', ara: '阿拉伯语', 'bs-Latn': '波斯尼亚语', bul: '保加利亚语', cht: '中文(繁体)', dan: '丹麦语', est: '爱沙尼亚语', fin: '芬兰语', fra: '法语', iw: '希伯来语', jp: '日语', kor: '韩语', kr: '韩语', pt_BR: '巴西语', rom: '罗马尼亚语', slo: '斯洛文尼亚语', spa: '西班牙语', swe: '瑞典语', tl: '塔加路语(菲律宾语)', vie: '越南语', zh: '中文(简体)', 'zh-CHS': '中文(简体)', 'zh-CHT': '中文(繁体)' } ================================================ FILE: src/_locales/zh-CN/menus.ts ================================================ export const locale = { baidu_page_translate: '百度网页翻译', baidu_search: '百度搜索', bing_dict: '必应词典', bing_search: '必应搜索', caiyuntrs: '彩云小译网页翻译', cambridge: '剑桥词典', copy_pdf_url: '复制PDF链接到剪贴板', dictcn: '海词词典', etymonline: '培根词源', google_cn_page_translate: '谷歌cn网页翻译', google_page_translate: '谷歌网页翻译', google_search: '谷歌搜索', google_translate: '谷歌翻译', google_cn_translate: '谷歌CN翻译', guoyu: '国语辞典', history_title: '查词历史记录', iciba: '金山词霸', liangan: '两岸词典', longman_business: '朗文商务', manual_title: '详细使用说明', merriam_webster: '韦氏词典', microsoft_page_translate: '微软网页翻译', notebook_title: '生词本', notification_youdao_err: '有道网页翻译2.0 加载无响应,\n可能扩展无权访问该页面,\n如加载成功请忽略本消息。', oxford: '牛津词典', page_permission_err: '沙拉查词「{{name}}」无权访问此页面。', page_translations: '网页翻译', saladict: '沙拉查词', saladict_standalone: '沙拉查词独立窗口', sogou: '搜狗翻译', sogou_page_translate: '搜狗网页翻译', termonline: '术语在线', view_as_pdf: '在 PDF 阅读器中打开', youdao: '有道词典', youdao_page_translate: '有道网页翻译', youglish: 'YouGlish' } ================================================ FILE: src/_locales/zh-CN/options.ts ================================================ export const locale = { title: '沙拉查词设置', previewPanel: '预览查词面板', shortcuts: '设置快捷键', msg_update_error: '设置更新失败', msg_updated: '设置已更新', msg_first_time_notice: '初次使用注意', msg_err_permission: '权限“{{permission}}”申请失败。', unsave_confirm: '修改尚未保存,确定放弃?', nativeSearch: '浏览器外划词', firefox_shortcuts: '地址栏输入 about:addons 打开,点击右上方的齿轮,选择最后一项管理扩展快捷键。', tutorial: '教程', page_selection: '网页划词', nav: { General: '基本选项', Notebook: '单词管理', Profiles: '情景模式', DictPanel: '查词面板', SearchModes: '查词习惯', Dictionaries: '词典设置', DictAuths: '词典帐号', Popup: '右上弹框', QuickSearch: '快捷查词', Pronunciation: '发音设置', PDF: 'PDF 设置', ContextMenus: '右键菜单', BlackWhiteList: '黑白名单', ImportExport: '导入导出', Privacy: '隐私设置', Permissions: '权限管理' }, config: { active: '启用划词翻译', active_help: '关闭后「快捷查词」功能依然可用。', animation: '开启动画过渡', animation_help: '在低性能设备上关闭过渡动画可减少渲染负担。', runInBg: '后台保持运行', runInBg_help: '让浏览器关闭后依然保持后台运行,从而继续响应快捷键以及浏览器外划词。', darkMode: '黑暗模式', langCode: '界面语言', editOnFav: '红心单词时弹出编辑面板', editOnFav_help: '关闭后,点击红心生词将自动添加到生词本,上下文翻译亦会自动获取。', searchHistory: '记录查词历史', searchHistory_help: '查词记录可能会泄漏您的浏览痕迹。', searchHistoryInco: '在私隐模式中记录', ctxTrans: '上下文翻译引擎', ctxTrans_help: '单词被添加进生词本前会自动翻译上下文。', searchSuggests: '输入时显示候选', panelMaxHeightRatio: '查词面板最高占屏幕比例', panelWidth: '查词面板宽度', fontSize: '词典内容字体大小', bowlOffsetX: '沙拉图标水平偏移', bowlOffsetY: '沙拉图标垂直偏移', panelCSS: '自定义查词面板样式', panelCSS_help: '为查词面板添加自定义 CSS 。词典面板使用 .dictPanel-Root 作为根,词典使用 .dictRoot 或者 .d-词典ID 作为根。', noTypeField: '不在输入框划词', noTypeField_help: '开启后,本扩展会自动识别输入框以及常见编辑器,如 CodeMirror、ACE 和 Monaco。', touchMode: '触摸模式', touchMode_help: '支持触摸相关选词。', language: '划词语言', language_help: '当选中的文字包含相应的语言时才进行查找。', language_extra: '注意日语与韩语也包含了汉字。法语、德语和西语也包含了英文。若取消了中文或英语而勾选了其它语言,则只匹配那些语言独有的部分,如日语只匹配假名。', doubleClickDelay: '双击间隔', mode: '普通划词', defaultPinned: '出现时钉住查词面板', panelMode: '查词面板内部划词', pinMode: '查词面板钉住后划词', qsPanelMode: '独立窗口响应页面划词', bowlHover: '图标悬停查词', bowlHover_help: '鼠标悬停在沙拉图标上触发查词,否则需要点击。', autopron: { cn: { dict: '中文自动发音' }, en: { dict: '英文自动发音', accent: '优先口音' }, machine: { dict: '机器自动发音', src: '机器发音部分', src_help: '机器翻译词典需要在下方添加并启用才会自动发音。', src_search: '朗读原文', src_trans: '朗读翻译' } }, pdfSniff: '嗅探 PDF 链接', pdfSniff_help: '开启后所有 PDF 链接将自动跳转到本扩展打开(包括本地,如果在扩展管理页面勾选了允许)。', pdfSniff_extra: '现在更推荐使用自己喜欢的本地阅读器搭配{浏览器外划词}。', pdfStandalone: '独立窗口', pdfStandalone_help: '在独立窗口中打开 PDF 阅读器。独立窗口只有标题栏,占用更少空间,但不能复制链接等操作。', baWidth: '弹窗宽度', baWidth_help: '右上弹框面板宽度。若为负数则取查词面板的宽度。', baHeight: '弹窗高度', baHeight_help: '右上弹框面板高度。', baOpen: '点击地址栏旁图标', baOpen_help: '点击地址栏旁 Saladict 图标时发生的操作。沿用了「右键菜单」的项目,可以前往该设置页面进行增加或编辑。', tripleCtrl: '启用 Ctrl 快捷键', tripleCtrl_help: '连续按三次{⌘ Command}(Mac)或者{Ctrl}(其它键盘)(或设置浏览器快捷键)将弹出词典界面。', qsLocation: '出现位置', qsFocus: '出现时获取焦点', qsStandalone: '独立窗口', qsStandalone_help: '显示为单独的窗口,支持响应{浏览器以外划词}。', qssaSidebar: '类侧边栏', qssaSidebar_help: '并排显示窗口以达到类似侧边栏的布局。', qssaHeight: '窗口高度', qssaPageSel: '响应划词', qssaPageSel_help: '响应网页划词。', qssaRectMemo: '记住位置与大小', qssaRectMemo_help: '独立窗口关闭时记住位置与大小。', updateCheck: '检查更新', updateCheck_help: '自动检查更新', analytics: '启用 Google Analytics', analytics_help: '提供匿名设备浏览器版本信息。因精力有限,沙拉查词作者会尽可能支持用户量更多的设备和浏览器。', opt: { reset: '重置设定', reset_confirm: '所有设定将还原到默认值,确定?', upload_error: '设置保存失败', accent: { uk: '英式', us: '美式' }, sel_blackwhitelist: '划词黑白名单', sel_blackwhitelist_help: '黑名单匹配的页面 Saladict 将不会响应鼠标划词。', pdf_blackwhitelist_help: '黑名单匹配的 PDF 链接将不会跳转到 Saladict 打开。', contextMenus_description: '设置右键菜单,可添加可自定义链接。网页翻译其实不需要沙拉查词,故已有的有道和谷歌网页翻译目前处于维护状态,没有计划添加新功能,请用其它官方扩展如彩云小译和谷歌翻译。', contextMenus_edit: '编辑右键菜单项目', contextMenus_url_rules: '链接中的 %s 会被替换为选词。', baOpen: { popup_panel: '显示查词面板', popup_fav: '添加选词到生词本', popup_options: '打开 Saladict 设置', popup_standalone: '打开快捷查词独立窗口' }, openQsStandalone: '打开独立窗口设置', pdfStandalone: { default: '从不', always: '总是', manual: '手动' } } }, matchPattern: { description: '网址支持{超链匹配}和{正则匹配}。留空保存即可清除。', url: '超链匹配', url_error: '不正确的超链接模式表达式。', regex: '正则匹配', regex_error: '不正确的正则表达式。' }, searchMode: { icon: '显示图标', icon_help: '在鼠标附近显示一个图标,鼠标移上去后才显示词典面板。', direct: '直接搜索', direct_help: '直接显示词典面板。', double: '双击搜索', double_help: '双击选择文本之后直接显示词典面板。', holding: '按住按键', holding_help: '在放开鼠标之前按住选择的按键才显示词典面板(Alt 为 macOS 上的 "⌥ Option"键。 Meta 键为 macOS 上的「⌘ Command」键以及其它键盘的「⊞ Windows」键)。', instant: '鼠标悬浮取词', instant_help: '自动选取鼠标附近的单词。', instantDirect: '直接取词', instantKey: '按键', instantKey_help: '因技术限制,悬浮取词通过自动选择鼠标附近单词实现,不设置按键直接取词可导致无法选词,建议配合快捷键开启关闭。', instantDelay: '取词延时' }, profiles: { opt: { add_name: '新增情景模式名称', delete_confirm: '「{{name}}」将被删除,确认?', edit_name: '更改情景模式名称', help: '每个情景模式相当于一套独立的设置,一些选项(带 {*})会随着情景模式变化。鼠标悬浮在查词面板的菜单图标上可快速切换,或者焦点选中菜单图标然后按{↓}。' } }, profile: { mtaAutoUnfold: '自动展开多行搜索框', waveform: '波形控制按钮', waveform_help: '在词典面板下方显示音频控制面板展开按钮。控制面板只會在展開時才載入。', stickyFold: '记忆折叠', stickyFold_help: '查词时记住之前手动展开与折叠词典的状态,仅在同个页面生效。', opt: { item_extra: '此选项会因「情景模式」而改变。', mtaAutoUnfold: { always: '保持展开', never: '从不展开', once: '展开一次', popup: '只在右上弹框展开', hide: '隐藏' }, dict_selected: '已选词典' } }, dict: { add: '添加词典', more_options: '更多设置', selectionLang: '划词语言', selectionLang_help: '当选中的文字包含相应的语言时才显示该词典。', defaultUnfold: '默认展开', defaultUnfold_help: '关闭后该词典将不会自动搜索,除非点击「展开」箭头。适合一些需要时再深入了解的词典,以加快初次查词速度。', selectionWC: '划词字数', selectionWC_help: '当选中文字的字数符合条件时才显示该词典。可设置 999999 如果不希望限制字数。', preferredHeight: '词典默认高度', preferredHeight_help: '词典初次出现的最大高度。超出此高度的内容将被隐藏并显示下箭头。可设置 999999 如果不希望限制高度。', lang: { de: '德', en: '英', es: '西', fr: '法', ja: '日', kor: '韩', zhs: '简', zht: '繁' } }, syncService: { description: '数据同步设置。', start: '同步进行中,结束前请勿关闭此页面。', finished: '同步结束', success: '同步成功', failed: '同步失败', close_confirm: '设置未保存,关闭?', delete_confirm: '清空同步设置?', shanbay: { description: '先去 shanbay.com 登录扇贝(退出后将失效)。开启后每次添加生词将自动单向同步到扇贝生词本(只从沙拉查词到扇贝),只同步新增单词(删除不同步),只同步单词本身(上下文等均不能同步)。生词需要扇贝单词库支持才能被添加。', login: '将打开扇贝官网,请登录再回来重新开启。', sync_all: '上传现有的所有生词', sync_all_confirm: '生词本存在较多单词,将分批上传。注意短时间上传太多有可能会导致封号,且不可恢复,确定继续?', sync_last: '上传最近的一个生词' }, eudic: { description: '使用欧路词典同步单词前,必须先在欧路官网(my.eudic.net/home/index)创建默认生词本(一般初次手动导入会自动生成且无法删除)。注意短时间内不要频繁同步,可能会造成暂时封停。', token: '授权信息', getToken: '获取授权', verify: '检查 授权信息', verified: '成功检查 欧路授权信息', enable_help: '开启后每次添加生词将自动单向同步到欧路默认生词本(salad到欧路生词本),只同步新增单词本身(删除不同步)', token_help: '请确认设置有效的个人授权信息,否则将同步失败。可点击底部按钮检查。', sync_all: '同步全部生词', sync_help: '将salad单词本中现有的所有生词,同步到欧路词典默认生词本中(需同时开启上方同步开关,点击保存)', sync_all_confirm: '注意短时间内频繁同步有可能会导致接下来一小段时间的封停,确定继续?' }, webdav: { description: '应用设置(包括本设置)已通过浏览器自动同步。生词本可通过本设置实现 WebDAV 同步。', jianguo: '参考坚果云设置', checking: '连接中...', exist_confirm: '服务器上已存在 Saladict 目录。是否下载合并到本地?', upload_confirm: '马上上传本地数据到服务器?', verify: '验证服务器', verified: '成功验证服务器', duration: '同步周期', duration_help: '添加生词后会马上上传,数据会在上传前保证同步,所以如果不需要多个浏览器实时查看更新,可将更新检测周期调大些以减少资源占用及避免服务器拒绝响应。', passwd: '密码', url: '服务器地址', user: '账户' }, ankiconnect: { description: '请确保 Anki Connect 插件已安装且 Anki 在后台运行。', checking: '连接中...', deck_confirm: '牌组「{{deck}}」不存在 Anki 中,是否自动添加?', deck_error: '无法创建牌组「{{deck}}」。', notetype_confirm: '笔记类型「{{noteType}}」不存在 Anki 中,是否自动添加?', notetype_error: '无法创建笔记类型「{{noteType}}」。', upload_confirm: '马上同步本地生词到 Anki?重复的单词(相同“Date”)会被跳过。', add_yourself: '请在 Anki 中自行添加。', verify: '检查 Anki Connect', verified: '成功检查 Anki Connect', enable_help: '开启后每次保存新生词都会自动同步到 Anki。Anki 上已存在的单词(以“Date”为准)可以在单词编辑器中编辑强制更新覆盖到 Anki。', host: '地址', port: '端口', key: 'Key', key_help: '可在 Anki Connect 插件中设置 key 以做简单令牌。', deckName: '牌组', deckName_help: '如果不存在的话可以点下方「检查 Anki Connect」让本设置生成默认牌组。', noteType: '笔记类型', noteType_help: 'Anki 笔记类型包括一套字段和卡片类型。如果不存在的话可以点下方「检查 Anki Connect」让本设置生成一套默认的笔记类型。如需自行在 Anki 添加或修改卡片模板请不要更改字段名字。', tags: '标签', tags_help: 'Anki 笔记可以附带标签。以逗号分割。', escapeHTML: '转义 HTML', escapeHTML_help: '对笔记内容中的 HTML 字符进行转义。如手动进行 HTML 排版请关闭选项。', syncServer: '同步服务器', syncServer_help: '单词添加到本地 Anki 后自动同步到服务器(如 AnkiWeb)。' } }, titlebarOffset: { title: '校准标题栏高度', help: '不同的系统以及不同的浏览器设置会影响标题栏高度,沙拉查词会尝试自动校准,如弹出窗口依然出现偏移可自行调整。', main: '普通窗口', main_help: '普通窗口可能没有标题栏。', panel: '简化窗口', panel_help: '沙拉查词的独立窗口快捷查词面板为简化窗口。', calibrate: '自动校准', calibrateSuccess: '自动校准成功', calibrateError: '自动校准失败' }, headInfo: { acknowledgement: { title: '特别鸣谢', yipanhuasheng: '添加韦氏词典、美国传统词典、牛津学习词典与欧路生词同步;更新 Urban 词典与 Naver 词典', naver: '协助添加 Naver 韩国语词典', shanbay: '编写扇贝词典模块', trans_tw: '提供部分繁体中文翻译', weblio: '协助添加 Weblio 辞書' }, contact_author: '联系作者', donate: '支持项目', instructions: '使用说明', report_issue: '反馈问题' }, form: { url_error: '不正确的超链接格式。', number_error: '不正确的数字' }, preload: { title: '预先加载', auto: '自动查词', auto_help: '查词面板出现时自动搜索预加载内容。', clipboard: '剪贴板', help: '查词面板出现时预先加载内容到搜索框。', selection: '页面划词' }, locations: { CENTER: '居中', TOP: '上方', RIGHT: '右方', BOTTOM: '下方', LEFT: '左方', TOP_LEFT: '左上', TOP_RIGHT: '右上', BOTTOM_LEFT: '左下', BOTTOM_RIGHT: '右下' }, import_export_help: '设定已通过浏览器自动同步,也可以手动导入导出。备份为明文保存,对安全性有要求的请自行加密。', import: { title: '导入设定', error: { title: '导入失败', parse: '备份解析失败,格式不正确。', load: '备份加载失败,浏览器无法获得本地备份。', empty: '备份中没有发现有效数据。' } }, export: { title: '导出设定', error: { title: '导出失败', empty: '没有设置可以导出。', parse: '设置解析失败,无法导出。' } }, dictAuth: { description: '随着沙拉查词用户增多,如经常使用机器翻译,建议到官网申请帐号以获得更稳定的体验以及更准确的结果。以下帐号数据只会保留在浏览器中。', dictHelp: '见{词典}官网。', manage: '管理私用帐号' }, third_party_privacy: '第三方隐私', third_party_privacy_help: '沙拉查词不会收集更多数据,但在查词时单词以及相关 cookies 数据会发送给第三方词典服务(与在该网站上查词一样),如果你不希望被该服务获取数据,请在「词典设置」中关闭相应词典。', third_party_privacy_extra: '本特性为沙拉查词核心功能,无法关闭。', permissions: { success: '申请权限成功', cancel_success: '取消权限成功', failed: '申请权限失败', cancelled: '申请权限被用户取消', missing: '缺少权限「{{permission}}」。请给予权限或者关闭相关功能。', clipboardRead: '读取剪贴板', clipboardRead_help: '快捷查词或者右上弹框设置预加载剪贴板时需要读取剪贴板权限。', clipboardWrite: '写入剪贴板', clipboardWrite_help: '机器翻译词典标题栏菜单复制原文译文或生词本导出到剪贴板需要写入剪贴板权限。' }, unsupportedFeatures: { ff: '火狐尚不支持「{{feature}}」功能。' } } ================================================ FILE: src/_locales/zh-CN/popup.ts ================================================ export const locale = { title: '沙拉查词-右上弹框', app_active_title: '启用划词', app_temp_active_title: '对当前页暂时关闭划词', instant_capture_pinned: '(钉住)', instant_capture_title: '开启鼠标悬浮取词', notebook_added: '已添加', notebook_empty: '当前页面没有发现选词', notebook_error: '无法添加选词到生词本', page_no_response: '页面无响应', qrcode_title: '当前页面二维码' } ================================================ FILE: src/_locales/zh-CN/wordpage.ts ================================================ export const locale = { title: { history: '沙拉查词-查词记录', notebook: '沙拉查词-生词本' }, localonly: '仅本地保存', column: { add: '添加', date: '日期', edit: '编辑', note: '笔记', source: '来源', trans: '翻译', word: '单词' }, delete: { title: '删除单词', all: '删除所有单词', confirm: ',确定?', page: '删除本页单词', selected: '删除选中单词' }, export: { title: '导出文本', all: '导出所有单词', description: '编写生成模板,描述每条记录生成的样子:', explain: '如何配合 ANKI 等工具', gencontent: '代表的内容', linebreak: { default: '保留换行', n: '换行替换为 \\n', br: '换行替换为
', p: '换行替换为

', space: '换行替换为空格' }, page: '导出本页单词', placeholder: '替换符', htmlescape: { title: '对笔记内容中的 HTML 字符进行转义', text: '转义 HTML' }, selected: '导出选中单词' }, filterWord: { chs: '中文', eng: '英文', word: '单词', phrase: '词组和句子' }, wordCount: { selected: '已选 {{count}} 项', selected_plural: '已选 {{count}} 项', total: '共 {{count}} 项', total_plural: '共 {{count}} 项' } } ================================================ FILE: src/_locales/zh-TW/background.ts ================================================ import { locale as _locale } from '../zh-CN/background' export const locale: typeof _locale = { app: { off: '沙拉查詞已關閉(快捷查詞依然可用)', tempOff: '沙拉查詞已對當前標籤關閉(快捷查詞依然可用)', unsupported: '內嵌查字介面不支援此類頁面(獨立視窗查字介面依然可用)' } } ================================================ FILE: src/_locales/zh-TW/common.ts ================================================ import { locale as _locale } from '../zh-CN/common' export const locale: typeof _locale = { add: '新增', delete: '删除', save: '保存', cancel: '取消', edit: '編輯', sort: '排序', rename: '重新命名', confirm: '確認', changes_confirm: '變更未儲存。確定關閉?', delete_confirm: '確定完全刪除該條目?', max: '最大', min: '最小', name: '名稱', none: '無', enable: '開啟', enabled: '已開啟', disabled: '已關閉', blacklist: '黑名單', whitelist: '白名單', import: '匯入', export: '匯出', lang: { chinese: '漢字', chs: '漢字', deutsch: '德文', eng: '英文', english: '英文', french: '法文', japanese: '日文', korean: '韓文', minor: '其它語言', matchAll: '所有的字元都必須匹配', others: '其它字元', spanish: '西班牙文' }, unit: { mins: '分鐘', ms: '毫秒', s: '秒', word: '个' }, note: { word: '單字', trans: '翻譯', note: '筆記', context: '上下文', contextCloze: '上下文填空', date: '日期', srcTitle: '來源標題', srcLink: '來源連結', srcFavicon: '來源圖示' }, profile: { daily: '日常模式', sentence: '句庫模式', default: '預設模式', scholar: '學術模式', translation: '翻譯模式', nihongo: '日語模式' } } ================================================ FILE: src/_locales/zh-TW/content.ts ================================================ import { locale as _locale } from '../zh-CN/content' export const locale: typeof _locale = { chooseLang: '-選擇其它語言-', standalone: '沙拉查詞-獨立查詞視窗', fetchLangList: '取得全部語言清單', transContext: '重新翻譯', neverShow: '不再彈出', fromSaladict: '来自沙拉查詞介面', tip: { historyBack: '上一個查單字記錄', historyNext: '下一個查單字記錄', searchText: '查單字', openOptions: '開啟設定', addToNotebook: '儲存單字到單字本,右点击開啟單字本', openNotebook: '開啟單字本', openHistory: '開啟查單字記錄', shareImg: '以圖片方式分享查單字結果', pinPanel: '釘選字典視窗', closePanel: '關閉字典視窗', sidebar: '切換側邊欄模式,右點選切換右側', focusPanel: '查詞時面板獲取焦點', unfocusPanel: '查詞時面板不獲取焦點' }, wordEditor: { title: '儲存到單字本', wordCardsTitle: '單字本其它記錄', deleteConfirm: '從單字本中移除?', closeConfirm: '記錄尚未儲存,確定關閉?', chooseCtxTitle: '選擇翻譯結果', ctxHelp: '如需相容選擇翻譯結果以及 Anki 生成表格請保持 [:: xxx ::] 和 --------------- 格式。' }, machineTrans: { switch: '變更語言', sl: '來源語言', tl: '目標語言', auto: '偵測語言', stext: '原文', showSl: '顯示原文', copySrc: '複製原文', copyTrans: '複製譯文', login: '請登入{詞典帳號}以使用。', dictAccount: '詞典帳號' }, updateAnki: { title: '更新到 Anki', success: '更新到 Anki 成功。', failed: '更新單詞到 Anki 失敗。' } } ================================================ FILE: src/_locales/zh-TW/langcode.ts ================================================ import zhTW from '@opentranslate/languages/locales/zh-TW.json' import { locale as _locale } from '../zh-CN/langcode' export const locale: typeof _locale = { ...zhTW, default: '同介面語言', ara: '阿拉伯語', 'bs-Latn': '波斯尼亞語', bul: '保加利亞語', cht: '中文(繁體)', dan: '丹麥語', est: '愛沙尼亞語', fin: '芬蘭語', fra: '法語', iw: '希伯來語', jp: '日語', kor: '韓語', kr: '韓語', pt_BR: '巴西語', rom: '羅馬尼亞語', slo: '斯洛維尼亞語', spa: '西班牙語', swe: '瑞典語', tl: '他加祿語(菲律賓語)', vie: '越南語', zh: '中文(簡體)', 'zh-CHS': '中文(簡體)', 'zh-CHT': '中文(繁體)' } ================================================ FILE: src/_locales/zh-TW/menus.ts ================================================ import { locale as _locale } from '../zh-CN/menus' export const locale: typeof _locale = { baidu_page_translate: '百度網頁翻譯', baidu_search: '百度搜尋', bing_dict: 'Bing 字典', bing_search: 'Bing 搜尋', caiyuntrs: '彩雲小譯網頁翻譯', cambridge: '劍橋字典', copy_pdf_url: '複製PDF連結到剪貼簿', dictcn: '海詞字典', etymonline: '培根字根', google_cn_page_translate: 'Google cn 網頁翻譯', google_page_translate: 'Google 網頁翻譯', google_search: 'Google 搜尋', google_translate: 'Google 翻譯', google_cn_translate: 'Google.cn 翻譯', guoyu: '國語字典', history_title: '查單字歷史記錄', iciba: '金山詞霸', liangan: '兩岸字典', longman_business: '朗文商務', manual_title: '詳細使用說明', merriam_webster: '韋氏字典', microsoft_page_translate: '微軟網頁翻譯', notebook_title: '生字本', notification_youdao_err: '有道網頁翻譯2.0 下載後無回應,\n可能是套件無權造訪該網站,\n如果下載成功後,請忽略本訊息。', oxford: '牛津字典', page_permission_err: '沙拉查詞「{{name}}」無權訪問此頁面。', page_translations: '網頁翻譯', saladict: '沙拉查詞', saladict_standalone: '沙拉查詞獨立視窗', sogou: '搜狗翻譯', sogou_page_translate: '搜狗網頁翻譯', termonline: '術語在線', view_as_pdf: '在 PDF 閱讀器中開啟', youdao: '有道字典', youdao_page_translate: '有道網頁翻譯', youglish: 'YouGlish' } ================================================ FILE: src/_locales/zh-TW/options.ts ================================================ import { locale as _locale } from '../zh-CN/options' export const locale: typeof _locale = { title: '沙拉查詞設定', previewPanel: '預覽字典介面', shortcuts: '設定快速鍵', msg_update_error: '設定更新失敗', msg_updated: '設定已更新', msg_first_time_notice: '初次使用注意', msg_err_permission: '許可權“{{permission}}”申請失敗。', unsave_confirm: '修改尚未儲存,確定放棄?', nativeSearch: '瀏覽器外選字翻譯', firefox_shortcuts: '位址列跳轉到 about:addons,點選右上方的齒輪,選擇最後一項管理擴充套件快捷鍵', tutorial: '教程', page_selection: '網頁選字', nav: { General: '基本選項', Notebook: '單字管理', Profiles: '情景模式', DictPanel: '字典介面', SearchModes: '查字習慣', Dictionaries: '字典設定', DictAuths: '詞典帳號', Popup: '右上彈出式視窗', QuickSearch: '迅速查字', Pronunciation: '朗讀設定', PDF: 'PDF 設定', ContextMenus: '右鍵選單', BlackWhiteList: '黑白名單', ImportExport: '匯入匯出', Privacy: '隱私設定', Permissions: '許可權管理' }, config: { active: '啟用滑鼠選字翻譯', active_help: '關閉後「迅速查字」功能依然可用。', animation: '啟用轉換動畫', animation_help: '在低效能裝置上關閉過渡動畫可減少渲染負擔。', runInBg: '保持瀏覽器執行', runInBg_help: '讓瀏覽器關閉後依然保持執行,從而繼續響應快捷鍵以及瀏覽器外劃字(見右上角官網使用說明)。', darkMode: '黑暗模式', langCode: '介面語言', editOnFav: '紅心單字時彈出編輯介面', editOnFav_help: '關閉後,點選紅心生詞將自動新增到生詞本,上下文翻譯亦會自動獲取。', searchHistory: '記錄查字歷史', searchHistory_help: '查字典記錄可能會泄漏您的瀏覽痕跡。', searchHistoryInco: '在無痕模式中記錄', ctxTrans: '上下文翻譯引擎', ctxTrans_help: '單字加入生字本前會自動翻譯上下文。', searchSuggests: '輸入時顯示候選', panelMaxHeightRatio: '字典介面最高占螢幕高度比例', panelWidth: '查字典介面寬度', fontSize: '字典內容字型大小', bowlOffsetX: '沙拉圖示水平偏移', bowlOffsetY: '沙拉圖示垂直偏移', panelCSS: '自訂查字介面樣式', panelCSS_help: '為查詞面板新增自定義 CSS 。詞典面板使用 .dictPanel-Root 作為根,詞典使用 .dictRoot 或者 .d-詞典ID 作為根。', noTypeField: '不在輸入框滑鼠滑字', noTypeField_help: '開啟後,本程式會自動識別輸入框以及常見編輯器,如 CodeMirror、ACE 和 Monaco。', touchMode: '觸控模式', touchMode_help: '支援觸控相關選字', language: '選詞語言', language_help: '當選取的文字包含相對應的語言時,才進行尋找。', language_extra: '注意日語與韓語也包含了漢字。法語、德語和西語也包含了英文。若取消了中文或英語而勾選了其它語言,則只翻譯那些語言獨有的部分,如日語只翻譯假名。', doubleClickDelay: '滑鼠按兩下間隔', mode: '普通選字', panelMode: '字典視窗介面內部選字', defaultPinned: '出現時釘住面板', pinMode: '字典視窗介面釘住后選字', qsPanelMode: '獨立字典視窗介面響應頁面選字', bowlHover: '圖示暫留查字', bowlHover_help: '滑鼠暫留在沙拉圖示上開啟字典介面,否則需要點選。', autopron: { cn: { dict: '中文自動發音' }, en: { dict: '英文自動發音', accent: '優先口音' }, machine: { dict: '機器自動發音', src: '機器發音部分', src_help: '機器翻譯字典需要在下方新增並啟用才會自動發音。', src_search: '朗讀原文', src_trans: '朗讀翻譯' } }, pdfSniff: '嗅探 PDF 連結', pdfSniff_help: '開啟後所有 PDF 連結將自動跳至本套件開啟(包括本機,如果在套件管理頁面勾選了允許)。', pdfSniff_extra: '現在更推薦使用自己喜歡的本地閱讀器搭配{瀏覽器外選字翻譯}。', pdfStandalone: '獨立視窗', pdfStandalone_help: '在獨立視窗中開啟 PDF 閱讀器。獨立視窗只有標題欄,佔用更少空間,但不能複製連結等操作。', baWidth: '彈窗寬度', baWidth_help: '右上彈框面板寬度。若為負數則取查字介面的寬度。', baHeight: '彈窗高度', baHeight_help: '右上彈框面板高度。', baOpen: '點選網址列旁圖示', baOpen_help: '點選網址列旁 Saladict 圖示時發生的操作。沿用了「右鍵選單」的條目,可以前往該設定頁面增加或編輯。', tripleCtrl: '啟用 Ctrl 快速鍵', tripleCtrl_help: '連續按三次{⌘ Command}(macOS)或者{Ctrl}(其它鍵盤)(或設定瀏覽器快速鍵),將會彈出字典視窗介面。', qsLocation: '出現位置', qsFocus: '出現時獲取焦點', qsStandalone: '獨立視窗', qsStandalone_help: '顯示為獨立的視窗,支援{瀏覽器外選字翻譯}。', qssaSidebar: '類側邊欄', qssaSidebar_help: '並排顯示視窗以達到類似側邊欄的配置。', qssaHeight: '視窗高度', qssaPageSel: '響應滑字', qssaPageSel_help: '對網頁滑鼠滑字作出反應。', qssaRectMemo: '記住位置和大小', qssaRectMemo_help: '獨立視窗關閉時記住位置和大小。', updateCheck: '檢查更新', updateCheck_help: '自動檢查更新', analytics: '啟用 Google Analytics', analytics_help: '提供匿名裝置瀏覽器版本資訊。因精力有限,沙拉查詞作者會盡可能支援使用者量更多的裝置和瀏覽器。', opt: { reset: '重設設定', reset_confirm: '所有設定將還原至預設值,確定?', upload_error: '設定儲存失敗', accent: { uk: '英式', us: '美式' }, sel_blackwhitelist: '選詞黑白名單', sel_blackwhitelist_help: '黑名單相符的頁面 Saladict 將不會響應滑鼠劃詞。', pdf_blackwhitelist_help: '黑名單相符的 PDF 連結將不會跳至 Saladict 開啟。', contextMenus_description: '設定右鍵選單,可新增可自定義連結。網頁翻譯其實不需要沙拉查詞,故已有的有道和谷歌網頁翻譯目前處於維護狀態,沒有計劃新增新功能,請用其它官方擴充套件如彩雲小譯和谷歌翻譯。', contextMenus_edit: '編輯右鍵選單項目', contextMenus_url_rules: '連結中的 %s 會被取代為選詞。', baOpen: { popup_panel: '開啟字典介面', popup_fav: '新增選詞到生字本', popup_options: '進入 Saladict 設定', popup_standalone: '開啟快捷查詞獨立視窗' }, openQsStandalone: '獨立視窗設定', pdfStandalone: { default: '從不', always: '總是', manual: '手動' } } }, matchPattern: { description: '網址支援{超鏈匹配}和{正則匹配}。留空儲存即可清除。', url: '連結匹配', url_error: '不正確的超連結模式匹配表示式。', regex: '正則匹配', regex_error: '不正確的正則表示式。' }, searchMode: { icon: '顯示圖示', icon_help: '在滑鼠附近顯示一個圖示,滑鼠移動到圖示後,會顯示出字典的視窗介面。', direct: '直接搜尋', direct_help: '直接顯示字典視窗介面。', double: '滑鼠按兩下', double_help: '滑鼠按兩下所選擇的句子或單字後,會直接顯示字典視窗介面。', holding: '按住按键', holding_help: '在放開滑鼠之前,需按住選擇的按鍵才顯示字典視窗介面(Alt 為 macOS 上的 "⌥ Option"鍵。Meta 鍵為 macOS 上的「⌘ Command」鍵以及其它鍵盤的「⊞ Windows」鍵)。', instant: '滑鼠懸浮取詞', instant_help: '自動選取滑鼠附近的單字。', instantDirect: '直接取詞', instantKey: '按鍵', instantKey_help: '因技術限制,懸浮取詞通過自動選擇滑鼠附近單字實現,不設定按鍵直接取詞可能導致滑鼠無法選字,建議配合快速鍵開啟關閉。', instantDelay: '取詞等待' }, profiles: { opt: { add_name: '新增情景模式名稱', delete_confirm: '「{{name}}」將被刪除,確認?', edit_name: '變更情景模式名稱', help: '每個情景模式相當於一套獨立的設定,一些選項(帶有 {*})會隨著情景模式變化。滑鼠懸浮在字典介面的選單圖示上可快速切換,或者焦點選中選單圖示然後按{↓}。' } }, profile: { mtaAutoUnfold: '自動展開多行搜尋框', waveform: '波形控制', waveform_help: '在字典介面下方顯示音訊控制面板展開按鈕。關閉依然可以播放音訊。', stickyFold: '記憶摺疊', stickyFold_help: '查字時記住之前手動展開和收起字典的狀態,只在同個頁面生效。', opt: { item_extra: '此選項會因「情景模式」而改變。', mtaAutoUnfold: { always: '保持展開', never: '永遠不展開', once: '展開一次', popup: '只在右上彈框展開', hide: '隱藏' }, dict_selected: '已選字典' } }, dict: { add: '新增字典', more_options: '更多設定', selectionLang: '選詞語言', selectionLang_help: '當選中的文字包含相對應的語言時才顯示這個字典。', defaultUnfold: '自動展開', defaultUnfold_help: '關閉後此字典將不會自動搜尋,除非點選「展開」箭頭。適合一些需要時再深入瞭解的字典,以加快初次查字典速度。', selectionWC: '選詞字數', selectionWC_help: '當選中文字的字數符合條件時才顯示該詞典。可設定 999999 如果不希望限制字數。', preferredHeight: '字典預設高度', preferredHeight_help: '字典初次出現的最大高度。超出此高度的內容將被隱藏並顯示下箭頭。可設定 999999 如果不希望限制高度。', lang: { de: '德', en: '英', es: '西', fr: '法', ja: '日', kor: '韓', zhs: '简', zht: '繁' } }, syncService: { description: '資料同步設定。', start: '同步進行中,結束前請勿關閉此頁面。', finished: '同步結束', success: '同步成功', failed: '同步失敗', close_confirm: '設定未儲存,關閉?', delete_confirm: '清空同步設定?', shanbay: { description: '先去 shanbay.com 登入扇貝(退出後將失效)。開啟後將單向同步到扇貝生詞本(只從沙拉查詞到扇貝),只同步新增單詞(刪除不同步),只同步單詞本身(上下文等均不能同步)。生詞需要扇貝單詞庫支援才能被新增。', login: '將開啟扇貝官網,請登入再回來重新開啟。', sync_all: '上傳現有的所有生字', sync_all_confirm: '生詞本存在較多單詞,將分批上傳。注意短時間上傳太多有可能會導致封號,且不可恢復,確定繼續?', sync_last: '上傳最近的一個生字' }, eudic: { description: '使用歐路詞典同步單詞前,必須先在歐路官網(my.eudic.net/home/index)創建默認生詞本(一般初次手動導入會自動生成且無法删除)。注意短時間內不要頻繁同步,可能會造成暫時封停。', token: '授權資訊', getToken: '獲取授權', verify: '檢查 授權資訊', verified: '成功檢查 歐路授權資訊', enable_help: '開啟後每次添加生詞將自動單向同步到歐路默認生詞本(salad到歐路生詞本),只同步新增單詞本身(删除不同步)', token_help: '請確認設定有效的個人授權資訊,否則將同步失敗。可點擊底部按鈕檢查。', sync_all: '同步全部生詞', sync_help: '將salad單詞本中現有的所有生詞,同步到歐路詞典默認生詞本中(需同時開啟上方同步開關,點擊保存)', sync_all_confirm: '注意短時間內頻繁同步有可能會導致接下來一小段時間的封停,確定繼續?' }, webdav: { description: '應用設定(包括本設定)已通過瀏覽器自動同步。生詞本可通過本設定實現 WebDAV 同步。', jianguo: '參考堅果雲設定', checking: '連線中...', exist_confirm: '伺服器上已存在 Saladict 目錄。是否下載合併到本地?', upload_confirm: '馬上上傳本地資料到伺服器?', verify: '驗證伺服器', verified: '成功驗證伺服器', duration: '同步頻率', duration_help: '新增生字後會馬上上傳,資料會在上傳前保證同步,所以如果不需要多個瀏覽器即時檢視更新,可將更新檢查週期調大些以減少資源佔用及避免伺服器拒絕回應。', passwd: '密碼', url: '伺服器位址', user: '帳戶' }, ankiconnect: { description: '請確保 Anki Connect 已安裝且 Anki 在執行。', checking: '連線中...', deck_confirm: '牌組「{{deck}}」不存在 Anki 中,是否自動新增?', deck_error: '無法建立牌組「{{deck}}」。', notetype_confirm: '筆記型別「{{noteType}}」不存在 Anki 中,是否自動新增?', notetype_error: '無法建立筆記型別「{{noteType}}」。', upload_confirm: '馬上同步本地生詞到 Anki?重複的單詞(相同“Date”)會被跳過。', add_yourself: '請在 Anki 中自行新增。', verify: '檢查 Anki Connect', verified: '成功檢查 Anki Connect', enable_help: '開啟後每次儲存新單字都會自動同步到 Anki。Anki 上已存在的單字(以“Date”為準)可以在單字編輯器中編輯強制更新覆蓋到 Anki。', host: '地址', port: '埠', key: 'Key', key_help: '可在 Anki Connect 外掛中設定 key 以做簡單令牌。', deckName: '牌組', deckName_help: '如果不存在的話可以點下方「檢查 Anki Connect」讓本設定生成預設牌組。', noteType: '筆記型別', noteType_help: 'Anki 筆記型別包括一套欄位和卡片型別。如果不存在的話可以點下方「檢查 Anki Connect」讓本設定生成一套預設的筆記型別。如需自行在 Anki 新增或修改卡片模板請不要更改欄位名字。', tags: '標籤', tags_help: 'Anki 筆記可以附帶標籤。以逗號分割。', escapeHTML: '轉義 HTML', escapeHTML_help: '對筆記內容中的 HTML 字元進行轉義。如手動進行 HTML 排版請關閉選項。', syncServer: '同步伺服器', syncServer_help: '單詞新增到本地 Anki 後自動同步到伺服器(如 AnkiWeb)。' } }, titlebarOffset: { title: '校準標題欄高度', help: '不同的系統以及不同的瀏覽器設定會影響標題欄高度,沙拉查詞會嘗試自動校準,如彈出視窗依然出現偏移可自行調整。', main: '普通視窗', main_help: '普通視窗可能沒有標題欄。', panel: '簡化視窗', panel_help: '沙拉查詞的獨立視窗快捷查詞介面為簡化視窗。', calibrate: '自動校準', calibrateSuccess: '自動校準成功', calibrateError: '自動校準失敗' }, headInfo: { acknowledgement: { title: '特別鳴謝', yipanhuasheng: '新增韋氏詞典、美國傳統詞典、牛津學習詞典與歐路生詞同步;更新 Urban 詞典與 Naver 詞典', naver: '協助新增 Naver 韓國語字典', shanbay: '編寫扇貝詞典模組', trans_tw: '提供部分繁體中文翻譯', weblio: '協助新增 Weblio 辭書' }, contact_author: '聯絡作者', donate: '支援項目', instructions: '使用說明', report_issue: '軟體使用疑問和建言' }, form: { url_error: '不正確的超連結格式。', number_error: '不正確的數字' }, preload: { title: '預先下載', auto: '自動查字', auto_help: '字典介面出現時自動搜尋預先載入內容。', clipboard: '剪貼簿', help: '字典介面出現時預先載入內容到搜尋框。', selection: '滑鼠選字' }, locations: { CENTER: '居中', TOP: '上方', RIGHT: '右方', BOTTOM: '下方', LEFT: '左方', TOP_LEFT: '左上', TOP_RIGHT: '右上', BOTTOM_LEFT: '左下', BOTTOM_RIGHT: '右下' }, import_export_help: '設定已通過瀏覽器自動同步,也可以手動匯入匯出。備份為明文儲存,對安全性有要求的請自行加密。', import: { title: '匯入設定', error: { title: '匯入失敗', parse: '備份解析失敗,格式不正確。', load: '備份載入失敗,瀏覽器無法獲得本地備份。', empty: '備份中沒有發現有效資料。' } }, export: { title: '匯出設定', error: { title: '匯出失敗', empty: '沒有設定可以匯出。', parse: '設定解析失敗,無法匯出。' } }, dictAuth: { description: '隨著沙拉查詞使用者增多,如經常使用機器翻譯,建議到官網申請帳號以獲得更穩定的體驗以及更準確的結果。以下帳號資料只會保留在瀏覽器中。', dictHelp: '見{詞典}官網。', manage: '管理私用帳號' }, third_party_privacy: '第三方隱私', third_party_privacy_help: '沙拉查詞不會收集更多資料,但在查詞時單詞以及相關 cookies 資料會發送給第三方詞典服務(與在該網站上查詞一樣),如果你不希望被該服務獲取資料,請在「詞典設定」中關閉相應詞典。', third_party_privacy_extra: '本特性為沙拉查詞核心功能,無法關閉。', permissions: { success: '申請許可權成功', cancel_success: '取消許可權成功', failed: '申請許可權失敗', cancelled: '申請許可權被使用者取消', missing: '缺少許可權「{{permission}}」。請給予許可權或者關閉相關功能。', clipboardRead: '讀取剪貼簿', clipboardRead_help: '快捷查詞或者右上彈框設定預載入剪貼簿時需要讀取剪貼簿許可權。', clipboardWrite: '寫入剪貼簿', clipboardWrite_help: '機器翻譯詞典標題欄選單複製原文譯文或生詞本匯出到剪貼簿需要寫入剪貼簿許可權。' }, unsupportedFeatures: { ff: '火狐尚不支援「{{feature}}」功能。' } } ================================================ FILE: src/_locales/zh-TW/popup.ts ================================================ import { locale as _locale } from '../zh-CN/popup' export const locale: typeof _locale = { title: '沙拉查詞-右上彈框', app_active_title: '啟用滑鼠選字', app_temp_active_title: '對目前頁面暫時關閉滑鼠選字', instant_capture_pinned: '(釘選)', instant_capture_title: '啟用滑鼠懸浮取詞', notebook_added: '已新增', notebook_empty: '目前頁面沒有發現選詞', notebook_error: '無法新增選詞到生字本', page_no_response: '頁面無回應', qrcode_title: '目前頁面二維條碼' } ================================================ FILE: src/_locales/zh-TW/wordpage.ts ================================================ import { locale as _locale } from '../zh-CN/wordpage' export const locale: typeof _locale = { title: { history: '沙拉查詞-查單字紀錄', notebook: '沙拉查詞-生字本' }, localonly: '僅本機儲存', column: { add: '新增', date: '日期', edit: '編輯', note: '筆記', source: '來源', trans: '翻譯', word: '單字' }, delete: { title: '刪除單字', all: '刪除所有單字', confirm: ',確認?', page: '刪除本頁單字', selected: '刪除選取單字' }, export: { title: '匯出文字', all: '匯出所有單字', description: '編寫產生的範本,描述每條記錄產生的樣子:', explain: '如何配合 ANKI 等工具', gencontent: '代表的內容', linebreak: { default: '保留換行', n: '換行替換為 \\n', br: '換行替換為
', p: '換行替換為

', space: '換行替換為空格' }, page: '輸出本頁單字', placeholder: '預留位置', htmlescape: { title: '對筆記內容中的 HTML 字元進行轉義', text: '轉義 HTML' }, selected: '輸出選中單字' }, filterWord: { chs: '中文', eng: '英文', word: '單字', phrase: '片語和句子' }, wordCount: { selected: '選中 {{count}} 個', selected_plural: '選中 {{count}} 個', total: '共 {{count}} 個', total_plural: '共 {{count}} 個' } } ================================================ FILE: src/_sass_shared/_fancy-scrollbar.scss ================================================ $scrollbar-size: 8px; $scrollbar-ff-width: auto; // FF-only accepts auto, thin, none $scrollbar-minlength: 50px; // Minimum length of scrollbar thumb $scrollbar-track-color: transparent; $scrollbar-color: rgba(138, 138, 138, 0.25); $scrollbar-color-hover: rgba(138, 138, 138, 0.35); $scrollbar-color-active: rgba(138, 138, 138, 0.6); .fancy-scrollbar { overscroll-behavior: contain; overflow-y: scroll; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; scrollbar-width: $scrollbar-ff-width; // Firefox has incorrect track color // scrollbar-color: $scrollbar-color $scrollbar-track-color; &::-webkit-scrollbar { height: $scrollbar-size; width: $scrollbar-size; } &::-webkit-scrollbar-track { background-color: $scrollbar-track-color; } &::-webkit-scrollbar-thumb { background-color: $scrollbar-color; border-radius: math.div($scrollbar-size, 2); } &::-webkit-scrollbar-thumb:hover { background-color: $scrollbar-color-hover; } &::-webkit-scrollbar-thumb:active { background-color: $scrollbar-color-active; } &::-webkit-scrollbar-thumb:vertical { min-height: $scrollbar-minlength; } &::-webkit-scrollbar-thumb:horizontal { min-width: $scrollbar-minlength; } } ================================================ FILE: src/_sass_shared/_global/_interfaces.scss ================================================ /*-----------------------------------------------*\ Tailored for exposed components \*-----------------------------------------------*/ %reset-important { background-attachment: scroll !important; background-color: transparent !important; background-image: none !important; background-position: 0 0 !important; background-repeat: repeat !important; border-color: transparent !important; border-radius: 0 !important; border-style: none !important; border-width: 0 !important; bottom: auto !important; clear: none !important; clip: auto !important; color: inherit !important; counter-increment: none !important; counter-reset: none !important; cursor: auto !important; direction: inherit !important; display: inline !important; float: none !important; font-family: inherit !important; font-size: inherit !important; font-style: inherit !important; font-variant: normal !important; font-weight: inherit !important; height: auto !important; left: auto !important; letter-spacing: normal !important; line-height: inherit !important; list-style-type: inherit !important; list-style-position: outside !important; list-style-image: none !important; margin: 0 !important; max-height: none !important; max-width: none !important; min-height: 0 !important; min-width: 0 !important; opacity: 1 !important; outline: invert none medium !important; overflow: visible !important; padding: 0 !important; position: static !important; quotes: "" "" !important; right: auto !important; table-layout: auto !important; text-align: inherit !important; text-decoration: inherit !important; text-indent: 0 !important; text-transform: none !important; top: auto !important; unicode-bidi: normal !important; vertical-align: baseline !important; visibility: inherit !important; white-space: normal !important; width: auto !important; word-spacing: normal !important; z-index: auto !important; /* CSS3 */ /* Including all prefixes according to http://caniuse.com/ */ /* CSS Animations don't cascade, so don't require resetting */ background-origin: padding-box !important; background-clip: border-box !important; background-size: auto !important; border-image: none !important; border-radius: 0 !important; box-shadow: none !important; box-sizing: content-box !important; column-count: auto !important; column-gap: normal !important; column-rule: medium none black !important; column-span: 1 !important; column-width: auto !important; font-feature-settings: normal !important; overflow-x: visible !important; overflow-y: visible !important; hyphens: manual !important; perspective: none !important; perspective-origin: 50% 50% !important; backface-visibility: visible !important; text-shadow: none !important; transition: all 0s ease 0s !important; transform: none; // In Firefox @keyframes doesn't override !important transform-origin: 50% 50% !important; transform-style: flat !important; word-break: normal !important; } ================================================ FILE: src/_sass_shared/_global/_mixins.scss ================================================ // Wrapper for @at-root. // Emits values with namespace selectors. // Usage // @include atRoot(.ns) { ... } // @include atRoot(.ns2, -enter) { ... } // @include atRoot(.ns3, -enter, -exit) { ... } @mixin atRoot($nsSelector, $modifiers...) { @if length($modifiers) > 0 { $selectors: (); @each $modifier in $modifiers { $selectors: append($selectors, #{$nsSelector}#{" "}#{&}#{$modifier}, comma); } @at-root #{$selectors} { @content; } } @else { @at-root #{$nsSelector} & { @content; } } } // Usage // @include isAnimate { ... } // @include isAnimate(-enter) { ... } // @include isAnimate(-enter, -exit) { ... } @mixin isAnimate($modifiers...) { @include atRoot('.isAnimate', $modifiers...) { @content; } } // Usage // @include isDarkMode { ... } @mixin isDarkMode($modifiers...) { @include atRoot('.darkMode', $modifiers...) { @content; } } ================================================ FILE: src/_sass_shared/_global/_variables.scss ================================================ ================================================ FILE: src/_sass_shared/_global/_z-indices.scss ================================================ // Max 2^31 − 1 = 2147483647 $global-zindex-dropdown-backdrop: 2147483639 !default; $global-zindex-navbar: 2147483640 !default; $global-zindex-dropdown: 2147483641 !default; $global-zindex-fixed: 2147483642 !default; $global-zindex-sticky: 2147483643 !default; $global-zindex-modal-backdrop: 2147483644 !default; $global-zindex-modal: 2147483645 !default; $global-zindex-popover: 2147483646 !default; $global-zindex-tooltip: 2147483647 !default; $global-zindex-dicteditor: 2147483646; $global-zindex-dictpanel-dragbg: 2147483646; $global-zindex-dictpanel: 2147483647; $global-zindex-bowl: 2147483647; ================================================ FILE: src/_sass_shared/_namespace.scss ================================================ @use "sass:math"; ================================================ FILE: src/_sass_shared/_reset.scss ================================================ /*-----------------------------------------------*\ Custom reset \*-----------------------------------------------*/ @import '~normalize-scss'; h1, h2, h3, h4, ul, li, button { margin: 0; padding: 0; } img { display: block; max-width: 95%; } p { margin: 0.5em 0; } ul li { list-style-type: none; } button { background: transparent; border: none; &:hover { outline: none; } } a { color: #f9690e; text-decoration: none; &:hover { text-decoration: underline; } } select { color: #666; border: 1px solid rgba(133, 133, 133, 0.28); background: transparent; } ================================================ FILE: src/_sass_shared/_theme.scss ================================================ .saladict-theme { background-color: #fff; color: #333; --color-brand: #5caf9e; --color-background: #fff; --color-rgb-background: 255, 255, 255; --color-font: #333; --color-font-grey: #666; --color-divider: #ddd; @include isDarkMode { background-color: #222; color: #ddd; --color-brand: #1e947e; --color-background: #222; --color-rgb-background: 34, 34, 34; --color-font: #ddd; --color-font-grey: #aaa; --color-divider: #4d4748; } } ================================================ FILE: src/app-config/auth.ts ================================================ import { auth as baidu } from '@/components/dictionaries/baidu/auth' import { auth as caiyun } from '@/components/dictionaries/caiyun/auth' import { auth as sogou } from '@/components/dictionaries/sogou/auth' import { auth as tencent } from '@/components/dictionaries/tencent/auth' import { auth as youdaotrans } from '@/components/dictionaries/youdaotrans/auth' export const defaultDictAuths = { baidu, caiyun, sogou, tencent, youdaotrans } export type DictAuths = typeof defaultDictAuths export const getDefaultDictAuths = (): DictAuths => JSON.parse(JSON.stringify(defaultDictAuths)) ================================================ FILE: src/app-config/context-menus.ts ================================================ export interface CustomContextItem { name: string url: string } export type ContextItem = string | CustomContextItem export function getAllContextMenus(): { [id: string]: ContextItem } { return { baidu_page_translate: 'x', baidu_search: 'https://www.baidu.com/s?ie=utf-8&wd=%s', bing_dict: 'https://cn.bing.com/dict/?q=%s', bing_search: 'https://www.bing.com/search?q=%s', caiyuntrs: 'x', cambridge: 'http://dictionary.cambridge.org/spellcheck/english-chinese-simplified/?q=%s', copy_pdf_url: 'x', dictcn: 'https://dict.eudic.net/dicts/en/%s', etymonline: 'http://www.etymonline.com/index.php?search=%s', google_cn_page_translate: 'x', google_page_translate: 'x', google_search: 'https://www.google.com/search?safe=off&newwindow=1&q=%s', google_translate: 'https://translate.google.com/#auto/zh-CN/%s', google_cn_translate: 'https://translate.google.cn/#auto/zh-CN/%s', guoyu: 'https://www.moedict.tw/%s', iciba: 'http://www.iciba.com/%s', liangan: 'https://www.moedict.tw/~%s', longman_business: 'http://www.ldoceonline.com/search/?q=%s', merriam_webster: 'http://www.merriam-webster.com/dictionary/%s', microsoft_page_translate: 'x', oxford: 'http://www.oxforddictionaries.com/us/definition/english/%s', saladict: 'x', saladict_standalone: 'x', sogou_page_translate: 'x', sogou: 'https://fanyi.sogou.com/#auto/zh-CHS/%s', termonline: 'https://www.termonline.cn/list.htm?k=%s', view_as_pdf: 'x', youdao_page_translate: 'x', youdao: 'http://dict.youdao.com/w/%s', youglish: 'https://youglish.com/search/%s' } } ================================================ FILE: src/app-config/dicts.ts ================================================ import { SupportedLangs } from '@/_helpers/lang-check' import baidu from '@/components/dictionaries/baidu/config' import bing from '@/components/dictionaries/bing/config' import ahdict from '@/components/dictionaries/ahdict/config' import oaldict from '@/components/dictionaries/oaldict/config' import caiyun from '@/components/dictionaries/caiyun/config' import cambridge from '@/components/dictionaries/cambridge/config' import cnki from '@/components/dictionaries/cnki/config' import cobuild from '@/components/dictionaries/cobuild/config' import etymonline from '@/components/dictionaries/etymonline/config' import eudic from '@/components/dictionaries/eudic/config' import google from '@/components/dictionaries/google/config' import googledict from '@/components/dictionaries/googledict/config' import guoyu from '@/components/dictionaries/guoyu/config' import hjdict from '@/components/dictionaries/hjdict/config' import jikipedia from '@/components/dictionaries/jikipedia/config' import jukuu from '@/components/dictionaries/jukuu/config' import lexico from '@/components/dictionaries/lexico/config' import liangan from '@/components/dictionaries/liangan/config' import longman from '@/components/dictionaries/longman/config' import macmillan from '@/components/dictionaries/macmillan/config' import mojidict from '@/components/dictionaries/mojidict/config' import naver from '@/components/dictionaries/naver/config' import renren from '@/components/dictionaries/renren/config' // import shanbay from '@/components/dictionaries/shanbay/config' import sogou from '@/components/dictionaries/sogou/config' import tencent from '@/components/dictionaries/tencent/config' import urban from '@/components/dictionaries/urban/config' import vocabulary from '@/components/dictionaries/vocabulary/config' import weblio from '@/components/dictionaries/weblio/config' import weblioejje from '@/components/dictionaries/weblioejje/config' import merriamwebster from '@/components/dictionaries/merriamwebster/config' import websterlearner from '@/components/dictionaries/websterlearner/config' import wikipedia from '@/components/dictionaries/wikipedia/config' import youdao from '@/components/dictionaries/youdao/config' import youdaotrans from '@/components/dictionaries/youdaotrans/config' import zdic from '@/components/dictionaries/zdic/config' // For TypeScript to generate typings // Follow alphabetical order for easy reading export const defaultAllDicts = { baidu: baidu(), bing: bing(), ahdict: ahdict(), oaldict: oaldict(), caiyun: caiyun(), cambridge: cambridge(), cnki: cnki(), cobuild: cobuild(), etymonline: etymonline(), eudic: eudic(), google: google(), googledict: googledict(), guoyu: guoyu(), hjdict: hjdict(), jikipedia: jikipedia(), jukuu: jukuu(), lexico: lexico(), liangan: liangan(), longman: longman(), macmillan: macmillan(), mojidict: mojidict(), naver: naver(), renren: renren(), // shanbay: shanbay(), sogou: sogou(), tencent: tencent(), urban: urban(), vocabulary: vocabulary(), weblio: weblio(), weblioejje: weblioejje(), merriamwebster: merriamwebster(), websterlearner: websterlearner(), wikipedia: wikipedia(), youdao: youdao(), youdaotrans: youdaotrans(), zdic: zdic() } export type AllDicts = typeof defaultAllDicts export const getAllDicts = (): AllDicts => JSON.parse(JSON.stringify(defaultAllDicts)) interface DictItemBase { /** * Supported language: en, zh-CN, zh-TW, ja, kor, fr, de, es * `1` for supported */ lang: string /** Show this dictionary when selection contains words in the chosen languages. */ selectionLang: SupportedLangs /** * If set to true, the dict start searching automatically. * Otherwise it'll only start seaching when user clicks the unfold button. * Default MUST be true and let user decide. */ defaultUnfold: SupportedLangs /** * This is the default height when the dict first renders the result. * If the content height is greater than the preferred height, * the preferred height is used and a mask with a view-more button is shown. * Otherwise the content height is used. */ selectionWC: { min: number max: number } /** Word count to start searching */ preferredHeight: number } /** * Optional dict custom options. Can only be boolean, number or string. * For string, add additional `options_sel` field to list out choices. */ type DictItemWithOptions< Options extends | { [option: string]: number | boolean | string } | undefined = undefined > = Options extends undefined ? DictItemBase : DictItemBase & { options: Options } /** Infer selectable options type */ export type SelectOptions< Options extends | { [option: string]: number | boolean | string } | undefined = undefined, Key extends keyof Options = Options extends undefined ? never : keyof Options > = { [opt in Key extends any ? Options[Key] extends string ? Key : never : never]: Options[opt][] } /** * If an option is of `string` type there will be an array * of options in `options_sel` field. */ export type DictItem< Options extends | { [option: string]: number | boolean | string } | undefined = undefined, Key extends keyof Options = Options extends undefined ? never : keyof Options > = Options extends undefined ? DictItemWithOptions : DictItemWithOptions & ((Key extends any ? Options[Key] extends string ? Key : never : never) extends never ? {} : { options_sel: SelectOptions }) ================================================ FILE: src/app-config/index.ts ================================================ import { DeepReadonly } from '@/typings/helpers' import { SupportedLangs } from '@/_helpers/lang-check' import { getAllDicts } from './dicts' import { getAllContextMenus } from './context-menus' import { MtaAutoUnfold as _MtaAutoUnfold } from './profiles' import { getDefaultDictAuths } from './auth' import { isFirefox } from '@/_helpers/saladict' export type LangCode = 'zh-CN' | 'zh-TW' | 'en' const langUI = browser.i18n.getUILanguage() const langCode: LangCode = langUI === 'zh-CN' ? 'zh-CN' : langUI === 'zh-TW' || langUI === 'zh-HK' ? 'zh-TW' : 'en' export type DictConfigsMutable = ReturnType export type DictConfigs = DeepReadonly export type DictID = keyof DictConfigsMutable export type MtaAutoUnfold = _MtaAutoUnfold export type TCDirection = | 'CENTER' | 'TOP' | 'RIGHT' | 'BOTTOM' | 'LEFT' | 'TOP_LEFT' | 'TOP_RIGHT' | 'BOTTOM_LEFT' | 'BOTTOM_RIGHT' export type InstantSearchKey = 'direct' | 'ctrl' | 'alt' | 'shift' /** '' means no preload */ export type PreloadSource = '' | 'clipboard' | 'selection' export type AllDicts = ReturnType export type AppConfigMutable = ReturnType export type AppConfig = DeepReadonly export const getDefaultConfig: () => AppConfig = _getDefaultConfig export default getDefaultConfig function _getDefaultConfig() { return { version: 14, /** activate app, won't affect triple-ctrl setting */ active: true, /** Run extension in background */ runInBg: false, /** enable Google analytics */ analytics: true, /** enable update check */ updateCheck: true, /** disable selection on type fields, like input and textarea */ noTypeField: false, /** use animation for transition */ animation: true, /** language code for locales */ langCode, /** panel width */ panelWidth: 450, /** panel max height in percentage, 0 < n < 100 */ panelMaxHeightRatio: 80, bowlOffsetX: 15, bowlOffsetY: -45, darkMode: false, /** custom panel css */ panelCSS: '', /** panel font-size */ fontSize: 13, /** sniff pdf request */ pdfSniff: false, /** * Open PDF viewer in standalone panel. * 'manual': do not redirect on web requests */ pdfStandalone: '' as '' | 'always' | 'manual', /** URLs, [regexp.source, match_pattern] */ pdfWhitelist: [] as [string, string][], /** URLs, [regexp.source, match_pattern] */ // tslint:disable-next-line: no-unnecessary-type-assertion pdfBlacklist: [ ['^(http|https)://[^/]*?cnki\\.net(/.*)?$', '*://*.cnki.net/*'], [ '^(http|https)://[^/]*?googleusercontent\\.com(/.*)?$', '*://*.googleusercontent.com/*' ], [ '^(http|https)://sh-download\\.weiyun\\.com(/.*)?$', '*://sh-download.weiyun.com/*' ] ] as [string, string][], /** track search history */ searchHistory: false, /** incognito mode */ searchHistoryInco: false, /** open word editor when adding a word to notebook */ editOnFav: true, /** Show suggestions when typing on search box */ searchSuggests: true, /** Enable touch related support */ touchMode: false, /** when and how to search text */ mode: { /** show pop icon first */ icon: true, /** how panel directly */ direct: false, /** double click */ double: false, /** holding a key */ holding: { alt: false, shift: false, ctrl: false, meta: false }, /** cursor instant capture */ instant: { enable: false, key: 'alt' as InstantSearchKey, delay: 600 } }, /** when and how to search text if the panel is pinned */ pinMode: { /** direct: on mouseup */ direct: true, /** double: double click */ double: false, /** holding a key */ holding: { alt: false, shift: false, ctrl: false, meta: false }, /** cursor instant capture */ instant: { enable: false, key: 'alt' as InstantSearchKey, delay: 600 } }, /** when and how to search text inside dict panel */ panelMode: { /** direct: on mouseup */ direct: false, /** double: double click */ double: false, /** holding a key */ holding: { alt: false, shift: false, ctrl: false, meta: false }, /** cursor instant capture */ instant: { enable: false, key: 'alt' as InstantSearchKey, delay: 600 } }, /** when this is a quick search standalone panel running */ qsPanelMode: { /** direct: on mouseup */ direct: true, /** double: double click */ double: false, /** holding a key */ holding: { alt: false, shift: false, ctrl: true, meta: false }, /** cursor instant capture */ instant: { enable: false, key: 'alt' as InstantSearchKey, delay: 600 } }, /** hover instead of click */ bowlHover: true, /** double click delay, in ms */ doubleClickDelay: 450, /** show quick search panel when triple press ctrl */ tripleCtrl: true, /** preload content on quick search panel */ qsPreload: 'selection' as PreloadSource, /** auto search when quick search panel opens */ qsAuto: false, /** where should the dict appears */ qsLocation: 'CENTER' as TCDirection, /** focus quick search panel when shows up */ qsFocus: true, /** pin panel when shows up */ defaultPinned: false, /** should panel be in a standalone window */ qsStandalone: true, /** standalone panel height */ qssaHeight: 600, /** resize main widnow to leave space to standalone window */ qssaSidebar: '' as '' | 'left' | 'right', /** should standalone panel response to page selection */ qssaPageSel: true, /** should standalone panel memo position and dimension on close */ qssaRectMemo: false, /** browser action panel width defaults to as wide as possible */ baWidth: -1, baHeight: 550, /** browser action panel preload source */ baPreload: 'selection' as PreloadSource, /** auto search when browser action panel shows */ baAuto: false, /** * browser action behavior * 'popup_panel' - show dict panel * 'popup_fav' - add selection to notebook * 'popup_options' - opten options * 'popup_standalone' - open standalone panel * others are same as context menus */ baOpen: 'popup_panel', /** context tranlate engines */ ctxTrans: { google: true, youdaotrans: true, baidu: true, tencent: false, caiyun: false, sogou: false }, /** start searching when source containing the languages */ language: { chinese: true, english: true, japanese: true, korean: true, french: true, spanish: true, deutsch: true, others: false, matchAll: false } as SupportedLangs, /** auto pronunciation */ autopron: { cn: { dict: '' as DictID | '', list: ['zdic', 'guoyu'] as DictID[] }, en: { dict: '' as DictID | '', list: [ 'bing', 'cambridge', 'cobuild', 'eudic', 'longman', 'macmillan', 'lexico', 'urban', 'websterlearner', 'youdao' ] as DictID[], accent: 'uk' as 'us' | 'uk' }, machine: { dict: '' as DictID | '', list: ['google', 'sogou', 'tencent', 'baidu', 'caiyun'], // play translation or source src: 'trans' as 'trans' | 'searchText' } }, /** URLs, [regexp.source, match_pattern] */ whitelist: [] as [string, string][], /** URLs, [regexp.source, match_pattern] */ // tslint:disable-next-line: no-unnecessary-type-assertion blacklist: [ ['^https://stackedit\\.io(/.*)?$', 'https://stackedit.io/*'], ['^https://docs\\.google\\.com(/.*)?$', 'https://docs.google.com/*'], ['^https://docs\\.qq\\.com(/.*)?$', 'https://docs.qq.com/*'] ] as [string, string][], contextMenus: { selected: isFirefox || !langCode.startsWith('zh-') ? ['view_as_pdf', 'google_translate', 'saladict'] : ['view_as_pdf', 'caiyuntrs', 'google_translate', 'saladict'], all: getAllContextMenus() }, /** Open settings on first switching "translation" profile */ showedDictAuth: false, dictAuth: getDefaultDictAuths() } } ================================================ FILE: src/app-config/merge-config.ts ================================================ import { getDefaultConfig, AppConfig, AppConfigMutable } from '@/app-config' import { defaultAllDicts } from './dicts' import forEach from 'lodash/forEach' import isNumber from 'lodash/isNumber' import isString from 'lodash/isString' import isBoolean from 'lodash/isBoolean' import get from 'lodash/get' import set from 'lodash/set' export default mergeConfig export function mergeConfig( oldConfig: AppConfig, baseConfig?: AppConfig ): AppConfig { const base: AppConfigMutable = baseConfig ? JSON.parse(JSON.stringify(baseConfig)) : getDefaultConfig() /* ----------------------------------------------- *\ Pre-merge Patch Start \* ----------------------------------------------- */ let oldVersion = oldConfig.version if (oldVersion < 13) { ;(oldConfig as AppConfigMutable).showedDictAuth = true } if (oldVersion <= 9) { oldVersion = 10 ;['mode', 'pinMode', 'panelMode', 'qsPanelMode'].forEach(mode => { base[mode].holding.shift = false base[mode].holding.ctrl = !!oldConfig[mode]['ctrl'] base[mode].holding.meta = !!oldConfig[mode]['ctrl'] delete oldConfig[mode]['ctrl'] }) } rename('tripleCtrlPreload', 'qsPreload') rename('tripleCtrlAuto', 'qsAuto') rename('tripleCtrlLocation', 'qsLocation') rename('tripleCtrlStandalone', 'qsStandalone') rename('tripleCtrlHeight', 'qssaHeight') rename('tripleCtrlSidebar', 'qssaSidebar') rename('tripleCtrlPageSel', 'qssaPageSel') /* ----------------------------------------------- *\ Pre-merge Patch End \* ----------------------------------------------- */ Object.keys(base).forEach(key => { switch (key) { case 'langCode': merge('langCode', val => /^(zh-CN|zh-TW|en)$/.test(val)) break case 'pdfWhitelist': case 'pdfBlacklist': case 'whitelist': case 'blacklist': merge(key, val => Array.isArray(val)) break case 'searhHistory': case 'searchHistory': base.searchHistory = oldConfig[key] break case 'searhHistoryInco': case 'searchHistoryInco': base.searchHistoryInco = oldConfig[key] break case 'mode': case 'pinMode': case 'panelMode': case 'qsPanelMode': if (key === 'mode') { mergeBoolean('mode.icon') } mergeBoolean(`${key}.direct`) mergeBoolean(`${key}.double`) mergeBoolean(`${key}.holding.alt`) mergeBoolean(`${key}.holding.shift`) mergeBoolean(`${key}.holding.ctrl`) mergeBoolean(`${key}.holding.meta`) mergeBoolean(`${key}.instant.enable`) merge(`${key}.instant.key`, val => /^(direct|ctrl|alt|shift)$/.test(val) ) mergeNumber(`${key}.instant.delay`) break case 'qsPreload': merge( 'qsPreload', val => val === '' || val === 'clipboard' || val === 'selection' ) break case 'qsLocation': merge( 'qsLocation', val => val === 'CENTER' || val === 'TOP' || val === 'RIGHT' || val === 'BOTTOM' || val === 'LEFT' || val === 'TOP_LEFT' || val === 'TOP_RIGHT' || val === 'BOTTOM_LEFT' || val === 'BOTTOM_RIGHT' ) break case 'baPreload': merge( 'baPreload', val => val === '' || val === 'clipboard' || val === 'selection' ) break case 'ctxTrans': forEach(base.ctxTrans, (value, key) => { mergeBoolean(`ctxTrans.${key}`) }) break case 'language': forEach(base.language, (value, key) => { mergeBoolean(`language.${key}`) }) break case 'autopron': merge('autopron.cn.dict', id => defaultAllDicts[id]) merge('autopron.en.dict', id => defaultAllDicts[id]) merge('autopron.en.accent', val => val === 'us' || val === 'uk') merge('autopron.machine.dict', id => defaultAllDicts[id]) merge( 'autopron.machine.src', val => val === 'trans' || val === 'searchText' ) break case 'contextMenus': forEach(oldConfig.contextMenus.all, (dict, id) => { if (typeof dict === 'string') { // default menus if (base.contextMenus.all[id]) { mergeString(`contextMenus.all.${id}`) } } else { // custom menus mergeString(`contextMenus.all.${id}.name`) mergeString(`contextMenus.all.${id}.url`) } }) mergeSelectedContextMenus('contextMenus') break case 'dictAuth': merge('dictAuth', Boolean) break default: switch (typeof base[key]) { case 'string': mergeString(key) break case 'boolean': mergeBoolean(key) break case 'number': mergeNumber(key) break default: console.error( new Error(`merge config: missing handler for '${key}'`) ) } break } }) /* ----------------------------------------------- *\ Post-merge Patch Start \* ----------------------------------------------- */ oldVersion = oldConfig.version if (oldVersion <= 10) { oldVersion = 11 base.contextMenus.selected.unshift('view_as_pdf') } if (oldVersion <= 11) { oldVersion = 12 base.blacklist.push([ '^https://stackedit.io(/.*)?$', 'https://stackedit.io/*' ]) } if (oldConfig.language['minor'] === false) { base.language.japanese = false base.language.korean = false base.language.french = false base.language.spanish = false base.language.deutsch = false } if (base.panelMaxHeightRatio < 1) { base.panelMaxHeightRatio = Math.round(base.panelMaxHeightRatio * 100) } /* ----------------------------------------------- *\ Post-merge Patch End \* ----------------------------------------------- */ return base function rename(oldName: string, newName: string): void { if ( !Object.prototype.hasOwnProperty.call(oldConfig, newName) && Object.prototype.hasOwnProperty.call(oldConfig, oldName) ) { ;(oldConfig as AppConfigMutable)[newName] = oldConfig[oldName] } } function mergeSelectedContextMenus(path: string): void { const selected = get(oldConfig, [path, 'selected']) if (Array.isArray(selected)) { if (selected.length === 0) { set(base, [path, 'selected'], []) } else { const allContextMenus = get(base, [path, 'all']) const arr = selected.filter(id => allContextMenus[id]) if (arr.length > 0) { set(base, [path, 'selected'], arr) } } } } function mergeNumber(path: string): void { return merge(path, isNumber) } function mergeString(path: string): void { return merge(path, isString) } function mergeBoolean(path: string): void { return merge(path, isBoolean) } function merge(path: string, predicate: (val) => boolean): void { const val = get(oldConfig, path) if (predicate(val)) { set(base, path, val) } } } ================================================ FILE: src/app-config/merge-profile.ts ================================================ import { Profile, ProfileMutable, getDefaultProfile } from './profiles' import forEach from 'lodash/forEach' import isNumber from 'lodash/isNumber' import isString from 'lodash/isString' import isBoolean from 'lodash/isBoolean' import get from 'lodash/get' import set from 'lodash/set' import { DictID } from '.' export function mergeProfile( oldProfile: Profile, baseProfile?: Profile ): Profile { const base: ProfileMutable = baseProfile ? JSON.parse(JSON.stringify(baseProfile)) : getDefaultProfile(oldProfile.id) Object.keys(base).forEach(key => { switch (key) { case 'dicts': mergeDicts() break default: switch (typeof base[key]) { case 'string': mergeString(key) break case 'boolean': mergeBoolean(key) break case 'number': mergeNumber(key) break default: console.error( new Error(`merge profile: missing handler for '${key}'`) ) } break } }) function mergeDicts() { mergeSelectedDicts('dicts') forEach(base.dicts.all, (dict, id) => { // legacy const unfold = get(oldProfile, `dicts.all.${id}.defaultUnfold`) if (isBoolean(unfold)) { set(base, `dicts.all.${id}.defaultUnfold`, { chinese: unfold, english: unfold, japanese: unfold, korean: unfold, french: unfold, spanish: unfold, deutsch: unfold, others: unfold }) } else { mergeBoolean(`dicts.all.${id}.defaultUnfold.chinese`) mergeBoolean(`dicts.all.${id}.defaultUnfold.english`) mergeBoolean(`dicts.all.${id}.defaultUnfold.japanese`) mergeBoolean(`dicts.all.${id}.defaultUnfold.korean`) mergeBoolean(`dicts.all.${id}.defaultUnfold.french`) mergeBoolean(`dicts.all.${id}.defaultUnfold.spanish`) mergeBoolean(`dicts.all.${id}.defaultUnfold.deutsch`) mergeBoolean(`dicts.all.${id}.defaultUnfold.others`) } // legacy const chs = get(oldProfile, `dicts.all.${id}.selectionLang.chs`) if (isBoolean(chs)) { set(base, `dicts.all.${id}.selectionLang.chinese`, chs) } else { mergeBoolean(`dicts.all.${id}.selectionLang.chinese`) } const eng = get(oldProfile, `dicts.all.${id}.selectionLang.eng`) if (isBoolean(eng)) { set(base, `dicts.all.${id}.selectionLang.english`, eng) } else { mergeBoolean(`dicts.all.${id}.selectionLang.english`) } mergeBoolean(`dicts.all.${id}.selectionLang.japanese`) mergeBoolean(`dicts.all.${id}.selectionLang.korean`) mergeBoolean(`dicts.all.${id}.selectionLang.french`) mergeBoolean(`dicts.all.${id}.selectionLang.spanish`) mergeBoolean(`dicts.all.${id}.selectionLang.deutsch`) mergeBoolean(`dicts.all.${id}.selectionLang.others`) mergeNumber(`dicts.all.${id}.preferredHeight`) mergeNumber(`dicts.all.${id}.selectionWC.min`) mergeNumber(`dicts.all.${id}.selectionWC.max`) if (dict['options']) { forEach(dict['options'], (value, opt) => { if (isNumber(value)) { mergeNumber(`dicts.all.${id}.options.${opt}`) } else if (isBoolean(value)) { mergeBoolean(`dicts.all.${id}.options.${opt}`) } else if (isString(value)) { const choice = get(oldProfile, `dicts.all.${id}.options.${opt}`) const options = get(base, `dicts.all.${id}.options_sel.${opt}`) set( base, `dicts.all.${id}.options.${opt}`, options.includes(choice) ? choice : value ) } }) // legacy bug // slInitial default to collapse if (!isNumber(oldProfile.version)) { const machineDicts: DictID[] = [ 'baidu', 'caiyun', 'google', 'sogou', 'tencent', 'youdaotrans' ] if ( machineDicts.every( id => get(base, `dicts.all.${id}.options.slInitial`) === 'hide' ) ) { machineDicts.forEach(id => { set(base, `dicts.all.${id}.options.slInitial`, 'collapse') }) } } // legacy const pdfNewline = get(oldProfile, `dicts.all.${id}.options.pdfNewline`) if (isBoolean(pdfNewline)) { set( base, `dicts.all.${id}.options.keepLF`, pdfNewline ? 'all' : 'webpage' ) } } }) } /* ----------------------------------------------- *\ Patch Start \* ----------------------------------------------- */ // hjdict changed korean location if ((base.dicts.all.hjdict.options.chsas as string) === 'kor') { base.dicts.all.hjdict.options.chsas = 'kr' } /* ----------------------------------------------- *\ Patch End \* ----------------------------------------------- */ return base function mergeSelectedDicts(path: string): void { const selected = get(oldProfile, [path, 'selected']) if (Array.isArray(selected)) { if (selected.length === 0) { set(base, [path, 'selected'], []) } else { const allDict = get(base, [path, 'all']) const arr = selected .map(id => (id === 'olad' ? 'lexico' : id)) .filter(id => allDict[id]) if (arr.length > 0) { set(base, [path, 'selected'], arr) } } } } function mergeNumber(path: string): void { return merge(path, isNumber) } function mergeString(path: string): void { return merge(path, isString) } function mergeBoolean(path: string): void { return merge(path, isBoolean) } function merge(path: string, predicate: (val) => boolean): void { const val = get(oldProfile, path) if (predicate(val)) { set(base, path, val) } } } ================================================ FILE: src/app-config/profiles.ts ================================================ import { DeepReadonly } from '@/typings/helpers' import { genUniqueKey } from '@/_helpers/uniqueKey' import { getAllDicts } from './dicts' export type MtaAutoUnfold = '' | 'once' | 'always' | 'popup' | 'hide' export type ProfileMutable = ReturnType export type Profile = DeepReadonly export interface ProfileID { id: string name: string } export type ProfileIDList = Array export const getDefaultProfile: (id?: string) => Profile = _getDefaultProfile export default getDefaultProfile export function _getDefaultProfile(id?: string) { return { version: 1, id: id || genUniqueKey(), /** auto unfold multiline textarea search box */ mtaAutoUnfold: '' as MtaAutoUnfold, /** show waveform control panel */ waveform: true, /** remember user manual dict folding on the same page */ stickyFold: false, dicts: { /** default selected dictionaries */ selected: [ 'bing', 'cobuild', 'cambridge', 'youdao', 'urban', 'vocabulary', 'caiyun', 'youdaotrans', 'zdic', 'guoyu', 'liangan', 'googledict' ] as Array>, // settings of each dict will be auto-generated all: getAllDicts() } } } export function getDefaultProfileID(id?: string): ProfileID { return { id: id || genUniqueKey(), name: '%%_default_%%' } } export interface ProfileStorage { idItem: ProfileID profile: Profile } export function genProfilesStorage(): { profileIDList: ProfileIDList profiles: Profile[] } { const defaultID = getDefaultProfileID() const defaultProfile = getDefaultProfile(defaultID.id) const sentenceStorage = sentence() const translationStorage = translation() const scholarStorage = scholar() const nihongoStorage = nihongo() return { profileIDList: [ defaultID, sentenceStorage.idItem, translationStorage.idItem, scholarStorage.idItem, nihongoStorage.idItem ], profiles: [ defaultProfile, sentenceStorage.profile, translationStorage.profile, scholarStorage.profile, nihongoStorage.profile ] } } export function sentence(): ProfileStorage { const idItem = getDefaultProfileID() idItem.name = '%%_sentence_%%' const profile = getDefaultProfile(idItem.id) as ProfileMutable profile.dicts.selected = [ 'jukuu', 'bing', 'cnki', 'renren', 'eudic', 'cobuild', 'cambridge', 'longman', 'macmillan' ] const allDict = profile.dicts.all allDict.bing.options.tense = false allDict.bing.options.phsym = false allDict.bing.options.cdef = false allDict.bing.options.related = false allDict.bing.options.sentence = 9999 allDict.cnki.options.dict = false allDict.eudic.options.resultnum = 9999 allDict.macmillan.options.related = false allDict.longman.options.wordfams = false allDict.longman.options.collocations = false allDict.longman.options.grammar = false allDict.longman.options.thesaurus = false allDict.longman.options.examples = true allDict.longman.options.bussinessFirst = false allDict.longman.options.related = false return { idItem, profile } } export function scholar(): ProfileStorage { const idItem = getDefaultProfileID() idItem.name = '%%_scholar_%%' const profile = getDefaultProfile(idItem.id) as ProfileMutable profile.dicts.selected = [ 'googledict', 'cambridge', 'cobuild', 'etymonline', 'cnki', 'macmillan', 'lexico', 'websterlearner', 'google', 'youdaotrans', 'zdic', 'guoyu', 'liangan' ] const allDict = profile.dicts.all allDict.macmillan.defaultUnfold = { matchAll: false, english: false, chinese: false, japanese: false, korean: false, french: false, spanish: false, deutsch: false, others: false } allDict.lexico.defaultUnfold = { matchAll: false, english: false, chinese: false, japanese: false, korean: false, french: false, spanish: false, deutsch: false, others: false } allDict.websterlearner.defaultUnfold = { matchAll: false, english: false, chinese: false, japanese: false, korean: false, french: false, spanish: false, deutsch: false, others: false } allDict.google.selectionWC.min = 5 allDict.youdaotrans.selectionWC.min = 5 return { idItem, profile } } export function translation(): ProfileStorage { const idItem = getDefaultProfileID() idItem.name = '%%_translation_%%' const profile = getDefaultProfile(idItem.id) as ProfileMutable profile.dicts.selected = [ 'google', 'tencent', 'baidu', 'caiyun', 'youdaotrans', 'zdic', 'guoyu', 'liangan' ] profile.mtaAutoUnfold = 'always' return { idItem, profile } } export function nihongo(): ProfileStorage { const idItem = getDefaultProfileID() idItem.name = '%%_nihongo_%%' const profile = getDefaultProfile(idItem.id) as ProfileMutable profile.dicts.selected = [ 'mojidict', 'hjdict', 'weblioejje', 'weblio', 'google', 'tencent', 'caiyun', 'googledict', 'wikipedia' ] profile.dicts.all.wikipedia.options.lang = 'ja' profile.waveform = false return { idItem, profile } } ================================================ FILE: src/audio-control/audio-control.scss ================================================ html, body { height: 165px; overflow: hidden; margin: 0; padding: 0; } @import '@/_sass_shared/_theme.scss'; @import '@/components/Waveform/Waveform.scss'; ================================================ FILE: src/audio-control/index.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import Waveform from '@/components/Waveform/Waveform' import './audio-control.scss' const searchParams = new URL(document.URL).searchParams const darkMode = Boolean(searchParams.get('darkmode')) ReactDOM.render( , document.getElementById('root') ) ================================================ FILE: src/background/__fake__/env.ts ================================================ import axios from 'axios' import AxiosMockAdapter from 'axios-mock-adapter' browser.runtime.sendMessage['_sender'].callsFake(() => ({ tab: { id: 'saladict-page' } })) // mock dict search requests const dictMock = new AxiosMockAdapter(axios) const dictMockReq = require.context( '../../../test/specs/components/dictionaries/', true, /requests\.mock\.ts$/ ) dictMockReq.keys().forEach(filename => { const { mockRequest } = dictMockReq(filename) mockRequest(dictMock) }) dictMock.onAny().reply(config => { console.warn(`Unmatch url: ${config.url}`, config) return [404, {}] }) ================================================ FILE: src/background/__mocks__/database.ts ================================================ export const db = jest.fn() export const isInNotebook = jest.fn() export const saveWord = jest.fn() export const deleteWord = jest.fn() export const getWordsByText = jest.fn() export const getAllWords = jest.fn() ================================================ FILE: src/background/audio-manager.ts ================================================ import { timer } from '@/_helpers/promise-more' /** * To make sure only one audio plays at a time */ export class AudioManager { private static instance: AudioManager static getInstance() { return AudioManager.instance || (AudioManager.instance = new AudioManager()) } // singleton // eslint-disable-next-line no-useless-constructor private constructor() {} private audio?: HTMLAudioElement currentSrc?: string reset() { if (this.audio) { this.audio.pause() this.audio.currentTime = 0 this.audio.src = '' this.audio.onended = null } this.currentSrc = '' } load(src: string): HTMLAudioElement { this.reset() this.currentSrc = src return (this.audio = new Audio(src)) } async play(src?: string): Promise { if (!src || src === this.currentSrc) { this.reset() return } const audio = this.load(src) const onEnd = Promise.race([ new Promise(resolve => { audio.onended = resolve }), timer(20000) ]) await audio.play() await onEnd this.currentSrc = '' } } ================================================ FILE: src/background/badge.ts ================================================ import { message } from '@/_helpers/browser-api' import { Subject } from 'rxjs' import { switchMapBy } from '@/_helpers/observables' import { timer } from '@/_helpers/promise-more' interface UpdateBadgeOptions { active: boolean tempDisable: boolean unsupported: boolean } const onUpdated$ = new Subject<{ delay?: boolean tabId: number options?: UpdateBadgeOptions }>() onUpdated$ .pipe( switchMapBy('tabId', async o => { if (o.options) { return o as Required } if (o.delay) { await timer(1000) } return { tabId: o.tabId, options: (await message .send<'GET_TAB_BADGE_INFO'>(o.tabId, { type: 'GET_TAB_BADGE_INFO' }) .catch(() => {})) || { active: window.appConfig.active, tempDisable: false, unsupported: true } } }) ) .subscribe(({ tabId, options }) => { if (!options.active) { return setOff(tabId) } if (options.tempDisable) { return setTempOff(tabId) } if (options.unsupported) { return setUnsupported(tabId) } return setDefault(tabId) }) export function initBadge() { /** Sent when content script loaded */ message.addListener('SEND_TAB_BADGE_INFO', ({ payload }, sender) => { if (sender.tab && sender.tab.id) { onUpdated$.next({ tabId: sender.tab.id, options: payload }) } }) browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => { if (changeInfo.status === 'complete') { onUpdated$.next({ tabId, delay: true }) } }) } function setOff(tabId: number) { setIcon(true, tabId) // browser.browserAction.setBadgeBackgroundColor({ color: '#E74C3C', tabId }) // browser.browserAction.setBadgeText({ text: 'off', tabId }) browser.browserAction.setTitle({ title: require('@/_locales/' + window.appConfig.langCode + '/background') .locale.app.off, tabId }) } function setTempOff(tabId: number) { setIcon(true, tabId) // browser.browserAction.setBadgeBackgroundColor({ color: '#F39C12', tabId }) // browser.browserAction.setBadgeText({ text: 'off', tabId }) browser.browserAction.setTitle({ title: require('@/_locales/' + window.appConfig.langCode + '/background') .locale.app.tempOff, tabId }) } function setUnsupported(tabId: number) { setIcon(true, tabId) browser.browserAction.setTitle({ title: require('@/_locales/' + window.appConfig.langCode + '/background') .locale.app.unsupported, tabId }) } function setDefault(tabId: number) { setIcon(false, tabId) // browser.browserAction.setBadgeText({ text: '', tabId }) // browser.browserAction.setTitle({ title: '', tabId }) } function setIcon(gray: boolean, tabId: number) { browser.browserAction.setIcon({ tabId, path: gray ? { 16: 'assets/icon-gray-16.png', 19: 'assets/icon-gray-19.png', 24: 'assets/icon-gray-24.png', 38: 'assets/icon-gray-38.png', 48: 'assets/icon-gray-48.png', 128: 'assets/icon-gray-128.png' } : { 16: 'assets/icon-16.png', 19: 'assets/icon-19.png', 24: 'assets/icon-24.png', 38: 'assets/icon-38.png', 48: 'assets/icon-48.png', 128: 'assets/icon-128.png' } }) } ================================================ FILE: src/background/clipboard-manager.ts ================================================ import { openUrl } from '@/_helpers/browser-api' export async function copyTextToClipboard(text: string): Promise { if ( !(await browser.permissions.contains({ permissions: ['clipboardWrite'] })) ) { openUrl( '/options.html?menuselected=Permissions&missing_permission=clipboardWrite', true ) return } const copyFrom = document.createElement('textarea') copyFrom.textContent = text document.body.appendChild(copyFrom) copyFrom.select() document.execCommand('copy') copyFrom.blur() document.body.removeChild(copyFrom) } export async function getTextFromClipboard(): Promise { if ( !(await browser.permissions.contains({ permissions: ['clipboardRead'] })) ) { openUrl( '/options.html?menuselected=Permissions&missing_permission=clipboardRead', true ) return '' } if (process.env.NODE_ENV === 'development') { return 'clipboard content' } else { let el = document.getElementById( 'saladict-paste' ) as HTMLTextAreaElement | null if (!el) { el = document.createElement('textarea') el.id = 'saladict-paste' document.body.appendChild(el) } el.value = '' el.focus() document.execCommand('paste') return el.value || '' } } ================================================ FILE: src/background/context-menus.ts ================================================ import { message, openUrl } from '@/_helpers/browser-api' import { AppConfig } from '@/app-config' import isEqual from 'lodash/isEqual' import { createConfigStream } from '@/_helpers/config-manager' import { isFirefox } from '@/_helpers/saladict' import { reportEvent } from '@/_helpers/analytics' import './types' import { TFunction } from 'i18next' import { I18nManager } from './i18n-manager' import { combineLatest } from 'rxjs' import { concatMap, filter, distinctUntilChanged } from 'rxjs/operators' import { openPDF, extractPDFUrl } from './pdf-sniffer' import { copyTextToClipboard } from './clipboard-manager' import { BackgroundServer } from './server' interface CreateMenuOptions { type?: browser.contextMenus.ItemType id?: string parentId?: string title?: string contexts?: browser.contextMenus.ContextType[] } type ContextMenusClickInfo = Pick< browser.contextMenus.OnClickData, 'menuItemId' | 'selectionText' | 'linkUrl' | 'pageUrl' > export class ContextMenus { static async getInstance() { if (!ContextMenus.instance) { const instance = new ContextMenus() ContextMenus.instance = instance const i18nManager = await I18nManager.getInstance() const contextMenusChanged$ = createConfigStream().pipe( distinctUntilChanged( (config1, config2) => config1 && config2 && isEqual( config1.contextMenus.selected, config2.contextMenus.selected ) ), filter(config => !!config) ) combineLatest(contextMenusChanged$, i18nManager.getFixedT$('menus')) .pipe(concatMap(instance.setContextMenus)) .subscribe() } return ContextMenus.instance } static init = ContextMenus.getInstance static openGoogle() { return tryExecuteScript( { file: '/assets/google-page-trans.js' }, 'google_page_translate' ) } static openCaiyunTrs() { // FF policy if (isFirefox) return return tryExecuteScript({ file: '/assets/trs.js' }, 'caiyuntrs') } static async openYoudao() { // FF policy if (isFirefox) return // inject youdao script, defaults to the active tab of the current window. const result = await tryExecuteScript( { file: '/assets/fanyi.youdao.2.0/main.js' }, 'youdao_page_translate' ) if (!result || ((result as any) !== 1 && result[0] !== 1)) { await browser.notifications.create({ type: 'basic', eventTime: Date.now() + 4000, iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: 'Saladict', message: (await I18nManager.getInstance()).i18n.t( 'menus:notification_youdao_err' ) }) } } static openBaiduPage() { browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { if (tabs.length > 0 && tabs[0].url) { const langCode = window.appConfig.langCode === 'zh-CN' ? 'zh' : window.appConfig.langCode === 'zh-TW' ? 'cht' : 'en' openUrl( `https://fanyi.baidu.com/transpage?query=${encodeURIComponent( tabs[0].url as string )}&from=auto&to=${langCode}&source=url&render=1` ) } }) } static openSogouPage() { browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { if (tabs.length > 0 && tabs[0].url) { const langCode = window.appConfig.langCode === 'zh-CN' ? 'zh-CHS' : 'en' openUrl( `https://translate.sogoucdn.com/pcvtsnapshot?from=auto&to=${langCode}&tfr=translatepc&url=${encodeURIComponent( tabs[0].url as string )}&domainType=sogou` ) } }) } static openMicrosoftPage() { browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { if (tabs.length > 0 && tabs[0].url) { const langCode = window.appConfig.langCode === 'zh-CN' ? 'zh-Hans' : window.appConfig.langCode === 'zh-TW' ? 'zh-Hant' : 'en' openUrl( `https://www.microsofttranslator.com/bv.aspx?from=auto&to=${langCode}&r=true&a=${encodeURIComponent( tabs[0].url as string )}` ) } }) } static requestSelection() { browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { if (tabs.length > 0 && tabs[0].id != null) { message.send(tabs[0].id as number, { type: 'EMIT_SELECTION' }) } }) } private handleContextMenusClick(info: ContextMenusClickInfo) { const menuItemId = String(info.menuItemId).replace(/_ba$/, '') const selectionText = info.selectionText || '' const linkUrl = info.linkUrl || '' switch (menuItemId) { case 'google_page_translate': ContextMenus.openGoogle() break case 'caiyuntrs': ContextMenus.openCaiyunTrs() break case 'google_cn_page_translate': ContextMenus.openGoogle() break case 'youdao_page_translate': ContextMenus.openYoudao() break case 'baidu_page_translate': ContextMenus.openBaiduPage() break case 'sogou_page_translate': ContextMenus.openSogouPage() break case 'microsoft_page_translate': ContextMenus.openMicrosoftPage() break case 'view_as_pdf': openPDF(linkUrl, info.menuItemId !== 'view_as_pdf_ba') break case 'copy_pdf_url': { const url = extractPDFUrl(info.pageUrl) if (url) { copyTextToClipboard(url) } break } case 'saladict': ContextMenus.requestSelection() break case 'saladict_standalone': BackgroundServer.getInstance().searchPageSelection() break case 'search_history': openUrl(browser.runtime.getURL('history.html')) break case 'notebook': openUrl(browser.runtime.getURL('notebook.html')) break default: { const item = window.appConfig.contextMenus.all[menuItemId] if (item) { const url = typeof item === 'string' ? item : item.url if (url) { openUrl(url.replace('%s', encodeURIComponent(selectionText))) } } } break } } private static instance: ContextMenus // singleton private constructor() { browser.contextMenus.onClicked.addListener(payload => { reportMenusEvent(payload.menuItemId, 'From_Context_Menus') return this.handleContextMenusClick(payload) }) message.addListener('CONTEXT_MENUS_CLICK', ({ payload }) => { reportMenusEvent(payload.menuItemId, 'From_Browser_Action') return this.handleContextMenusClick(payload) }) } private async setContextMenus([{ searchHistory, contextMenus }, t]: [ AppConfig, TFunction ]): Promise { if (!browser.extension.inIncognitoContext) { // In 'split' incognito mode, this will also remove the items on normal mode windows await browser.contextMenus.removeAll() } const ctx: browser.contextMenus.ContextType[] = [ 'audio', 'editable', 'frame', 'image', 'link', 'selection', 'page', 'video' ] // top level context menus item const containerCtx = new Set([ 'selection' ]) const optionList: CreateMenuOptions[] = [] const browserActionItems: string[] = [] for (const id of contextMenus.selected) { let contexts: browser.contextMenus.ContextType[] switch (id) { case 'caiyuntrs': case 'google_page_translate': case 'google_cn_page_translate': case 'youdao_page_translate': case 'sogou_page_translate': case 'baidu_page_translate': case 'microsoft_page_translate': // two for browser action contexts = ctx browserActionItems.push(id) break case 'view_as_pdf': containerCtx.add('link') containerCtx.add('page') contexts = ['link', 'page'] break case 'copy_pdf_url': containerCtx.add('page') contexts = ['page'] break default: contexts = ['selection'] break } optionList.push({ id, title: getTitle(id), contexts }) } if (optionList.length > 1) { if (browserActionItems.length > 0) { ctx.forEach(type => containerCtx.add(type)) } await createContextMenu({ id: 'saladict_container', title: t('saladict'), contexts: [...containerCtx] }) for (const opt of optionList) { opt.parentId = 'saladict_container' await createContextMenu(opt) } } else if (optionList.length > 0) { // only one item, no need for parent container await createContextMenu(optionList[0]) } await createContextMenu({ id: 'view_as_pdf_ba', title: t('view_as_pdf'), contexts: ['browser_action', 'page_action'] }) if (browserActionItems.length > 2) { await createContextMenu({ id: 'saladict_ba_container', title: t('page_translations'), contexts: ['browser_action', 'page_action'] }) for (const id of browserActionItems) { await createContextMenu({ id: id + '_ba', parentId: 'saladict_ba_container', title: getTitle(id), contexts: ['browser_action', 'page_action'] }) } } else if (browserActionItems.length > 0) { for (const id of browserActionItems) { await createContextMenu({ id: id + '_ba', title: getTitle(id), contexts: ['browser_action', 'page_action'] }) } } else { // Add only to browser action if not selected await createContextMenu({ id: 'google_cn_page_translate_ba', title: t('google_cn_page_translate'), contexts: ['browser_action', 'page_action'] }) await createContextMenu({ id: 'youdao_page_translate_ba', title: t('youdao_page_translate'), contexts: ['browser_action', 'page_action'] }) } await createContextMenu({ type: 'separator', id: Date.now().toString(), contexts: ['browser_action'] }) if (searchHistory) { // search history await createContextMenu({ id: 'search_history', title: t('history_title'), contexts: ['browser_action'] }) } // Manual await createContextMenu({ id: 'notebook', title: t('notebook_title'), contexts: ['browser_action'] }) function getTitle(id: string): string { const item = contextMenus.all[id] return !item || typeof item === 'string' ? t(id) : item.name } function createContextMenu( createProperties: CreateMenuOptions ): Promise { return new Promise(resolve => { browser.contextMenus.create(createProperties, () => { if (browser.runtime.lastError) { console.error(browser.runtime.lastError) } resolve() }) }) } } } async function tryExecuteScript( details: browser.extensionTypes.InjectDetails, nameKey: string ) { try { return await browser.tabs.executeScript(details) } catch (error) { const { i18n } = await I18nManager.getInstance() await browser.notifications.create({ type: 'basic', eventTime: Date.now() + 4000, iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: 'Saladict', message: i18n.t('menus:page_permission_err', { name: i18n.t(`menus:${nameKey}`) }) }) return error } } function reportMenusEvent( menuItemId: string | number, label: 'From_Browser_Action' | 'From_Context_Menus' ) { menuItemId = String(menuItemId).replace(/_ba$/, '') switch (menuItemId) { case 'google_page_translate': reportEvent({ category: 'Page_Translate', action: 'Open_Google', label }) break case 'caiyuntrs': reportEvent({ category: 'Page_Translate', action: 'Open_Caiyun', label }) break case 'google_cn_page_translate': reportEvent({ category: 'Page_Translate', action: 'Open_Google', label }) break case 'youdao_page_translate': reportEvent({ category: 'Page_Translate', action: 'Open_Youdao', label }) break case 'view_as_pdf': reportEvent({ category: 'PDF_Viewer', action: 'Open_PDF_Viewer', label }) break } } ================================================ FILE: src/background/database/core.ts ================================================ import Dexie from 'dexie' import { Word } from '@/_helpers/record-manager' export class SaladictDB extends Dexie { // @ts-ignore notebook: Dexie.Table // @ts-ignore history: Dexie.Table // @ts-ignore syncmeta: Dexie.Table<{ id: string; json: string }, string> constructor() { super('SaladictWords') this.version(1).stores({ notebook: 'date,text,context,url', history: 'date,text,context,url', syncmeta: 'id' }) // The following lines are needed if your typescript // is compiled using babel instead of tsc: this.notebook = this.table('notebook') this.history = this.table('history') this.syncmeta = this.table('syncmeta') } } let db: SaladictDB | undefined export async function getDB() { if (!db) { db = new SaladictDB() } if (!db.isOpen()) { await db.open() } return db } ================================================ FILE: src/background/database/index.ts ================================================ export { isInNotebook, getWordsByText, getWords } from './read' import { saveWord as _saveWord, saveWords as _saveWords, deleteWords as _deleteWords } from './write' import { syncServiceUpload } from '../sync-manager' // prevent circular dependencies export const saveWord: typeof _saveWord = options => { if (options.area === 'notebook' && options.word) { syncServiceUpload({ action: 'ADD', words: [options.word] }) } return _saveWord(options) } export const saveWords: typeof _saveWords = options => { if (options.area === 'notebook' && options.words.length > 0) { syncServiceUpload({ action: 'ADD', words: options.words }) } return _saveWords(options) } export const deleteWords: typeof _deleteWords = options => { if (options.area === 'notebook') { syncServiceUpload({ action: 'DELETE', dates: options.dates }) } return _deleteWords(options) } ================================================ FILE: src/background/database/read.ts ================================================ import { isContainChinese, isContainEnglish } from '@/_helpers/lang-check' import { Message, MessageResponse } from '@/typings/message' import { getDB } from './core' export async function isInNotebook(word: Message<'IS_IN_NOTEBOOK'>['payload']) { const db = await getDB() return db.notebook .where('text') .equalsIgnoreCase(word.text) .count() .then(count => count > 0) } export async function getWordsByText({ area, text }: Message<'GET_WORDS_BY_TEXT'>['payload']) { const db = await getDB() return db[area] .where('text') .equalsIgnoreCase(text) .toArray() } export async function getWords({ area, itemsPerPage, pageNum, filters = {}, sortField = 'date', sortOrder = 'descend', searchText }: Message<'GET_WORDS'>['payload']): Promise> { const db = await getDB() const collection = db[area].orderBy( sortField ? Array.isArray(sortField) ? sortField.map(str => String(str)) : String(sortField) : 'date' ) if (!sortOrder || sortOrder === 'descend') { collection.reverse() } const shouldFilter = Array.isArray(filters.text) && filters.text.length > 0 if (shouldFilter || searchText) { const validLangs = shouldFilter ? (filters.text as string[]).reduce((o, l) => { o[l] = true return o }, {}) : {} const ls = searchText ? searchText.toLocaleLowerCase() : '' collection.filter(record => { const rText = shouldFilter ? (validLangs['en'] && isContainEnglish(record.text)) || (validLangs['ch'] && isContainChinese(record.text)) || (validLangs['word'] && !/\s/.test(record.text)) || (validLangs['phra'] && /\s/.test(record.text)) : true const rSearch = searchText ? Object.values(record).some( v => typeof v === 'string' && v.toLocaleLowerCase().indexOf(ls) !== -1 ) : true return rText && rSearch }) } const total = await collection.count() if (typeof itemsPerPage !== 'undefined' && typeof pageNum !== 'undefined') { collection.offset(itemsPerPage * (pageNum - 1)).limit(itemsPerPage) } const words = await collection.toArray() return { total, words } } ================================================ FILE: src/background/database/sync-meta.ts ================================================ import { getDB } from './core' export async function getSyncMeta(serviceID: string) { const db = await getDB() return db.syncmeta .where('id') .equals(serviceID) .first(record => record && record.json) .catch(e => { if (process.env.DEBUG) { console.error(e) } }) } export async function setSyncMeta(serviceID: string, text: string) { const db = await getDB() return db.syncmeta.put({ id: serviceID, json: text }) } export async function deleteSyncMeta(serviceID: string) { const db = await getDB() return db.syncmeta.delete(serviceID).catch(e => { if (process.env.DEBUG) { console.error(e) } }) } ================================================ FILE: src/background/database/write.ts ================================================ import { Word, DBArea } from '@/_helpers/record-manager' import { Message } from '@/typings/message' import { getDB } from './core' export async function saveWord({ area, word }: Message<'SAVE_WORD'>['payload']) { const db = await getDB() return db[area].put(word) } export async function saveWords({ area, words }: { area: DBArea words: Word[] }) { if (process.env.DEBUG) { if (words.length !== new Set(words.map(w => w.date)).size) { console.error('save Words: duplicate records') } } const db = await getDB() return db[area].bulkPut(words) } export async function deleteWords({ area, dates }: Message<'DELETE_WORDS'>['payload']) { const db = await getDB() return Array.isArray(dates) ? db[area].bulkDelete(dates) : db[area].clear() } ================================================ FILE: src/background/env.ts ================================================ export {} window.__SALADICT_INTERNAL_PAGE__ = true window.__SALADICT_BACKGROUND_PAGE__ = true ================================================ FILE: src/background/i18n-manager.ts ================================================ import i18next, { TFunction } from 'i18next' import { i18nLoader, Namespace } from '@/_helpers/i18n' import { BehaviorSubject, Observable } from 'rxjs' import { switchMap } from 'rxjs/operators' export class I18nManager { private static instance: I18nManager static async getInstance() { if (!I18nManager.instance) { const instance = new I18nManager() I18nManager.instance = instance instance.i18n = await i18nLoader() instance.i18n$$.next(instance.i18n) } return I18nManager.instance } i18n: i18next.i18n readonly i18n$$: BehaviorSubject // singleton private constructor() { this.i18n = i18next this.i18n$$ = new BehaviorSubject(this.i18n) this.i18n.on('languageChanged', () => { this.i18n$$.next(this.i18n) }) } getFixedT$(ns: Namespace | Namespace[]): Observable { return this.i18n$$.pipe( switchMap(async i18n => { await this.i18n.loadNamespaces(ns) return i18n.getFixedT(i18n.language, ns) }) ) } } ================================================ FILE: src/background/index.ts ================================================ import './env' import './initialization' import { getConfig, addConfigListener } from '@/_helpers/config-manager' import { createActiveProfileStream, createProfileIDListStream } from '@/_helpers/profile-manager' import { message } from '@/_helpers/browser-api' import { startSyncServiceInterval } from './sync-manager' import { init as initPdf } from './pdf-sniffer' import { ContextMenus } from './context-menus' import { BackgroundServer } from './server' import { initBadge } from './badge' import { setupCaiyunTrsBackend } from './page-translate/caiyun' import { setupRequestGAListener } from '@/_helpers/analytics' import './types' // init first to recevice self messaging message.self.initServer() startSyncServiceInterval() ContextMenus.init() BackgroundServer.init() setupCaiyunTrsBackend() setupRequestGAListener() getConfig().then(async config => { window.appConfig = config initPdf(config) initBadge() addConfigListener(({ newConfig }) => { window.appConfig = newConfig }) }) createActiveProfileStream().subscribe(profile => { window.activeProfile = profile }) createProfileIDListStream().subscribe(list => { window.profileIDList = list }) ================================================ FILE: src/background/initialization.ts ================================================ import mapValues from 'lodash/mapValues' import { message, storage, openUrl } from '@/_helpers/browser-api' import { isExtTainted } from '@/_helpers/integrity' import { checkUpdate } from '@/_helpers/check-update' import { updateConfig, initConfig } from '@/_helpers/config-manager' import { initProfiles, updateActiveProfileID } from '@/_helpers/profile-manager' import { injectDictPanel } from '@/_helpers/injectSaladictInternal' import { isFirefox } from '@/_helpers/saladict' import { timer } from '@/_helpers/promise-more' import { getTitlebarOffset, setTitlebarOffset, calibrateTitlebarOffset } from '@/_helpers/titlebar-offset' import { reportEvent } from '@/_helpers/analytics' import { ContextMenus } from './context-menus' import { BackgroundServer } from './server' import { openPDF } from './pdf-sniffer' import './types' browser.runtime.onInstalled.addListener(onInstalled) browser.runtime.onStartup.addListener(onStartup) if (browser.notifications) { browser.notifications.onClicked.addListener( genClickListener('https://saladict.crimx.com/releases/') ) if (browser.notifications.onButtonClicked) { // Firefox doesn't support browser.notifications.onButtonClicked.addListener( genClickListener('https://saladict.crimx.com/releases/') ) } } browser.commands.onCommand.addListener(onCommand) const getText = decodeURI function onCommand(command: string) { switch (command) { case 'toggle-active': updateConfig({ ...window.appConfig, active: !window.appConfig.active }) break case 'toggle-instant': browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { if (tabs.length <= 0 || tabs[0].id == null) { return } message .send<'QUERY_PIN_STATE', boolean>(tabs[0].id, { type: 'QUERY_PIN_STATE' }) .then(isPinned => { const config = window.appConfig const { enable } = config[isPinned ? 'pinMode' : 'mode'].instant updateConfig({ ...config, mode: { ...config.mode, instant: { ...config.mode.instant, enable: !enable } }, pinMode: { ...config.pinMode, instant: { ...config.pinMode.instant, enable: !enable } } }) }) }) break case 'open-quick-search': BackgroundServer.getInstance().openQSPanel() break case 'open-google': ContextMenus.openGoogle() reportEvent({ category: 'Page_Translate', action: 'Open_Google', label: 'From_Browser_Shortcut' }) break case 'open-youdao': ContextMenus.openYoudao() reportEvent({ category: 'Page_Translate', action: 'Open_Youdao', label: 'From_Browser_Shortcut' }) break case 'open-caiyun': ContextMenus.openCaiyunTrs() reportEvent({ category: 'Page_Translate', action: 'Open_Caiyun', label: 'From_Browser_Shortcut' }) break case 'open-pdf': openPDF() reportEvent({ category: 'PDF_Viewer', action: 'Open_PDF_Viewer', label: 'From_Browser_Shortcut' }) break case 'search-clipboard': BackgroundServer.getInstance().searchClipboard() break case 'next-history': case 'prev-history': // Send to browser action panel first message .send<'SWITCH_HISTORY', boolean>({ type: 'SWITCH_HISTORY', payload: command === 'next-history' ? 'next' : 'prev' }) .then(received => { if (received) return // browser action panel is opened return browser.tabs .query({ active: true, currentWindow: true }) .then(tabs => { if (tabs.length <= 0 || tabs[0].id == null) { return } return message.send<'SWITCH_HISTORY', boolean>(tabs[0].id, { type: 'SWITCH_HISTORY', payload: command === 'next-history' ? 'next' : 'prev' }) }) }) break case 'next-profile': case 'prev-profile': { const curID = window.activeProfile.id const curIndex = window.profileIDList.findIndex( ({ id }) => id === curID ) const offset = command === 'next-profile' ? 1 : -1 const nextIndex = curIndex < 0 ? 0 : (curIndex + offset) % window.profileIDList.length updateActiveProfileID(window.profileIDList[nextIndex].id).then( searchTextBox ) } break case 'profile-1': case 'profile-2': case 'profile-3': case 'profile-4': case 'profile-5': { const index = +command.slice(-1) if ( index < window.profileIDList.length && window.profileIDList[index].id !== window.activeProfile.id ) { updateActiveProfileID(window.profileIDList[index].id).then( searchTextBox ) } } break case 'add-notebook': addNotebook() break } } async function onInstalled({ reason, previousVersion }: { reason: string previousVersion?: string }) { window.appConfig = await initConfig() window.activeProfile = await initProfiles() await storage.local.set( mapValues(await storage.local.get(null), (value, key) => { if (key.startsWith('dict_')) return null if (key === 'lastCheckUpdate') return Date.now() return value }) ) if (reason === 'install') { if ( !(await storage.sync.get('hasInstructionsShown')).hasInstructionsShown ) { openUrl('options.html?menuselected=Privacy&nopanel=true', true) if (window.appConfig.langCode.startsWith('zh')) { openUrl('https://saladict.crimx.com/notice.html') } else { openUrl('https://saladict.crimx.com/en/notice.html') } storage.sync.set({ hasInstructionsShown: true }) } } else if (reason === 'update') { if (!process.env.DEBUG) { const curr = await checkUpdate(browser.runtime.getManifest().version) // same version as server if (curr.data && curr.diff === 0) { const { diff, data } = await checkUpdate(previousVersion, curr.data) if (data && diff >= 2) { setTimeout(() => { const isZh = window.appConfig.langCode.startsWith('zh') const options = { type: 'basic', iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: isZh ? `沙拉查词已更新到 ${data.version}` : `Saladict has updated to ${data.version}`, message: data.data .map((line, i) => `${i + 1}. ${line}`) .join('\n'), priority: 2, eventTime: Date.now() + 5000 } as any if (!isFirefox) { options.buttons = [{ title: isZh ? '查看更新介绍' : 'More Info' }] options.silent = true } if (browser.notifications) { browser.notifications.create('sd-install', options) } }, 5000) } } } } loadDictPanelToAllTabs() // firefox users may want to calibrate manually if (!isFirefox && !(await getTitlebarOffset())) { const offset = await calibrateTitlebarOffset() if (offset) { setTitlebarOffset(offset) } } } function onStartup(): void { setTimeout(() => { // wait for appConfig being loaded if (!process.env.DEBUG && window.appConfig.updateCheck) { storage.local .get<{ lastCheckUpdate: number }>('lastCheckUpdate') .then(async ({ lastCheckUpdate }) => { const today = Date.now() if (!lastCheckUpdate) { storage.local.set({ lastCheckUpdate: today }) } else if (today - lastCheckUpdate > 7 * 24 * 60 * 60 * 1000) { storage.local.set({ lastCheckUpdate: today }) const { data, diff } = await checkUpdate( browser.runtime.getManifest().version ) if (data && diff > 0) { const options: browser.notifications.CreateNotificationOptions = { type: 'basic', iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: getText('%E6%B2%99%E6%8B%89%E6%9F%A5%E8%AF%8D'), message: `${getText('%E5%8F%AF%E6%9B%B4%E6%96%B0%E8%87%B3')}【${ data.version }】` } if (!isFirefox) { options.buttons = [ { title: getText('%E6%9F%A5%E7%9C%8B%E6%9B%B4%E6%96%B0') } ] } if (browser.notifications) { browser.notifications.create('sd-update', options) } } } }) } }, 1000) if (!process.env.DEBUG && isExtTainted) { storage.local.get<{ swat: number }>('swat').then(({ swat }) => { const today = Date.now() if (!swat) { storage.local.set({ swat: today }) } else if (today - swat > 10 * 24 * 60 * 60 * 1000) { storage.local.set({ swat: today }) const options: browser.notifications.CreateNotificationOptions = { type: 'basic', iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: getText('%E6%B2%99%E6%8B%89%E6%9F%A5%E8%AF%8D'), message: getText( '%E6%AD%A4%E3%80%8C%E6%B2%99%E6%8B%89%E6%9F%A5%E8%' + 'AF%8D%E3%80%8D%E6%89%A9%E5%B1%95%E5%B7%B2%E8%A2' + '%AB%E4%BA%8C%E6%AC%A1%E6%89%93%E5%8C%85%EF%BC%8' + 'C%E8%AF%B7%E5%9C%A8%E5%AE%98%E6%96%B9%E5%BB%BA%' + 'E8%AE%AE%E7%9A%84%E5%B9%B3%E5%8F%B0%E5%AE%89%E8' + '%A3%85%E3%80%82' ) } if (!isFirefox) { options.buttons = [ { title: getText( '%E6%9F%A5%E7%9C%8B%E5%8F%AF%E9%9D%A0%E7%9A%84%E5%B9%B3%E5%8F%B0' ) } ] } if (browser.notifications) { browser.notifications.create('sd-update', options) } } }) } // Chrome fails to inject css via manifest if the page is loaded // as "last opened tabs" when browser opens. setTimeout(() => { loadDictPanelToAllTabs() }, 1000) } function genClickListener(url: string) { return function clickListener(notificationId: string) { switch (notificationId) { case 'sd-install': case 'sd-update': openUrl(url) if (browser.notifications) { browser.notifications.getAll().then(notifications => { Object.keys(notifications).forEach(id => browser.notifications.clear(id) ) }) } break } } } async function loadDictPanelToAllTabs() { ;(await browser.tabs.query({})).forEach(async tab => { if (tab.id && tab.url && tab.url.startsWith('http')) { try { await injectDictPanel(tab) } catch (e) { console.warn(e) } } }) } /** Search text box text on active tab */ async function searchTextBox() { await timer(10) if (await message.send<'SEARCH_TEXT_BOX'>({ type: 'SEARCH_TEXT_BOX' })) { return // popup page received } const tabs = await browser.tabs.query({ active: true, currentWindow: true }) if (tabs.length <= 0 || tabs[0].id == null) { return } message.send(tabs[0].id, { type: 'SEARCH_TEXT_BOX' }) } async function addNotebook() { if ( await message.send<'ADD_NOTEBOOK'>({ type: 'ADD_NOTEBOOK', payload: { popup: true } }) ) { return // popup page received } const tabs = await browser.tabs.query({ active: true, currentWindow: true }) if (tabs.length <= 0 || tabs[0].id == null) { return } message.send(tabs[0].id, { type: 'ADD_NOTEBOOK', payload: { popup: false } }) } ================================================ FILE: src/background/page-translate/caiyun.ts ================================================ export function setupCaiyunTrsBackend() { browser.runtime.onMessage.addListener(msg => { if (msg.contentScriptQuery === 'fetchUrl') { const requestInit: RequestInit = { method: msg.method || 'GET', credentials: 'include', headers: { ...(msg.headers || {}), 'content-type': 'application/json' } } if (msg.data) { try { requestInit.body = JSON.stringify(msg.data) } catch (error) { if (process.env.DEBUG) { console.error('Caiyun trs message data error:', error) } } } return fetch(msg.url, requestInit) .then(response => response.text()) .then(text => ({ status: 'ok', data: text })) .catch(error => { if (process.env.DEBUG) { console.error('Caiyun trs requestAuthURL error:', error) } return { status: 'error', error: error } }) } }) } ================================================ FILE: src/background/pdf-sniffer.ts ================================================ /** * Open pdf link directly */ import { AppConfig } from '@/app-config' import { addConfigListener } from '@/_helpers/config-manager' import { openUrl } from '@/_helpers/browser-api' export function init(config: AppConfig) { if (browser.webRequest.onBeforeRequest.hasListener(otherPdfListener)) { return } if (config.pdfSniff) { startListening() } addConfigListener(({ newConfig, oldConfig }) => { if (newConfig) { if (!oldConfig || newConfig.pdfSniff !== oldConfig.pdfSniff) { if (newConfig.pdfSniff) { startListening() } else { stopListening() } } } }) } /** * @param url provide a url * @param force load the current tab anyway */ export async function openPDF(url?: string, force?: boolean) { let pdfURL = browser.runtime.getURL('assets/pdf/web/viewer.html') if (url) { pdfURL += '?file=' + encodeURIComponent(url) } else { const tabs = await browser.tabs.query({ active: true, currentWindow: true }) if (tabs.length > 0 && tabs[0].url) { const curURL = tabs[0].url if (curURL.startsWith(pdfURL)) { if (window.appConfig.pdfStandalone) { if (tabs[0].id != null) { await browser.tabs.remove(tabs[0].id) } pdfURL = curURL } else { return // ignore pdf viewer url } } else if (force || curURL.endsWith('pdf')) { pdfURL += '?file=' + encodeURIComponent(curURL) } } } return window.appConfig.pdfStandalone ? openPDFStandalone(pdfURL) : openUrl({ url: pdfURL, unique: false }) } export function extractPDFUrl(fullurl?: string): string | void { if (!fullurl) { return } const searchURL = new URL(fullurl) return decodeURIComponent(searchURL.searchParams.get('file') || '') } function startListening() { if (!browser.webRequest.onBeforeRequest.hasListener(otherPdfListener)) { browser.webRequest.onBeforeRequest.addListener( otherPdfListener, { urls: [ 'ftp://*/*.pdf', 'ftp://*/*.PDF', 'file://*/*.pdf', 'file://*/*.PDF' ], types: ['main_frame', 'sub_frame'] }, ['blocking'] ) } if (!browser.webRequest.onHeadersReceived.hasListener(httpPdfListener)) { browser.webRequest.onHeadersReceived.addListener( httpPdfListener, { urls: ['https://*/*', 'https://*/*', 'http://*/*', 'http://*/*'], types: ['main_frame', 'sub_frame'] }, ['blocking', 'responseHeaders'] ) } } function stopListening() { browser.webRequest.onBeforeRequest.removeListener(otherPdfListener) browser.webRequest.onHeadersReceived.removeListener(httpPdfListener) } function otherPdfListener({ tabId, url }: Parameters< Parameters[0] >[0]) { const matchURL = ([r]: ReadonlyArray) => new RegExp(r).test(url) if ( window.appConfig.pdfBlacklist.some(matchURL) && !window.appConfig.pdfWhitelist.some(matchURL) ) { return } const redirectUrl = browser.runtime.getURL( `assets/pdf/web/viewer.html?file=${encodeURIComponent(url)}` ) if (tabId !== -1 && window.appConfig.pdfStandalone === 'always') { browser.tabs.remove(tabId) openPDFStandalone(redirectUrl) return { cancel: true } } return { redirectUrl } } function httpPdfListener({ tabId, responseHeaders, url }: Parameters< Parameters[0] >[0]) { if (!responseHeaders) { return } const matchURL = ([r]: ReadonlyArray) => new RegExp(r).test(url) if ( window.appConfig.pdfBlacklist.some(matchURL) && !window.appConfig.pdfWhitelist.some(matchURL) ) { return } const contentTypeHeader = responseHeaders.find( ({ name }) => name.toLowerCase() === 'content-type' ) if (contentTypeHeader && contentTypeHeader.value) { const contentType = contentTypeHeader.value.toLowerCase() if ( contentType.endsWith('pdf') || (contentType === 'application/octet-stream' && url.endsWith('.pdf')) ) { const redirectUrl = browser.runtime.getURL( `assets/pdf/web/viewer.html?file=${encodeURIComponent(url)}` ) if (tabId !== -1 && window.appConfig.pdfStandalone === 'always') { browser.tabs.remove(tabId) openPDFStandalone(redirectUrl) return { cancel: true } } return { redirectUrl } } } } function openPDFStandalone(url: string) { return browser.windows.create({ type: 'popup', url }) } ================================================ FILE: src/background/server.ts ================================================ import { message, openUrl } from '@/_helpers/browser-api' import { timeout, timer } from '@/_helpers/promise-more' import { getSuggests } from '@/_helpers/getSuggests' import { injectDictPanel } from '@/_helpers/injectSaladictInternal' import { newWord, Word } from '@/_helpers/record-manager' import { Message, MessageResponse } from '@/typings/message' import { SearchFunction, DictSearchResult, GetSrcPageFunction } from '@/components/dictionaries/helpers' import { isInNotebook, saveWord, deleteWords, getWordsByText, getWords } from './database' import { AudioManager } from './audio-manager' import { QsPanelManager } from './windows-manager' import { getTextFromClipboard, copyTextToClipboard } from './clipboard-manager' import './types' import { DictID } from '@/app-config' /** * background script as transfer station */ export class BackgroundServer { private static instance: BackgroundServer static getInstance() { return ( BackgroundServer.instance || (BackgroundServer.instance = new BackgroundServer()) ) } static init = BackgroundServer.getInstance static getDictEngine

( id: DictID ): Promise<{ search: SearchFunction, P> getSrcPage: GetSrcPageFunction }> { return import( /* webpackInclude: /engine\.ts$/ */ /* webpackMode: "lazy" */ `@/components/dictionaries/${id}/engine.ts` ) } private qsPanelManager: QsPanelManager // singleton private constructor() { this.qsPanelManager = new QsPanelManager() message.addListener((msg, sender: browser.runtime.MessageSender) => { switch (msg.type) { case 'OPEN_DICT_SRC_PAGE': return this.openSrcPage(msg.payload) case 'OPEN_URL': return openUrl(msg.payload) case 'PLAY_AUDIO': return AudioManager.getInstance().play(msg.payload) case 'STOP_AUDIO': AudioManager.getInstance().reset() return case 'FETCH_DICT_RESULT': return this.fetchDictResult(msg.payload) case 'DICT_ENGINE_METHOD': return this.callDictEngineMethod(msg.payload) case 'GET_CLIPBOARD': return getTextFromClipboard() case 'SET_CLIPBOARD': return Promise.resolve(copyTextToClipboard(msg.payload)) case 'INJECT_DICTPANEL': return injectDictPanel(sender.tab) case 'QUERY_QS_PANEL': return this.qsPanelManager.hasCreated() case 'OPEN_QS_PANEL': return this.openQSPanel() case 'CLOSE_QS_PANEL': AudioManager.getInstance().reset() return this.qsPanelManager.destroy() case 'QS_SWITCH_SIDEBAR': return this.qsPanelManager.toggleSidebar(msg.payload) case 'IS_IN_NOTEBOOK': return isInNotebook(msg.payload) case 'SAVE_WORD': return saveWord(msg.payload).then(response => { this.notifyWordSaved() return response }) case 'DELETE_WORDS': return deleteWords(msg.payload).then(response => { this.notifyWordSaved() return response }) case 'GET_WORDS_BY_TEXT': return getWordsByText(msg.payload) case 'GET_WORDS': return getWords(msg.payload) case 'GET_SUGGESTS': return getSuggests(msg.payload) case 'YOUDAO_TRANSLATE_AJAX': return this.youdaoTranslateAjax(msg.payload) } }) browser.runtime.onConnect.addListener(port => { if (port.name === 'popup') { // This is a workaround for browser action page // which does not fire beforeunload event port.onDisconnect.addListener(() => { AudioManager.getInstance().reset() }) } }) } async openQSPanel(): Promise { if (await this.qsPanelManager.hasCreated()) { await this.qsPanelManager.focus() return } await this.qsPanelManager.create() } async searchClipboard(): Promise { const word = newWord({ text: await getTextFromClipboard() }) if (await this.qsPanelManager.hasCreated()) { await message.send({ type: 'QS_PANEL_SEARCH_TEXT', payload: word }) return } await this.qsPanelManager.create(word) } async searchPageSelection(): Promise { const tabs = await browser.tabs.query({ active: true, lastFocusedWindow: true }) let word: Word | undefined if (tabs.length > 0 && tabs[0].id != null) { word = await message.send<'PRELOAD_SELECTION'>(tabs[0].id, { type: 'PRELOAD_SELECTION' }) } const hasCreated = await this.qsPanelManager.hasCreated() if (hasCreated) { await this.qsPanelManager.focus() } else { await this.qsPanelManager.create(word) } } async openSrcPage({ id, text, active }: Message<'OPEN_DICT_SRC_PAGE'>['payload']): Promise { const engine = await BackgroundServer.getDictEngine(id) return openUrl({ url: await engine.getSrcPage( text, window.appConfig, window.activeProfile ), active }) } async fetchDictResult( data: Message<'FETCH_DICT_RESULT'>['payload'] ): Promise> { const payload = data.payload || {} let response: DictSearchResult | undefined try { const { search } = await BackgroundServer.getDictEngine< NonNullable >(data.id) try { response = await timeout( search(data.text, window.appConfig, window.activeProfile, payload), 25000 ) } catch (e) { if (e.message === 'NETWORK_ERROR') { // retry once await timer(500) response = await timeout( search(data.text, window.appConfig, window.activeProfile, payload), 25000 ) } else { throw e } } } catch (e) { if (process.env.DEBUG) { console.warn(data.id, e) } } const result = response ? { ...response, id: data.id } : { result: null, id: data.id } if (process.env.DEBUG) { console.log(`Search Engine ${data.id}`, data.text, result) } return result } async callDictEngineMethod(data: Message<'DICT_ENGINE_METHOD'>['payload']) { const engine = await BackgroundServer.getDictEngine(data.id) return engine[data.method](...(data.args || [])) } notifyWordSaved() { browser.tabs.query({}).then(tabs => { tabs.forEach(async tab => { if (tab.id && tab.url) { try { await message.send(tab.id, { type: 'WORD_SAVED' }) } catch (e) { console.warn(e) } } }) }) } /** Bypass http restriction */ youdaoTranslateAjax(request: any): Promise { return new Promise(resolve => { const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) { const data = xhr.status === 200 ? xhr.responseText : null resolve({ response: data, index: request.index }) } } xhr.open(request.type, request.url, true) if (request.type === 'POST') { xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' ) xhr.send(request.data) } else { xhr.send(null as any) } }) } } ================================================ FILE: src/background/sync-manager/__mocks__/helpers.ts ================================================ import { EMPTY, Observable } from 'rxjs' const emptyPromise = (): Promise => Promise.resolve() export const setSyncConfig = jest.fn(emptyPromise) export const getSyncConfig = jest.fn(emptyPromise) export const createSyncConfigStream = jest.fn((): Observable => EMPTY) export const setMeta = jest.fn(emptyPromise) export const getMeta = jest.fn(emptyPromise) export const deleteMeta = jest.fn(emptyPromise) export const setNotebook = jest.fn(emptyPromise) export const getNotebook = jest.fn(emptyPromise) ================================================ FILE: src/background/sync-manager/helpers.ts ================================================ import { storage } from '@/_helpers/browser-api' import { Word } from '@/_helpers/record-manager' import { getWords } from '@/background/database/read' import { saveWords } from '@/background/database/write' import { getSyncMeta, setSyncMeta, deleteSyncMeta } from '@/background/database/sync-meta' import { I18nManager } from '../i18n-manager' export interface StorageSyncConfig { syncConfig: { [id: string]: any } } export async function setSyncConfig( serviceID: string, config: T ): Promise { let { syncConfig } = await storage.sync.get('syncConfig') if (!syncConfig) { syncConfig = {} } syncConfig[serviceID] = config await storage.sync.set({ syncConfig }) } export async function getSyncConfig( serviceID: string ): Promise { const { syncConfig } = await storage.sync.get('syncConfig') if (syncConfig !== undefined) { return syncConfig[serviceID] } } export async function removeSyncConfig(serviceID?: string): Promise { if (serviceID) { await setSyncConfig(serviceID, null) } else { await storage.sync.remove('syncConfig') } } /** * Service meta data is saved with the database * so that it can be shared across browser vendors. */ export async function setMeta( serviceID: string, meta: T ): Promise { await setSyncMeta(serviceID, JSON.stringify(meta)) } /** * Service meta data is saved with the database * so that it can be shared across browser vendors. */ export async function getMeta(serviceID: string): Promise { const text = await getSyncMeta(serviceID) if (text) { return JSON.parse(text) } } /** * Service meta data is saved with the database * so that it can be shared across browser vendors. */ export async function deleteMeta(serviceID: string): Promise { await deleteSyncMeta(serviceID) } export async function setNotebook(words: Word[]): Promise { await saveWords({ area: 'notebook', words }) } export async function getNotebook(): Promise { return (await getWords({ area: 'notebook' })).words || [] } export async function notifyError( id: string, error: Error | string, msgPrefix = '', msgPostfix = '' ): Promise { const { i18n } = await I18nManager.getInstance() await i18n.loadNamespaces('sync') const errorText = typeof error === 'string' ? error : error.message const msgPath = `sync:${id}.error.${errorText}` const msg = i18n.exists(msgPath) ? i18n.t(msgPath) : `Unknown error: ${errorText}` browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: `Saladict ${i18n.t(`sync:${id}.title`)}`, message: msgPrefix + msg + msgPostfix, eventTime: Date.now() + 20000, priority: 2 }) } ================================================ FILE: src/background/sync-manager/index.ts ================================================ import { SyncService, SyncServiceConstructor } from './interface' import { concat, from } from 'rxjs' import { filter, pluck, map } from 'rxjs/operators' import { storage } from '@/_helpers/browser-api' import { Word } from '@/_helpers/record-manager' import { notifyError } from './helpers' const reqServices = require.context('./services', true, /index\.ts$/) const Services = reqServices.keys().reduce((map, path) => { const Servicex = reqServices(path).Service return map.set(Servicex.id, Servicex) }, new Map()) const activeServices: Map = new Map() export function startSyncServiceInterval() { concat( from(storage.sync.get('syncConfig')).pipe(pluck('syncConfig')), storage.sync.createStream('syncConfig').pipe(pluck('newValue')) ) .pipe( filter((v): v is { [id: string]: any } => !!v), map(syncConfig => { // legacy fix if ( syncConfig.webdav && !Object.prototype.hasOwnProperty.call(syncConfig.webdav, 'enable') ) { syncConfig.webdav.enable = !!syncConfig.webdav.url } return syncConfig }) ) .subscribe(async syncConfig => { try { await Promise.all( [...activeServices.values()].map(service => service.destroy()) ) } catch (e) { console.error(e) } activeServices.clear() if (syncConfig) { Services.forEach(Service => { if (syncConfig[Service.id]?.enable) { const newService = new Service(syncConfig[Service.id]) activeServices.set(Service.id, newService) newService.onStart() } }) } if (process.env.DEBUG) { console.log(`Active Sync Services:`, [...activeServices.keys()]) } }) } export async function syncServiceUpload( options: | { action: 'ADD' words?: Word[] force?: boolean } | { action: 'DELETE' dates?: number[] force?: boolean } ) { activeServices.forEach(async (service, id) => { try { if (options.action === 'ADD') { await service.add({ words: options.words, force: options.force }) } else if (options.action === 'DELETE') { await service.delete({ dates: options.dates, force: options.force }) } } catch (error) { notifyError( id, error, options.action === 'ADD' && options.words?.[0] ? `「${options.words?.[0].text}」` : '' ) } }) } ================================================ FILE: src/background/sync-manager/interface.ts ================================================ import { Word } from '@/_helpers/record-manager' export interface NotebookFile { timestamp: number words: Word[] } export interface DlResponse { json: NotebookFile etag: string } export interface AddConfig { readonly words?: ReadonlyArray> /** Do not sync before upload */ readonly force?: boolean } export interface DeleteConfig { readonly dates?: ReadonlyArray /** Do not sync before upload */ readonly force?: boolean } export interface DownloadConfig { /** Test connectivity. Do not update anything. */ readonly testConfig?: Readonly /** ignore server 304 cache */ readonly noCache?: boolean } export interface SyncServiceConfigBase { enable: boolean } export abstract class SyncService< Config extends SyncServiceConfigBase = any, Meta = any > { static readonly id: string /** * Service config that is saved with browser sync storage. * It is updated automatically. */ config: Config /** service data that is saved with the database */ meta?: Meta static getDefaultConfig() { return {} } static getDefaultMeta() { return {} } constructor(config: Config) { this.config = config } /** Called when user updates config. Check env, save config etc */ abstract init(): Promise /** add words */ abstract add(config: AddConfig): Promise /** delete words */ async delete(config: DeleteConfig): Promise {} /** Clean up side-effects */ async destroy() {} /** Download code */ async download(config: DownloadConfig): Promise {} /** Called on browser start or config changes */ onStart() {} } type SyncServiceAbstractClass = typeof SyncService export interface SyncServiceConstructor extends SyncServiceAbstractClass {} ================================================ FILE: src/background/sync-manager/services/ankiconnect/_locales/en.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: 'Anki Connect', error: { server: 'Cannot connect to Anki Connect. Please make sure Anki is running.', deck: 'Deck not found in Anki.', notetype: 'Note type not found in Anki.', add: 'Failed to add word to Anki.' } } ================================================ FILE: src/background/sync-manager/services/ankiconnect/_locales/zh-CN.ts ================================================ export const locale = { title: 'Anki Connect', error: { server: '无法连接 Anki Connect,请确认 Anki 已在运行。', deck: 'Anki 中没有找到相应牌组。', notetype: 'Anki 中没有找到相应笔记类型。', add: '添加单词到 Anki 失败。' } } ================================================ FILE: src/background/sync-manager/services/ankiconnect/_locales/zh-TW.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: 'Anki Connect', error: { server: '無法連線 Anki Connect,請確認 Anki 已在執行。', deck: 'Anki 中沒有找到相應牌組。', notetype: 'Anki 中沒有找到相應筆記型別。', add: '新增單詞到 Anki 失敗。' } } ================================================ FILE: src/background/sync-manager/services/ankiconnect/index.ts ================================================ import axios from 'axios' import { Word } from '@/_helpers/record-manager' import { parseCtxText } from '@/_helpers/translateCtx' import { AddConfig, SyncService } from '../../interface' import { getNotebook } from '../../helpers' import { message } from '@/_helpers/browser-api' import { Message } from '@/typings/message' export interface SyncConfig { enable: boolean key: string | null host: string port: string deckName: string noteType: string /** Note tags */ tags: string escapeContext: boolean escapeTrans: boolean escapeNote: boolean /** Sync to AnkiWeb after added */ syncServer: boolean } export class Service extends SyncService { static readonly id = 'ankiconnect' static getDefaultConfig(): SyncConfig { return { enable: false, host: '127.0.0.1', port: '8765', key: null, deckName: 'Saladict', noteType: 'Saladict Word', tags: '', escapeContext: true, escapeTrans: true, escapeNote: true, syncServer: false } } noteFileds: string[] | undefined async init() { if (!(await this.isServerUp())) { throw new Error('server') } const decks = await this.request('deckNames') if (!decks?.includes(this.config.deckName)) { throw new Error('deck') } const noteTypes = await this.request('modelNames') if (!noteTypes?.includes(this.config.noteType)) { throw new Error('notetype') } } handleMessage = (msg: Message) => { switch (msg.type) { case 'ANKI_CONNECT_FIND_WORD': return this.findNote(msg.payload).catch(() => '') case 'ANKI_CONNECT_UPDATE_WORD': return this.updateWord(msg.payload.cardId, msg.payload.word).catch(e => Promise.reject(e) ) } } onStart() { message.addListener(this.handleMessage) } async destroy() { message.removeListener(this.handleMessage) } async findNote(date: number): Promise { if (!this.noteFileds) { this.noteFileds = await this.getNotefields() } try { const notes = await this.request('findNotes', { query: `deck:${this.config.deckName} ${this.noteFileds[0]}:${date}` }) return notes[0] } catch (e) { if (process.env.DEBUG) { console.error(e) } } } async add({ words, force }: AddConfig) { if (!(await this.isServerUp())) { throw new Error('server') } if (force) { words = await getNotebook() } if (!words || words.length <= 0) { return } await Promise.all( words.map(async word => { if (!(await this.findNote(word.date))) { try { await this.addWord(word) } catch (e) { if (process.env.DEBUG) { console.warn(e) } throw new Error('add') } } }) ) if (this.config.syncServer) { try { await this.request('sync') } catch (e) { if (process.env.DEBUG) { console.warn(e) } } } } async addWord(word: Readonly) { return this.request('addNote', { note: { deckName: this.config.deckName, modelName: this.config.noteType, options: { allowDuplicate: false, duplicateScope: 'deck' }, tags: this.extractTags(), fields: await this.wordToFields(word) } }) } async updateWord(noteId: number, word: Readonly) { return this.request('updateNoteFields', { note: { id: noteId, fields: await this.wordToFields(word) } }) } async addDeck() { return this.request('createDeck', { deck: this.config.deckName }) } async addNoteType() { this.noteFileds = [ 'Date', 'Text', 'Translation', 'Context', 'ContextCloze', 'Note', 'Title', 'Url', 'Favicon', 'Audio' ] await this.request('createModel', { modelName: this.config.noteType, inOrderFields: this.noteFileds, css: cardCss(), cardTemplates: [ { Name: 'Saladict Cloze', Front: cardText(true, this.noteFileds), Back: cardText(false, this.noteFileds) } ] }) // Anki Connect could tranlate the field names // Update again this.noteFileds = await this.getNotefields() await this.request('updateModelTemplates', { model: { name: this.config.noteType, templates: { 'Saladict Cloze': { Front: cardText(true, this.noteFileds), Back: cardText(false, this.noteFileds) } } } }) } async request(action: string, params?: any): Promise { const { data } = await axios({ method: 'post', url: `http://${this.config.host}:${this.config.port}`, data: { key: this.config.key || null, version: 6, action, params: params || {} } }) if (process.env.DEBUG) { console.log(`Anki Connect ${action} response`, data) } if (!data || !Object.prototype.hasOwnProperty.call(data, 'result')) { throw new Error('Deprecated Anki Connect version') } if (data.error) { throw new Error(data.error) } return data.result } async wordToFields(word: Readonly): Promise<{ [k: string]: string }> { if (!this.noteFileds) { this.noteFileds = await this.getNotefields() } return { // Date [this.noteFileds[0]]: `${word.date}`, // Text [this.noteFileds[1]]: word.text || '', // Translation [this.noteFileds[2]]: this.parseTrans( word.trans, this.config.escapeTrans ), // Context [this.noteFileds[3]]: this.multiline( word.context, this.config.escapeContext ), // ContextCloze [this.noteFileds[4]]: this.multiline( word.context.split(word.text).join(`{{c1::${word.text}}}`), this.config.escapeContext ) || `{{c1::${word.text}}}`, // Note [this.noteFileds[5]]: this.multiline(word.note, this.config.escapeNote), // Title [this.noteFileds[6]]: word.title || '', // Url [this.noteFileds[7]]: word.url || '', // Favicon [this.noteFileds[8]]: word.favicon || '', // Audio [this.noteFileds[9]]: '' // @TODO } } async getNotefields(): Promise { const nf = await this.request('modelFieldNames', { modelName: this.config.noteType }) // Anki connect bug return nf?.includes('Date.') ? [ 'Date.', 'Text.', 'Translation.', 'Context.', 'ContextCloze.', 'Note.', 'Title.', 'Url.', 'Favicon.', 'Audio.' ] : nf?.includes('日期') ? [ '日期', '文字', 'Translation', 'Context', 'ContextCloze', '笔记', 'Title', 'Url', 'Favicon', 'Audio' ] : [ 'Date', 'Text', 'Translation', 'Context', 'ContextCloze', 'Note', 'Title', 'Url', 'Favicon', 'Audio' ] } multiline(text: string, escape: boolean): string { text = text.trim() if (!text) return '' if (escape) { text = this.escapeHTML(text) } return text.trim().replace(/\n/g, '
') } parseTrans(text: string, escape: boolean): string { text = text.trim() if (!text) return '' const ctx = parseCtxText(text) const ids = Object.keys(ctx) if (ids.length <= 0) { return this.multiline(text, escape) } const trans = ids .map( id => `${id}

${ctx[id]}
` ) .join('') return text .split(/\[:: \w+ ::\](?:[\s\S]+?)(?:-{15})/) .map(text => this.multiline(text, escape)) .join(`
${trans}
`) } private _div: HTMLElement | undefined escapeHTML(text: string): string { if (!this._div) { this._div = document.createElement('div') this._div.appendChild(document.createTextNode('')) } this._div.firstChild!.nodeValue = text return this._div.innerHTML } extractTags(): string[] { return this.config.tags .split(/,|,/) .map(t => t.trim()) .filter(Boolean) } async isServerUp(): Promise { try { return (await this.request('version')) != null } catch (e) { if (process.env.DEBUG) { console.error(e) } return false } } } function cardText(front: boolean, nf: string[]) { return `{{#${nf[4]}}}
{{cloze:${nf[4]}}}
{{type:cloze:${nf[4]}}}
{{#${nf[2]}}}
{{${nf[2]}}}
{{/${nf[2]}}} {{/${nf[4]}}} {{^${nf[4]}}}

{{${nf[1]}}}

{{#${nf[2]}}}
{{${nf[2]}}}
{{/${nf[2]}}} {{/${nf[4]}}} {{#${nf[5]}}}
{{${(front ? 'hint:' : '') + nf[5]}}}
{{/${nf[5]}}} {{#${nf[6]}}}

{{#${nf[8]}}} {{/${nf[8]}}} {{${nf[6]}}}
{{/${nf[6]}}} ` } function cardCss() { return `.card { font-family: arial; font-size: 20px; text-align: center; color: #333; background-color: white; } a { color: #5caf9e; } input { border: 1px solid #eee; } section { margin: 1em 0; } .trans { border: 1px solid #eee; padding: 0.5em; } .trans_title { display: block; font-size: 0.9em; font-weight: bold; } .trans_content { margin-bottom: 0.5em; } .cloze { font-weight: bold; color: #f9690e; } .tsource { position: relative; font-size: .8em; } .tsource img { height: .7em; } .tsource a { text-decoration: none; } .typeGood { color: #fff; background: #1EBC61; } .typeBad { color: #fff; background: #F75C4C; } .typeMissed { color: #fff; background: #7C8A99; } .favicon { display: inline-block; width: 1em; height: 1em; background: center/cover no-repeat; } ` } ================================================ FILE: src/background/sync-manager/services/eudic/_locales/en.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: 'Eudic Word Syncing', open: 'Open', error: { network: 'Unable to access the new word book of Eudic, please check the network.', illegal_token: 'Please set legal Eudic authorization information.', no_wordbook: 'Unable to add to the new word book of European dictionary. Please go to the European official website to generate the default new word book first.' } } ================================================ FILE: src/background/sync-manager/services/eudic/_locales/zh-CN.ts ================================================ export const locale = { title: '欧路单词同步', open: '打开', error: { network: '无法访问欧路词典生词本,请检查网络。', illegal_token: '请设置合法的欧路词典授权信息', no_wordbook: '无法添加到欧路词典生词本,请先上欧路官网生成默认生词本' } } ================================================ FILE: src/background/sync-manager/services/eudic/_locales/zh-TW.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: '歐路單字同步', open: '開啟', error: { network: '無法訪問歐路詞典生詞本,請檢查網絡。', illegal_token: '請設定合法的歐路詞典授權資訊', no_wordbook: '無法添加到歐路詞典生詞本,請先上歐路官網生成默認生詞本' } } ================================================ FILE: src/background/sync-manager/services/eudic/index.ts ================================================ import { AddConfig, SyncService } from '../../interface' import { getNotebook } from '../../helpers' import axios from 'axios' export interface SyncConfig { enable: boolean token: string syncAll: boolean } interface Books { id: string language: string name: string } export class Service extends SyncService { static readonly id = 'eudic' static getDefaultConfig(): SyncConfig { return { enable: false, token: '', syncAll: false } } async init() { const wordbooks = await this.getWordbooks() if (wordbooks.length === 0 || !wordbooks) { throw new Error('no_wordbook') } } async add(config: AddConfig) { await this.addWordOrPatch(config) } /** * sync a word or patch words */ async addWordOrPatch({ words, force }: AddConfig) { if (!this.config.enable) { return 0 } if (force) { words = await getNotebook() } if (!words || words.length <= 0) { return 0 } const payload = force ? words.map(word => word.text) : words[0].text await this.requestAddWords(payload) } /** * get the user's wordbooks and judge the correctness of the authorization information */ async getWordbooks(): Promise { const result = await axios({ method: 'get', url: `https://api.frdic.com/api/open/v1/studylist/category`, params: { language: 'en' }, headers: { Authorization: this.config.token } }).catch(e => { if (e.response && e.response.status === 401) { throw new Error('illegal_token') } else { throw new Error('network') } }) if (!result?.data) { throw new Error('network') } const { data } = result if (process.env.DEBUG) { console.log(`Eudic Connect response(wordbook list)`, data) } if (!data || !Object.prototype.hasOwnProperty.call(data, 'data')) { throw new Error('network') } return data.data } async requestAddWords(words: string | string[]) { return await axios({ method: 'post', url: `https://api.frdic.com/api/open/v1/studylist/words`, data: { id: '0', // id of default wordbook language: 'en', words: typeof words === 'string' ? [words] : words }, headers: { Authorization: this.config.token } }).catch(e => { if (process.env.DEBUG) { console.error(e) } if (e.response && e.response.status === 401) { throw new Error('illegal_token') } else { throw new Error('network') } }) } } ================================================ FILE: src/background/sync-manager/services/shanbay/_locales/en.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: 'Shanbay Word Syncing', open: 'Open', error: { login: 'Shanbay login failed. Click to open shanbay.com.', network: 'Unable to access shanbay.com. Please check your network connection.', word: "Unable to add to Shanbay notebook. This word is not in Shanbay's vocabulary database." } } ================================================ FILE: src/background/sync-manager/services/shanbay/_locales/zh-CN.ts ================================================ export const locale = { title: '扇贝单词同步', open: '打开', error: { login: '扇贝登录已失效,请点击打开官网重新登录。', network: '无法访问扇贝生词本,请检查网络。', word: '无法添加到扇贝生词本,扇贝单词库没有收录此单词。' } } ================================================ FILE: src/background/sync-manager/services/shanbay/_locales/zh-TW.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: '扇貝單字同步', open: '開啟', error: { login: '扇貝登入已失效,請點選開啟官網重新登入。', network: '無法訪問扇貝生字本,請檢查網路。', word: '無法新增到扇貝生字本,扇貝單字庫沒有收錄此單字。' } } ================================================ FILE: src/background/sync-manager/services/shanbay/index.ts ================================================ import { AddConfig, SyncService } from '../../interface' import { getNotebook, notifyError } from '../../helpers' import { openUrl } from '@/_helpers/browser-api' import { timer } from '@/_helpers/promise-more' import { isFirefox } from '@/_helpers/saladict' import { I18nManager } from '@/background/i18n-manager' export interface SyncConfig { enable: boolean } export class Service extends SyncService { static readonly id = 'shanbay' static getDefaultConfig(): SyncConfig { return { enable: false } } static openLogin() { return openUrl('https://www.shanbay.com/web/account/login') } async init() { if (!(await this.isLogin())) { throw new Error('login') } } async add(config: AddConfig) { await this.addInternal(config) } /** * @returns failed words */ async addInternal({ words, force }: AddConfig): Promise { if (!this.config.enable) { return 0 } if (!(await this.isLogin())) { this.notifyLogin() return 0 } if (force) { words = await getNotebook() } if (!words || words.length <= 0) { return 0 } let errorCount = 0 for (let i = 0; i < words.length; i++) { try { await this.addWord(words[i].text) } catch (error) { if (error.message !== 'word') { throw error } errorCount += 1 notifyError(Service.id, 'word', `「${words[i].text}」`) } if ((i + 1) % 50 === 0) { await timer(15 * 60000) } else { await timer(500) } } return errorCount } async addWord(text: string) { let word: { id: string } | undefined try { const url = 'https://apiv3.shanbay.com/abc/words/senses?vocabulary_content=' + encodeURIComponent(text) word = await fetch(url).then(r => r.json()) } catch (e) { throw new Error('network') } if (!word || !word.id) { throw new Error('word') } let uploadResult try { uploadResult = await fetch( 'https://apiv3.shanbay.com/wordscollection/words', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vocab_id: word.id, business_id: 6 }) } ).then(r => r.json()) } catch (e) { if (process.env.DEBUG) { console.error(e) } throw new Error('network') } if (!uploadResult || !uploadResult.created_at) { throw new Error('word') } } async isLogin(): Promise { return Boolean( await browser.cookies.get({ url: 'http://www.shanbay.com', name: 'auth_token' }) ) } async notifyLogin() { const { i18n } = await I18nManager.getInstance() await i18n.loadNamespaces('sync') if (browser.notifications) { browser.notifications.onClicked.addListener(handleLoginNotification) browser.notifications.onClosed.removeListener(removeNotificationHandler) if (browser.notifications.onButtonClicked) { browser.notifications.onButtonClicked.addListener( handleLoginNotification ) } const options: browser.notifications.CreateNotificationOptions = { type: 'basic', iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: `Saladict ${i18n.t(`sync:shanbay.title`)}`, message: i18n.t('sync:shanbay.error.login'), eventTime: Date.now() + 10000, priority: 2 } if (!isFirefox) { options.buttons = [{ title: i18n.t('sync:shanbay.open') }] } browser.notifications.create('shanbay-login', options) } } } function handleLoginNotification(id: string) { if (id === 'shanbay-login') { Service.openLogin() removeNotificationHandler(id) } } function removeNotificationHandler(id: string) { if (id === 'shanbay-login') { if (browser.notifications) { browser.notifications.onClicked.removeListener(handleLoginNotification) browser.notifications.onClosed.removeListener(removeNotificationHandler) if (browser.notifications.onButtonClicked) { browser.notifications.onButtonClicked.removeListener( handleLoginNotification ) } } } } ================================================ FILE: src/background/sync-manager/services/webdav/_locales/en.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: 'WebDAV Word Syncing', error: { dir: 'Incorrect "Saladict" directory on server.', download: 'Download failed. Unable to connect WebDAV Server. If browser proxy is enabled please adjust rules to bypass WebDAV server.', internal: 'Unable to save settings.', missing: 'Missing "Saladict" directory on server.', mkcol: 'Cannot create "Saladict" directory on server. Please create the directory manualy on server.', network: 'Network error. Cannot connect to server.', parse: 'Incorrect response XML from server.', unauthorized: 'Incorrect account or password.' } } ================================================ FILE: src/background/sync-manager/services/webdav/_locales/zh-CN.ts ================================================ export const locale = { title: 'WebDAV 单词同步', error: { dir: '服务器上“Saladict”目录格式不正确,请检查。', download: '下载失败。无法访问 WebDAV 服务器。如启用了浏览器代理请调整规则,不要代理 WebDAV 服务器。', internal: '无法保存。', missing: '服务器上缺少“Saladict”目录。', mkcol: '无法在服务器创建“Saladict”目录。请手动在服务器上创建。', network: '连接服务器失败。', parse: '服务器返回 XML 格式不正确。', unauthorized: '账户或密码不正确。' } } ================================================ FILE: src/background/sync-manager/services/webdav/_locales/zh-TW.ts ================================================ import { locale as _locale } from './zh-CN' export const locale: typeof _locale = { title: 'WebDAV 單字同步', error: { dir: '伺服器上“Saladict”目錄格式不正確,請檢查。', download: '下載失敗。無法訪問 WebDAV 伺服器。如啟用了瀏覽器代理請調整規則,不要代理 WebDAV 伺服器。', internal: '無法儲存。', missing: '伺服器上缺少“Saladict”目錄。', mkcol: '無法在伺服器建立“Saladict”目錄。請手動在伺服器上建立。', network: '連線伺服器失敗。', parse: '伺服器返回 XML 格式不正確。', unauthorized: '帳戶或密碼不正確。' } } ================================================ FILE: src/background/sync-manager/services/webdav/index.ts ================================================ import { NotebookFile, AddConfig, DownloadConfig, SyncService, SyncServiceConfigBase } from '../../interface' import { getNotebook, setNotebook, setMeta, getMeta, notifyError } from '../../helpers' import { Mutable } from '@/typings/helpers' import { storage } from '@/_helpers/browser-api' export interface SyncConfig extends SyncServiceConfigBase { /** Server address. Ends with '/'. */ readonly url: string readonly user: string readonly passwd: string /** In min */ readonly duration: number } export interface SyncMeta { readonly etag?: string readonly timestamp?: number } export class Service extends SyncService { static readonly id = 'webdav' static getDefaultConfig(): SyncConfig { return { enable: false, url: '', user: '', passwd: '', duration: 15 } } meta: SyncMeta = {} async onStart() { if (process.env.DEBUG) { console.log(`Sync Service WebDAV starts interval.`) } if (!this.config.enable) { if (process.env.DEBUG) { console.warn(`Sync Service WebDAV already started.`) } return } this.meta = (await getMeta(Service.id)) || this.meta await browser.alarms.clear('webdav') browser.alarms.onAlarm.addListener(this.handleSyncAlarm) if (typeof this.config.url === 'string' && !this.config.url.endsWith('/')) { ;(this.config as Mutable).url += '/' } if (this.config.url) { const duration = +this.config.duration || 15 const now = Date.now() let nextInterval: number = +(await storage.local.get('webdavInterval')) .webdavInterval if ( !nextInterval || nextInterval < now || now + duration * 60000 < nextInterval ) { nextInterval = now + 1000 } await storage.local.set({ webdavInterval: nextInterval }) browser.alarms.create('webdav', { when: nextInterval, periodInMinutes: duration }) } else { await storage.local.set({ webdavInterval: 0 }) } } handleSyncAlarm = async (alarm: browser.alarms.Alarm) => { if (alarm.name !== 'webdav') { return } if (process.env.DEBUG) { console.log('Sync Service WebDAV Interval Alarm triggered.') } try { await this.download({}) } catch (e) { console.error(e) notifyError(Service.id, 'download') } const duration = this.config.duration * 60000 || 15 * 60000 await storage.local.set({ webdavInterval: Date.now() + duration }) } async checkDir(): Promise { let text = '' try { const response = await fetch(this.config.url, { method: 'PROPFIND', headers: { Authorization: 'Basic ' + window.btoa(`${this.config.user}:${this.config.passwd}`), 'Content-Type': 'application/xml; charset="utf-8"', Depth: '1' } }) if (!response.ok) { if (response.status === 401) { throw new Error('unauthorized') } throw new Error(`Network error: ${response.status}`) } text = await response.text() } catch (e) { throw new Error('network') } let doc: Document | undefined try { if (text) { doc = new DOMParser().parseFromString(text, 'text/xml') } } catch (e) { throw new Error('parse') } if (!doc) { throw new Error('parse') } const $responses = Array.from(doc.querySelectorAll('response')) for (const i in $responses) { const href = $responses[i].querySelector('href') if (href && href.textContent && href.textContent.endsWith('/Saladict/')) { // is Saladict if ($responses[i].querySelector('resourcetype collection')) { // is collection return true } else { throw new Error('dir') } } } return false } /** * Check server and create a Saladict Directory if not exist. */ async init() { const dir = await this.checkDir() if (!dir) { // create directory const response = await fetch(this.config.url + 'Saladict', { method: 'MKCOL', headers: { Authorization: 'Basic ' + window.btoa(`${this.config.user}:${this.config.passwd}`) } }) if (!response.ok) { // cannot create directory throw new Error('mkcol') } } if (dir) { try { await this.download({ testConfig: this.config, noCache: true }) } catch (e) { // Download failed, which is desired. return } // An old file exists on server. // Let user decide whether to upload. throw new Error('exist') } } async add({ force }: AddConfig) { if (!this.config.url) { if (process.env.DEBUG) { console.warn(`sync service ${Service.id} upload: empty url`) } return } if (!force) { await this.download({}) } const words = await getNotebook() if (!words || words.length <= 0) { return } const timestamp = Date.now() try { var body = JSON.stringify({ timestamp, words } as NotebookFile) } catch (e) { if (process.env.DEBUG) { console.error('WebDAV: Stringify notebook failed', words) } throw new Error('parse') } try { const response = await fetch(this.config.url + 'Saladict/notebook.json', { method: 'PUT', headers: { Authorization: 'Basic ' + window.btoa(`${this.config.user}:${this.config.passwd}`) }, body }) if (!response.ok) { throw new Error('network') } } catch (e) { if (process.env.DEBUG) { console.error('WebDAV: upload failed', e) } throw new Error('network') } await this.setMeta({ timestamp, etag: '' }) } delete({ force }) { // full sync anyway return this.add({ force }) } async download({ testConfig, noCache }: DownloadConfig): Promise { const config = testConfig || this.config if (!config.url) { if (process.env.DEBUG) { console.warn(`sync service ${Service.id} download: empty url`) } return } const headers: { [name: string]: string } = { Authorization: 'Basic ' + window.btoa(`${config.user}:${config.passwd}`) } if (!testConfig && !noCache && this.meta.etag != null) { headers['If-None-Match'] = this.meta.etag headers['If-Modified-Since'] = this.meta.etag } try { var response = await fetch( config.url + (config.url.endsWith('/') ? '' : '/') + 'Saladict/notebook.json', { method: 'GET', headers } ) if (response.status === 304) { return } if (!response.ok) { throw new Error() } } catch (e) { if (process.env.DEBUG) { console.error(e) } throw new Error('network') } try { var json: NotebookFile = await response.json() } catch (e) { if (process.env.DEBUG) { console.error('Fetch webdav notebook.json error', response) } throw new Error('parse') } if (process.env.DEBUG) { if (!response.headers.get('ETag')) { console.warn('webdav notebook.json no etag', response) } } if (!Array.isArray(json.words) || json.words.some(w => !w.date)) { if (process.env.DEBUG) { console.error('Parse webdav notebook.json error: incorrect words', json) } throw new Error('format') } if (!json.timestamp) { if (process.env.DEBUG) { console.error('webdav notebook.json no timestamp', json) } throw new Error('timestamp') } if (testConfig) { // connectivity is ok return } const oldMeta = this.meta if (!oldMeta.timestamp || json.timestamp >= oldMeta.timestamp) { await this.setMeta({ timestamp: json.timestamp, etag: response.headers.get('ETag') || oldMeta.etag || '' }) } if (!noCache && oldMeta.timestamp && json.timestamp <= oldMeta.timestamp) { // older file return } await setNotebook(json.words) if (process.env.DEBUG) { console.log('Webdav download', json) } } async destroy() { browser.alarms.onAlarm.removeListener(this.handleSyncAlarm) await browser.alarms.clear('webdav') } setMeta(meta: SyncMeta) { this.meta = meta return setMeta(Service.id, meta) } async getMeta() { const meta = await getMeta(Service.id) this.meta = meta || ({} as SyncMeta) } } ================================================ FILE: src/background/types.ts ================================================ import { AppConfig } from '@/app-config' import { Profile, ProfileIDList } from '@/app-config/profiles' declare global { interface Window { appConfig: AppConfig activeProfile: Profile profileIDList: ProfileIDList } } ================================================ FILE: src/background/windows-manager.ts ================================================ import { message, storage } from '@/_helpers/browser-api' import { Word } from '@/_helpers/record-manager' import { isFirefox } from '@/_helpers/saladict' import { getTitlebarOffset } from '@/_helpers/titlebar-offset' interface WinRect { width: number height: number left: number top: number } const safeUpdateWindow: typeof browser.windows.update = (...args) => browser.windows.update(...args).catch(console.warn as (m: any) => undefined) /** * Manipulate main window */ export class MainWindowsManager { /** Main window snapshot */ snapshot: browser.windows.Window | null = null async correctTop(originTop?: number) { if (!originTop) return originTop const offset = await getTitlebarOffset() if (!offset) return originTop return originTop - offset.main } async focus(): Promise { if (this.snapshot && this.snapshot.id != null) { await safeUpdateWindow(this.snapshot.id, { focused: true }) } } async takeSnapshot(): Promise { this.snapshot = null try { const win = await browser.windows.getLastFocused({ windowTypes: ['normal'] }) if (win.focused && win.type === 'normal' && win.state !== 'minimized') { this.snapshot = win } else if (isFirefox) { // Firefox does not support windowTypes in getLastFocused const wins = (await browser.windows.getAll()).filter( win => win.focused && win.type === 'normal' && win.state !== 'minimized' ) if (wins.length === 1) { this.snapshot = wins[0] } else { const focusedWins = wins.filter(win => win.focused) if (focusedWins.length === 1) { this.snapshot = focusedWins[0] } } } } catch (e) { console.warn(e) } return this.snapshot } destroySnapshot(): void { this.snapshot = null } async makeRoomForSidebar( side: 'left' | 'right', sidebarSnapshot: browser.windows.Window | null ): Promise { const mainWin = this.snapshot if (!mainWin || mainWin.id == null) { return } const sidebarWidth = (sidebarSnapshot && sidebarSnapshot.width) || window.appConfig.panelWidth const updateInfo = mainWin.top != null && mainWin.left != null && mainWin.width != null && mainWin.height != null ? { top: await this.correctTop(mainWin.top), left: side === 'right' ? mainWin.left : mainWin.left + sidebarWidth, width: mainWin.width - sidebarWidth, height: mainWin.height, state: 'normal' as 'normal' } : { top: 0, left: side === 'right' ? 0 : sidebarWidth, width: window.screen.availWidth - sidebarWidth, height: window.screen.availHeight, state: 'normal' as 'normal' } if (side === 'right') { // fix a chrome bug by moving 1 extra pixal then to 0 await safeUpdateWindow(mainWin.id, { ...updateInfo, left: updateInfo.left + 1 }) } await safeUpdateWindow(mainWin.id, updateInfo) } async restoreSnapshot(): Promise { if (this.snapshot && this.snapshot.id != null) { await safeUpdateWindow( this.snapshot.id, this.snapshot.state === 'normal' ? { top: await this.correctTop(this.snapshot.top), left: this.snapshot.left, width: this.snapshot.width, height: this.snapshot.height } : { state: this.snapshot.state } ) } } } /** * Manipulate Standalone Quick Search Panel */ export class QsPanelManager { private qsPanelId: number | null = null private snapshot: browser.windows.Window | null = null private isSidebar: boolean = false private mainWindowsManager = new MainWindowsManager() async correctTop(originTop?: number) { if (!originTop) return originTop const offset = await getTitlebarOffset() if (!offset) return originTop return originTop - offset.panel } /** * @param preload force preload word. otherwise let the panel decide. */ async create(preload?: Word): Promise { this.isSidebar = false let wordString = '' let lastTabString = '' if (preload) { try { wordString = '&word=' + encodeURIComponent(JSON.stringify(preload)) } catch (error) { if (process.env.DEBUG) { console.error(error) } } } else { if (window.appConfig.qsPreload === 'selection') { const tab = ( await browser.tabs.query({ active: true, lastFocusedWindow: true }) )[0] if (tab && tab.id) { lastTabString = '&lastTab=' + tab.id } } } await this.mainWindowsManager.takeSnapshot() const qsPanelRect = window.appConfig.qssaSidebar ? await this.getSidebarRect(window.appConfig.qssaSidebar) : (window.appConfig.qssaRectMemo && (await this.getStorageRect())) || this.getDefaultRect() let qsPanelWin: browser.windows.Window | undefined try { qsPanelWin = await browser.windows.create({ ...qsPanelRect, type: 'popup', url: browser.runtime.getURL( `quick-search.html?sidebar=${window.appConfig.qssaSidebar}${wordString}${lastTabString}` ) }) } catch (err) { browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL(`assets/icon-128.png`), title: `Saladict`, message: err.message, priority: 2, eventTime: Date.now() + 5000 }) } if (qsPanelWin && qsPanelWin.id) { if (isFirefox) { // Firefox needs an extra push safeUpdateWindow(qsPanelWin.id, qsPanelRect) } this.qsPanelId = qsPanelWin.id if (window.appConfig.qssaSidebar) { this.isSidebar = true await this.mainWindowsManager.makeRoomForSidebar( window.appConfig.qssaSidebar, qsPanelWin ) } if (!window.appConfig.qsFocus) { await this.mainWindowsManager.focus() } // notify all tabs ;(await browser.tabs.query({})).forEach(tab => { if (tab.id && tab.windowId !== this.qsPanelId) { message.send(tab.id, { type: 'QS_PANEL_CHANGED', payload: this.qsPanelId != null }) } }) } } async getWin(): Promise { if (!this.qsPanelId) { return null } return browser.windows.get(this.qsPanelId).catch(() => null) } async destroy(): Promise { ;(await browser.tabs.query({})).forEach(tab => { if (tab.id && tab.windowId !== this.qsPanelId) { message.send(tab.id, { type: 'QS_PANEL_CHANGED', payload: false }) } }) this.qsPanelId = null this.isSidebar = false this.destroySnapshot() await this.mainWindowsManager.restoreSnapshot() this.mainWindowsManager.destroySnapshot() } isQsPanel(winId?: number): boolean { return winId != null && winId === this.qsPanelId } async hasCreated(): Promise { const win = await this.getWin() if (!win) { this.qsPanelId = null } return !!win } async focus(): Promise { if (this.qsPanelId != null) { await safeUpdateWindow(this.qsPanelId, { focused: true }) const [tab] = await browser.tabs.query({ windowId: this.qsPanelId }) if (tab && tab.id) { await message.send(tab.id, { type: 'QS_PANEL_FOCUSED' }) } } } async takeSnapshot(): Promise { if (this.qsPanelId != null) { this.snapshot = await browser.windows .get(this.qsPanelId) .catch(() => null) } } destroySnapshot(): void { this.snapshot = null } async restoreSnapshot(): Promise { // restore main window first so that it will be at the bottom await this.mainWindowsManager.restoreSnapshot() if (this.snapshot != null && this.snapshot.id != null) { await safeUpdateWindow(this.snapshot.id, { top: await this.correctTop(this.snapshot.top), left: this.snapshot.left, width: this.snapshot.width, height: this.snapshot.height }) } else if (this.qsPanelId != null) { await safeUpdateWindow(this.qsPanelId, { focused: true, ...this.getDefaultRect() }) } this.destroySnapshot() } async moveToSidebar(side: 'left' | 'right'): Promise { if (this.qsPanelId != null) { await this.takeSnapshot() await safeUpdateWindow(this.qsPanelId, await this.getSidebarRect(side)) await this.mainWindowsManager.makeRoomForSidebar(side, this.snapshot) } } async toggleSidebar(side: 'left' | 'right'): Promise { if (!(await this.hasCreated())) { return } if (this.isSidebar) { await this.restoreSnapshot() } else { await this.moveToSidebar(side) } this.isSidebar = !this.isSidebar } getDefaultRect(): WinRect { const { qsLocation, qssaHeight } = window.appConfig let qsPanelLeft = 10 let qsPanelTop = 30 const qsPanelWidth = window.appConfig.panelWidth const qsPanelHeight = window.appConfig.qssaHeight switch (qsLocation) { case 'CENTER': qsPanelLeft = (window.screen.availWidth - qsPanelWidth) / 2 qsPanelTop = (window.screen.availHeight - qssaHeight) / 2 break case 'TOP': qsPanelLeft = (window.screen.availWidth - qsPanelWidth) / 2 qsPanelTop = 30 break case 'RIGHT': qsPanelLeft = window.screen.availWidth - qsPanelWidth - 30 qsPanelTop = (window.screen.availHeight - qssaHeight) / 2 break case 'BOTTOM': qsPanelLeft = (window.screen.availWidth - qsPanelWidth) / 2 qsPanelTop = window.screen.availHeight - qsPanelHeight - 10 break case 'LEFT': qsPanelLeft = 10 qsPanelTop = (window.screen.availHeight - qssaHeight) / 2 break case 'TOP_LEFT': qsPanelLeft = 10 qsPanelTop = 30 break case 'TOP_RIGHT': qsPanelLeft = window.screen.availWidth - qsPanelWidth - 30 qsPanelTop = 30 break case 'BOTTOM_LEFT': qsPanelLeft = 10 qsPanelTop = window.screen.availHeight - qsPanelHeight - 10 break case 'BOTTOM_RIGHT': qsPanelLeft = window.screen.availWidth - qsPanelWidth - 30 qsPanelTop = window.screen.availHeight - qsPanelHeight - 10 break } // coords must be integer // plus offset of other screen return { top: Math.round(qsPanelTop + (window.screen['availTop'] || 0)), left: Math.round(qsPanelLeft + (window.screen['availLeft'] || 0)), width: Math.round(qsPanelWidth), height: Math.round(qsPanelHeight) } } /** get saved panel rect */ async getStorageRect(): Promise { const { qssaRect } = await storage.local.get<{ qssaRect: WinRect }>( 'qssaRect' ) if (!qssaRect) return null return { ...qssaRect, top: (await this.correctTop(qssaRect.top)) || 0 } } async getSidebarRect(side: 'left' | 'right'): Promise { const panelWidth = (this.snapshot && this.snapshot.width) || window.appConfig.panelWidth const mainWin = this.mainWindowsManager.snapshot return mainWin && mainWin.state === 'normal' && mainWin.top != null && mainWin.left != null && mainWin.width != null && mainWin.height != null ? // coords must be integer { top: Math.round( (await this.mainWindowsManager.correctTop(mainWin.top)) || 0 ), left: Math.round( side === 'right' ? Math.max(mainWin.width - panelWidth, panelWidth) : mainWin.left ), width: Math.round(panelWidth), height: Math.round(mainWin.height) } : { top: 0, left: Math.round( side === 'right' ? window.screen.availWidth - panelWidth : 0 ), width: Math.round(panelWidth), height: Math.round(window.screen.availHeight) } } } ================================================ FILE: src/components/AntdRoot/AntdRootContainer.tsx ================================================ import React, { FC, useEffect, useMemo } from 'react' import { shallowEqual } from 'react-redux' import { ConfigProvider as AntdConfigProvider } from 'antd' import zh_CN from 'antd/lib/locale-provider/zh_CN' import zh_TW from 'antd/lib/locale-provider/zh_TW' import en_US from 'antd/lib/locale-provider/en_US' import { useSelector } from '@/content/redux' import { reportPageView } from '@/_helpers/analytics' const antdLocales = (saladictLocale: string) => { switch (saladictLocale) { case 'zh-CN': return zh_CN case 'zh-TW': return zh_TW default: return en_US } } export interface AntdRootContainerProps { /** Render Props */ render: () => React.ReactNode /** Analytics path */ gaPath?: string } /** Inner Component so that it can access Redux store */ export const AntdRootContainer: FC = props => { const { langCode, analytics, darkMode } = useSelector(state => { const { langCode, analytics, darkMode } = state.config return { langCode, analytics, darkMode } }, shallowEqual) const locale = useMemo(() => antdLocales(langCode), [langCode]) const bgStyles = useMemo( () => ({ backgroundColor: darkMode ? '#000' : '#f0f2f5' }), [darkMode] ) useEffect(() => { if (analytics && props.gaPath) { reportPageView(props.gaPath) } }, [analytics, props.gaPath]) return (
{props.render()}
) } ================================================ FILE: src/components/AntdRoot/_style.scss ================================================ html { background-color: #888; } #root { &::after { content: ''; position: fixed; z-index: 2147483647; top: 0; left: 0; bottom: 0; right: 0; margin: auto; background-color: #888; transition: background-color 0.3s; opacity: 0; // for initial hardware acceleration pointer-events: none; } &.saladict-theme-dark::after { background-color: #000; opacity: 1; } &.saladict-theme-bright::after { background-color: #f0f2f5; opacity: 1; } &.saladict-theme-loaded::after { transition: opacity 0.4s; opacity: 0; } &.saladict-theme-loading::after { opacity: 1; } } // Fix incorrect antd pagination arrow position on Firefox @-moz-document url-prefix() { .ant-pagination-item-link > .anticon { height: 100%; display: flex; justify-content: center; align-items: center; } } ================================================ FILE: src/components/AntdRoot/index.tsx ================================================ import React from 'react' import { Provider as ReduxProvider } from 'react-redux' import ReactDOM from 'react-dom' import { createStore } from '@/content/redux' import SaladBowlContainer from '@/content/components/SaladBowl/SaladBowl.container' import DictPanelContainer from '@/content/components/DictPanel/DictPanel.container' import WordEditorContainer from '@/content/components/WordEditor/WordEditor.container' import { I18nContextProvider } from '@/_helpers/i18n' import { timer } from '@/_helpers/promise-more' import { AntdRootContainer } from './AntdRootContainer' import './_style.scss' export const initAntdRoot = async ( render: () => React.ReactNode, gaPath?: string ): Promise => { const store = await createStore() // update theme as quickly as possible let { darkMode } = store.getState().config await switchAntdTheme(darkMode) store.subscribe(() => { const { config } = store.getState() if (config.darkMode !== darkMode) { darkMode = config.darkMode switchAntdTheme(darkMode) } }) ReactDOM.render( , document.getElementById('root') ) } async function switchAntdTheme(darkMode: boolean): Promise { const $root = document.querySelector('#root')! await new Promise(resolve => { const filename = `antd${darkMode ? '.dark' : ''}.min.css` const href = process.env.NODE_ENV === 'development' ? `https://cdnjs.cloudflare.com/ajax/libs/antd/4.1.0/${filename}` : `/assets/${filename}` let $link = document.head.querySelector( 'link#saladict-antd-theme' ) if ($link && $link.getAttribute('href') === href) { resolve() return } // smooth dark/bright transition $root.classList.toggle('saladict-theme-dark', darkMode) $root.classList.toggle('saladict-theme-bright', !darkMode) $root.classList.toggle('saladict-theme-loading', true) if ($link) { $link.setAttribute('href', href) } else { $link = document.createElement('link') $link.setAttribute('id', 'saladict-antd-theme') $link.setAttribute('rel', 'stylesheet') $link.setAttribute('href', href) document.head.insertBefore($link, document.head.firstChild) } let loaded = false // @ts-ignore $link.onreadystatechange = function() { // @ts-ignore if (this.readyState === 'complete' || this.readyState === 'loaded') { if (loaded === false) { resolve() } loaded = true } } $link.onload = function() { if (loaded === false) { resolve() } loaded = true } const img = document.createElement('img') img.onerror = function() { if (loaded === false) { resolve() } loaded = true } img.src = href }) await timer(500) $root.classList.toggle('saladict-theme-loaded', true) $root.classList.toggle('saladict-theme-loading', false) } ================================================ FILE: src/components/EntryBox/EntryBox.scss ================================================ .entryBox-Wrap { padding-top: 0.8em; } .entryBox { position: relative; border: 1px solid #c76e06; border-radius: 5px; margin-bottom: 1em; padding: 1em 0.5em 0.5em 0.5em; } .entryBox-Title { position: absolute; top: 0; left: 1em; transform: translateY(-50%); max-width: 90%; overflow: hidden; padding: 0 5px; white-space: nowrap; text-overflow: ellipsis; font-size: 1.2em; background: var(--color-background); } ================================================ FILE: src/components/EntryBox/EntryBox.stories.tsx ================================================ import React from 'react' import { storiesOf } from '@storybook/react' import { withKnobs, text } from '@storybook/addon-knobs' import { withSaladictPanel } from '@/_helpers/storybook' import { EntryBox } from './index' storiesOf('Content Scripts|Components', module) .addParameters({ backgrounds: [ { name: 'Saladict', value: '#5caf9e', default: true }, { name: 'Black', value: '#000' }, { name: 'White', value: '#fff' } ] }) .addDecorator(withKnobs) .addDecorator( withSaladictPanel({ head: }) ) .add('EntryBox', () => ( {text( 'Content', 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Incidunt recusandae exercitationem minus autem repellendus soluta nulla laudantium nobis! Excepturi, dolorem. Doloremque exercitationem dolores voluptatum sint. Perspiciatis reiciendis doloribus mollitia nisi.' )} )) ================================================ FILE: src/components/EntryBox/index.tsx ================================================ import React, { FC, ComponentProps, ReactNode } from 'react' export interface EntryBoxProps extends Omit, 'title'> { title: ReactNode } /** * Box-wrapped content */ export const EntryBox: FC = props => { const { title, className, children, ...restProps } = props return (

{title}

{children}
) } export default EntryBox ================================================ FILE: src/components/ErrorBoundary.tsx ================================================ import React, { ComponentType } from 'react' interface ErrorBoundaryProps { /** Reanders on error */ error?: ComponentType } interface ErrorBoundaryState { hasError: boolean } export class ErrorBoundary extends React.PureComponent< ErrorBoundaryProps, ErrorBoundaryState > { state: ErrorBoundaryState = { hasError: false } static getDerivedStateFromError() { return { hasError: true } } render() { return this.state.hasError ? this.props.error ? React.createElement(this.props.error) : null : this.props.children } } ================================================ FILE: src/components/FloatBox/FloatBox.scss ================================================ .floatBox-Container { position: relative; overflow: hidden; box-sizing: border-box; word-break: keep-all; white-space: nowrap; border-radius: 10px; background: #fff; box-shadow: 0px 4px 31px -8px rgba(0, 0, 0, 0.8); font-size: var(--panel-font-size); @include isDarkMode { background: #2d3338; } @include isAnimate { transition: width 0.4s, height 0.4s; } } .floatBox-Measure { position: absolute; top: 0; left: 0; width: max-content; // for including key: string value: string options: Array<{ value: string label: string }> title?: string } export interface FloatBoxProps { list?: FloatBoxItem[] /** compact layout */ compact?: boolean /** Box container */ ref?: Ref /** When a item is selected */ onSelect?: (key: string, value: string) => any /** When a item is focused */ onFocus?: (e: React.FocusEvent) => any /** When a item is blur */ onBlur?: (e: React.FocusEvent) => any /** When mouse over on panel */ onMouseOver?: (e: React.MouseEvent) => any /** When mouse out on panel */ onMouseOut?: (e: React.MouseEvent) => any /** When ArrowUp key if pressed on the first item */ onArrowUpFirst?: (container: HTMLDivElement) => any /** When ArrowDown key if pressed on the last item */ onArrowDownLast?: (container: HTMLDivElement) => any /** When the panel is about to close */ onClose?: (container: HTMLDivElement) => any /** When box height is changed */ onHeightChanged?: (height: number) => any } /** * A box that is meant to be on top of other elements */ export const FloatBox: FC = React.forwardRef( (props: FloatBoxProps, containerRef: React.Ref) => { const [height, _setHeight] = useState(0) const [width, _setWidth] = useState(0) const updateHeight = useCallback( (newWidth: number, newHeight: number) => { _setWidth(newWidth) _setHeight(newHeight) if (props.onHeightChanged && newHeight !== height) { props.onHeightChanged(newHeight) } }, [props.onHeightChanged] ) return (
{!props.list ? (
) : (
{props.list.map(renderBoxItem)}
)}
) function renderBoxItem(item: FloatBoxItem) { if (item.options) { return ( ) } return ( ) } function onSelectItemChange(e: React.ChangeEvent) { if (props.onSelect) { const { dataset: { key }, value } = e.currentTarget props.onSelect(key!, value) } } function onBtnItemClick(e: React.MouseEvent) { if (props.onSelect) { const { key, value } = e.currentTarget.dataset props.onSelect(key!, value!) } } function onBtnItemKeyDown(e: React.KeyboardEvent) { if (e.key === 'ArrowDown') { e.preventDefault() e.stopPropagation() const $nextLi = e.currentTarget.nextSibling if ($nextLi) { ;($nextLi as HTMLButtonElement).focus() } else if (props.onArrowDownLast) { props.onArrowDownLast(e.currentTarget.parentElement as HTMLDivElement) } } else if (e.key === 'ArrowUp') { e.preventDefault() e.stopPropagation() const $prevLi = e.currentTarget.previousSibling if ($prevLi) { ;($prevLi as HTMLButtonElement).focus() } else if (props.onArrowUpFirst) { props.onArrowUpFirst(e.currentTarget.parentElement as HTMLDivElement) } } else if (e.key === 'Escape') { // prevent the dict panel being closed e.preventDefault() e.stopPropagation() if (props.onClose) { props.onClose(e.currentTarget.parentElement as HTMLDivElement) } } } } ) ================================================ FILE: src/components/HoverBox/HoverBox.scss ================================================ @import '@/components/FloatBox/FloatBox.scss'; .hoverBox-Container { display: inline-block; position: relative; } .hoverBox-FloatBox { position: absolute; z-index: $global-zindex-dictpanel; } .csst-hoverBox { @include isAnimate(-enter) { opacity: 0; transition: opacity 0.4s; } @include isAnimate(-enter-active, -exit) { opacity: 1; transition: opacity 0.4s; } @include isAnimate(-exit-active) { opacity: 0; transition: opacity 0.4s; } } ================================================ FILE: src/components/HoverBox/index.tsx ================================================ import React, { FC, useContext, useRef, useState } from 'react' import CSSTransition from 'react-transition-group/CSSTransition' import { useObservable, useObservableCallback, useObservableState, identity } from 'observable-hooks' import { merge } from 'rxjs' import { hover, hoverWithDelay, focusBlur, mapToTrue } from '@/_helpers/observables' import { FloatBox, FloatBoxItem } from '../FloatBox' import { createPortal } from 'react-dom' export type HoverBoxItem = FloatBoxItem /** * Accept a optional root element via Context which * will be the parent element of the float boxes. * This is for bypassing z-index restriction, making sure * the float boxes is always on top of other elements. */ export const HoverBoxContext = React.createContext< React.RefObject >({ current: null }) export interface HoverBoxProps { Button: React.ComponentType> items: HoverBoxItem[] /** Compact float box */ compact?: boolean /** box top offset */ top?: number /** box left offset */ left?: number onSelect?: (key: string, value: string) => void /** return false to prevent showing float box */ onBtnClick?: () => boolean onHeightChanged?: (height: number) => void } /** * A button and a FloatBox that shows when hovering. */ export const HoverBox: FC = props => { const portalRootRef = useContext(HoverBoxContext) const containerRef = useRef(null) const boxRef = useRef(null) const [onHoverBtn, onHoverBtn$] = useObservableCallback< boolean, React.MouseEvent >(hoverWithDelay) const [onBtnClick, onBtnClick$] = useObservableCallback( mapToTrue ) const [onHoverBox, onHoverBox$] = useObservableCallback< boolean, React.MouseEvent >(hover) const [onFocusBlur, focusBlur$] = useObservableCallback(focusBlur) const [showBox, showBox$] = useObservableCallback(identity) const isOnBtn = useObservableState( useObservable(() => merge(onHoverBtn$, onBtnClick$)), false ) const isOnBox = useObservableState( useObservable(() => merge(onHoverBox$, focusBlur$, showBox$)), false ) const isShowBox = isOnBtn || isOnBox const [floatBoxStyle, setFloatBoxStyle] = useState(() => props.left == null ? { top: props.top == null ? 40 : props.top, left: '50%', transform: 'translateX(-50%)' } : { top: props.top == null ? 40 : props.top, left: props.left } ) return (
{ switch (e.key) { case 'ArrowDown': // Show float box or jump focus to the first item e.preventDefault() e.stopPropagation() if (isShowBox) { if (boxRef.current) { const firstBtn = boxRef.current.firstElementChild if (firstBtn) { ;(firstBtn as HTMLButtonElement | HTMLSelectElement).focus() } } } else { showBox(true) } break case 'Tab': // Jump focus to the first item if (!e.shiftKey && isShowBox && boxRef.current) { e.preventDefault() e.stopPropagation() const firstBtn = boxRef.current.firstElementChild if (firstBtn) { ;(firstBtn as HTMLButtonElement | HTMLSelectElement).focus() } } break } }} onMouseOver={onHoverBtn} onMouseOut={onHoverBtn} onClick={() => { if (!props.onBtnClick || props.onBtnClick() !== false) { onBtnClick() } }} /> { if (portalRootRef.current && containerRef.current) { const portalRootRect = portalRootRef.current.getBoundingClientRect() const containerRect = containerRef.current.getBoundingClientRect() setFloatBoxStyle({ top: containerRect.y - portalRootRect.y + (props.top == null ? 40 : props.top), left: containerRect.x - portalRootRect.x + (props.left == null ? -Math.floor(containerRect.width / 2) : props.left) }) } }} onExited={() => props.onHeightChanged && props.onHeightChanged(0)} > {() => { const floatBox = (
(container.lastElementChild as HTMLButtonElement).focus() } onArrowDownLast={container => (container.firstElementChild as HTMLButtonElement).focus() } onSelect={props.onSelect} onHeightChanged={props.onHeightChanged} onClose={() => showBox(false)} />
) return portalRootRef.current && containerRef.current ? createPortal(floatBox, portalRootRef.current) : floatBox }}
) } ================================================ FILE: src/components/MachineTrans/MachineTrans.scss ================================================ .MachineTrans-Text { .saladict-Speaker { position: absolute; left: 0; top: 2px; margin: 0; } summary { cursor: pointer; } } .MachineTrans-Lines { position: relative; margin: 0.5em 0; padding-left: 1.5em; p { margin: 0.3em 0; } } .MachineTrans-Lines-collapse { overflow: hidden; position: relative; &::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; background: linear-gradient(90deg, rgba(255,255,255,0) 50%, var(--color-background) 90%); pointer-events: none; } button { height: 1.5em; padding: 0; text-align: start; word-break: keep-all; white-space: nowrap; font-size: 1em; font-weight: normal; font-family: inherit; color: currentColor; cursor: pointer; } } /*-----------------------------------------------*\ States \*-----------------------------------------------*/ .MachineTrans-lang-ar, // Arabic .MachineTrans-lang-ara, // Arabic .MachineTrans-lang-az, // Azerbaijani .MachineTrans-lang-fa, // Persian .MachineTrans-lang-he, // Hebrew .MachineTrans-lang-iw, // Hebrew .MachineTrans-lang-ku, // Kurdish .MachineTrans-lang-ug, // Uighur .MachineTrans-lang-ur // Urdu { direction: rtl !important; &.MachineTrans-Lines-collapse { &::after { background: linear-gradient(270deg, rgba(255,255,255,0) 50%, var(--color-background) 90%); } } } .MachineTrans-has-rtl { text-align: right; .MachineTrans-Lines { padding-left: 0; padding-right: 1.5em; } .saladict-Speaker { left: auto; right: 0; } } @font-face { font-family: "UKIJ Tuz Basma"; src: url("https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.woff2") format("woff2"), url("https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.woff") format("woff"), url("https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.ttf") format("truetype"), url("https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.svg#UKIJTuzBasma-Bold") format("svg"); font-style: normal; } .MachineTrans-lang-ug { font-family: "UKIJ Tuz Basma" !important; } ================================================ FILE: src/components/MachineTrans/MachineTrans.stories.tsx ================================================ import React from 'react' import { Subject } from 'rxjs' import faker from 'faker' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' import { withKnobs, boolean } from '@storybook/addon-knobs' import { jsxDecorator } from 'storybook-addon-jsx' import { withPropsTable } from 'storybook-addon-react-docgen' import { withSaladictPanel, withi18nNS, withSideEffect, mockRuntimeMessage } from '@/_helpers/storybook' import { DictItemHead } from '@/content/components/DictItem/DictItemHead' import { MachineTrans } from './MachineTrans' import { machineResult } from './engine' storiesOf('Content Scripts|Components', module) .addDecorator(withPropsTable) .addDecorator(jsxDecorator) .addDecorator(withKnobs) .addDecorator( withSideEffect( mockRuntimeMessage(async message => { action(message.type)(message['payload']) }) ) ) .addDecorator( withSaladictPanel({ head: ( ) }) ) .addDecorator(withi18nNS(['content', 'langcode'])) .add('MachineTrans', () => { const rtl = boolean('rtl', true) return ( ) }) .add('MachineTransCatalog', () => { const rtl = boolean('rtl', false) const noop = () => {} const catalogSelect$ = new Subject<{ key: string; value: string }>() const mt = machineResult( { result: { id: 'google', sl: rtl ? 'ara' : 'en', tl: 'zh', slInitial: 'hide', searchText: { paragraphs: [faker.lorem.paragraph()], tts: faker.internet.url() }, trans: { paragraphs: [faker.lorem.paragraph()], tts: faker.internet.url() } } }, ['zh', 'cht', 'en'] ) return ( <> catalogSelect$.next(v)} catalog={mt.catalog} /> ) }) ================================================ FILE: src/components/MachineTrans/MachineTrans.tsx ================================================ import React, { FC, useState, useCallback, useLayoutEffect, useRef } from 'react' import { useSubscription } from 'observable-hooks' import Speaker from '@/components/Speaker' import { ViewPorps } from '@/components/dictionaries/helpers' import { DictID } from '@/app-config' import { message } from '@/_helpers/browser-api' import { MachineTranslateResult } from './engine' import { Trans, useTranslate } from '@/_helpers/i18n' const rtlLangs = new Set([ 'ar', // Arabic 'ara', // Arabic 'az', // Azerbaijani 'fa', // Persian 'he', // Hebrew 'iw', // Hebrew 'ku', // Kurdish 'ug', // Uighur 'ur' // Urdu ]) const TSpeaker = React.memo<{ result: MachineTranslateResult source: 'searchText' | 'trans' }>(({ result, source }) => ( { console.log({ type: 'DICT_ENGINE_METHOD', payload: { id: result.id, method: 'getTTS', args: [ result[source].paragraphs.join(' '), source === 'trans' ? result.tl : result.sl ] } }) return message.send<'DICT_ENGINE_METHOD', string>({ type: 'DICT_ENGINE_METHOD', payload: { id: result.id, method: 'getTTS', args: [ result[source].paragraphs.join(' '), source === 'trans' ? result.tl : result.sl ] } }) } : result[source].tts } /> )) /** text with a speaker at the beginning */ const TText = React.memo<{ result: MachineTranslateResult source: 'searchText' | 'trans' lang: string }>(({ result, source, lang }) => (
{result[source].paragraphs.map((line, i) => (

{line}

))}
)) const TTextCollapsable = React.memo<{ result: MachineTranslateResult source: 'searchText' | 'trans' lang: string }>(({ result, source, lang }) => { const [collapse, setCollapse] = useState(false) const expand = useCallback(() => setCollapse(false), [setCollapse]) const containerRef = useRef(null) useLayoutEffect(() => { if (collapse || !containerRef.current) return // count lines if (containerRef.current.querySelectorAll('p').length > 1) { // multiple paragraphs setCollapse(true) return } const text = containerRef.current.querySelector('p span') if (text && text.getClientRects().length > 1) { // multiple lines setCollapse(true) return } }, []) return (
{collapse ? (
) : ( result[source].paragraphs.map((line, i) => (

{line}

)) )}
) }) export type MachineTransProps = ViewPorps> /** Template for machine translations */ export const MachineTrans: FC = props => { const { tl, sl } = props.result const [slState, setSlState] = useState< MachineTransProps['result']['slInitial'] >(props.result.slInitial) useSubscription(props.catalogSelect$, ({ key, value }) => { switch (key) { case 'showSl': setSlState('full') break case 'sl': case 'tl': props.searchText({ id: props.result.id, payload: { sl, tl, [key]: value } }) break case 'copySrc': message.send({ type: 'SET_CLIPBOARD', payload: props.result.searchText.paragraphs.join('\n') }) break case 'copyTrans': message.send({ type: 'SET_CLIPBOARD', payload: props.result.trans.paragraphs.join('\n') }) break default: break } }) if (props.result.requireCredential) { return renderCredential() } return (
{slState === 'full' ? ( ) : slState === 'collapse' ? ( ) : null}
) } function renderCredential() { const { t } = useTranslate('content') return ( {t('machineTrans.dictAccount')} ) } ================================================ FILE: src/components/MachineTrans/engine.ts ================================================ import { DictID, AppConfig } from '@/app-config' import { Language } from '@opentranslate/languages' import { Translator } from '@opentranslate/translator' import { DictItem, SelectOptions } from '@/app-config/dicts' import { isContainJapanese, isContainKorean } from '@/_helpers/lang-check' import { DictSearchResult } from '../dictionaries/helpers' export interface MachineTranslatePayload { sl?: Lang tl?: Lang } export interface MachineTranslateResult { id: ID slInitial: 'hide' | 'collapse' | 'full' /** Source language */ sl: string /** Target language */ tl: string searchText: { paragraphs: string[] tts?: string } trans: { paragraphs: string[] tts?: string } requireCredential?: boolean } type DefaultMachineOptions = { /** Keep linebreaks */ keepLF: 'none' | 'all' | 'webpage' | 'pdf' /** Source language initial state */ slInitial: 'hide' | 'collapse' | 'full' tl: 'default' | Lang tl2: 'default' | Lang } export type MachineDictItem< Lang extends Language, Options extends { [option: string]: number | boolean | string } = {} > = DictItem> export type ExtractLangFromConfig = Config extends MachineDictItem< infer Lang, infer Options > ? Lang : never export type ExtractOptionsFromConfig = Config extends MachineDictItem< infer Lang, infer Options > ? Omit> : never /** * Get Machine Translate arguments */ export async function getMTArgs( translator: Translator, text: string, { options, options_sel }: { options: { tl: 'default' | Language tl2: 'default' | Language keepLF: 'none' | 'all' | 'webpage' | 'pdf' } options_sel: { tl: ReadonlyArray<'default' | Language> tl2: ReadonlyArray<'default' | Language> } }, config: AppConfig, payload: { sl?: Language tl?: Language isPDF?: boolean } ): Promise<{ sl: Language; tl: Language; text: string }> { if ( options.keepLF === 'none' || (options.keepLF === 'pdf' && !payload.isPDF) || (options.keepLF === 'webpage' && payload.isPDF) ) { text = text.replace(/\n+/g, ' ') } let sl = payload.sl if (!sl) { if (isContainJapanese(text)) { sl = 'ja' } else if (isContainKorean(text)) { sl = 'ko' } } if (!sl) { sl = await translator.detect(text) } let tl: Language | '' = '' if (payload.tl) { tl = payload.tl } else if (options.tl === 'default') { if (options_sel.tl.includes(config.langCode)) { tl = config.langCode } } else { tl = options.tl } if (!tl) { tl = options_sel.tl.find((lang): lang is Language => lang !== 'default') || 'en' } if (sl === tl) { if (!payload.tl) { if (options.tl2 === 'default') { if (tl !== config.langCode) { tl = config.langCode } else if (tl !== 'en') { tl = 'en' } else { tl = options_sel.tl.find( (lang): lang is Language => lang !== 'default' && lang !== tl ) || 'en' } } else { tl = options.tl2 } } else if (!payload.sl) { sl = 'auto' } } return { sl, tl, text } } export function machineConfig>( langs: ExtractLangFromConfig[], /** overwrite configs */ config: Partial, options: ExtractOptionsFromConfig, optionsSel: SelectOptions> ): Config { return { lang: '11111111', selectionLang: { english: true, chinese: true, japanese: true, korean: true, french: true, spanish: true, deutsch: true, others: true, matchAll: false }, defaultUnfold: { english: true, chinese: true, japanese: true, korean: true, french: true, spanish: true, deutsch: true, others: true, matchAll: false }, preferredHeight: 320, selectionWC: { min: 1, max: 999999999999999 }, ...config, options: { keepLF: 'webpage', slInitial: 'collapse', tl: 'default', tl2: 'default', ...(options as any) }, options_sel: { keepLF: ['none', 'all', 'webpage', 'pdf'], slInitial: ['collapse', 'hide', 'full'], tl: ['default', ...langs], tl2: ['default', ...langs], ...optionsSel } } as Config } /** Generate catalog */ export function machineResult( data: DictSearchResult>, langcodes: ReadonlyArray ): DictSearchResult> { const langCodesOptions = [ { value: 'auto', label: '%t(content:machineTrans.auto)' } ] for (const lang of langcodes) { langCodesOptions.push({ value: lang, label: `${lang} %t(langcode:${lang})` }) } const catalog: DictSearchResult>['catalog'] = [ { key: 'sl', value: data.result.sl, title: '%t(content:machineTrans.sl)', options: langCodesOptions }, { key: 'tl', value: data.result.tl, title: '%t(content:machineTrans.tl)', options: langCodesOptions }, { key: 'copySrc', value: 'copySrc', label: '%t(content:machineTrans.copySrc)' }, { key: 'copyTrans', value: 'copyTrans', label: '%t(content:machineTrans.copyTrans)' } ] if (data.result.slInitial === 'hide') { catalog.push({ key: 'showSl', value: '', label: '%t(content:machineTrans.showSl)' }) } return { ...data, catalog } } ================================================ FILE: src/components/ShadowPortal/ShadowPortal.scss ================================================ .shadowPortal-appear, .shadowPortal-enter { opacity: 0; } .shadowPortal-appear-active, .shadowPortal-enter-active { opacity: 1; transition: opacity 0.4s; } .shadowPortal-exit { opacity: 1; } .shadowPortal-exit-active { opacity: 0; transition: opacity 0.1s; } ================================================ FILE: src/components/ShadowPortal/index.tsx ================================================ import React, { useMemo, useEffect, ReactNode } from 'react' import ReactDOM from 'react-dom' import CSSTransition, { CSSTransitionProps } from 'react-transition-group/CSSTransition' import root from 'react-shadow' import { SALADICT_EXTERNAL } from '@/_helpers/saladict' export const defaultTimeout = { enter: 400, exit: 100, appear: 400 } export const defaultClassNames = 'shadowPortal' // prevent styles in shadow dom from inheriting outside rules const styleResetBoundary: React.CSSProperties = { all: 'initial' } export interface ShadowPortalOwnProps { /** Unique id for the injected element */ id: string /** Static content before the children */ head?: ReactNode shadowRootClassName?: string innerRootClassName?: string panelCSS?: string } export type ShadowPortalProps = ShadowPortalOwnProps & CSSTransitionProps /** * Render a shadow DOM on Portal to a removable element with transition. * Insert the element to DOM when the Component mounts. * Remove the element from DOM when the Component unmounts. */ export const ShadowPortal = (props: ShadowPortalProps) => { const { id, head, shadowRootClassName, innerRootClassName, panelCSS, onEnter, onExited, ...restProps } = props const $root = useMemo(() => { let $root = document.getElementById(id) if (!$root) { $root = document.createElement('div') $root.id = id $root.className = `saladict-div ${shadowRootClassName || SALADICT_EXTERNAL}` } return $root }, [shadowRootClassName]) // unmout element when React node unmounts useEffect( () => () => { if ($root.parentNode) { $root.remove() } }, [] ) return ReactDOM.createPortal(
{head} {panelCSS ? : null} { if (!$root.parentNode) { document.documentElement.appendChild($root) } if (onEnter) { return onEnter(...args) } }} onExited={(...args) => { if ($root.parentNode) { $root.remove() } if (onExited) { return onExited(...args) } }} />
, $root ) } export default ShadowPortal ================================================ FILE: src/components/Speaker/Speaker.scss ================================================ $speaker-duration: 1s !default; .saladict-Speaker { display: inline-block; width: 1.1em; height: 1.1em; text-decoration: none; margin: 0 5px; padding: 0; line-height: 1; vertical-align: text-bottom; border: none; background: no-repeat left / cover url('~@/assets/Speaker.svg'); user-select: none; cursor: pointer; &:hover { outline: none; } } .saladict-Speaker.isActive { animation: saladict-Speaker-playing $speaker-duration steps(6) infinite; } @keyframes saladict-Speaker-playing { from { background-position-x: 0; } 70% { background-position-x: 100%; } 100% { background-position-x: 100%; } } ================================================ FILE: src/components/Speaker/Speaker.stories.tsx ================================================ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' import { withKnobs, text, number } from '@storybook/addon-knobs' import { jsxDecorator } from 'storybook-addon-jsx' import { withPropsTable } from 'storybook-addon-react-docgen' import { withSaladictPanel, withSideEffect, mockRuntimeMessage } from '@/_helpers/storybook' import { Speaker, StaticSpeakerContainer, getStaticSpeakerString, getStaticSpeaker } from './index' import { timer } from '@/_helpers/promise-more' import { StrElm } from '../StrElm' storiesOf('Content Scripts|Components', module) .addDecorator(withPropsTable) .addDecorator(jsxDecorator) .addDecorator(withKnobs) .addDecorator( withSideEffect( mockRuntimeMessage(async message => { if (message.type === 'PLAY_AUDIO') { action('Play Audio')(message.payload) await timer(Math.random() * 5000) action('Audio End')(message.payload) } }) ) ) .addDecorator( withSaladictPanel({ head: }) ) .add('Speaker', () => { return ( ) }) .add('StaticSpeakerContainer', () => { const textStr = text( 'Audio URL for getStaticSpeakerString', 'https://example.com/a.mp3' ) const textNode = text( 'Audio URL for getStaticSpeaker', 'https://example.com/b.mp3' ) const node = getStaticSpeaker(textNode) return ( action('On Play Start')(src)} > ${getStaticSpeakerString(textStr)} ${textStr}

${node && node.outerHTML} ${textNode}

`} />
) }) ================================================ FILE: src/components/Speaker/index.tsx ================================================ import React, { FC, ComponentProps, useCallback, useState, useContext } from 'react' import { useUpdateEffect } from 'react-use' import { timer, reflect } from '@/_helpers/promise-more' import { isTagName } from '@/_helpers/dom' /** onPlayStart */ const StaticSpeakerContext = React.createContext< (src: string) => Promise >(async () => {}) export interface SpeakerProps { /** render nothing when no src */ readonly src?: string | (() => Promise) /** @default 1.2em */ readonly width?: number | string /** @default 1.2em */ readonly height?: number | string } /** * Speaker for playing audio files */ export const Speaker: FC = props => { const [src, setSrc] = useState(() => typeof props.src === 'string' ? props.src : '#' ) const onPlayStart = useContext(StaticSpeakerContext) useUpdateEffect(() => { setSrc(typeof props.src === 'string' ? props.src : '#') }, [props.src]) if (!props.src) return null const width = props.width || props.height || '1.2em' const height = props.height || width return ( { if (src === '#' && typeof props.src === 'function') { e.stopPropagation() e.preventDefault() const result = await props.src() onPlayStart(result) setSrc(result) } }} > ) } export default React.memo(Speaker) export interface StaticSpeakerContainerProps extends Omit, 'onClick'> { onPlayStart: (src: string) => Promise } /** * Listens to HTML injected Speakers in childern */ export const StaticSpeakerContainer: FC = props => { const { onPlayStart, ...restProps } = props const onClick = useCallback( (e: React.MouseEvent) => { if ( e.target && isTagName(e.target, 'a') && e.target['href'] && e.target['href'] !== '#' && e.target['classList'] && e.target['classList'].contains('saladict-Speaker') ) { e.preventDefault() e.stopPropagation() const target = e.target as HTMLAnchorElement target.classList.add('isActive') reflect([timer(1000), onPlayStart(target.href)]).then(() => { target.classList.remove('isActive') }) } }, [onPlayStart] ) return (
) } /** * Returns a anchor element */ export const getStaticSpeaker = (src?: string | null) => { if (!src) { return '' } const $a = document.createElement('a') $a.target = '_blank' $a.href = src $a.className = 'saladict-Speaker' return $a } /** * Returns an anchor element string */ export const getStaticSpeakerString = (src?: string | null) => src ? `` : '' ================================================ FILE: src/components/StarRates/index.tsx ================================================ import React from 'react' export interface StarRatesProps { className?: string rate?: number height?: number gutter?: number style?: React.CSSProperties max?: number } export default class StarRates extends React.PureComponent { render() { const className = this.props.className || 'widget-StarRates' const max = this.props.max || 5 const rate = Number(this.props.rate) % (max + 1) || 0 const height = this.props.height || '1.5em' const gutter = this.props.gutter || '0.3em' const style = { height: height, ...(this.props.style || {}) } return (
{Array.from(Array(max)).map((_, i) => ( ))}
) } } ================================================ FILE: src/components/StrElm/index.tsx ================================================ import React, { PropsWithChildren, useMemo, useState } from 'react' import { useIsomorphicLayoutEffect } from 'react-use' export type StrElmProps< T extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements > = { tag?: T html: string } & JSX.IntrinsicElements[T] export const StrElm = < T extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements >( props: PropsWithChildren> ) => { const { tag = 'div', html, ...restProps } = props const child = useMemo(() => { try { const fragment = document.createDocumentFragment() const doc = new DOMParser().parseFromString(html, 'text/html') Array.from(doc.body.childNodes).forEach(el => { fragment.appendChild(el) }) return fragment } catch (e) { if (process.env.DEBUG) { console.error(e) } } return null }, [html]) const [node, setNode] = useState(null) useIsomorphicLayoutEffect(() => { if (child && node) { while (node.childNodes.length > 0) { node.childNodes[0].remove() } node.appendChild(child) } }, [child, node]) return React.createElement(tag, { ...restProps, ref: setNode }) } ================================================ FILE: src/components/Waveform/Waveform.scss ================================================ #waveform-container { min-height: 128px; background: var(--color-background); } .saladict-waveformWrap { overflow: hidden; height: 165px; } .saladict-waveformCtrl { display: flex; justify-content: center; align-items: center; font-size: 16px; } $waveformPlayWidth: 1.3em; $halfWaveformPlayWidth: math.div($waveformPlayWidth, 2); %palyFGBlock { content: ''; position: absolute; top: 0; width: 0; height: 0; border: $halfWaveformPlayWidth solid transparent; border-width: $halfWaveformPlayWidth $waveformPlayWidth; border-left-color: var(--color-font); transition-property: border, height, right; transition-duration: 0.25s; transition-timing-function: ease; } .saladict-waveformPlay { margin: 0.5em; padding: 3px; border: none; background: none; cursor: pointer; &:hover { outline: none; } } .saladict-waveformPlay_FG { position: relative; width: $waveformPlayWidth; height: $waveformPlayWidth; overflow: hidden; &:before { @extend %palyFGBlock; left: 0; } &.isPlaying:before { height: $waveformPlayWidth; border-width: 0 ($halfWaveformPlayWidth - 0.05em); } &:after { @extend %palyFGBlock; right: 0; } &.isPlaying:after { right: -($halfWaveformPlayWidth - 0.05em); height: $waveformPlayWidth; border-width: 0 ($halfWaveformPlayWidth - 0.05em); } } .saladict-waveformSpeed { width: 3em; text-align: center; } .saladict-waveformBtn_label { display: block; width: 1.3em; height: 1.3em; overflow: hidden; margin: 0.5em; cursor: pointer; } .saladict-waveformBtn_label > input { display: inline-block; position: absolute; width: 0; height: 0; opacity: 0; z-index: -20000px; } .saladict-waveformPitch { margin: 0.5em 0; } ================================================ FILE: src/components/Waveform/Waveform.stories.tsx ================================================ import React from 'react' import { storiesOf } from '@storybook/react' import { jsxDecorator } from 'storybook-addon-jsx' import { withPropsTable } from 'storybook-addon-react-docgen' import { withKnobs, boolean } from '@storybook/addon-knobs' import { withSaladictPanel, withSideEffect, mockRuntimeMessage } from '@/_helpers/storybook' import { Waveform } from './Waveform' storiesOf('Content Scripts|Dict Panel', module) .addParameters({ backgrounds: [ { name: 'Saladict', value: '#5caf9e', default: true }, { name: 'Black', value: '#000' } ] }) .addDecorator(withPropsTable) .addDecorator(jsxDecorator) .addDecorator(withKnobs) .addDecorator( withSaladictPanel({ head: , height: 'auto' }) ) .addDecorator( withSideEffect( mockRuntimeMessage(async message => { switch (message.type as string) { case 'PAGE_INFO': return { pageId: 'page-id' } case '[[LAST_PLAY_AUDIO]]': return require('@sb/assets/shewalksinbeauty.mp3') default: break } }) ) ) .add('Waveform', () => { const darkMode = boolean('Dark Mode', true) return }) ================================================ FILE: src/components/Waveform/Waveform.tsx ================================================ import * as React from 'react' import classNames from 'classnames' import WaveSurfer from 'wavesurfer.js' import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.min.js' import NumberEditor from 'react-number-editor' import { message, storage } from '@/_helpers/browser-api' import { isFirefox } from '@/_helpers/saladict' import { SoundTouch, SimpleFilter, getWebAudioNode } from 'soundtouchjs' interface AnyObject { [index: string]: any } interface WaveformProps { darkMode: boolean } interface WaveformState { blob?: Blob isPlaying: boolean speed: number loop: boolean /** use pitch stretcher */ pitchStretch: boolean } export class Waveform extends React.PureComponent< WaveformProps, WaveformState > { containerRef = React.createRef() wavesurfer: WaveSurfer | null | undefined region: AnyObject | null | undefined soundTouch: AnyObject | null | undefined soundTouchNode: AnyObject | null | undefined /** Sync Wavesurfer & SoundTouch position */ shouldSTSync: boolean = false /** play when file is loaded */ playOnLoad = true src?: string state: WaveformState = { isPlaying: false, speed: 1, loop: false, pitchStretch: !isFirefox } initSoundTouch = (wavesurfer: WaveSurfer) => { const buffer = wavesurfer.backend.buffer const bufferLength = buffer.length const lChannel = buffer.getChannelData(0) const rChannel = buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : lChannel let seekingDiff = 0 const source = { extract: (target, numFrames, position) => { if (this.shouldSTSync) { // get the new diff seekingDiff = ~~(wavesurfer.backend.getPlayedPercents() * bufferLength) - position this.shouldSTSync = false } position += seekingDiff for (let i = 0; i < numFrames; i++) { target[i * 2] = lChannel[i + position] target[i * 2 + 1] = rChannel[i + position] } return Math.min(numFrames, bufferLength - position) } } this.soundTouch = new SoundTouch(wavesurfer.backend.ac.sampleRate) this.soundTouchNode = getWebAudioNode( wavesurfer.backend.ac, new SimpleFilter(source, this.soundTouch) ) wavesurfer.backend.setFilter(this.soundTouchNode) } initWavesurfer = () => { const wavesurfer = WaveSurfer.create({ container: this.containerRef.current!, waveColor: '#f9690e', progressColor: '#B71C0C', plugins: [RegionsPlugin.create()] }) this.wavesurfer = wavesurfer wavesurfer.enableDragSelection({}) wavesurfer.on('region-created', region => { this.removeRegion() this.region = region }) wavesurfer.on('region-update-end', this.play) wavesurfer.on('region-out', this.onPlayEnd) wavesurfer.on('seek', () => { if (!this.isInRegion()) { this.removeRegion() } this.shouldSTSync = true }) wavesurfer.on('ready', this.onLoad) wavesurfer.on('finish', this.onPlayEnd) } onLoad = () => { if (this.playOnLoad) { this.play() } // reset state this.playOnLoad = true } play = () => { this.setState({ isPlaying: true }) if (this.wavesurfer) { if ( this.state.pitchStretch && this.soundTouchNode && this.wavesurfer.getFilters().length <= 0 ) { this.wavesurfer.backend.setFilter(this.soundTouchNode) } if (this.region && !this.isInRegion()) { this.wavesurfer.play(this.region.start) } else { this.wavesurfer.play() } } this.shouldSTSync = true } pause = () => { this.setState({ isPlaying: false }) if (this.soundTouchNode) { this.soundTouchNode.disconnect() } if (this.wavesurfer) { this.wavesurfer.pause() this.wavesurfer.backend.disconnectFilters() } } togglePlay = () => { this.state.isPlaying ? this.pause() : this.play() } onPlayEnd = () => { // could be region end this.state.loop ? this.play() : this.pause() } updateSpeed = (speed: number) => { this.setState({ speed }) if (speed < 0.1 || speed > 3) { return } if (this.wavesurfer) { this.wavesurfer.setPlaybackRate(speed) if (speed !== 1 && this.state.pitchStretch && !this.soundTouch) { this.initSoundTouch(this.wavesurfer) } if (this.soundTouch) { this.soundTouch.tempo = speed } } this.shouldSTSync = true } toggleLoop = (e: React.ChangeEvent) => { this.setState({ loop: e.currentTarget.checked }) if (e.currentTarget.checked && !this.state.isPlaying) { this.play() } } togglePitchStretch = (e: React.ChangeEvent) => { this.updatePitchStretch(e.currentTarget.checked) storage.local.set({ waveform_pitch: e.currentTarget.checked }) } updatePitchStretch = (flag: boolean) => { this.setState({ pitchStretch: flag }) if (flag) { if ( this.state.speed !== 1 && this.soundTouchNode && this.wavesurfer && this.wavesurfer.getFilters().length <= 0 ) { this.wavesurfer.backend.setFilter(this.soundTouchNode) this.shouldSTSync = true } } else { if (this.soundTouchNode) { this.soundTouchNode.disconnect() } if (this.wavesurfer) { this.wavesurfer.backend.disconnectFilters() } } } isInRegion = (region = this.region): boolean => { if (region && this.wavesurfer) { const curTime = this.wavesurfer.getCurrentTime() return curTime >= region.start && curTime <= region.end } return false } removeRegion = () => { if (this.region) { this.region.remove() } this.region = null } reset = () => { this.removeRegion() this.updateSpeed(1) if (this.wavesurfer) { this.wavesurfer.pause() this.wavesurfer.empty() this.wavesurfer.backend.disconnectFilters() } if (this.soundTouch) { this.soundTouch.clear() this.soundTouch.tempo = 1 } if (this.soundTouchNode) { this.soundTouchNode.disconnect() } this.soundTouch = null this.soundTouchNode = null this.shouldSTSync = false } load = (src: string) => { if (src) { if (this.wavesurfer) { this.reset() } else { this.initWavesurfer() } if (this.wavesurfer) { this.wavesurfer.load(src) // https://github.com/katspaugh/wavesurfer.js/issues/1657 if ( this.wavesurfer.backend.ac.state === 'suspended' && this.playOnLoad ) { // fallback new Audio(src).play() } } } else { this.reset() } } async componentDidMount() { message.self.addListener('PLAY_AUDIO', async ({ payload: src }) => { this.load(src) }) message.self .send<'LAST_PLAY_AUDIO'>({ type: 'LAST_PLAY_AUDIO' }) .then(response => { if ( response && response.src && response.timestamp - Date.now() < 10000 ) { this.load(response.src) } else { this.playOnLoad = false this.load( // Nothing to play `https://fanyi.sogou.com/reventondc/synthesis?text=Nothing%20to%20play&speed=1&lang=en&from=translateweb` ) } }) storage.local.get('waveform_pitch').then(({ waveform_pitch }) => { if (waveform_pitch != null) { this.updatePitchStretch(Boolean(waveform_pitch)) } }) } componentWillUnmount() { this.reset() if (this.wavesurfer) { this.wavesurfer.destroy() this.wavesurfer = null } } render() { return (
{!isFirefox && ( // @TOFIX SoundTouch bug with Firefox )}
) } } export default Waveform ================================================ FILE: src/components/WordPage/ExportModal/Linebreak.tsx ================================================ import React, { FC } from 'react' import { Select } from 'antd' import { TFunction } from 'i18next' export type LineBreakOption = '' | 'n' | 'p' | 'br' | 'space' export interface LineBreakProps { t: TFunction value: LineBreakOption onChange: (value: LineBreakOption) => void } export const LineBreak: FC = ({ t, value, onChange }) => ( ) export const LineBreakMemo = React.memo(LineBreak) ================================================ FILE: src/components/WordPage/ExportModal/PlaceholderTable.tsx ================================================ import React, { FC } from 'react' import { Table } from 'antd' import { TFunction } from 'i18next' export interface PlaceholderTableProps { t: TFunction } export const PlaceholderTable: FC = ({ t }) => ( ) export const PlaceholderTableMemo = React.memo(PlaceholderTable) ================================================ FILE: src/components/WordPage/ExportModal/index.tsx ================================================ import React, { FC, useState, useEffect, useContext } from 'react' import { Modal, Layout, Switch } from 'antd' import escapeHTML from 'lodash/escape' import { Word, newWord } from '@/_helpers/record-manager' import { useTranslate, I18nContext } from '@/_helpers/i18n' import { storage } from '@/_helpers/browser-api' import { LineBreakMemo, LineBreakOption } from './Linebreak' import { PlaceholderTableMemo } from './PlaceholderTable' const keywordMatchStr = `%(${Object.keys(newWord()).join('|')}|contextCloze)%` export type ExportModalTitle = 'all' | 'selected' | 'page' | '' export interface ExportModalProps { title: ExportModalTitle rawWords: Word[] onCancel: (e: React.MouseEvent) => any } export const ExportModal: FC = props => { const lang = useContext(I18nContext) const { t } = useTranslate(['wordpage', 'common']) const [template, setTemplate] = useState('%text%\n%trans%\n%context%\n') const [lineBreak, setLineBreak] = useState('') const [escape, setEscape] = useState(false) const [output, setOutput] = useState('') useEffect(() => { setOutput( props.rawWords .map(word => template.replace(new RegExp(keywordMatchStr, 'g'), (match, k) => { switch (k) { case 'date': return new Date(word.date).toLocaleDateString(lang) case 'trans': case 'note': case 'context': case 'contextCloze': { let text = word[k === 'contextCloze' ? 'context' : k] || '' if (escape) { text = escapeHTML(text) } switch (lineBreak) { case 'n': text = text.replace(/\n|\r\n/g, '\\n') break case 'br': text = text.replace(/\n|\r\n/g, '
') break case 'p': text = text .split(/\n|\r\n/) .map(line => `

${line}

`) .join('') break case 'space': text = text.replace(/\n|\r\n/g, ' ') break default: break } if (k === 'contextCloze' && word.text) { const matcher = new RegExp( word.text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi' ) text = text.replace( matcher, ''.padStart(word.text.length, '_') ) } return text } default: return word[k] || '' } }) ) .join('\n') ) }, [props.rawWords, lang, template, lineBreak, escape]) useEffect(() => { storage.sync .get<{ wordpageTemplate: string wordpageLineBreak: LineBreakOption }>(['wordpageTemplate', 'wordpageLineBreak']) .then(({ wordpageTemplate, wordpageLineBreak }) => { if (wordpageTemplate != null) { setTemplate(wordpageTemplate) } if (wordpageLineBreak != null) { setLineBreak(wordpageLineBreak) } }) storage.local .get<{ wordpageHTMLEscape: boolean }>('wordpageHTMLEscape') .then(({ wordpageHTMLEscape }) => { if (wordpageHTMLEscape != null) { setEscape(wordpageHTMLEscape) } }) }, []) const exportWords = () => { browser.runtime.getPlatformInfo().then(({ os }) => { const content = os === 'win' ? output.replace(/\r\n|\n/g, '\r\n') : output const file = new Blob([content], { type: 'text/plain;charset=utf-8' }) const a = document.createElement('a') a.style.display = 'none' a.href = URL.createObjectURL(file) a.download = `saladict-words-${Date.now()}.txt` // firefox a.target = '_blank' document.body.appendChild(a) a.click() }) } return (

{t('export.description')} {t('export.explain')}

{ setLineBreak(value) storage.sync.set({ wordpageLineBreak: value }) if (value === 'br' || value === 'p') { setEscape(true) storage.local.set({ wordpageHTMLEscape: true }) } }} /> { setEscape(checked) storage.local.set({ wordpageHTMLEscape: checked }) }} checkedChildren={t('export.htmlescape.text')} unCheckedChildren={t('export.htmlescape.text')} />