// ==UserScript==
// @name MetaTranslator
// @name:fa مترجم متا
// @namespace Violentmonkey Scripts
// @version 0.3
// @author maanimis <[email protected]>
// @source https://github.com/maanimis/MetaTranslator
// @license MIT
// @match *://*/*
// @description Show translated tooltip on text selection
// @description:fa مترجم متن
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addValueChangeListener
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_xmlhttpRequest
// @icon https://www.google.com/s2/favicons?sz=64&domain=translate.google.com
// @run-at document-end
// @inject-into content
// ==/UserScript==
/******/ (() => { // webpackBootstrap
/******/ "use strict";
;// ./src/services/menu/menu.service.ts
class MenuCommandRepository {
commands = new Map();
add(command) {
this.commands.set(command.name, command);
}
remove(name) {
const command = this.commands.get(name);
if (command) {
this.commands.delete(name);
}
return command;
}
get(name) {
return this.commands.get(name);
}
getAll() {
return Array.from(this.commands.values());
}
clear() {
this.commands.clear();
}
has(name) {
return Boolean(this.commands.get(name));
}
}
class MenuCommandService {
_repository;
constructor(_repository) {
this._repository = _repository;
}
register(name, callback) {
this.unregister(name);
const id = GM_registerMenuCommand(name, callback);
const command = { id, name, callback };
this._repository.add(command);
return command;
}
unregister(name) {
const command = this._repository.remove(name);
if (command) {
GM_unregisterMenuCommand(command.id);
}
}
unregisterAll() {
this._repository.getAll().forEach((command) => {
GM_unregisterMenuCommand(command.id);
});
this._repository.clear();
}
}
const createMenuCommandManager = () => {
const repository = new MenuCommandRepository();
return new MenuCommandService(repository);
};
const menuCommandManager = createMenuCommandManager();
const registerMenuCommand = (name, callback) => menuCommandManager.register(name, callback).id;
const unregisterMenuCommand = (name) => menuCommandManager.unregister(name);
;// ./src/services/menu/index.ts
;// ./src/components/tooltip.component.ts
class DOMTooltip {
element;
constructor() {
this.element = document.createElement("div");
this.setupStyles();
document.body.appendChild(this.element);
}
setupStyles() {
Object.assign(this.element.style, {
position: "absolute",
background: "rgba(0, 0, 0, 0.85)",
color: "#fff",
padding: "6px 12px",
borderRadius: "6px",
fontSize: "14px",
pointerEvents: "none",
zIndex: "9999",
maxWidth: "350px",
lineHeight: "1.4",
boxShadow: "0 4px 10px rgba(0,0,0,0.3)",
display: "none",
transition: "opacity 0.2s ease",
whiteSpace: "pre-line",
direction: "rtl",
textAlign: "right",
});
}
show(text, x, y) {
this.element.innerHTML = text;
this.element.style.top = `${y}px`;
this.element.style.left = `${x}px`;
this.element.style.display = "block";
this.element.style.opacity = "1";
}
hide() {
this.element.style.opacity = "0";
this.element.style.display = "none";
}
}
;// ./src/components/translation-formatter.component.ts
class GoogleTranslationFormatter {
format(result) {
let output = `<b>${result.translation}</b>`;
if (result.dictionary && result.dictionary.length > 0) {
output += "\n\n";
result.dictionary.forEach((entry) => {
const posTitle = entry.pos.charAt(0).toUpperCase() + entry.pos.slice(1);
output += `<b>${posTitle}:</b> ${entry.terms.join(", ")}\n`;
});
}
return output;
}
}
;// ./src/utils/sanitize-filename.util.ts
const INVALID_CHARS = /[<>:"/\\|?*]/g;
const RESERVED_NAMES = new Set([
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
]);
function sanitizeWindowsName(name, options = { isFolder: true }) {
if (!name.trim()) {
return null;
}
let sanitized = name
.replace(INVALID_CHARS, "_")
.trim()
.replace(/[. ]+$/, "");
if (RESERVED_NAMES.has(sanitized.toUpperCase())) {
sanitized += "_safe";
}
if (options.appendTimestamp) {
const timestamp = Date.now();
sanitized = options.isFolder
? `${sanitized}_${timestamp}`
: sanitized.replace(/(\.[^.]+)?$/, `_${timestamp}$1`);
}
return sanitized;
}
;// ./src/utils/debouncer.util.ts
class Debouncer {
timer = null;
debounce(callback, delay) {
return () => {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = window.setTimeout(callback, delay);
};
}
}
;// ./src/utils/index.ts
;// ./src/services/http-client/http-client.service.ts
class HTTPClient {
static DEFAULT_HEADERS = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0",
};
static async get(url, headers = {}) {
return this.request("GET", url, { headers });
}
static async post(url, data, headers = {}) {
return this.request("POST", url, {
headers: { ...this.DEFAULT_HEADERS, ...headers },
body: JSON.stringify(data),
});
}
static async request(method, url, options = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method,
url,
data: options.body,
headers: options.headers || {},
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(response.responseText);
}
else {
reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
}
},
onerror: () => reject(new Error("Network request failed")),
ontimeout: () => reject(new Error("Request timed out")),
onabort: () => reject(new Error("Request was aborted")),
});
});
}
}
;// ./src/services/http-client/index.ts
;// ./src/services/translators/apibots/google/translator.google.ts
class GoogleTranslator {
sourceLang;
getTargetLang;
constructor(sourceLang, getTargetLang) {
this.sourceLang = sourceLang;
this.getTargetLang = getTargetLang;
}
async translate(text) {
const url = this.buildTranslateUrl(text, this.getTargetLang);
try {
const responseText = await HTTPClient.get(url);
if (typeof responseText !== "string") {
throw new Error("Invalid response type");
}
return this.parseTranslationResponse(responseText);
}
catch (error) {
throw new Error(`Translation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
buildTranslateUrl(text, targetLang) {
return (`https://translate.googleapis.com/translate_a/single?` +
`client=gtx&sl=${this.sourceLang}&tl=${targetLang}` +
`&dt=t&dt=bd&dj=1&q=${encodeURIComponent(text)}`);
}
parseTranslationResponse(responseText) {
try {
const data = JSON.parse(responseText);
const translated = data.sentences?.map((s) => s.trans).join("") ||
"No translation found.";
const result = {
translation: translated,
};
if (data.dict && Array.isArray(data.dict)) {
result.dictionary = data.dict.map((entry) => ({
pos: entry.pos,
terms: entry.terms || [],
}));
}
return result;
}
catch (error) {
throw new Error("Failed to parse translation response");
}
}
}
;// ./src/services/storage/cache.storage.ts
class SessionStorageService {
get(key, defaultValue) {
const item = sessionStorage.getItem(key);
return item !== null ? JSON.parse(item) : defaultValue;
}
set(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}
remove(key) {
sessionStorage.removeItem(key);
}
onChange(key, callback) {
const storageHandler = (event) => {
if (event.storageArea === sessionStorage && event.key === key) {
const newValue = event.newValue ? JSON.parse(event.newValue) : null;
const oldValue = event.oldValue ? JSON.parse(event.oldValue) : null;
callback(newValue, oldValue);
}
};
window.addEventListener("storage", storageHandler);
}
}
;// ./src/services/storage/storage.service.ts
class GMStorageService {
get(key, defaultValue) {
return GM_getValue(key, defaultValue);
}
set(key, value) {
GM_setValue(key, value);
}
remove(key) {
GM_deleteValue(key);
}
onChange(key, callback) {
GM_addValueChangeListener(key, callback);
}
}
;// ./src/services/storage/handler.storage.ts
class StorageHandler {
sessionStorageService;
gmStorageService;
constructor(sessionStorageService, gmStorageService) {
this.sessionStorageService = sessionStorageService;
this.gmStorageService = gmStorageService;
}
get(key, defaultValue) {
const sessionValue = this.sessionStorageService.get(key, defaultValue);
if (sessionValue !== undefined && sessionValue !== null) {
return sessionValue;
}
return this.gmStorageService.get(key, defaultValue);
}
set(key, value) {
this.sessionStorageService.set(key, value);
this.gmStorageService.set(key, value);
}
remove(key) {
this.sessionStorageService.remove(key);
this.gmStorageService.remove(key);
}
onChange(key, callback) {
this.sessionStorageService.onChange(key, callback);
this.gmStorageService.onChange(key, callback);
}
}
const sessionStorageService = new SessionStorageService();
const gmStorageService = new GMStorageService();
const storageHandler = new StorageHandler(sessionStorageService, gmStorageService);
;// ./src/services/storage/index.ts
;// ./src/services/translators/language-storage.service.ts
class LocalStorageLanguageService {
getTargetLanguage() {
return storageHandler.get("targetLang", "fa");
}
setTargetLanguage(lang) {
storageHandler.set("targetLang", lang);
}
}
;// ./src/services/translators/selection.service.ts
class BrowserSelectionService {
TOOLTIP_OFFSET_Y = 40;
getSelectedText() {
const selection = window.getSelection();
const text = selection?.toString().trim();
return text || null;
}
getSelectionPosition() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0)
return null;
const rect = selection.getRangeAt(0).getBoundingClientRect();
return {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY - this.TOOLTIP_OFFSET_Y,
};
}
}
;// ./src/services/translators/apibots/index.ts
class TranslationHandler {
tooltip;
translator;
formatter;
selectionService;
languageStorage;
DEBOUNCE_DELAY = 300;
constructor(tooltip, translator, formatter, selectionService, languageStorage) {
this.tooltip = tooltip;
this.translator = translator;
this.formatter = formatter;
this.selectionService = selectionService;
this.languageStorage = languageStorage;
this.initialize();
}
initialize() {
const debouncer = new Debouncer();
document.addEventListener("mouseup", debouncer.debounce(() => this.onTextSelect(), this.DEBOUNCE_DELAY));
document.addEventListener("mousedown", () => this.tooltip.hide());
this.registerLanguageMenu();
}
async onTextSelect() {
const selectedText = this.selectionService.getSelectedText();
if (!selectedText)
return this.tooltip.hide();
const position = this.selectionService.getSelectionPosition();
if (!position)
return;
const cachedResult = sessionStorageService.get(selectedText, null);
if (cachedResult)
return this.tooltip.show(cachedResult, position.x, position.y);
await this.fetchAndShowTranslation(selectedText, position);
}
async fetchAndShowTranslation(selectedText, position) {
try {
const translationResult = await this.translator.translate(selectedText);
if (translationResult.translation === selectedText)
return;
const formattedText = this.formatter.format(translationResult);
sessionStorageService.set(selectedText, formattedText);
this.tooltip.show(formattedText, position.x, position.y);
}
catch (error) {
this.tooltip.show(`Error: ${error}`, position.x, position.y);
}
}
registerLanguageMenu() {
registerMenuCommand("Set Target Language", () => {
const currentLang = this.languageStorage.getTargetLanguage();
const input = prompt("Enter target language (fa,en,fr,de,...):", currentLang);
if (input) {
this.languageStorage.setTargetLanguage(input);
ProgressUI.showQuick("[+]Refresh the page", {
percent: 100,
duration: 3000,
});
}
});
}
}
function initApiTranslation() {
const languageStorage = new LocalStorageLanguageService();
const targetLang = languageStorage.getTargetLanguage();
const selectionService = new BrowserSelectionService();
const tooltip = new DOMTooltip();
const translator = new GoogleTranslator("auto", targetLang);
const formatter = new GoogleTranslationFormatter();
return new TranslationHandler(tooltip, translator, formatter, selectionService, languageStorage);
}
;// ./src/index.ts
async function main() {
const translationHandler = initApiTranslation();
registerMenuCommand("Translate Selected Text", () => translationHandler.onTextSelect());
}
main().catch((e) => {
console.log(e);
});
/******/ })()
;