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..4894f4105 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 @@ -20,7 +20,10 @@ 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 boolean exportInfo = true; 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 +78,22 @@ 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 (exportInfo) { + 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 String formatString(String textString, Object... parameters) { diff --git a/webapp/src/main/webapp/js/translations.js b/webapp/src/main/webapp/js/translations.js new file mode 100644 index 000000000..08b403495 --- /dev/null +++ b/webapp/src/main/webapp/js/translations.js @@ -0,0 +1,431 @@ +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'; + + +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); + } +} + +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 createTranslationsInterface() { + var container = document.createElement("div"); + container.setAttribute("id", "translationsContainer"); + container.setAttribute("style", "font-size:0.8em !important;width: 440px; resize: horizontal; overflow: auto; padding: 10px; position: absolute;background-color:orange;top:0px;border:2px;"); + document.body.appendChild(container); + createTableFromPageTranslations(container); + var table = document.createElement("table"); + + var cleanButton = document.createElement("button"); + cleanButton.textContent = "Clean All"; + cleanButton.setAttribute("onclick","cleanTranslations()"); + container.appendChild(cleanButton); + + var exportFileInput = document.createElement("input"); + exportFileInput.type = "file"; + exportFileInput.setAttribute("id", "exportFile"); + exportFileInput.setAttribute("accept", ".properties"); + var exportFileLabel = document.createElement("label"); + exportFileLabel.setAttribute("for","exportFile"); + exportFileLabel.textContent = "Update file"; + container.appendChild(exportFileLabel); + container.appendChild(exportFileInput); + exportFileInput.addEventListener("change", onExportFileUpload); + + var importFileInput = document.createElement("input"); + importFileInput.type = "file"; + importFileInput.setAttribute("id", "importFile"); + importFileInput.setAttribute("accept", ".properties"); + var importFileLabel = document.createElement("label"); + importFileLabel.setAttribute("for","importFile"); + importFileLabel.textContent = "Import from file"; + container.appendChild(importFileLabel); + container.appendChild(importFileInput); + importFileInput.addEventListener("change", onImportFileUpload); + + //$(document.getElementById("translationsContainer")).draggable(); +} + +function cleanTranslations(){ + overridenTranslations.clear(); + saveTranslations(); + location.reload(); +} + +function onImportFileUpload(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 = goesToNextLine(lines[i]); + var lineValue = lines[i].replace(/\\$/,""); + overridenTranslations.set(lineKey, overridenTranslations.get(lineKey) + lineValue); + } else { + followLine = goesToNextLine(lines[i]); + lineKey = getLineKey(lines[i]); + if (lineKey.trim() != ""){ + let lineValue = getLineValue(lines[i]); + overridenTranslations.set(lineKey,lineValue); + } + } + } + } + saveTranslations(); + location.reload() + } + reader.readAsText(file); + } +} + +function onExportFileUpload(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 = goesToNextLine(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 = goesToNextLine(lines[i]); + lineKey = getLineKey(lines[i]); + if (overridenTranslations.has(lineKey)){ + var value = overridenTranslations.get(lineKey); + lines[i] = lineKey + " = " + value; + keyLineHasChanged = true; + } + } + } + } + exportFile(fileName, lines); + } + reader.readAsText(file); + } + //const selectedFile = document.getElementById('exportFile').files[0]; +} + +function exportFile(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 goesToNextLine(line){ + return line.match(/\\(\\\\)*$/) != null; +} + +function isCommentLine(line){ + return line.match(/^\s*[#!]/) != null; +} + +function createTableFromPageTranslations(container) { + var table = document.createElement("table"); + table.setAttribute("id", "translationsTable"); + table.setAttribute("style", "width:100%;"); + + document.getElementById("translationsContainer").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 = "green"; + } 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() { + onTranslationChange(this); + }); + } +} + +function onTranslationChange(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 != value) { + input.style.backgroundColor = "green"; + overridenTranslations.set(key, value); + } else { + input.style.backgroundColor = "white"; + overridenTranslations.delete(key); + } + } + saveTranslations(); + if (jsHasChanged(key)){ + location.reload(); + } + updateTranslationOnPage(key, value); + } +} + +function jsHasChanged(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 translationsParsing() { + 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); + addPropsFromAttribute(node); + } + xpath = "//*[text()[contains(.,'" + startSep + "')]]"; + result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null); + while (node = result.iterateNext()) { + translatedTexts.push(node); + addPropsFromTextNode(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.endsWith("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"); + //console.log(regexStr); + var newString = content.replaceAll(regEx, resultSep + "$1" + resultSep); + node.textContent = newString; + } + } +} + +function addPropsFromTextNode(node) { + var textString = node.textContent; + var i = 0; + 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, []); + addProp(prop, address); + i++; + } +} + +function addPropsFromAttribute(node) { + var attrString = node.textContent; + var i = 0 + while (attrString.indexOf(startSep) >= 0) { + attrString = attrString.substring(attrString.indexOf(startSep) + startSep.length); + var prop = attrString.substring(0, attrString.indexOf(endSep)); + var address = new PropAddr(node, i, []); + addProp(prop, address); + i++; + } +} + +function addProp(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 = []; + var i = 0; + while (prop.indexOf(intSep) >= 0) { + var textArg = prop.substring(0, prop.indexOf(intSep)) + prop = prop.substring(prop.indexOf(intSep) + intSep.length); + textArgs.push(textArg); + i++; + } + address.args = textArgs; + var formText = prop; + var propInfo = null; + if (pageTranslations.has(key)) { + propInfo = pageTranslations.get(key); + //TODO: CHECK ADDRESS BEFORE ADDING ANOTHER ONE + propInfo.addresses.push(address); + } else { + propInfo = new PropInfo(rawText, formText, address); + pageTranslations.set(key, propInfo) + } + return propInfo; +} + +window.addEventListener('load', function() { + setTimeout(function() { + translationsParsing(); + createTranslationsInterface(); + }, 1000); +}) \ No newline at end of file diff --git a/webapp/src/main/webapp/templates/freemarker/page/partials/headScripts.ftl b/webapp/src/main/webapp/templates/freemarker/page/partials/headScripts.ftl index 4cb139f9d..5dc202214 100644 --- a/webapp/src/main/webapp/templates/freemarker/page/partials/headScripts.ftl +++ b/webapp/src/main/webapp/templates/freemarker/page/partials/headScripts.ftl @@ -9,6 +9,8 @@ var i18nStrings = { + + <#-- script for enabling new HTML5 semantic markup in IE browsers -->