|
| 1 | +"use strict"; |
| 2 | + |
1 | 3 | const rosetta_settings = JSON.parse(document.getElementById("rosetta-settings-js").textContent);
|
2 | 4 |
|
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 | + } |
14 | 15 |
|
| 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 |
15 | 71 | if (rosetta_settings.ENABLE_TRANSLATION_SUGGESTIONS) {
|
16 | 72 | 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) |
26 | 75 | .replace(/<br\s?\/?>/g, "\n")
|
27 | 76 | .replace(/<code>/g, "")
|
28 | 77 | .replace(/<\/code>/g, "")
|
29 | 78 | .replace(/>/g, ">")
|
30 | 79 | .replace(/</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) => { |
41 | 89 | if (data.success) {
|
42 |
| - trans.val( |
| 90 | + setTranslation( |
43 | 91 | unescape(data.translation)
|
44 | 92 | .replace(/'/g, "'")
|
45 | 93 | .replace(/"/g, '"')
|
46 | 94 | .replace(/%\s+(\([^)]+\))\s*s/g, " %$1s "),
|
47 | 95 | );
|
48 |
| - a.hide(); |
49 | 96 | } else {
|
50 |
| - a.text(data.error); |
| 97 | + setError(data); |
51 | 98 | }
|
52 |
| - }, |
53 |
| - ); |
| 99 | + }) |
| 100 | + .catch(setError); |
54 | 101 | });
|
55 | 102 | } 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 = { |
68 | 108 | error: "onTranslationError",
|
69 | 109 | success: "onTranslationComplete",
|
70 | 110 | lang: lang,
|
71 | 111 | key: rosetta_settings.YANDEX_TRANSLATE_KEY,
|
72 | 112 | format: "html",
|
73 | 113 | text: orig,
|
74 | 114 | };
|
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(/</g, "<") |
87 |
| - .replace(/>/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(/</g, "<") |
| 122 | + .replace(/>/g, ">"), |
| 123 | + ); |
| 124 | + } else { |
| 125 | + setError(response); |
| 126 | + } |
97 | 127 | });
|
98 | 128 | });
|
99 | 129 | }
|
100 | 130 | }
|
101 | 131 |
|
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 | + }); |
108 | 150 | });
|
| 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)); |
109 | 187 | });
|
110 | 188 |
|
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 | + }); |
155 | 217 | });
|
156 | 218 | });
|
| 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 | + } |
157 | 238 | });
|
0 commit comments