Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit a4068c66974d3ccc13e3a08a1c8bd7613f079e71
Author: Marco Bonetti <marco@cruncher.ch>
Date:   Mon Oct 7 13:54:41 2024 +0200

    Changelog

commit ca7d41224cce4cdeca82ee759197d4cdae0bcb00
Merge: 6139629 6304a7a
Author: Marco Bonetti <marco@cruncher.ch>
Date:   Mon Oct 7 12:06:29 2024 +0200

    Merge branch 'javascript-rewrite' of github.com:balazs-endresz/django-rosetta into balazs-endresz-javascript-rewrite

commit 6304a7a
Author: Balazs Endresz <balazs.endresz@gmail.com>
Date:   Thu Oct 3 20:17:47 2024 +0200

    Fix js errors on file list page

commit 3fdca87
Author: Balazs Endresz <balazs.endresz@gmail.com>
Date:   Thu Oct 3 20:01:33 2024 +0200

    Warn about unmatched variables when using curly braces with modifiers

commit cf84ac5
Author: Balazs Endresz <balazs.endresz@gmail.com>
Date:   Thu Oct 3 19:51:52 2024 +0200

    Autofit textareas on window resize too

commit 71c9f34
Author: Balazs Endresz <balazs.endresz@gmail.com>
Date:   Thu Oct 3 19:51:04 2024 +0200

    Move some code to separate functions for readability

commit b63db38
Author: Balazs Endresz <balazs.endresz@gmail.com>
Date:   Thu Oct 3 19:46:56 2024 +0200

    Don't focus first textarea on page load

commit 5c57d5a
Author: Balazs Endresz <balazs.endresz@gmail.com>
Date:   Sat Sep 28 12:07:25 2024 +0200

    Fix error when reflang is disabled, use optional chaining

commit 9c83ddd
Author: Balazs Endresz <balazs.endresz@gmail.com>
Date:   Fri Sep 27 17:17:34 2024 +0200

    Rewrite rosetta.js
    * drop jQuery
    * fix various js bugs
    * add some new improvements
  • Loading branch information
mbi committed Oct 7, 2024
1 parent 6139629 commit 03d9f38
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 127 deletions.
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
2 changes: 1 addition & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Version 0.10.2 (unreleased)
* Tests: update flake8 in tox tests
* Format all rendered assets (html, css, js) in a pre-commit task. (PR #294, thanks @balazs-endresz)
* Fix Deepl translations containing variables (#276, PR #290, thanks @halitcelik)

* Rewrite rosetta.js: drop jQuery and modernize rosetta.js (PR #295, thanks @balazs-endresz)


Version 0.10.1
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;
}
319 changes: 200 additions & 119 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>/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

0 comments on commit 03d9f38

Please sign in to comment.