// ==UserScript==
// @name Waze Translate
// @namespace https://github.com/SaiCode-DEV
// @version 0.03
// @description Auto Translate using DeepL or LibreTranslate API for Waze Map Editor (WME)
// @author SaiCode
// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @icon https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_addStyle
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
/* global I18n, $ */
(() => {
"use strict";
/**
* Load the translation library
*/
function loadTranslationLib() {
if (unsafeWindow.Translate) return;
function Cache() {
let e = Object.create(null);
function a(a) {
delete e[a];
}
(this.set = function(n, i, r) {
if (void 0 !== r && (typeof r !== "number" || isNaN(r) || r <= 0)) throw new Error("Cache timeout must be a positive number");
const t = e[n];
t && clearTimeout(t.timeout);
const o = { value: i, expire: r + Date.now() };
return (
isNaN(o.expire) || (o.timeout = setTimeout(() => a(n), r)),
(e[n] = o),
i
);
}),
(this.del = function(n) {
let i = !0;
const r = e[n];
return (
r
? (clearTimeout(r.timeout),
!isNaN(r.expire) && r.expire < Date.now() && (i = !1))
: (i = !1),
i && a(n),
i
);
}),
(this.clear = function() {
for (const a in e) clearTimeout(e[a].timeout);
e = Object.create(null);
}),
(this.get = function(a) {
const n = e[a];
if (void 0 !== n) {
if (isNaN(n.expire) || n.expire >= Date.now()) return n.value;
delete e[a];
}
return null;
});
}
const cache = new Cache();
cache.Cache = Cache;
const googleUrl = "https://translate.googleapis.com/translate_a/single";
const libreUrl = "https://libretranslate.com/translate";
const google = {
fetch: ({
key, from, to, text,
}) => [
`${googleUrl}?client=gtx&sl=${from}&tl=${to}&dt=t&q=${encodeURI(text)}`,
],
parse: res => res.json().then(body => {
if (
!(body = body
&& body[0]
&& body[0][0]
&& body[0].map(e => e[0]).join(""))
) throw new Error("Translation not found");
return body;
}),
};
const yandex = {
needkey: !0,
fetch: ({
key: e, from, to, text,
}) => [
`https://translate.yandex.net/api/v1.5/tr.json/translate?key=${e}&lang=${from}-${to}&text=${encodeURIComponent(
text,
)}`,
{ method: "POST", body: "" },
],
parse: res => res.json().then(body => {
if (body.code !== 200) throw new Error(body.message);
return body.text[0];
}),
};
const libre = {
needkey: !1,
fetch: ({
url: e = libreUrl, key, from, to, text,
}) => [
e,
{
method: "POST",
body: JSON.stringify({
q: text,
source: from,
target: to,
api_key: key,
}),
headers: { "Content-Type": "application/json" },
},
],
parse: res => res.json().then(body => {
if (!body) throw new Error("No response found");
if (body.error) throw new Error(body.error);
if (!body.translatedText) throw new Error("No response found");
return body.translatedText;
}),
};
const deepl = {
needkey: !0,
fetch: ({
key, from, to, text,
}) => [
`https://api${
key.endsWith(":fx") ? "-free" : ""
}.deepl.com/v2/translate?auth_key=${key}&source_lang=${from}&target_lang=${to}&text=${(text = encodeURIComponent(text))}`,
{ method: "POST", body: "" },
],
parse: async res => {
if (!res.ok) {
if (res.status === 403) throw new Error("Auth Error, please review the key for DeepL");
throw new Error(`Error ${res.status}`);
}
return res.json().then(e => e.translations[0].text);
},
};
const engines = {
google,
yandex,
libre,
deepl,
};
const iso = {
aar: "aa",
abk: "ab",
afr: "af",
aka: "ak",
alb: "sq",
amh: "am",
ara: "ar",
arg: "an",
arm: "hy",
asm: "as",
ava: "av",
ave: "ae",
aym: "ay",
aze: "az",
bak: "ba",
bam: "bm",
baq: "eu",
bel: "be",
ben: "bn",
bih: "bh",
bis: "bi",
bos: "bs",
bre: "br",
bul: "bg",
bur: "my",
cat: "ca",
cha: "ch",
che: "ce",
chi: "zh",
chu: "cu",
chv: "cv",
cor: "kw",
cos: "co",
cre: "cr",
cze: "cs",
dan: "da",
div: "dv",
dut: "nl",
dzo: "dz",
eng: "en",
epo: "eo",
est: "et",
ewe: "ee",
fao: "fo",
fij: "fj",
fin: "fi",
fre: "fr",
fry: "fy",
ful: "ff",
geo: "ka",
ger: "de",
gla: "gd",
gle: "ga",
glg: "gl",
glv: "gv",
gre: "el",
grn: "gn",
guj: "gu",
hat: "ht",
hau: "ha",
heb: "he",
her: "hz",
hin: "hi",
hmo: "ho",
hrv: "hr",
hun: "hu",
ibo: "ig",
ice: "is",
ido: "io",
iii: "ii",
iku: "iu",
ile: "ie",
ina: "ia",
ind: "id",
ipk: "ik",
ita: "it",
jav: "jv",
jpn: "ja",
kal: "kl",
kan: "kn",
kas: "ks",
kau: "kr",
kaz: "kk",
khm: "km",
kik: "ki",
kin: "rw",
kir: "ky",
kom: "kv",
kon: "kg",
kor: "ko",
kua: "kj",
kur: "ku",
lao: "lo",
lat: "la",
lav: "lv",
lim: "li",
lin: "ln",
lit: "lt",
ltz: "lb",
lub: "lu",
lug: "lg",
mac: "mk",
mah: "mh",
mal: "ml",
mao: "mi",
mar: "mr",
may: "ms",
mlg: "mg",
mlt: "mt",
mon: "mn",
nau: "na",
nav: "nv",
nbl: "nr",
nde: "nd",
ndo: "ng",
nep: "ne",
nno: "nn",
nob: "nb",
nor: "no",
nya: "ny",
oci: "oc",
oji: "oj",
ori: "or",
orm: "om",
oss: "os",
pan: "pa",
per: "fa",
pli: "pi",
pol: "pl",
por: "pt",
pus: "ps",
que: "qu",
roh: "rm",
rum: "ro",
run: "rn",
rus: "ru",
sag: "sg",
san: "sa",
sin: "si",
slo: "sk",
slv: "sl",
sme: "se",
smo: "sm",
sna: "sn",
snd: "sd",
som: "so",
sot: "st",
spa: "es",
srd: "sc",
srp: "sr",
ssw: "ss",
sun: "su",
swa: "sw",
swe: "sv",
tah: "ty",
tam: "ta",
tat: "tt",
tel: "te",
tgk: "tg",
tgl: "tl",
tha: "th",
tib: "bo",
tir: "ti",
ton: "to",
tsn: "tn",
tso: "ts",
tuk: "tk",
tur: "tr",
twi: "tw",
uig: "ug",
ukr: "uk",
urd: "ur",
uzb: "uz",
ven: "ve",
vie: "vi",
vol: "vo",
wel: "cy",
wln: "wa",
wol: "wo",
xho: "xh",
yid: "yi",
yor: "yo",
zha: "za",
zul: "zu",
};
const names = {
afar: "aa",
abkhazian: "ab",
afrikaans: "af",
akan: "ak",
albanian: "sq",
amharic: "am",
arabic: "ar",
aragonese: "an",
armenian: "hy",
assamese: "as",
avaric: "av",
avestan: "ae",
aymara: "ay",
azerbaijani: "az",
bashkir: "ba",
bambara: "bm",
basque: "eu",
belarusian: "be",
bengali: "bn",
"bihari languages": "bh",
bislama: "bi",
tibetan: "bo",
bosnian: "bs",
breton: "br",
bulgarian: "bg",
burmese: "my",
catalan: "ca",
valencian: "ca",
czech: "cs",
chamorro: "ch",
chechen: "ce",
chinese: "zh",
"church slavic": "cu",
"old slavonic": "cu",
"church slavonic": "cu",
"old bulgarian": "cu",
"old church slavonic": "cu",
chuvash: "cv",
cornish: "kw",
corsican: "co",
cree: "cr",
welsh: "cy",
danish: "da",
german: "de",
divehi: "dv",
dhivehi: "dv",
maldivian: "dv",
dutch: "nl",
flemish: "nl",
dzongkha: "dz",
greek: "el",
english: "en",
esperanto: "eo",
estonian: "et",
ewe: "ee",
faroese: "fo",
persian: "fa",
fijian: "fj",
finnish: "fi",
french: "fr",
"western frisian": "fy",
fulah: "ff",
georgian: "ka",
gaelic: "gd",
"scottish gaelic": "gd",
irish: "ga",
galician: "gl",
manx: "gv",
guarani: "gn",
gujarati: "gu",
haitian: "ht",
"haitian creole": "ht",
hausa: "ha",
hebrew: "he",
herero: "hz",
hindi: "hi",
"hiri motu": "ho",
croatian: "hr",
hungarian: "hu",
igbo: "ig",
icelandic: "is",
ido: "io",
"sichuan yi": "ii",
nuosu: "ii",
inuktitut: "iu",
interlingue: "ie",
occidental: "ie",
interlingua: "ia",
indonesian: "id",
inupiaq: "ik",
italian: "it",
javanese: "jv",
japanese: "ja",
kalaallisut: "kl",
greenlandic: "kl",
kannada: "kn",
kashmiri: "ks",
kanuri: "kr",
kazakh: "kk",
"central khmer": "km",
kikuyu: "ki",
gikuyu: "ki",
kinyarwanda: "rw",
kirghiz: "ky",
kyrgyz: "ky",
komi: "kv",
kongo: "kg",
korean: "ko",
kuanyama: "kj",
kwanyama: "kj",
kurdish: "ku",
lao: "lo",
latin: "la",
latvian: "lv",
limburgan: "li",
limburger: "li",
limburgish: "li",
lingala: "ln",
lithuanian: "lt",
luxembourgish: "lb",
letzeburgesch: "lb",
"luba-katanga": "lu",
ganda: "lg",
macedonian: "mk",
marshallese: "mh",
malayalam: "ml",
maori: "mi",
marathi: "mr",
malay: "ms",
malagasy: "mg",
maltese: "mt",
mongolian: "mn",
nauru: "na",
navajo: "nv",
navaho: "nv",
"ndebele, south": "nr",
"south ndebele": "nr",
"ndebele, north": "nd",
"north ndebele": "nd",
ndonga: "ng",
nepali: "ne",
"norwegian nynorsk": "nn",
"nynorsk, norwegian": "nn",
"norwegian bokmål": "nb",
"bokmål, norwegian": "nb",
norwegian: "no",
chichewa: "ny",
chewa: "ny",
nyanja: "ny",
occitan: "oc",
ojibwa: "oj",
oriya: "or",
oromo: "om",
ossetian: "os",
ossetic: "os",
panjabi: "pa",
punjabi: "pa",
pali: "pi",
polish: "pl",
portuguese: "pt",
pushto: "ps",
pashto: "ps",
quechua: "qu",
romansh: "rm",
romanian: "ro",
moldavian: "ro",
moldovan: "ro",
rundi: "rn",
russian: "ru",
sango: "sg",
sanskrit: "sa",
sinhala: "si",
sinhalese: "si",
slovak: "sk",
slovenian: "sl",
"northern sami": "se",
samoan: "sm",
shona: "sn",
sindhi: "sd",
somali: "so",
"sotho, southern": "st",
spanish: "es",
castilian: "es",
sardinian: "sc",
serbian: "sr",
swati: "ss",
sundanese: "su",
swahili: "sw",
swedish: "sv",
tahitian: "ty",
tamil: "ta",
tatar: "tt",
telugu: "te",
tajik: "tg",
tagalog: "tl",
thai: "th",
tigrinya: "ti",
tonga: "to",
tswana: "tn",
tsonga: "ts",
turkmen: "tk",
turkish: "tr",
twi: "tw",
uighur: "ug",
uyghur: "ug",
ukrainian: "uk",
urdu: "ur",
uzbek: "uz",
venda: "ve",
vietnamese: "vi",
volapük: "vo",
walloon: "wa",
wolof: "wo",
xhosa: "xh",
yiddish: "yi",
yoruba: "yo",
zhuang: "za",
chuang: "za",
zulu: "zu",
};
const isoKeys = Object.values(iso).sort();
const languages = e => {
if (typeof e !== "string") throw new Error(`The "language" must be a string, received ${typeof e}`);
if (e.length > 100) throw new Error(`The "language" is too long at ${e.length} characters`);
if (
((e = e.toLowerCase()),
(e = names[e] || iso[e] || e),
!isoKeys.includes(e))
) throw new Error(`The language "${e}" is not part of the ISO 639-1`);
return e;
};
const Translate = function(e = {}) {
if (!(this instanceof Translate)) return new Translate(e);
const defaults = {
from: "en",
to: "en",
cache: void 0,
engine: "google",
key: void 0,
url: void 0,
languages,
engines,
keys: {},
};
const translate = async(text, opts = {}) => {
typeof opts === "string" && (opts = { to: opts });
const invalidKey = Object.keys(opts).find(
e => e !== "from" && e !== "to",
);
if (invalidKey) {
throw new Error(`Invalid option with the name '${invalidKey}'`);
}
opts.text = text;
opts.from = languages(opts.from || translate.from);
opts.to = languages(opts.to || translate.to);
opts.cache = translate.cache;
opts.engine = translate.engine;
opts.url = translate.url;
opts.id = `${opts.url}:${opts.from}:${opts.to}:${opts.engine}:${opts.text}`;
opts.keys = translate.keys || {};
for (const name in translate.keys) {
opts.keys[name] = opts.keys[name] || translate.keys[name];
}
opts.key = opts.key || translate.key || opts.keys[opts.engine];
const engine = translate.engines[opts.engine];
const cached = cache.get(opts.id);
if (cached) return Promise.resolve(cached);
// Target is the same as origin, just return the string
if (opts.to === opts.from) return Promise.resolve(opts.text);
if (engine.needkey && !opts.key) {
throw new Error(
`The engine "${opts.engine}" needs a key, please provide it`,
);
}
const fetchOpts = engine.fetch(opts);
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: fetchOpts[1].method || "GET",
url: fetchOpts[0],
headers: fetchOpts[1].headers || {},
data: fetchOpts[1].body || "",
responseType: "json",
onload(response) {
console.log(response);
const result = engine.parse(response.responseText);
result.then(translated => {
cache.set(opts.id, translated, opts.cache);
resolve(translated);
});
},
onerror(error) {
reject(error);
},
});
});
return fetch(...fetchOpts)
.then(engine.parse)
.then(translated => cache.set(opts.id, translated, opts.cache));
};
for (const key in defaults) translate[key] = void 0 === e[key] ? defaults[key] : e[key];
return translate;
};
console.info("Translation library loaded");
if (!unsafeWindow.Translate) unsafeWindow.Translate = Translate;
return Translate;
}
const Tranlate = loadTranslationLib();
const lTrans = new Tranlate({
engine: "libre",
url: "https://translate.saicloud.de/translate",
});
const gTrans = new Tranlate({ engine: "google" });
// test translation
gTrans("en", "de", "Hello World").then(console.log);
})();
GM_addStyle(`
`);