Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite rosetta.js #295

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
browser: true,
node: true,
},
parserOptions: { ecmaVersion: 9 },
parserOptions: { ecmaVersion: 2020 },
globals: {
$: "readonly",
},
Expand Down
6 changes: 4 additions & 2 deletions rosetta/static/admin/rosetta/css/rosetta.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ td .context {
}
td.translation textarea {
width: 98.5%;
min-height: 25px;
margin: 2px 0;
}
.rtl td.translation textarea {
Expand Down Expand Up @@ -100,7 +99,6 @@ tr.row1 td.original code {
.alert {
font-weight: bold;
padding: 4px 5px 4px 25px;
margin-left: 1em;
color: red;
background: transparent
url(data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%201792%201792%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cpath%20fill%3D%22%23efb80b%22%20d%3D%22M1024%201375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13%200-22.5%209.5t-9.5%2023.5v190q0%2014%209.5%2023.5t22.5%209.5h192q13%200%2022.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11%200-24%2011-10%207-10%2021l17%20457q0%2010%2010%2016.5t24%206.5h185q14%200%2023.5-6.5t10.5-16.5zm-14-934l768%201408q35%2063-2%20126-17%2029-46.5%2046t-63.5%2017h-1536q-34%200-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31%2047-49t65-18%2065%2018%2047%2049z%22%2F%3E%0A%3C%2Fsvg%3E%0A)
Expand Down Expand Up @@ -158,3 +156,7 @@ div.module {
#action-toggle {
display: inline;
}
a.suggest {
display: block;
margin-bottom: 5px;
}
321 changes: 201 additions & 120 deletions rosetta/static/admin/rosetta/js/rosetta.js
Original file line number Diff line number Diff line change
@@ -1,157 +1,238 @@
"use strict";

const rosetta_settings = JSON.parse(document.getElementById("rosetta-settings-js").textContent);

$(document).ready(function () {
$(".location a")
.show()
.toggle(
function () {
$(".hide", $(this).parent()).show();
},
function () {
$(".hide", $(this).parent()).hide();
},
);
document.addEventListener("DOMContentLoaded", () => {
// Get original html that corresponds to a given textarea containing the translation
function originalForTextarea(textarea) {
const textareasInCell = textarea.closest("td").querySelectorAll("textarea");
const nth = Array.from(textareasInCell).indexOf(textarea) + 1;
return textarea
.closest("tr")
.querySelector(".original")
.querySelector(`.message, .part:nth-of-type(${nth})`).innerHTML;
}

// Common code for handling translation suggestions
function suggest(translate) {
document.querySelectorAll("a.suggest").forEach((a) => {
a.addEventListener("click", (event) => {
event.preventDefault();
const textarea = a.previousElementSibling;
const orig = originalForTextarea(textarea);
a.classList.add("suggesting");
a.textContent = "...";
translate(
orig,
(translation) => {
textarea.value = translation;
textarea.dispatchEvent(new Event("input"));
textarea.dispatchEvent(new Event("change"));
textarea.dispatchEvent(new Event("blur"));
a.style.visibility = "hidden";
},
(error) => {
console.error("Rosetta translation suggestion error:", error);
let errorMsg;
if (error?.message) {
errorMsg = error.message;
} else if (error?.error) {
errorMsg = error.error;
} else if (typeof error === "object") {
errorMsg = JSON.stringify(error);
} else {
errorMsg = error || "Error loading translation";
}
a.textContent = String(errorMsg).trim().substring(0, 100);
alignPlurals();
},
);
});
});
}

function jsonp(url, params, callback) {
var callbackName = "rosetta_jsonp_callback_" + Math.random().toString(36).substr(2, 8);
window[callbackName] = function (response) {
callback(response);
delete window[callbackName];
};
params.callback = callbackName;
var script = document.createElement("script");
script.src = `${url}?${new URLSearchParams(params).toString()}`;
document.body.appendChild(script);
script.onerror = function () {
callback("Failed to load translation with jsonp request");
delete window[callbackName];
};
}

// Translation suggestions
if (rosetta_settings.ENABLE_TRANSLATION_SUGGESTIONS) {
if (rosetta_settings.server_auth_key) {
$("a.suggest").click(function (e) {
e.preventDefault();
var a = $(this);
var orig = $(".original .message", a.parents("tr")).html();
var trans = $("textarea", a.parent());
var sourceLang = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE;
var destLang = rosetta_settings.rosetta_i18n_lang_code_normalized;

orig = unescape(orig)
suggest((orig, setTranslation, setError) => {
const origUnescaped = unescape(orig)
.replace(/<br\s?\/?>/g, "\n")
.replace(/<code>/, "")
.replace(/<code>/g, "")
.replace(/<\/code>/g, "")
.replace(/&gt;/g, ">")
.replace(/&lt;/g, "<");
a.attr("class", "suggesting").html("...");

$.getJSON(
rosetta_settings.translate_text_url,
{
from: sourceLang,
to: destLang,
text: orig,
},
function (data) {
const params = new URLSearchParams({
from: rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE,
to: rosetta_settings.rosetta_i18n_lang_code_normalized,
text: origUnescaped,
});
const url = `${rosetta_settings.translate_text_url}?${params.toString()}`;
fetch(url)
.then((r) => r.json())
.then((data) => {
if (data.success) {
trans.val(
setTranslation(
unescape(data.translation)
.replace(/&#39;/g, "'")
.replace(/&quot;/g, '"')
.replace(/%\s+(\([^)]+\))\s*s/g, " %$1s "),
);
a.hide();
} else {
a.text(data.error);
setError(data);
}
},
);
})
.catch(setError);
});
} else if (rosetta_settings.YANDEX_TRANSLATE_KEY) {
$("a.suggest").click(function (e) {
e.preventDefault();
var a = $(this);
var orig = $(".original .message", a.parents("tr")).html();
var trans = $("textarea", a.parent());
var apiUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate";
var destLangRoot = rosetta_settings.rosetta_i18n_lang_code.split("-")[0];
var lang = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE + "-" + destLangRoot;

a.attr("class", "suggesting").html("...");

var apiData = {
suggest((orig, setTranslation, setError) => {
const apiUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate";
const destLangRoot = rosetta_settings.rosetta_i18n_lang_code.split("-")[0];
const lang = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE + "-" + destLangRoot;
const apiData = {
error: "onTranslationError",
success: "onTranslationComplete",
lang: lang,
key: rosetta_settings.YANDEX_TRANSLATE_KEY,
format: "html",
text: orig,
};

$.ajax({
url: apiUrl,
data: apiData,
dataType: "jsonp",
success: function (response) {
if (response.code == 200) {
trans.val(
response.text[0]
.replace(/<br>/g, "\n")
.replace(/<\/?code>/g, "")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">"),
);
a.hide();
} else {
a.text(response);
}
},
error: function (response) {
a.text(response);
},
jsonp(apiUrl, apiData, (response) => {
if (response.code === 200) {
setTranslation(
response.text[0]
.replace(/< ?br>/g, "\n")
.replace(/< ?\/? ?code>/g, "")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">"),
);
} else {
setError(response);
}
});
});
}
}

$("td.plural").each(function () {
var td = $(this);
var trY = parseInt(td.closest("tr").offset().top);
$("textarea", $(this).closest("tr")).each(function (j) {
var textareaY = parseInt($(this).offset().top) - trY;
$($(".part", td).get(j)).css("top", textareaY + "px");
// Make textarea height adapt to the contents
function autofitTextarea(textarea) {
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
}

// If there are multiple textareas for plurals then align the originals vertically with the textareas
function alignPlurals() {
document.querySelectorAll(".results td.plural").forEach((td) => {
const tr = td.closest("tr");
const trY = tr.getBoundingClientRect().top + window.scrollY;
tr.querySelectorAll("textarea").forEach((textarea, i) => {
const part = td.querySelectorAll(".part")[i];
if (part) {
const textareaY = textarea.getBoundingClientRect().top + window.scrollY - trY;
part.style.top = textareaY + "px";
}
});
});
}

// Show warning if the variables in the original and the translation don't match
function validateTranslation(textarea) {
const orig = originalForTextarea(textarea);
const variablePattern = /%(?:\([^\s)]*\))?[sdf]|\{[^\s}]*\}/g;
const origVars = orig.match(variablePattern) || [];
const transVars = textarea.value.match(variablePattern) || [];
const everyOrigVarUsed = origVars.every((origVar) => transVars.includes(origVar));
const onlyValidVarsUsed = transVars.every((transVar) => origVars.includes(transVar));
const valid = everyOrigVarUsed && onlyValidVarsUsed;
textarea.previousElementSibling.classList.toggle("hidden", valid);
}

// Select all the textareas that are used for translations
const textareas = document.querySelectorAll(".translation textarea");

// For each translation field textarea
textareas.forEach((textarea) => {
// On page load make textarea height adapt to its contents
autofitTextarea(textarea);

// On input
textarea.addEventListener("input", () => {
// Make textarea height adapt to its contents
autofitTextarea(textarea);

// If there are multiple textareas for plurals then align the originals vertically with the textareas
alignPlurals();

// Once users start editing the translation untick the fuzzy checkbox automatically
textarea.closest("tr").querySelector('td.c input[type="checkbox"]').checked = false;
});

// On blur show warnings for unmatched variables in translations
textarea.addEventListener("blur", () => validateTranslation(textarea));
});

$(".translation textarea")
.blur(function () {
if ($(this).val()) {
$(".alert", $(this).parents("tr")).remove();
var RX = /%(?:\([^\s)]*\))?[sdf]|\{[\w\d_]+?\}/g;
var origs = $(this).parents("tr").find(".original span").html().match(RX);
var trads = $(this).val().match(RX);
var error = $('<span class="alert">Unmatched variables</span>');

if (origs && trads) {
for (var i = trads.length; i--; ) {
var key = trads[i];
if (-1 == $.inArray(key, origs)) {
$(this).before(error);
return false;
}
}
return true;
} else {
if (!(origs === null && trads === null)) {
$(this).before(error);
return false;
}
}
return true;
}
})
.keyup(function () {
var cb = $(this).parents("tr").find('td.c input[type="checkbox"]');
if (cb.is(":checked")) {
cb[0].checked = false;
cb.removeAttr("checked");
}
})
.eq(0)
.focus();

$("#action-toggle").change(function () {
$('tbody td.c input[type="checkbox"]').each(function (i, e) {
if ($("#action-toggle").is(":checked")) {
$(e).attr("checked", "checked");
} else {
$(e).removeAttr("checked");
}
// On window resize make textarea height adapt to their contents
window.addEventListener("resize", () => textareas.forEach(autofitTextarea), { passive: true });

// On page load if there are multiple textareas in a cell for plurals then align the originals vertically with them
alignPlurals();

// Reload page when changing ref-language
document.getElementById("ref-language-selector")?.addEventListener("change", function () {
window.location.href = this.value;
});

// Toggle fuzzy state for all entries on the current page
document.getElementById("action-toggle")?.addEventListener("change", function () {
const checkboxes = document.querySelectorAll('tbody td.c input[type="checkbox"]');
checkboxes.forEach((checkbox) => (checkbox.checked = this.checked));
});

// Toggle additional locations that are initially hidden
document.querySelectorAll(".location a").forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
const prevText = link.innerText;
link.innerText = link.dataset.prevText;
link.dataset.prevText = prevText;
link.parentElement.querySelectorAll(".hide").forEach((loc) => {
const hidden = loc.style.display === "none" || loc.style.display === "";
loc.style.display = hidden ? "block" : "none";
});
});
});

// Warn about any unsaved changes before navigating away from the page
const form = document.querySelector("form.results");
function formToJsonString() {
const obj = {};
new FormData(form).forEach((value, key) => (obj[key] = value));
return JSON.stringify(obj);
}
if (form) {
const initialDataJson = formToJsonString();
let isSubmitting = false;
form.addEventListener("submit", () => (isSubmitting = true));
window.addEventListener("beforeunload", (event) => {
if (!isSubmitting && initialDataJson !== formToJsonString()) {
event.preventDefault();
event.returnValue = "";
}
});
}
});
Loading
Loading