From cfcb01c93d68fa140dedd24195d6c2a33e502152 Mon Sep 17 00:00:00 2001 From: sienori Date: Thu, 3 Mar 2022 20:46:48 +0900 Subject: [PATCH] Support DeepL API --- src/_locales/en/messages.json | 54 +++++++++++ src/common/generateLangOptions.js | 6 +- src/common/translate.js | 65 +++++++++---- src/content/components/TranslateContainer.js | 4 +- src/options/components/OptionContainer.js | 6 +- src/popup/components/Footer.js | 8 +- src/popup/components/PopupPage.js | 11 ++- src/settings/defaultSettings.js | 99 +++++++++++++++++++- 8 files changed, 217 insertions(+), 36 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 751cb8b..4ce22b5 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -50,6 +50,42 @@ "generalLabel": { "message": "General" }, + "translationApiLabel": { + "message": "Translation engine" + }, + "googleApiLabel": { + "message": "Google translate API" + }, + "googleApiCaptionLabel": { + "message": "Use Google Translate API. No registration is required." + }, + "deeplApiLabel": { + "message": "DeepL API" + }, + "deeplApiCaptionLabel": { + "message": "Use DeepL API. You must register with DeepL API Free or DeepL API Pro to obtain an authorization key." + }, + "howToUseDeeplLabel": { + "message": "How to register DeepL API" + }, + "deeplPlanLabel": { + "message": "DeepL API plan" + }, + "deeplPlanCaptionLabel": { + "message": "Select the DeepL API plan for which you registered." + }, + "deeplFreeLabel": { + "message": "DeepL API Free" + }, + "deeplProLabel": { + "message": "DeepL API Pro" + }, + "deeplAuthKeyLabel": { + "message": "Authorization key" + }, + "deeplAuthKeyCaptionLabel": { + "message": "Enter the authentication key for the DeepL API." + }, "targetLangCaptionLabel": { "message": "Select the default target language." }, @@ -383,6 +419,9 @@ "unavailableError": { "message": "Error: Service usage limit reached. Please wait a while and try again." }, + "deeplAuthError": { + "message": "Error: Authentication of DeepL API failed. Please set the authentication key and plan correctly on the settings page." + }, "unknownError": { "message": "Error: Unknown error" }, @@ -724,5 +763,20 @@ }, "lang_zu": { "message": "Zulu" + }, + "lang_en-US": { + "message": "English (American)" + }, + "lang_en-GB": { + "message": "English (British)" + }, + "lang_pt-PT": { + "message": "Portuguese" + }, + "lang_pt-BR": { + "message": "Portuguese (Brazilian)" + }, + "lang_zh": { + "message": "Chinese" } } \ No newline at end of file diff --git a/src/common/generateLangOptions.js b/src/common/generateLangOptions.js index eb0ef50..ddfdc22 100644 --- a/src/common/generateLangOptions.js +++ b/src/common/generateLangOptions.js @@ -2,9 +2,11 @@ import browser from "webextension-polyfill"; const alphabeticallySort = (a, b) => a.name.localeCompare(b.name); const langListGoogle = ["af", "sq", "am", "ar", "hy", "az", "eu", "be", "bn", "bs", "bg", "ca", "ceb", "zh-CN", "zh-TW", "co", "hr", "cs", "da", "nl", "en", "eo", "et", "fi", "fr", "fy", "gl", "ka", "de", "el", "gu", "ht", "ha", "haw", "he", "hi", "hmn", "hu", "is", "ig", "id", "ga", "it", "ja", "jv", "kn", "kk", "km", "rw", "ko", "ku", "ky", "lo", "lv", "lt", "lb", "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mn", "my", "ne", "no", "ny", "or", "ps", "fa", "pl", "pt", "pa", "ro", "ru", "sm", "gd", "sr", "st", "sn", "sd", "si", "sk", "sl", "so", "es", "su", "sw", "sv", "tl", "tg", "ta", "tt", "te", "th", "tr", "tk", "uk", "ur", "ug", "uz", "vi", "cy", "xh", "yi", "yo", "zu"]; +const langListDeepl = ["bg", "cs", "da", "de", "el", "en-GB", "en-US", "es", "et", "fi", "fr", "hu", "it", "ja", "lt", "lv", "nl", "pl", "pt-PT", "pt-BR", "ro", "ru", "sk", "sl", "sv", "zh"]; -export default () => { - const langOptions = langListGoogle.map(lang => ({ +export default (translationApi) => { + const langList = translationApi === "google" ? langListGoogle : langListDeepl; + const langOptions = langList.map(lang => ({ value: lang, name: browser.i18n.getMessage("lang_" + lang) })); diff --git a/src/common/translate.js b/src/common/translate.js index 755d7e9..3a3c5b7 100644 --- a/src/common/translate.js +++ b/src/common/translate.js @@ -1,30 +1,34 @@ import log from "loglevel"; import axios from "axios"; +import { getSettings } from "src/settings/settings"; + let translationHistory = []; const logDir = "common/translate"; -const getHistory = (sourceWord, sourceLang, targetLang) => { +const getHistory = (sourceWord, sourceLang, targetLang, translationApi) => { const history = translationHistory.find( history => history.sourceWord == sourceWord && history.sourceLang == sourceLang && history.targetLang == targetLang && - history.result.statusText == "OK" + history.translationApi == translationApi && + !history.result.isError ); return history; }; -const setHistory = (sourceWord, sourceLang, targetLang, formattedResult) => { +const setHistory = (sourceWord, sourceLang, targetLang, translationApi, result) => { translationHistory.push({ sourceWord: sourceWord, sourceLang: sourceLang, targetLang: targetLang, - result: formattedResult + translationApi: translationApi, + result: result }); }; -const sendRequest = async (word, sourceLang, targetLang) => { +const sendRequestToGoogle = async (word, sourceLang, targetLang) => { const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dt=bd&dj=1&q=${encodeURIComponent( word )}`; @@ -64,24 +68,47 @@ const sendRequest = async (word, sourceLang, targetLang) => { }; const sendRequestToDeepL = async (word, sourceLang, targetLang) => { - log.log(logDir, "sendRequestToDeepL()"); - let params = new URLSearchParams(); - - const key = "f5a2c02c-7871-af5c-0d6a-244a9e6d4a1f:fx"; - params.append("auth_key", key); + const authKey = getSettings("deeplAuthKey"); + params.append("auth_key", authKey); params.append("text", word); - params.append("target_lang", "ja"); + params.append("target_lang", targetLang); + const url = getSettings("deeplPlan") === "deeplFree" ? + "https://api-free.deepl.com/v2/translate" : + "https://api.deepl.com/v2/translate"; + const result = await axios.post(url, params).catch(e => e.response); - const url = "https://api-free.deepl.com/v2/translate"; + const resultData = { + resultText: "", + candidateText: "", + sourceLanguage: "", + percentage: 0, + isError: false, + errorMessage: "" + }; - const res = await axios.post(url, params).catch(e => e.response); - console.log("!!!!!!!!!!!!!!!", res); + if (!result || result?.status !== 200) { + resultData.isError = true; + + if (!result || result.status === 0) resultData.errorMessage = browser.i18n.getMessage("networkError"); + else if (result.status === 403) resultData.errorMessage = browser.i18n.getMessage("deeplAuthError"); + else resultData.errorMessage = `${browser.i18n.getMessage("unknownError")} [${result?.status} ${result?.statusText}] ${result?.data.message}`; + + log.error(logDir, "sendRequestToDeepL()", result); + return resultData; + } + + resultData.resultText = result.data.translations[0].text; + resultData.sourceLanguage = result.data.translations[0].detected_source_language.toLowerCase(); + resultData.percentage = 1; + + log.log(logDir, "sendRequestToDeepL()", resultData); + return resultData; }; -export default async (sourceWord, sourceLang = "auto", targetLang) => { - log.log(logDir, "tranlate()", sourceWord, targetLang); +export default async (sourceWord, sourceLang = "auto", targetLang, translationApi) => { + log.log(logDir, "tranlate()", sourceWord, targetLang, translationApi); sourceWord = sourceWord.trim(); if (sourceWord === "") return { @@ -95,7 +122,9 @@ export default async (sourceWord, sourceLang = "auto", targetLang) => { const history = getHistory(sourceWord, sourceLang, targetLang); if (history) return history.result; - const result = await sendRequest(sourceWord, sourceLang, targetLang); - setHistory(sourceWord, sourceLang, targetLang, result); + const result = getSettings("translationApi") === "google" ? + await sendRequestToGoogle(sourceWord, sourceLang, targetLang) : + await sendRequestToDeepL(sourceWord, sourceLang, targetLang); + setHistory(sourceWord, sourceLang, targetLang, translationApi, result); return result; }; diff --git a/src/content/components/TranslateContainer.js b/src/content/components/TranslateContainer.js index f680cab..62af2a5 100644 --- a/src/content/components/TranslateContainer.js +++ b/src/content/components/TranslateContainer.js @@ -26,7 +26,7 @@ const matchesTargetLang = async selectedText => { const isNotText = result.percentage === 0; if (isNotText) return true; - const matchsLangs = targetLang === result.sourceLanguage; + const matchsLangs = targetLang.split("-")[0] === result.sourceLanguage.split("-")[0]; // split("-")[0] : deepLでenとen-USを区別しないために必要 return matchsLangs; }; @@ -90,7 +90,7 @@ export default class TranslateContainer extends Component { const secondLang = getSettings("secondTargetLang"); const shouldSwitchSecondLang = getSettings("ifChangeSecondLangOnPage") && - result.sourceLanguage === targetLang && result.percentage > 0 && targetLang !== secondLang; + result.sourceLanguage.split("-")[0] === targetLang.split("-")[0] && result.percentage > 0 && targetLang !== secondLang; if (shouldSwitchSecondLang) result = await translateText(this.selectedText, secondLang); this.setState({ diff --git a/src/options/components/OptionContainer.js b/src/options/components/OptionContainer.js index 0b52abe..d030628 100644 --- a/src/options/components/OptionContainer.js +++ b/src/options/components/OptionContainer.js @@ -18,6 +18,8 @@ export default props => { } setSettings(id, value); + + if (props.handleChange) props.handleChange(); }; const handleCheckedChange = e => { @@ -113,8 +115,8 @@ export default props => { formId = id; optionForm = (
- + {(typeof props.options === 'function' ? props.options() : props.options).map((option, index) => ( diff --git a/src/popup/components/Footer.js b/src/popup/components/Footer.js index 8481972..1c6d325 100644 --- a/src/popup/components/Footer.js +++ b/src/popup/components/Footer.js @@ -1,13 +1,11 @@ import React, { Component } from "react"; import browser from "webextension-polyfill"; -import generateLangOptions from "src/common/generateLangOptions"; import openUrl from "src/common/openUrl"; import "../styles/Footer.scss"; export default class Footer extends Component { constructor(props) { super(props); - this.langList = generateLangOptions(); } handleLinkClick = async () => { @@ -23,7 +21,7 @@ export default class Footer extends Component { }; render() { - const { tabUrl, targetLang, langHistory } = this.props; + const { tabUrl, targetLang, langHistory, langList } = this.props; return ( ); diff --git a/src/settings/defaultSettings.js b/src/settings/defaultSettings.js index 86f687d..e05fc7f 100644 --- a/src/settings/defaultSettings.js +++ b/src/settings/defaultSettings.js @@ -1,5 +1,7 @@ +import React from "react"; import browser from "webextension-polyfill"; import generateLangOptions from "src/common/generateLangOptions"; +import { getSettings, setSettings } from "./settings"; const getDefaultLangs = () => { const uiLang = browser.i18n.getUILanguage(); @@ -12,7 +14,31 @@ const getDefaultLangs = () => { return { targetLang, secondTargetLang }; }; -const langListOptions = generateLangOptions(); +const updateLangsWhenChangeTranslationApi = () => { + const translationApi = getSettings("translationApi"); + const targetLang = getSettings("targetLang"); + const secondTargetLang = getSettings("secondTargetLang");; + const currentLangs = generateLangOptions(translationApi).map(option => option.value); + + const mappingLang = lang => { + switch (lang) { + case "en": return "en-US"; + case "en-US": + case "en-GB": return "en"; + case "zh": return "zh-CN"; + case "zh-CN": + case "zh-TW": return "zh"; + case "pt": return "pt-PT"; + case "pt-PT": + case "pt-BR": return "pt"; + default: return currentLangs[0]; + } + }; + + if (!currentLangs.includes(targetLang)) setSettings("targetLang", mappingLang(targetLang)); + if (!currentLangs.includes(secondTargetLang)) setSettings("secondTargetLang", mappingLang(secondTargetLang)); +}; + const defaultLangs = getDefaultLangs(); const getTheme = () => window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light"; @@ -21,13 +47,77 @@ export default [ { category: "generalLabel", elements: [ + { + id: "translationApi", + title: "translationApiLabel", + captions: [], + type: "none", + default: "google", + new: true, + childElements: [ + { + id: "translationApi", + title: "googleApiLabel", + captions: ["googleApiCaptionLabel"], + type: "radio", + value: "google", + handleChange: () => updateLangsWhenChangeTranslationApi() + }, + { + id: "translationApi", + title: "deeplApiLabel", + captions: ["deeplApiCaptionLabel"], + extraCaption: + React.createElement("p", + { className: "caption" }, + React.createElement("a", + { + href: "https://github.com/sienori/simple-translate/wiki/How-to-register-DeepL-API", + target: "_blank" + }, + browser.i18n.getMessage("howToUseDeeplLabel")) + ), + type: "radio", + value: "deepl", + handleChange: () => updateLangsWhenChangeTranslationApi() + }, + { + id: "deeplPlan", + title: "deeplPlanLabel", + captions: ["deeplPlanCaptionLabel"], + type: "select", + default: "deeplFree", + shouldShow: () => (getSettings("translationApi") === "deepl"), + hr: true, + options: [ + { + name: "deeplFreeLabel", + value: "deeplFree" + }, + { + name: "deeplProLabel", + value: "deeplPro" + }, + ] + }, + { + id: "deeplAuthKey", + title: "deeplAuthKeyLabel", + captions: ["deeplAuthKeyCaptionLabel"], + type: "text", + default: "", + placeholder: "00000000-0000-0000-0000-00000000000000:fx", + shouldShow: () => (getSettings("translationApi") === "deepl"), + } + ] + }, { id: "targetLang", title: "targetLangLabel", captions: ["targetLangCaptionLabel"], type: "select", default: defaultLangs.targetLang, - options: langListOptions, + options: () => generateLangOptions(getSettings("translationApi")), useRawOptionName: true }, { @@ -36,7 +126,7 @@ export default [ captions: ["secondTargetLangCaptionLabel"], type: "select", default: defaultLangs.secondTargetLang, - options: langListOptions, + options: () => generateLangOptions(getSettings("translationApi")), useRawOptionName: true }, { @@ -44,7 +134,8 @@ export default [ title: "ifShowCandidateLabel", captions: ["ifShowCandidateCaptionLabel"], type: "checkbox", - default: true + default: true, + shouldShow: () => (getSettings("translationApi") === "google") } ] },