Skip to content

Commit 03d9f38

Browse files
committed
Squashed commit of the following:
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
1 parent 6139629 commit 03d9f38

File tree

6 files changed

+212
-127
lines changed

6 files changed

+212
-127
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = {
44
browser: true,
55
node: true,
66
},
7-
parserOptions: { ecmaVersion: 9 },
7+
parserOptions: { ecmaVersion: 2020 },
88
globals: {
99
$: "readonly",
1010
},

CHANGES

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Version 0.10.2 (unreleased)
66
* Tests: update flake8 in tox tests
77
* Format all rendered assets (html, css, js) in a pre-commit task. (PR #294, thanks @balazs-endresz)
88
* Fix Deepl translations containing variables (#276, PR #290, thanks @halitcelik)
9-
9+
* Rewrite rosetta.js: drop jQuery and modernize rosetta.js (PR #295, thanks @balazs-endresz)
1010

1111

1212
Version 0.10.1

rosetta/static/admin/rosetta/css/rosetta.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ td .context {
2828
}
2929
td.translation textarea {
3030
width: 98.5%;
31-
min-height: 25px;
3231
margin: 2px 0;
3332
}
3433
.rtl td.translation textarea {
@@ -100,7 +99,6 @@ tr.row1 td.original code {
10099
.alert {
101100
font-weight: bold;
102101
padding: 4px 5px 4px 25px;
103-
margin-left: 1em;
104102
color: red;
105103
background: transparent
106104
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)
@@ -158,3 +156,7 @@ div.module {
158156
#action-toggle {
159157
display: inline;
160158
}
159+
a.suggest {
160+
display: block;
161+
margin-bottom: 5px;
162+
}
Lines changed: 200 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,238 @@
1+
"use strict";
2+
13
const rosetta_settings = JSON.parse(document.getElementById("rosetta-settings-js").textContent);
24

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

16+
// Common code for handling translation suggestions
17+
function suggest(translate) {
18+
document.querySelectorAll("a.suggest").forEach((a) => {
19+
a.addEventListener("click", (event) => {
20+
event.preventDefault();
21+
const textarea = a.previousElementSibling;
22+
const orig = originalForTextarea(textarea);
23+
a.classList.add("suggesting");
24+
a.textContent = "...";
25+
translate(
26+
orig,
27+
(translation) => {
28+
textarea.value = translation;
29+
textarea.dispatchEvent(new Event("input"));
30+
textarea.dispatchEvent(new Event("change"));
31+
textarea.dispatchEvent(new Event("blur"));
32+
a.style.visibility = "hidden";
33+
},
34+
(error) => {
35+
console.error("Rosetta translation suggestion error:", error);
36+
let errorMsg;
37+
if (error?.message) {
38+
errorMsg = error.message;
39+
} else if (error?.error) {
40+
errorMsg = error.error;
41+
} else if (typeof error === "object") {
42+
errorMsg = JSON.stringify(error);
43+
} else {
44+
errorMsg = error || "Error loading translation";
45+
}
46+
a.textContent = String(errorMsg).trim().substring(0, 100);
47+
alignPlurals();
48+
},
49+
);
50+
});
51+
});
52+
}
53+
54+
function jsonp(url, params, callback) {
55+
var callbackName = "rosetta_jsonp_callback_" + Math.random().toString(36).substr(2, 8);
56+
window[callbackName] = function (response) {
57+
callback(response);
58+
delete window[callbackName];
59+
};
60+
params.callback = callbackName;
61+
var script = document.createElement("script");
62+
script.src = `${url}?${new URLSearchParams(params).toString()}`;
63+
document.body.appendChild(script);
64+
script.onerror = function () {
65+
callback("Failed to load translation with jsonp request");
66+
delete window[callbackName];
67+
};
68+
}
69+
70+
// Translation suggestions
1571
if (rosetta_settings.ENABLE_TRANSLATION_SUGGESTIONS) {
1672
if (rosetta_settings.server_auth_key) {
17-
$("a.suggest").click(function (e) {
18-
e.preventDefault();
19-
var a = $(this);
20-
var orig = $(".original .message", a.parents("tr")).html();
21-
var trans = $("textarea", a.parent());
22-
var sourceLang = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE;
23-
var destLang = rosetta_settings.rosetta_i18n_lang_code_normalized;
24-
25-
orig = unescape(orig)
73+
suggest((orig, setTranslation, setError) => {
74+
const origUnescaped = unescape(orig)
2675
.replace(/<br\s?\/?>/g, "\n")
2776
.replace(/<code>/g, "")
2877
.replace(/<\/code>/g, "")
2978
.replace(/&gt;/g, ">")
3079
.replace(/&lt;/g, "<");
31-
a.attr("class", "suggesting").html("...");
32-
33-
$.getJSON(
34-
rosetta_settings.translate_text_url,
35-
{
36-
from: sourceLang,
37-
to: destLang,
38-
text: orig,
39-
},
40-
function (data) {
80+
const params = new URLSearchParams({
81+
from: rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE,
82+
to: rosetta_settings.rosetta_i18n_lang_code_normalized,
83+
text: origUnescaped,
84+
});
85+
const url = `${rosetta_settings.translate_text_url}?${params.toString()}`;
86+
fetch(url)
87+
.then((r) => r.json())
88+
.then((data) => {
4189
if (data.success) {
42-
trans.val(
90+
setTranslation(
4391
unescape(data.translation)
4492
.replace(/&#39;/g, "'")
4593
.replace(/&quot;/g, '"')
4694
.replace(/%\s+(\([^)]+\))\s*s/g, " %$1s "),
4795
);
48-
a.hide();
4996
} else {
50-
a.text(data.error);
97+
setError(data);
5198
}
52-
},
53-
);
99+
})
100+
.catch(setError);
54101
});
55102
} else if (rosetta_settings.YANDEX_TRANSLATE_KEY) {
56-
$("a.suggest").click(function (e) {
57-
e.preventDefault();
58-
var a = $(this);
59-
var orig = $(".original .message", a.parents("tr")).html();
60-
var trans = $("textarea", a.parent());
61-
var apiUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate";
62-
var destLangRoot = rosetta_settings.rosetta_i18n_lang_code.split("-")[0];
63-
var lang = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE + "-" + destLangRoot;
64-
65-
a.attr("class", "suggesting").html("...");
66-
67-
var apiData = {
103+
suggest((orig, setTranslation, setError) => {
104+
const apiUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate";
105+
const destLangRoot = rosetta_settings.rosetta_i18n_lang_code.split("-")[0];
106+
const lang = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE + "-" + destLangRoot;
107+
const apiData = {
68108
error: "onTranslationError",
69109
success: "onTranslationComplete",
70110
lang: lang,
71111
key: rosetta_settings.YANDEX_TRANSLATE_KEY,
72112
format: "html",
73113
text: orig,
74114
};
75-
76-
$.ajax({
77-
url: apiUrl,
78-
data: apiData,
79-
dataType: "jsonp",
80-
success: function (response) {
81-
if (response.code == 200) {
82-
trans.val(
83-
response.text[0]
84-
.replace(/<br>/g, "\n")
85-
.replace(/<\/?code>/g, "")
86-
.replace(/&lt;/g, "<")
87-
.replace(/&gt;/g, ">"),
88-
);
89-
a.hide();
90-
} else {
91-
a.text(response);
92-
}
93-
},
94-
error: function (response) {
95-
a.text(response);
96-
},
115+
jsonp(apiUrl, apiData, (response) => {
116+
if (response.code === 200) {
117+
setTranslation(
118+
response.text[0]
119+
.replace(/< ?br>/g, "\n")
120+
.replace(/< ?\/? ?code>/g, "")
121+
.replace(/&lt;/g, "<")
122+
.replace(/&gt;/g, ">"),
123+
);
124+
} else {
125+
setError(response);
126+
}
97127
});
98128
});
99129
}
100130
}
101131

102-
$("td.plural").each(function () {
103-
var td = $(this);
104-
var trY = parseInt(td.closest("tr").offset().top);
105-
$("textarea", $(this).closest("tr")).each(function (j) {
106-
var textareaY = parseInt($(this).offset().top) - trY;
107-
$($(".part", td).get(j)).css("top", textareaY + "px");
132+
// Make textarea height adapt to the contents
133+
function autofitTextarea(textarea) {
134+
textarea.style.height = "auto";
135+
textarea.style.height = textarea.scrollHeight + "px";
136+
}
137+
138+
// If there are multiple textareas for plurals then align the originals vertically with the textareas
139+
function alignPlurals() {
140+
document.querySelectorAll(".results td.plural").forEach((td) => {
141+
const tr = td.closest("tr");
142+
const trY = tr.getBoundingClientRect().top + window.scrollY;
143+
tr.querySelectorAll("textarea").forEach((textarea, i) => {
144+
const part = td.querySelectorAll(".part")[i];
145+
if (part) {
146+
const textareaY = textarea.getBoundingClientRect().top + window.scrollY - trY;
147+
part.style.top = textareaY + "px";
148+
}
149+
});
108150
});
151+
}
152+
153+
// Show warning if the variables in the original and the translation don't match
154+
function validateTranslation(textarea) {
155+
const orig = originalForTextarea(textarea);
156+
const variablePattern = /%(?:\([^\s)]*\))?[sdf]|\{[^\s}]*\}/g;
157+
const origVars = orig.match(variablePattern) || [];
158+
const transVars = textarea.value.match(variablePattern) || [];
159+
const everyOrigVarUsed = origVars.every((origVar) => transVars.includes(origVar));
160+
const onlyValidVarsUsed = transVars.every((transVar) => origVars.includes(transVar));
161+
const valid = everyOrigVarUsed && onlyValidVarsUsed;
162+
textarea.previousElementSibling.classList.toggle("hidden", valid);
163+
}
164+
165+
// Select all the textareas that are used for translations
166+
const textareas = document.querySelectorAll(".translation textarea");
167+
168+
// For each translation field textarea
169+
textareas.forEach((textarea) => {
170+
// On page load make textarea height adapt to its contents
171+
autofitTextarea(textarea);
172+
173+
// On input
174+
textarea.addEventListener("input", () => {
175+
// Make textarea height adapt to its contents
176+
autofitTextarea(textarea);
177+
178+
// If there are multiple textareas for plurals then align the originals vertically with the textareas
179+
alignPlurals();
180+
181+
// Once users start editing the translation untick the fuzzy checkbox automatically
182+
textarea.closest("tr").querySelector('td.c input[type="checkbox"]').checked = false;
183+
});
184+
185+
// On blur show warnings for unmatched variables in translations
186+
textarea.addEventListener("blur", () => validateTranslation(textarea));
109187
});
110188

111-
$(".translation textarea")
112-
.blur(function () {
113-
if ($(this).val()) {
114-
$(".alert", $(this).parents("tr")).remove();
115-
var RX = /%(?:\([^\s)]*\))?[sdf]|\{[\w\d_]+?\}/g;
116-
var origs = $(this).parents("tr").find(".original span").html().match(RX);
117-
var trads = $(this).val().match(RX);
118-
var error = $('<span class="alert">Unmatched variables</span>');
119-
120-
if (origs && trads) {
121-
for (var i = trads.length; i--; ) {
122-
var key = trads[i];
123-
if (-1 == $.inArray(key, origs)) {
124-
$(this).before(error);
125-
return false;
126-
}
127-
}
128-
return true;
129-
} else {
130-
if (!(origs === null && trads === null)) {
131-
$(this).before(error);
132-
return false;
133-
}
134-
}
135-
return true;
136-
}
137-
})
138-
.keyup(function () {
139-
var cb = $(this).parents("tr").find('td.c input[type="checkbox"]');
140-
if (cb.is(":checked")) {
141-
cb[0].checked = false;
142-
cb.removeAttr("checked");
143-
}
144-
})
145-
.eq(0)
146-
.focus();
147-
148-
$("#action-toggle").change(function () {
149-
$('tbody td.c input[type="checkbox"]').each(function (i, e) {
150-
if ($("#action-toggle").is(":checked")) {
151-
$(e).attr("checked", "checked");
152-
} else {
153-
$(e).removeAttr("checked");
154-
}
189+
// On window resize make textarea height adapt to their contents
190+
window.addEventListener("resize", () => textareas.forEach(autofitTextarea), { passive: true });
191+
192+
// On page load if there are multiple textareas in a cell for plurals then align the originals vertically with them
193+
alignPlurals();
194+
195+
// Reload page when changing ref-language
196+
document.getElementById("ref-language-selector")?.addEventListener("change", function () {
197+
window.location.href = this.value;
198+
});
199+
200+
// Toggle fuzzy state for all entries on the current page
201+
document.getElementById("action-toggle")?.addEventListener("change", function () {
202+
const checkboxes = document.querySelectorAll('tbody td.c input[type="checkbox"]');
203+
checkboxes.forEach((checkbox) => (checkbox.checked = this.checked));
204+
});
205+
206+
// Toggle additional locations that are initially hidden
207+
document.querySelectorAll(".location a").forEach((link) => {
208+
link.addEventListener("click", (event) => {
209+
event.preventDefault();
210+
const prevText = link.innerText;
211+
link.innerText = link.dataset.prevText;
212+
link.dataset.prevText = prevText;
213+
link.parentElement.querySelectorAll(".hide").forEach((loc) => {
214+
const hidden = loc.style.display === "none" || loc.style.display === "";
215+
loc.style.display = hidden ? "block" : "none";
216+
});
155217
});
156218
});
219+
220+
// Warn about any unsaved changes before navigating away from the page
221+
const form = document.querySelector("form.results");
222+
function formToJsonString() {
223+
const obj = {};
224+
new FormData(form).forEach((value, key) => (obj[key] = value));
225+
return JSON.stringify(obj);
226+
}
227+
if (form) {
228+
const initialDataJson = formToJsonString();
229+
let isSubmitting = false;
230+
form.addEventListener("submit", () => (isSubmitting = true));
231+
window.addEventListener("beforeunload", (event) => {
232+
if (!isSubmitting && initialDataJson !== formToJsonString()) {
233+
event.preventDefault();
234+
event.returnValue = "";
235+
}
236+
});
237+
}
157238
});

0 commit comments

Comments
 (0)