Support DeepL API

This commit is contained in:
sienori 2022-03-03 20:46:48 +09:00
parent a262963906
commit cfcb01c93d
8 changed files with 217 additions and 36 deletions

View file

@ -50,6 +50,42 @@
"generalLabel": { "generalLabel": {
"message": "General" "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": { "targetLangCaptionLabel": {
"message": "Select the default target language." "message": "Select the default target language."
}, },
@ -383,6 +419,9 @@
"unavailableError": { "unavailableError": {
"message": "Error: Service usage limit reached. Please wait a while and try again." "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": { "unknownError": {
"message": "Error: Unknown error" "message": "Error: Unknown error"
}, },
@ -724,5 +763,20 @@
}, },
"lang_zu": { "lang_zu": {
"message": "Zulu" "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"
} }
} }

View file

@ -2,9 +2,11 @@ import browser from "webextension-polyfill";
const alphabeticallySort = (a, b) => a.name.localeCompare(b.name); 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 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 () => { export default (translationApi) => {
const langOptions = langListGoogle.map(lang => ({ const langList = translationApi === "google" ? langListGoogle : langListDeepl;
const langOptions = langList.map(lang => ({
value: lang, value: lang,
name: browser.i18n.getMessage("lang_" + lang) name: browser.i18n.getMessage("lang_" + lang)
})); }));

View file

@ -1,30 +1,34 @@
import log from "loglevel"; import log from "loglevel";
import axios from "axios"; import axios from "axios";
import { getSettings } from "src/settings/settings";
let translationHistory = []; let translationHistory = [];
const logDir = "common/translate"; const logDir = "common/translate";
const getHistory = (sourceWord, sourceLang, targetLang) => { const getHistory = (sourceWord, sourceLang, targetLang, translationApi) => {
const history = translationHistory.find( const history = translationHistory.find(
history => history =>
history.sourceWord == sourceWord && history.sourceWord == sourceWord &&
history.sourceLang == sourceLang && history.sourceLang == sourceLang &&
history.targetLang == targetLang && history.targetLang == targetLang &&
history.result.statusText == "OK" history.translationApi == translationApi &&
!history.result.isError
); );
return history; return history;
}; };
const setHistory = (sourceWord, sourceLang, targetLang, formattedResult) => { const setHistory = (sourceWord, sourceLang, targetLang, translationApi, result) => {
translationHistory.push({ translationHistory.push({
sourceWord: sourceWord, sourceWord: sourceWord,
sourceLang: sourceLang, sourceLang: sourceLang,
targetLang: targetLang, 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( const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dt=bd&dj=1&q=${encodeURIComponent(
word word
)}`; )}`;
@ -64,24 +68,47 @@ const sendRequest = async (word, sourceLang, targetLang) => {
}; };
const sendRequestToDeepL = async (word, sourceLang, targetLang) => { const sendRequestToDeepL = async (word, sourceLang, targetLang) => {
log.log(logDir, "sendRequestToDeepL()");
let params = new URLSearchParams(); let params = new URLSearchParams();
const authKey = getSettings("deeplAuthKey");
const key = "f5a2c02c-7871-af5c-0d6a-244a9e6d4a1f:fx"; params.append("auth_key", authKey);
params.append("auth_key", key);
params.append("text", word); 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); if (!result || result?.status !== 200) {
console.log("!!!!!!!!!!!!!!!", res); 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) => { export default async (sourceWord, sourceLang = "auto", targetLang, translationApi) => {
log.log(logDir, "tranlate()", sourceWord, targetLang); log.log(logDir, "tranlate()", sourceWord, targetLang, translationApi);
sourceWord = sourceWord.trim(); sourceWord = sourceWord.trim();
if (sourceWord === "") if (sourceWord === "")
return { return {
@ -95,7 +122,9 @@ export default async (sourceWord, sourceLang = "auto", targetLang) => {
const history = getHistory(sourceWord, sourceLang, targetLang); const history = getHistory(sourceWord, sourceLang, targetLang);
if (history) return history.result; if (history) return history.result;
const result = await sendRequest(sourceWord, sourceLang, targetLang); const result = getSettings("translationApi") === "google" ?
setHistory(sourceWord, sourceLang, targetLang, result); await sendRequestToGoogle(sourceWord, sourceLang, targetLang) :
await sendRequestToDeepL(sourceWord, sourceLang, targetLang);
setHistory(sourceWord, sourceLang, targetLang, translationApi, result);
return result; return result;
}; };

View file

@ -26,7 +26,7 @@ const matchesTargetLang = async selectedText => {
const isNotText = result.percentage === 0; const isNotText = result.percentage === 0;
if (isNotText) return true; 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; return matchsLangs;
}; };
@ -90,7 +90,7 @@ export default class TranslateContainer extends Component {
const secondLang = getSettings("secondTargetLang"); const secondLang = getSettings("secondTargetLang");
const shouldSwitchSecondLang = const shouldSwitchSecondLang =
getSettings("ifChangeSecondLangOnPage") && 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); if (shouldSwitchSecondLang) result = await translateText(this.selectedText, secondLang);
this.setState({ this.setState({

View file

@ -18,6 +18,8 @@ export default props => {
} }
setSettings(id, value); setSettings(id, value);
if (props.handleChange) props.handleChange();
}; };
const handleCheckedChange = e => { const handleCheckedChange = e => {
@ -113,8 +115,8 @@ export default props => {
formId = id; formId = id;
optionForm = ( optionForm = (
<div className="selectWrap"> <div className="selectWrap">
<select id={formId} onChange={handleValueChange} defaultValue={currentValue}> <select id={formId} onChange={handleValueChange} value={currentValue}>
{props.options.map((option, index) => ( {(typeof props.options === 'function' ? props.options() : props.options).map((option, index) => (
<option value={option.value} key={index}> <option value={option.value} key={index}>
{props.useRawOptionName ? option.name : browser.i18n.getMessage(option.name)} {props.useRawOptionName ? option.name : browser.i18n.getMessage(option.name)}
</option> </option>

View file

@ -1,13 +1,11 @@
import React, { Component } from "react"; import React, { Component } from "react";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import generateLangOptions from "src/common/generateLangOptions";
import openUrl from "src/common/openUrl"; import openUrl from "src/common/openUrl";
import "../styles/Footer.scss"; import "../styles/Footer.scss";
export default class Footer extends Component { export default class Footer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.langList = generateLangOptions();
} }
handleLinkClick = async () => { handleLinkClick = async () => {
@ -23,7 +21,7 @@ export default class Footer extends Component {
}; };
render() { render() {
const { tabUrl, targetLang, langHistory } = this.props; const { tabUrl, targetLang, langHistory, langList } = this.props;
return ( return (
<div id="footer"> <div id="footer">
@ -39,7 +37,7 @@ export default class Footer extends Component {
> >
<optgroup label={browser.i18n.getMessage("recentLangLabel")}> <optgroup label={browser.i18n.getMessage("recentLangLabel")}>
{this.langList.filter(option => langHistory.includes(option.value)) {langList.filter(option => langHistory.includes(option.value))
.map(option => ( .map(option => (
<option value={option.value} key={option.value}> <option value={option.value} key={option.value}>
{option.name} {option.name}
@ -47,7 +45,7 @@ export default class Footer extends Component {
))} ))}
</optgroup> </optgroup>
<optgroup label={browser.i18n.getMessage("allLangLabel")}> <optgroup label={browser.i18n.getMessage("allLangLabel")}>
{this.langList.map(option => ( {langList.map(option => (
<option value={option.value} key={option.value}> <option value={option.value} key={option.value}>
{option.name} {option.name}
</option> </option>

View file

@ -4,6 +4,7 @@ import log from "loglevel";
import { initSettings, getSettings, setSettings } from "src/settings/settings"; import { initSettings, getSettings, setSettings } from "src/settings/settings";
import { updateLogLevel, overWriteLogLevel } from "src/common/log"; import { updateLogLevel, overWriteLogLevel } from "src/common/log";
import translate from "src/common/translate"; import translate from "src/common/translate";
import generateLangOptions from "src/common/generateLangOptions";
import Header from "./Header"; import Header from "./Header";
import InputArea from "./InputArea"; import InputArea from "./InputArea";
import ResultArea from "./ResultArea"; import ResultArea from "./ResultArea";
@ -42,6 +43,7 @@ export default class PopupPage extends Component {
sourceLang: "", sourceLang: "",
isError: false, isError: false,
errorMessage: "", errorMessage: "",
langList: [],
tabUrl: "", tabUrl: "",
isConnected: true, isConnected: true,
isEnabledOnPage: true, isEnabledOnPage: true,
@ -66,7 +68,8 @@ export default class PopupPage extends Component {
} }
this.setState({ this.setState({
targetLang: targetLang, targetLang: targetLang,
langHistory: langHistory langHistory: langHistory,
langList: generateLangOptions(getSettings("translationApi"))
}); });
const tabInfo = await getTabInfo(); const tabInfo = await getTabInfo();
@ -128,9 +131,10 @@ export default class PopupPage extends Component {
if (defaultTargetLang === secondLang) return; if (defaultTargetLang === secondLang) return;
const equalsSourceAndTarget = const equalsSourceAndTarget =
result.sourceLanguage === this.state.targetLang && result.percentage > 0; result.sourceLanguage.split("-")[0] === this.state.targetLang.split("-")[0] && result.percentage > 0;
const equalsSourceAndDefault = const equalsSourceAndDefault =
result.sourceLanguage === defaultTargetLang && result.percentage > 0; result.sourceLanguage.split("-")[0] === defaultTargetLang.split("-")[0] && result.percentage > 0;
// split("-")[0] : deepLでenとen-USを区別しないために必要
if (!this.isSwitchedSecondLang) { if (!this.isSwitchedSecondLang) {
if (equalsSourceAndTarget && equalsSourceAndDefault) { if (equalsSourceAndTarget && equalsSourceAndDefault) {
@ -184,6 +188,7 @@ export default class PopupPage extends Component {
targetLang={this.state.targetLang} targetLang={this.state.targetLang}
langHistory={this.state.langHistory} langHistory={this.state.langHistory}
handleLangChange={this.handleLangChange} handleLangChange={this.handleLangChange}
langList={this.state.langList}
/> />
</div> </div>
); );

View file

@ -1,5 +1,7 @@
import React from "react";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import generateLangOptions from "src/common/generateLangOptions"; import generateLangOptions from "src/common/generateLangOptions";
import { getSettings, setSettings } from "./settings";
const getDefaultLangs = () => { const getDefaultLangs = () => {
const uiLang = browser.i18n.getUILanguage(); const uiLang = browser.i18n.getUILanguage();
@ -12,7 +14,31 @@ const getDefaultLangs = () => {
return { targetLang, secondTargetLang }; 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 defaultLangs = getDefaultLangs();
const getTheme = () => const getTheme = () =>
window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light"; window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light";
@ -21,13 +47,77 @@ export default [
{ {
category: "generalLabel", category: "generalLabel",
elements: [ 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", id: "targetLang",
title: "targetLangLabel", title: "targetLangLabel",
captions: ["targetLangCaptionLabel"], captions: ["targetLangCaptionLabel"],
type: "select", type: "select",
default: defaultLangs.targetLang, default: defaultLangs.targetLang,
options: langListOptions, options: () => generateLangOptions(getSettings("translationApi")),
useRawOptionName: true useRawOptionName: true
}, },
{ {
@ -36,7 +126,7 @@ export default [
captions: ["secondTargetLangCaptionLabel"], captions: ["secondTargetLangCaptionLabel"],
type: "select", type: "select",
default: defaultLangs.secondTargetLang, default: defaultLangs.secondTargetLang,
options: langListOptions, options: () => generateLangOptions(getSettings("translationApi")),
useRawOptionName: true useRawOptionName: true
}, },
{ {
@ -44,7 +134,8 @@ export default [
title: "ifShowCandidateLabel", title: "ifShowCandidateLabel",
captions: ["ifShowCandidateCaptionLabel"], captions: ["ifShowCandidateCaptionLabel"],
type: "checkbox", type: "checkbox",
default: true default: true,
shouldShow: () => (getSettings("translationApi") === "google")
} }
] ]
}, },