diff --git a/package-lock.json b/package-lock.json index 9232671..c28ebaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1137,6 +1137,12 @@ "uri-js": "^4.2.1" } }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, "ajv-keywords": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", @@ -5367,6 +5373,12 @@ "brorand": "^1.0.1" } }, + "mime": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", + "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==", + "dev": true + }, "mime-db": { "version": "1.36.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", @@ -9705,6 +9717,30 @@ } } }, + "url-loader": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", + "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "mime": "^2.0.3", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index 04a1889..b3cf711 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "sass-loader": "^7.0.3", "style-loader": "^0.21.0", "uglifyjs-webpack-plugin": "^1.2.5", + "url-loader": "^1.1.2", "webextension-polyfill": "^0.3.1", "webpack": "^4.10.2", "webpack-cli": "^3.0.1", diff --git a/src/common/translate.js b/src/common/translate.js index 8734e91..6025ee0 100644 --- a/src/common/translate.js +++ b/src/common/translate.js @@ -1,86 +1,75 @@ -/* Copyright (c) 2017-2018 Sienori All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +let translationHistory = []; -class Translate { - constructor() { - this.history = []; - } +const getHistory = (sourceWord, sourceLang, targetLang) => { + const history = translationHistory.find( + history => + history.sourceWord == sourceWord && + history.sourceLang == sourceLang && + history.targetLang == targetLang && + history.result.statusText == "OK" + ); + return history; +}; - getHistory(sourceWord, sourceLang, targetLang) { - const history = this.history.find( - history => - history.sourceWord == sourceWord && - history.sourceLang == sourceLang && - history.targetLang == targetLang && - history.result.statusText == "OK" - ); - return history; - } +const setHistory = (sourceWord, sourceLang, targetLang, formattedResult) => { + translationHistory.push({ + sourceWord: sourceWord, + sourceLang: sourceLang, + targetLang: targetLang, + result: formattedResult + }); +}; - setHistory(sourceWord, sourceLang, targetLang, formattedResult) { - this.history.push({ - sourceWord: sourceWord, - sourceLang: sourceLang, - targetLang: targetLang, - result: formattedResult - }); - } +const sendRequest = (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 + )}`; + const xhr = new XMLHttpRequest(); + xhr.responseType = "json"; + xhr.open("GET", url); + xhr.send(); - async translate(sourceWord, sourceLang = "auto", targetLang) { - sourceWord = sourceWord.trim(); - - const history = this.getHistory(sourceWord, sourceLang, targetLang); - if (history) return history.result; - - const result = await this.sendRequest(sourceWord, sourceLang, targetLang); - const formattedResult = this.formatResult(result); - this.setHistory(sourceWord, sourceLang, targetLang, formattedResult); - - return formattedResult; - } - - sendRequest(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 - )}`; - const xhr = new XMLHttpRequest(); - xhr.responseType = "json"; - xhr.open("GET", url); - xhr.send(); - - return new Promise((resolve, reject) => { - xhr.onload = () => { - resolve(xhr); - }; - xhr.onerror = () => { - resolve(xhr); - }; - }); - } - - formatResult(result) { - const resultData = { - resultText: "", - candidateText: "", - sourceLanguage: "", - percentage: 0, - statusText: "" + return new Promise((resolve, reject) => { + xhr.onload = () => { + resolve(xhr); }; + xhr.onerror = () => { + resolve(xhr); + }; + }); +}; - resultData.statusText = result.statusText; - if (resultData.statusText !== "OK") return resultData; +const formatResult = result => { + const resultData = { + resultText: "", + candidateText: "", + sourceLanguage: "", + percentage: 0, + statusText: "" + }; - resultData.sourceLanguage = result.response.src; - resultData.percentage = result.response.confidence; - resultData.resultText = result.response.sentences.map(sentence => sentence.trans).join(""); - if (result.response.dict) { - resultData.candidateText = result.response.dict - .map(dict => `${dict.pos}${dict.pos != "" ? ": " : ""}${dict.terms.join(", ")}\n`) - .join(""); - } + resultData.statusText = result.statusText; + if (resultData.statusText !== "OK") return resultData; - return resultData; + resultData.sourceLanguage = result.response.src; + resultData.percentage = result.response.confidence; + resultData.resultText = result.response.sentences.map(sentence => sentence.trans).join(""); + if (result.response.dict) { + resultData.candidateText = result.response.dict + .map(dict => `${dict.pos}${dict.pos != "" ? ": " : ""}${dict.terms.join(", ")}\n`) + .join(""); } -} + + return resultData; +}; + +export default async (sourceWord, sourceLang = "auto", targetLang) => { + sourceWord = sourceWord.trim(); + const history = getHistory(sourceWord, sourceLang, targetLang); + if (history) return history.result; + + const result = await sendRequest(sourceWord, sourceLang, targetLang); + const formattedResult = formatResult(result); + setHistory(sourceWord, sourceLang, targetLang, formattedResult); + return formattedResult; +}; diff --git a/src/content/components/TranslateButton.js b/src/content/components/TranslateButton.js new file mode 100644 index 0000000..d8081ac --- /dev/null +++ b/src/content/components/TranslateButton.js @@ -0,0 +1,37 @@ +import React from "react"; +import { getSettings } from "src/settings/settings"; +import "../styles/TranslateButton.scss"; + +const calcPosition = () => { + const buttonSize = parseInt(getSettings("buttonSize")); + const offset = 10; + switch (getSettings("buttonPosition")) { + case "rightUp": + return { top: -buttonSize - offset, left: offset }; + case "rightDown": + return { top: offset, left: offset }; + case "leftUp": + return { top: -buttonSize - offset, left: -buttonSize - offset }; + case "leftDown": + return { top: offset, left: -buttonSize - offset }; + } +}; + +export default props => { + const { position, shouldShow } = props; + const buttonSize = parseInt(getSettings("buttonSize")); + const { top, left } = calcPosition(); + const buttonStyle = { + height: buttonSize, + width: buttonSize, + top: top + position.y, + left: left + position.x + }; + return ( + + ); +}; diff --git a/src/content/components/TranslateContainer.js b/src/content/components/TranslateContainer.js new file mode 100644 index 0000000..2cd5320 --- /dev/null +++ b/src/content/components/TranslateContainer.js @@ -0,0 +1,181 @@ +import React, { Component } from "react"; +import browser from "webextension-polyfill"; +import translate from "src/common/translate"; +import { initSettings, getSettings, handleSettingsChange } from "src/settings/settings"; +import TranslateButton from "./TranslateButton"; +import TranslatePanel from "./TranslatePanel"; +import "../styles/TranslateContainer.scss"; + +const getSelectedText = () => { + const element = document.activeElement; + const isInTextField = element.tagName === "INPUT" || element.tagName === "TEXTAREA"; + const selectedText = isInTextField + ? element.value.substring(element.selectionStart, element.selectionEnd) + : window.getSelection().toString(); + return selectedText; +}; + +const getSelectedPosition = () => { + const element = document.activeElement; + const isInTextField = element.tagName === "INPUT" || element.tagName === "TEXTAREA"; + const selectedRect = isInTextField + ? element.getBoundingClientRect() + : window + .getSelection() + .getRangeAt(0) + .getBoundingClientRect(); + const selectedPosition = { + x: selectedRect.left + selectedRect.width / 2, + y: selectedRect.bottom + }; + return selectedPosition; +}; + +const translateText = async text => { + const targetLang = getSettings("targetLang"); + const result = await translate(text, "auto", targetLang); + return result; +}; + +const matchesTargetLang = async selectedText => { + const targetLang = getSettings("targetLang"); + //detectLanguageで判定 + const langInfo = await browser.i18n.detectLanguage(selectedText); + const matchsLangsByDetect = langInfo.isReliable && langInfo.languages[0].language === targetLang; + if (matchsLangsByDetect) return true; + + //先頭100字を翻訳にかけて判定 + const partSelectedText = selectedText.substring(0, 100); + const result = await translateText(partSelectedText); + const matchsLangs = targetLang === result.sourceLanguage && result.percentage > 0; + return matchsLangs; +}; + +export default class TranslateContainer extends Component { + constructor(props) { + super(props); + this.state = { + isInit: false, + shouldShowButton: false, + buttonPosition: { x: 0, y: 0 }, + shouldShowPanel: false, + panelPosition: { x: 0, y: 0 }, + resultText: "", + candidateText: "" + }; + this.selectedText = ""; + this.init(); + } + + init = async () => { + await initSettings(); + this.setState({ isInit: true }); + document.addEventListener("mouseup", e => setTimeout(() => this.handleMouseUp(e), 0)); + document.addEventListener("keydown", this.handleKeyDown); + browser.storage.onChanged.addListener(handleSettingsChange); + browser.runtime.onMessage.addListener(this.handleMessage); + }; + + handleMessage = async request => { + switch (request.message) { + case "getTabInfo": + const tabInfo = { url: location.href, selectedText: this.selectedText }; + return tabInfo; + case "translateSelectedText": + this.selectedText = getSelectedText(); + const position = getSelectedPosition(); + if (this.selectedText.length === 0) return; + this.hideButton(); + this.showPanel(position); + break; + } + }; + + handleKeyDown = e => { + if (e.key === "Escape") { + this.hideButton(); + this.hidePanel(); + } + }; + + showButton = position => { + this.setState({ shouldShowButton: true, buttonPosition: position }); + }; + + hideButton = () => { + this.setState({ shouldShowButton: false }); + }; + + handleButtonClick = e => { + const position = { x: e.clientX, y: e.clientY }; + this.showPanel(position); + this.hideButton(); + }; + + showPanel = async position => { + const result = await translateText(this.selectedText); + this.setState({ + shouldShowPanel: true, + panelPosition: position, + resultText: result.resultText, + candidateText: getSettings("ifShowCandidate") ? result.candidateText : "" + }); + }; + + hidePanel = () => { + this.setState({ shouldShowPanel: false }); + }; + + handleMouseUp = e => { + const isLeftClick = e.button === 0; + const isInPasswordField = e.target.tagName === "INPUT" && e.target.type === "password"; + const isInThisElement = document.querySelector("#simple-translate").contains(e.target); + if (!isLeftClick) return; + if (isInPasswordField) return; + if (isInThisElement) return; + this.hideButton(); + this.hidePanel(); + + this.selectedText = getSelectedText(); + const position = { x: e.clientX, y: e.clientY }; + + if (this.selectedText.length === 0) return; + this.handleTextSelect(position); + }; + + handleTextSelect = async position => { + const onSelectBehavior = getSettings("whenSelectText"); + if (onSelectBehavior === "dontShowButton") return; + + if (getSettings("ifCheckLang")) { + const matchesLang = await matchesTargetLang(this.selectedText); + if (matchesLang) return; + } + + if (onSelectBehavior === "showButton") { + this.showButton(position); + } else if (onSelectBehavior === "showPanel") { + this.showPanel(position); + } + }; + + render = () => { + if (!this.state.isInit) return null; + return ( +
+ {this.props.resultText} +
++ {this.props.candidateText} +
+