From b6f0ed229c0b5623381a06b6a82bc8a4a2b84445 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Thu, 14 Oct 2021 18:35:58 +0200 Subject: [PATCH] added online Translations --- .../mannlib/vitro/webapp/i18n/I18nBundle.java | 28 +- .../vitro/webapp/utils/developer/Key.java | 5 +- .../config/example.developer.properties | 1 + .../js/account/accountProxyItemsPanel.js | 8 +- .../src/main/webapp/js/developer/FileSaver.js | 188 +++++++ .../webapp/js/developer/developerPanel.js | 1 + .../main/webapp/js/developer/translations.js | 484 ++++++++++++++++++ .../freemarker/page/partials/developer.ftl | 2 + .../page/partials/developerPanel.ftl | 5 + 9 files changed, 714 insertions(+), 8 deletions(-) create mode 100644 webapp/src/main/webapp/js/developer/FileSaver.js create mode 100644 webapp/src/main/webapp/js/developer/translations.js diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java index 0d57cb8fd..c2060d99b 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java @@ -10,6 +10,9 @@ import java.util.ResourceBundle; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings; +import edu.cornell.mannlib.vitro.webapp.utils.developer.Key; + /** * A wrapper for a ResourceBundle that will not throw an exception, no matter * what string you request. @@ -20,7 +23,9 @@ import org.apache.commons.logging.LogFactory; */ public class I18nBundle { private static final Log log = LogFactory.getLog(I18nBundle.class); - + private static final String startSep = "\u25a4"; + private static final String endSep = "\u25a5"; + private static final String intSep = "\u25a6"; private static final String MESSAGE_BUNDLE_NOT_FOUND = "Text bundle ''{0}'' not found."; private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''"; @@ -75,13 +80,26 @@ public class I18nBundle { key); log.warn(message); textString = "ERROR: " + message; - } - String result = formatString(textString, parameters); + } + String message = formatString(textString, parameters); if (i18nLogger != null) { - i18nLogger.log(bundleName, key, parameters, textString, result); + i18nLogger.log(bundleName, key, parameters, textString, message); } - return result; + if (isNeedExportInfo()) { + String separatedArgs = ""; + for (int i = 0; i < parameters.length; i++) { + separatedArgs += parameters[i] + intSep; + } + return startSep + key + intSep + textString + intSep + separatedArgs + message + endSep; + } else { + return message; + } + + } + + private static boolean isNeedExportInfo() { + return DeveloperSettings.getInstance().getBoolean(Key.I18N_ONLINE_TRANSLATION); } private static String formatString(String textString, Object... parameters) { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/developer/Key.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/developer/Key.java index 4414c1b8d..1037330e5 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/developer/Key.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/developer/Key.java @@ -43,7 +43,10 @@ public enum Key { * Load language property files every time they are requested. */ I18N_DEFEAT_CACHE("developer.i18n.defeatCache", true), - + /** + * Enable online translations. + */ + I18N_ONLINE_TRANSLATION("developer.i18n.onlineTranslation", true), /** * Enable the I18nLogger to log each string request. */ diff --git a/home/src/main/resources/config/example.developer.properties b/home/src/main/resources/config/example.developer.properties index 67b119f92..7c5ab1ba4 100644 --- a/home/src/main/resources/config/example.developer.properties +++ b/home/src/main/resources/config/example.developer.properties @@ -49,6 +49,7 @@ # developer.i18n.defeatCache = false # developer.i18n.logStringRequests = false +# developer.i18n.onlineTranslation = false #------------------------------------------------------------------------------ diff --git a/webapp/src/main/webapp/js/account/accountProxyItemsPanel.js b/webapp/src/main/webapp/js/account/accountProxyItemsPanel.js index b1ae3cc47..7dd659c7f 100644 --- a/webapp/src/main/webapp/js/account/accountProxyItemsPanel.js +++ b/webapp/src/main/webapp/js/account/accountProxyItemsPanel.js @@ -208,10 +208,14 @@ $(document).ready(function() { $(document).ajaxStart(function(){ - progressImage.removeClass('hidden').css('display', 'inline-block'); + if (progressImage){ + progressImage.removeClass('hidden').css('display', 'inline-block'); + } }); $(document).ajaxStop(function(){ - progressImage.hide().addClass('hidden'); + if (progressImage){ + progressImage.hide().addClass('hidden'); + } }); }); diff --git a/webapp/src/main/webapp/js/developer/FileSaver.js b/webapp/src/main/webapp/js/developer/FileSaver.js new file mode 100644 index 000000000..54fc09004 --- /dev/null +++ b/webapp/src/main/webapp/js/developer/FileSaver.js @@ -0,0 +1,188 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof exports !== "undefined") { + factory(); + } else { + var mod = { + exports: {} + }; + factory(); + global.FileSaver = mod.exports; + } +})(this, function () { + "use strict"; + + /* + * FileSaver.js + * A saveAs() FileSaver implementation. + * + * By Eli Grey, http://eligrey.com + * + * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) + * source : http://purl.eligrey.com/github/FileSaver.js + */ + // The one and only way of getting global scope in all environments + // https://stackoverflow.com/q/3277182/1008999 + var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0; + + function bom(blob, opts) { + if (typeof opts === 'undefined') opts = { + autoBom: false + };else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object'); + opts = { + autoBom: !opts + }; + } // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { + type: blob.type + }); + } + + return blob; + } + + function download(url, name, opts) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'blob'; + + xhr.onload = function () { + saveAs(xhr.response, name, opts); + }; + + xhr.onerror = function () { + console.error('could not download file'); + }; + + xhr.send(); + } + + function corsEnabled(url) { + var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker + + xhr.open('HEAD', url, false); + + try { + xhr.send(); + } catch (e) {} + + return xhr.status >= 200 && xhr.status <= 299; + } // `a.click()` doesn't work for all browsers (#465) + + + function click(node) { + try { + node.dispatchEvent(new MouseEvent('click')); + } catch (e) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + node.dispatchEvent(evt); + } + } // Detect WebView inside a native macOS app by ruling out all browsers + // We just need to check for 'Safari' because all other browsers (besides Firefox) include that too + // https://www.whatismybrowser.com/guides/the-latest-user-agent/macos + + + var isMacOSWebView = /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); + var saveAs = _global.saveAs || ( // probably in some web worker + typeof window !== 'object' || window !== _global ? function saveAs() {} + /* noop */ + // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) { + var URL = _global.URL || _global.webkitURL; + var a = document.createElement('a'); + name = name || blob.name || 'download'; + a.download = name; + a.rel = 'noopener'; // tabnabbing + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob; + + if (a.origin !== location.origin) { + corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); + } else { + click(a); + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob); + setTimeout(function () { + URL.revokeObjectURL(a.href); + }, 4E4); // 40s + + setTimeout(function () { + click(a); + }, 0); + } + } // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { + name = name || blob.name || 'download'; + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts); + } else { + var a = document.createElement('a'); + a.href = blob; + a.target = '_blank'; + setTimeout(function () { + click(a); + }); + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name); + } + } // Fallback to using FileReader and a popup + : function saveAs(blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank'); + + if (popup) { + popup.document.title = popup.document.body.innerText = 'downloading...'; + } + + if (typeof blob === 'string') return download(blob, name, opts); + var force = blob.type === 'application/octet-stream'; + + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; + + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); + + if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader(); + + reader.onloadend = function () { + var url = reader.result; + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); + if (popup) popup.location.href = url;else location = url; + popup = null; // reverse-tabnabbing #460 + }; + + reader.readAsDataURL(blob); + } else { + var URL = _global.URL || _global.webkitURL; + var url = URL.createObjectURL(blob); + if (popup) popup.location = url;else location.href = url; + popup = null; // reverse-tabnabbing #460 + + setTimeout(function () { + URL.revokeObjectURL(url); + }, 4E4); // 40s + } + }); + _global.saveAs = saveAs.saveAs = saveAs; + + if (typeof module !== 'undefined') { + module.exports = saveAs; + } +}); diff --git a/webapp/src/main/webapp/js/developer/developerPanel.js b/webapp/src/main/webapp/js/developer/developerPanel.js index b92f2abdf..d8cf42b47 100644 --- a/webapp/src/main/webapp/js/developer/developerPanel.js +++ b/webapp/src/main/webapp/js/developer/developerPanel.js @@ -52,6 +52,7 @@ function DeveloperPanel(developerAjaxUrl) { document.getElementById("developer_pageContents_logCustomShortView").disabled = !developerEnabled; document.getElementById("developer_i18n_defeatCache").disabled = !developerEnabled; document.getElementById("developer_i18n_logStringRequests").disabled = !developerEnabled; + document.getElementById("developer_i18n_onlineTranslation").disabled = !developerEnabled; document.getElementById("developer_loggingRDFService_enable").disabled = !developerEnabled; document.getElementById("developer_searchIndex_enable").disabled = !developerEnabled; document.getElementById("developer_searchIndex_logIndexingBreakdownTimings").disabled = !developerEnabled; diff --git a/webapp/src/main/webapp/js/developer/translations.js b/webapp/src/main/webapp/js/developer/translations.js new file mode 100644 index 000000000..8fd9075c2 --- /dev/null +++ b/webapp/src/main/webapp/js/developer/translations.js @@ -0,0 +1,484 @@ +class PropAddr { + constructor(node, number, args) { + this.node = node; + this.number = number; + this.args = args; + } +} + +class PropInfo { + constructor(rawText, formText, address) { + this.rawText = rawText; + this.formText = formText; + this.addresses = []; + this.addresses.push(address); + } +} + + var pageTranslations = new Map(); + var overridenTranslations = new Map(); + var startSep = '\u25a4'; + var endSep = '\u25a5'; + var intSep = '\u25a6'; + var resultSep = '\u200b\uFEFF\u200b\uFEFF\u200b'; + var resultSepChars = '\u200b\uFEFF'; + + function saveTranslations() { + var storage = window.localStorage; + var serializedTranslations = JSON.stringify(Array.from(overridenTranslations.entries())); + storage.setItem("overridenTranslations", serializedTranslations); + } + + function readTranslations() { + var storage = window.localStorage; + var serializedTranslations = storage.getItem("overridenTranslations"); + if (serializedTranslations != null) { + overridenTranslations = new Map(JSON.parse(serializedTranslations)); + } + } + + function createTranslationPanel() { + var devPanel = document.getElementById("developerPanel"); + if (devPanel !== null) { + var container = document.createElement("div"); + container.setAttribute("id", "translationPanel"); + container.setAttribute("style", "font-size:0.8em !important;width: 440px; resize: horizontal; \ + overflow: auto; padding: 10px; position: absolute;background-color:#f7dd8a;border:1px dotted;z-index:10000"); + devPanel.parentNode.insertBefore(container, devPanel.nextSibling); + createTranslationControls(container); + createPageTranslationsTable(container); + } + } + + function createTranslationControls(container) { + var controls = document.createElement("div"); + controls.setAttribute("id", "translationControls"); + controls.setAttribute("style", "margin-bottom:8px;") + container.appendChild(controls); + + var cleanButton = document.createElement("button"); + cleanButton.textContent = "Clean All"; + cleanButton.setAttribute("onclick", "cleanTranslationStorage()"); + cleanButton.setAttribute("style", "margin-right:10px;"); + controls.appendChild(cleanButton); + + var exportAllButton = document.createElement("button"); + exportAllButton.textContent = "Export All"; + exportAllButton.setAttribute("onclick", "exportTranslations()"); + exportAllButton.setAttribute("style", "margin-right:10px;"); + controls.appendChild(exportAllButton); + + var updateFileInput = document.createElement("input"); + var updateFileButton = document.createElement("button"); + updateFileButton.setAttribute("style", "margin-right:10px;"); + updateFileInput.type = "file"; + updateFileInput.setAttribute("id", "exportFile"); + updateFileInput.setAttribute("style", "display:none;"); + updateFileInput.setAttribute("accept", ".properties"); + var updateFileLabel = document.createElement("label"); + updateFileLabel.setAttribute("for", "exportFile"); + updateFileLabel.textContent = "Update file"; + updateFileLabel.setAttribute("style", "margin:0px;color:black;") + updateFileButton.appendChild(updateFileLabel); + controls.appendChild(updateFileButton); + controls.appendChild(updateFileInput); + updateFileInput.addEventListener("change", updateTranslationsFile); + + var importFileInput = document.createElement("input"); + var importFileButton = document.createElement("button"); + importFileInput.type = "file"; + importFileInput.setAttribute("style", "display:none;"); + importFileInput.setAttribute("id", "importFile"); + importFileInput.setAttribute("accept", ".properties"); + var importFileLabel = document.createElement("label"); + importFileLabel.setAttribute("style", "margin:0px;color:black;") + importFileLabel.setAttribute("for", "importFile"); + importFileLabel.textContent = "Import from file"; + importFileButton.appendChild(importFileLabel); + controls.appendChild(importFileButton); + controls.appendChild(importFileInput); + importFileInput.addEventListener("change", importTranslationsFromFile); + } + + function cleanTranslationStorage() { + overridenTranslations.clear(); + saveTranslations(); + location.reload(); + } + + function importTranslationsFromFile(e) { + const fileList = e.target.files; + const numFiles = fileList.length; + if (numFiles > 0) { + const file = fileList[0]; + var reader = new FileReader(); + reader.onload = function(progressEvent) { + var lines = this.result.split(/\r\n|\n\r|\n|\r/); + var followLine = false; + var lineKey = null; + for (var i = 0; i < lines.length; i++) { + if (!isCommentLine(lines[i])) { + if (followLine) { + followLine = isNextLineFollow(lines[i]); + var lineValue = lines[i].replace(/\\$/, ""); + overridenTranslations.set(lineKey, overridenTranslations.get(lineKey) + unescapeHTML(lineValue)); + } else { + followLine = isNextLineFollow(lines[i]); + lineKey = getLineKey(lines[i]); + if (lineKey.trim() != "") { + let lineValue = getLineValue(lines[i]); + overridenTranslations.set(lineKey, unescapeHTML(lineValue)); + } + } + } + } + saveTranslations(); + location.reload() + } + reader.readAsText(file); + } + } + + function updateTranslationsFile(e) { + const fileList = e.target.files; + const numFiles = fileList.length; + if (numFiles > 0) { + const file = fileList[0]; + var fileName = e.target.value.split(/(\\|\/)/g).pop(); + var reader = new FileReader(); + reader.onload = function(progressEvent) { + var lines = this.result.split(/\r\n|\n\r|\n|\r/); + var followLine = false; + var keyLineHasChanged = false; + var lineKey = null; + for (var i = 0; i < lines.length; i++) { + if (!isCommentLine(lines[i])) { + if (followLine) { + followLine = isNextLineFollow(lines[i]); + if (keyLineHasChanged) { + //clean line as it's upper content has changed + lines[i] = ""; + if (!followLine) { + keyLineHasChanged = false; + } + } + // skip line + } else { + keyLineHasChanged = false; + followLine = isNextLineFollow(lines[i]); + lineKey = getLineKey(lines[i]); + if (overridenTranslations.has(lineKey)) { + var value = overridenTranslations.get(lineKey); + lines[i] = lineKey + " = " + value; + keyLineHasChanged = true; + } + } + } + } + saveFile(fileName, lines); + } + reader.readAsText(file); + } + } + + function exportTranslations() { + var date = new Date; + var fileName = "export_" + date.toLocaleString() + "_all.properties"; + var lines = []; + for (let [key, value] of overridenTranslations) { + lines.push(key + " = " + escapeHTML(value)); + } + saveFile(fileName, lines); + } + + function saveFile(fileName, lines) { + var blob = new Blob([lines.join("\n")], { type: 'text/plain;charset=utf-8' }); + saveAs(blob, fileName); + } + + function getLineKey(line) { + var matches = line.match(/^\s*[^=\s]*(?=\s*=)/); + var key; + if (matches == null) { + key = ""; + } else { + key = matches[0].trim(); + } + return key; + } + + function getLineValue(line) { + var value = line.replace(/^\s*[^=\s]*\s*=\s*/, ""); + value = value.replace(/\\$/, ""); + return value; + } + + function isNextLineFollow(line) { + return line.match(/\\(\\\\)*$/) != null; + } + + function isCommentLine(line) { + return line.match(/^\s*[#!]/) != null; + } + + function createPageTranslationsTable(container) { + var table = document.createElement("table"); + table.setAttribute("id", "translationsTable"); + table.setAttribute("style", "width:100%;"); + + document.getElementById("translationPanel").appendChild(table); + for (let [key, propInfo] of pageTranslations) { + var tr = document.createElement("tr"); + table.appendChild(tr); + var td1 = document.createElement("td"); + td1.setAttribute("style", " width:1%;white-space:nowrap;"); + var keyText = document.createTextNode(key); + var td2 = document.createElement("td"); + var rawText = document.createElement("input"); + rawText.setAttribute("style", "width:100%; "); + if (overridenTranslations.has(key)) { + rawText.value = overridenTranslations.get(key); + rawText.style.backgroundColor = "#8BAB2E"; + } else { + rawText.value = propInfo.rawText; + } + var rawTextHidden = document.createElement("input"); + rawTextHidden.setAttribute("style", "display:none;"); + rawTextHidden.value = propInfo.rawText; + td1.appendChild(keyText); + tr.appendChild(td1); + td2.appendChild(rawText); + td2.appendChild(rawTextHidden); + tr.appendChild(td2); + rawText.addEventListener("blur", function() { + updateTranslation(this); + }); + } + } + + function updateTranslation(input) { + if (input.value != input.nextSibling.value) { + var key = input.parentElement.previousSibling.firstChild.textContent; + if (input.value == "") { + input.value = input.nextSibling.value; + input.style.backgroundColor = "white"; + overridenTranslations.delete(key); + var value = input.nextSibling.value; + } else { + var value = input.value; + if (pageTranslations.get(key).rawText != escapeHTML(value)) { + input.style.backgroundColor = "#8BAB2E"; + overridenTranslations.set(key, value); + } else { + input.style.backgroundColor = "white"; + overridenTranslations.delete(key); + } + } + saveTranslations(); + if (isJSHasChanged(key)) { + location.reload(); + } + updateTranslationOnPage(key, value); + } + } + + function isJSHasChanged(key) { + var result = false; + if (pageTranslations.has(key)) { + var addresses = pageTranslations.get(key).addresses; + for (let i = 0; i < addresses.length; i++) { + var nodeName = addresses[i].node.nodeName; + if (nodeName == "SCRIPT") { + result = true; + } + } + } + return result; + } + + function updateTranslationOnPage(key, value) { + var propInfo = pageTranslations.get(key); + var addresses = propInfo.addresses; + for (let i = 0; i < addresses.length; i++) { + var node = addresses[i].node; + var number = addresses[i].number + 1; + var content = node.textContent; + var formattedValue = formatTranslation(value, addresses[i].args); + var regexStr = resultSep + "[^" + resultSepChars + "]*" + resultSep; + const regEx = new RegExp("^(?:[^" + resultSepChars + "]*" + regexStr + "){" + number + "}"); + var newString = content.replace(regEx, + function(x) { + return x.replace(RegExp(regexStr + "$"), resultSep + formattedValue + resultSep); + }); + node.textContent = newString; + } + } + + function formatTranslation(value, args) { + for (let i = 0; i < args.length; i++) { + value = value.replaceAll("{" + i + "}", args[i]); + } + return value; + } + + function parseHTMLTranslations() { + var translatedTexts = []; + var translatedAttrs = []; + var xpath = "//attribute::*[contains(., '" + startSep + "')]"; + var result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null); + var node = null; + var node = null; + while (node = result.iterateNext()) { + translatedAttrs.push(node); + parsePropsInNode(node); + } + xpath = "//*[text()[contains(.,'" + startSep + "')]]"; + result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null); + while (node = result.iterateNext()) { + translatedTexts.push(node); + parsePropsInNode(node); + } + readTranslations(); + removePropInfoFromPage(); + updatePageWithOverridenTranslations(); + reloadJS(); + } + + function reloadJS() { + var scriptBlocks = document.getElementsByTagName('script'); + for (let i = 0; i < scriptBlocks.length; i++) { + var scriptBlock = scriptBlocks[i]; + if (scriptBlock.hasAttribute("src")) { + var srcAttr = scriptBlock.getAttribute("src"); + if (!srcAttr.includes("translations.js")) { + if (srcAttr.indexOf("?") == -1) { + srcAttr += "?" + new Date().getTime(); + } else { + srcAttr += "&" + new Date().getTime(); + } + scriptBlock.remove(); + addJSLink(srcAttr); + } + } else { + var content = scriptBlock.textContent; + scriptBlock.remove(); + addInlineJS(content); + } + } + } + + function addInlineJS(content) { + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.textContent = content; + head.appendChild(script); + } + + function addJSLink(fileUrl) { + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = "text/javascript"; + script.src = fileUrl; + head.appendChild(script); + } + + function updatePageWithOverridenTranslations() { + for (let [key, value] of overridenTranslations) { + if (pageTranslations.has(key)) { + updateTranslationOnPage(key, value); + } + } + } + + function removePropInfoFromPage() { + for (let [key, propInfo] of pageTranslations) { + var addresses = propInfo.addresses; + for (let i = 0; i < addresses.length; i++) { + var node = addresses[i].node; + var content = node.textContent; + var regexStr = startSep + "[^" + endSep + "]*" + intSep + "([^" + endSep + intSep + "]*)" + endSep; + const regEx = new RegExp(regexStr, "g"); + var newString = content.replaceAll(regEx, resultSep + "$1" + resultSep); + node.textContent = newString; + } + } + } + + function parsePropsInNode(node) { + + if (node.nodeType === 1){ + var childs = node.childNodes; + childs.forEach(function(child){ + if (child.nodeType === 3){ + parsePropsInTextNode(child); + } + }); + }else if(node.nodeType === 2){ + parsePropsInTextNode(node); + } + + } + function parsePropsInTextNode(node){ + var i = 0; + var textString = node.textContent; + while (textString.indexOf(startSep) >= 0) { + textString = textString.substring(textString.indexOf(startSep) + startSep.length); + var prop = textString.substring(0, textString.indexOf(endSep)); + var address = new PropAddr(node, i, []); + addToPageTranslations(prop, address); + i++; + } + } + + function addToPageTranslations(prop, address) { + var key = prop.substring(0, prop.indexOf(intSep)); + prop = prop.substring(prop.indexOf(intSep) + intSep.length); + var rawText = prop.substring(0, prop.indexOf(intSep)); + prop = prop.substring(prop.indexOf(intSep) + intSep.length); + var textArgs = []; + while (prop.indexOf(intSep) >= 0) { + var textArg = prop.substring(0, prop.indexOf(intSep)); + prop = prop.substring(prop.indexOf(intSep) + intSep.length); + textArgs.push(textArg); + } + address.args = textArgs; + var formText = prop; + var propInfo = null; + if (pageTranslations.has(key)) { + propInfo = pageTranslations.get(key); + propInfo.addresses.push(address); + } else { + propInfo = new PropInfo(rawText, formText, address); + pageTranslations.set(key, propInfo); + } + } + function escapeHTML(input) { + return input + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/‘/g, "‘") + .replace(/’/g, "’"); + } + function unescapeHTML(input) { + return input + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, "\"") + .replace(/'/g, "'") + .replace(/‘/g, "‘") + .replace(/’/g, "’"); + } + +window.addEventListener('load', function() { + setTimeout(function() { + var developerSetting = document.getElementById("developer_i18n_onlineTranslation"); + if (developerSetting !== null && developerSetting.checked) { + parseHTMLTranslations(); + createTranslationPanel(); + } + }, 1000); +}) diff --git a/webapp/src/main/webapp/templates/freemarker/page/partials/developer.ftl b/webapp/src/main/webapp/templates/freemarker/page/partials/developer.ftl index 359935a28..70489d4fd 100644 --- a/webapp/src/main/webapp/templates/freemarker/page/partials/developer.ftl +++ b/webapp/src/main/webapp/templates/freemarker/page/partials/developer.ftl @@ -7,3 +7,5 @@ ${scripts.add('')} ${scripts.add('')} +${scripts.add('')} +${scripts.add('')} diff --git a/webapp/src/main/webapp/templates/freemarker/page/partials/developerPanel.ftl b/webapp/src/main/webapp/templates/freemarker/page/partials/developerPanel.ftl index 4ab08a563..06ad70855 100644 --- a/webapp/src/main/webapp/templates/freemarker/page/partials/developerPanel.ftl +++ b/webapp/src/main/webapp/templates/freemarker/page/partials/developerPanel.ftl @@ -19,6 +19,9 @@ <#elseif !settings.mayControl>

${siteName} is running in developer mode.

+
<#else>
@@ -56,6 +59,8 @@ "Defeat the cache of language property files" /> <@showCheckbox "developer_i18n_logStringRequests", "Log the retrieval of language strings" /> + <@showCheckbox "developer_i18n_onlineTranslation", + "Enable online translation" />