-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
6 changed files
with
212 additions
and
127 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(/>/g, ">") | ||
.replace(/</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(/'/g, "'") | ||
.replace(/"/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(/</g, "<") | ||
.replace(/>/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(/</g, "<") | ||
.replace(/>/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 = ""; | ||
} | ||
}); | ||
} | ||
}); |
Oops, something went wrong.