Add gtranslate script
This commit is contained in:
parent
b549d619e7
commit
41f0a7e01d
7 changed files with 156 additions and 11 deletions
|
@ -2,6 +2,10 @@ import { browser, Runtime, Tabs } from "webextension-polyfill-ts";
|
||||||
import { GTranslateScraper } from "./gtranslate_scraper";
|
import { GTranslateScraper } from "./gtranslate_scraper";
|
||||||
|
|
||||||
console.log("Background script loaded");
|
console.log("Background script loaded");
|
||||||
|
|
||||||
|
const scraper = new GTranslateScraper();
|
||||||
|
console.log(scraper);
|
||||||
|
|
||||||
const getTab = async (s: Runtime.MessageSender): Promise<Tabs.Tab> => {
|
const getTab = async (s: Runtime.MessageSender): Promise<Tabs.Tab> => {
|
||||||
if (s.tab) return s.tab;
|
if (s.tab) return s.tab;
|
||||||
let tabs = await browser.tabs.query({
|
let tabs = await browser.tabs.query({
|
||||||
|
@ -47,6 +51,8 @@ browser.runtime.onMessage.addListener(
|
||||||
return setEnabledCommand(c.value, s);
|
return setEnabledCommand(c.value, s);
|
||||||
case commands.getEnabled:
|
case commands.getEnabled:
|
||||||
return getEnabledCommand(s);
|
return getEnabledCommand(s);
|
||||||
|
case commands.translate:
|
||||||
|
return scraper.translate(c.toTranslate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -58,5 +64,3 @@ browser.tabs.onUpdated.addListener(async (tabID, changeInfo) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
new GTranslateScraper();
|
|
||||||
|
|
46
src/background/gtranslate_content_script.ts
Normal file
46
src/background/gtranslate_content_script.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { browser } from "webextension-polyfill-ts";
|
||||||
|
import "./gtranslate_types";
|
||||||
|
declare interface Window {
|
||||||
|
hasRun: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
if (window.location.host != "translate.google.com" || window.hasRun) return;
|
||||||
|
window.hasRun = true;
|
||||||
|
|
||||||
|
const conn = browser.runtime.connect(<any>{
|
||||||
|
name: "gtranslate_scraper_conn",
|
||||||
|
});
|
||||||
|
|
||||||
|
const src = <HTMLTextAreaElement>document.querySelector("textarea#source");
|
||||||
|
|
||||||
|
const result = <HTMLDivElement>(
|
||||||
|
document.querySelector("div.results-container")
|
||||||
|
);
|
||||||
|
|
||||||
|
const isTranslating = () => result.classList.contains("translating");
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
console.log(mutations);
|
||||||
|
const wasTranslating = mutations.some((m) =>
|
||||||
|
m.oldValue.split(" ").some((c) => c == "translating")
|
||||||
|
);
|
||||||
|
if (wasTranslating && !isTranslating()) {
|
||||||
|
sendTranslation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(result, {
|
||||||
|
attributes: true,
|
||||||
|
attributeOldValue: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendTranslation = () => {
|
||||||
|
conn.postMessage({ src: src.value, result: result.innerText.trim() });
|
||||||
|
};
|
||||||
|
|
||||||
|
conn.onMessage.addListener((to_translate: string) => {
|
||||||
|
if (src.value == to_translate && !isTranslating()) sendTranslation();
|
||||||
|
else src.value = to_translate;
|
||||||
|
});
|
||||||
|
})();
|
77
src/background/gtranslate_scraper.ts
Normal file
77
src/background/gtranslate_scraper.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import "./gtranslate_types";
|
||||||
|
import { browser, WebRequest, Runtime } from "webextension-polyfill-ts";
|
||||||
|
|
||||||
|
export class GTranslateScraper {
|
||||||
|
iframe: HTMLIFrameElement;
|
||||||
|
conn?: Runtime.Port;
|
||||||
|
resolveFn?: (value: TranslationResponse) => void;
|
||||||
|
rejectFn?: (reason: String) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
browser.runtime.onConnect.addListener((p) => {
|
||||||
|
if (p.name != "gtranslate_scraper_conn") return;
|
||||||
|
this.conn = p;
|
||||||
|
p.onMessage.addListener((m) => this.onTranslationRecieved(m));
|
||||||
|
});
|
||||||
|
|
||||||
|
//This allows us to create the google translate iframe by removing the x-frame-options header
|
||||||
|
browser.webRequest.onHeadersReceived.addListener(
|
||||||
|
allowIFrameAccess,
|
||||||
|
|
||||||
|
{
|
||||||
|
urls: ["<all_urls>"],
|
||||||
|
types: ["sub_frame"],
|
||||||
|
},
|
||||||
|
["blocking", "responseHeaders"]
|
||||||
|
);
|
||||||
|
let iframe = <HTMLIFrameElement>document.createElement("iframe");
|
||||||
|
|
||||||
|
//TODO make the language customizable
|
||||||
|
iframe.src = "https://translate.google.com/?op=translate&sl=de&tl=en";
|
||||||
|
this.iframe = iframe;
|
||||||
|
|
||||||
|
//Registers a temp content script, because we cannot inject scripts into iframes created on the background html page, because they have no tabId
|
||||||
|
const js = {
|
||||||
|
file: "gtranslate_scraper.bundle.js",
|
||||||
|
};
|
||||||
|
browser.contentScripts
|
||||||
|
.register({
|
||||||
|
matches: ["<all_urls>"],
|
||||||
|
allFrames: true,
|
||||||
|
js: [js],
|
||||||
|
runAt: "document_end",
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
iframe.addEventListener("load", r.unregister);
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onTranslationRecieved(message: TranslationResponse) {
|
||||||
|
this.resolveFn(message);
|
||||||
|
this.resolveFn = null;
|
||||||
|
this.rejectFn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
translate(to_translate: string): Promise<TranslationResponse> {
|
||||||
|
if (this.rejectFn) this.rejectFn("Got a different translation request");
|
||||||
|
if (!this.conn) return Promise.reject("The translator is not yet ready");
|
||||||
|
|
||||||
|
this.conn.postMessage(to_translate);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.resolveFn = resolve;
|
||||||
|
this.rejectFn = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowIFrameAccess = (info: WebRequest.OnHeadersReceivedDetailsType) => {
|
||||||
|
//Make sure that only the background page can access this iframe
|
||||||
|
if (info.documentUrl != document.URL) return;
|
||||||
|
|
||||||
|
let headers = info.responseHeaders;
|
||||||
|
headers = headers.filter((header) => {
|
||||||
|
let h = header.name.toString().toLowerCase();
|
||||||
|
h != "x-frame-options" && h != "frame-options";
|
||||||
|
});
|
||||||
|
return { responseHeaders: headers };
|
||||||
|
};
|
5
src/background/gtranslate_types.ts
Normal file
5
src/background/gtranslate_types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
type TranslationRequest = string;
|
||||||
|
interface TranslationResponse {
|
||||||
|
src: string;
|
||||||
|
result: string;
|
||||||
|
}
|
17
src/types.d.ts
vendored
17
src/types.d.ts
vendored
|
@ -1,11 +1,12 @@
|
||||||
declare module "*?file" {
|
declare module "*?raw" {
|
||||||
const file: string;
|
const contents: string;
|
||||||
export = file;
|
export = contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const enum commands {
|
declare const enum commands {
|
||||||
setEnabled = "toggleCommand",
|
setEnabled = "setEnabled",
|
||||||
getEnabled = "getEnabled",
|
getEnabled = "getEnabled",
|
||||||
|
translate = "translate",
|
||||||
}
|
}
|
||||||
interface setEnabled {
|
interface setEnabled {
|
||||||
commandKind: commands.setEnabled;
|
commandKind: commands.setEnabled;
|
||||||
|
@ -15,4 +16,10 @@ interface setEnabled {
|
||||||
interface getEnabled {
|
interface getEnabled {
|
||||||
commandKind: commands.getEnabled;
|
commandKind: commands.getEnabled;
|
||||||
}
|
}
|
||||||
type command = setEnabled | getEnabled;
|
|
||||||
|
interface translate {
|
||||||
|
commandKind: commands.translate;
|
||||||
|
toTranslate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type command = setEnabled | getEnabled | translate;
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"jsx": "react",
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
|
|
|
@ -16,6 +16,12 @@ let options = {
|
||||||
"content_script",
|
"content_script",
|
||||||
"content_script.ts"
|
"content_script.ts"
|
||||||
),
|
),
|
||||||
|
gtranslate_scraper: path.join(
|
||||||
|
__dirname,
|
||||||
|
"src",
|
||||||
|
"background",
|
||||||
|
"gtranslate_content_script.ts"
|
||||||
|
),
|
||||||
background: path.join(__dirname, "src", "background", "background.ts"),
|
background: path.join(__dirname, "src", "background", "background.ts"),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
@ -39,7 +45,8 @@ let options = {
|
||||||
oneOf: [
|
oneOf: [
|
||||||
{
|
{
|
||||||
resourceQuery: /file/,
|
resourceQuery: /file/,
|
||||||
use: ["ts-loader", "file-loader"],
|
use: ["file-loader", "ts-loader"],
|
||||||
|
type: "javascript/esm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
use: "ts-loader",
|
use: "ts-loader",
|
||||||
|
@ -51,8 +58,8 @@ let options = {
|
||||||
test: /\.(c|s[ac])ss$/i,
|
test: /\.(c|s[ac])ss$/i,
|
||||||
oneOf: [
|
oneOf: [
|
||||||
{
|
{
|
||||||
resourceQuery: /file/,
|
resourceQuery: /raw/,
|
||||||
use: ["file-loader", "css-loader", "sass-loader"],
|
use: ["raw-loader", "sass-loader"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
use: ["style-loader", "css-loader", "sass-loader"],
|
use: ["style-loader", "css-loader", "sass-loader"],
|
||||||
|
|
Loading…
Reference in a new issue