From edffd9e2f8b67b50e43db351105bad57ed6cfc80 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 14 Nov 2016 22:02:12 +0100 Subject: [PATCH] Also translate HTML elements --- app/ui.js | 3 + core/util.js | 76 +++++++++++++- package.json | 2 + po/Makefile | 7 +- po/noVNC.pot | 254 +++++++++++++++++++++++++++++++++++++++++++++-- po/xgettext-html | 117 ++++++++++++++++++++++ vnc.html | 4 +- 7 files changed, 450 insertions(+), 13 deletions(-) create mode 100755 po/xgettext-html diff --git a/app/ui.js b/app/ui.js index f1e63651..3e8951a0 100644 --- a/app/ui.js +++ b/app/ui.js @@ -96,6 +96,9 @@ var UI; UI.initSettings(); + // Translate the DOM + Util.Localisation.translateDOM(); + // Adapt the interface for touch screen devices if (Util.isTouchDevice) { document.documentElement.classList.add("noVNC_touch"); diff --git a/core/util.js b/core/util.js index 8dd99cb8..5054ee9e 100644 --- a/core/util.js +++ b/core/util.js @@ -440,7 +440,81 @@ Util.Localisation = { } else { return id; } - } + }, + + // Traverses the DOM and translates relevant fields + // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate + translateDOM: function () { + function process(elem, enabled) { + function isAnyOf(searchElement, items) { + return items.indexOf(searchElement) !== -1; + } + + function translateAttribute(elem, attr) { + var str = elem.getAttribute(attr); + str = Util.Localisation.get(str); + elem.setAttribute(attr, str); + } + + function translateTextNode(node) { + var str = node.data.trim(); + str = Util.Localisation.get(str); + node.data = str; + } + + if (elem.hasAttribute("translate")) { + if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { + enabled = true; + } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { + enabled = false; + } + } + + if (enabled) { + if (elem.hasAttribute("abbr") && + elem.tagName === "TH") { + translateAttribute(elem, "abbr"); + } + if (elem.hasAttribute("alt") && + isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { + translateAttribute(elem, "alt"); + } + if (elem.hasAttribute("download") && + isAnyOf(elem.tagName, ["A", "AREA"])) { + translateAttribute(elem, "download"); + } + if (elem.hasAttribute("label") && + isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", + "OPTION", "TRACK"])) { + translateAttribute(elem, "label"); + } + // FIXME: Should update "lang" + if (elem.hasAttribute("placeholder") && + isAnyOf(elem.tagName in ["INPUT", "TEXTAREA"])) { + translateAttribute(elem, "placeholder"); + } + if (elem.hasAttribute("title")) { + translateAttribute(elem, "title"); + } + if (elem.hasAttribute("value") && + elem.tagName === "INPUT" && + isAnyOf(elem.getAttribute("type"), ["reset", "button"])) { + translateAttribute(elem, "value"); + } + } + + for (var i = 0;i < elem.childNodes.length;i++) { + node = elem.childNodes[i]; + if (node.nodeType === node.ELEMENT_NODE) { + process(node, enabled); + } else if (node.nodeType === node.TEXT_NODE && enabled) { + translateTextNode(node); + } + } + } + + process(document.body, true); + }, }; /* [module] export default Util; */ diff --git a/package.json b/package.json index 985f0647..52f76f6d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "chai": "^3.5.0", "commander": "^2.9.0", "fs-extra": "^1.0.0", + "jsdom": "*", "karma": "^1.3.0", "karma-chai": "^0.1.0", "karma-mocha": "^1.3.0", @@ -46,6 +47,7 @@ "karma-sinon": "^1.0.5", "karma-sinon-chai-latest": "^0.1.0", "mocha": "^3.1.2", + "node-getopt": "*", "open": "^0.0.5", "phantomjs-prebuilt": "^2.1.13", "po2json": "*", diff --git a/po/Makefile b/po/Makefile index d09d1645..b64b7638 100644 --- a/po/Makefile +++ b/po/Makefile @@ -17,7 +17,7 @@ update-js: $(JSFILES) ./po2js $< $@ update-pot: - xgettext --output=noVNC.pot \ + xgettext --output=noVNC.js.pot \ --copyright-holder="Various Authors" \ --package-name="noVNC" \ --package-version="$(VERSION)" \ @@ -27,3 +27,8 @@ update-pot: ../app/*.js \ ../core/*.js \ ../core/input/*.js + ./xgettext-html --output=noVNC.html.pot \ + ../vnc.html + msgcat --output-file=noVNC.pot \ + --sort-by-file noVNC.js.pot noVNC.html.pot + rm -f noVNC.js.pot noVNC.html.pot diff --git a/po/noVNC.pot b/po/noVNC.pot index ec6f9603..160a2d47 100644 --- a/po/noVNC.pot +++ b/po/noVNC.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: noVNC 0.6.1\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2016-11-15 08:11+0100\n" +"POT-Creation-Date: 2016-11-15 19:32+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,35 +17,35 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: ../app/ui.js:402 +#: ../app/ui.js:405 msgid "Connecting..." msgstr "" -#: ../app/ui.js:409 +#: ../app/ui.js:412 msgid "Connected (encrypted) to " msgstr "" -#: ../app/ui.js:411 +#: ../app/ui.js:414 msgid "Connected (unencrypted) to " msgstr "" -#: ../app/ui.js:416 +#: ../app/ui.js:419 msgid "Disconnecting..." msgstr "" -#: ../app/ui.js:421 +#: ../app/ui.js:424 msgid "Disconnected" msgstr "" -#: ../app/ui.js:1006 ../core/rfb.js:278 +#: ../app/ui.js:1009 ../core/rfb.js:278 msgid "Must set host and port" msgstr "" -#: ../app/ui.js:1059 +#: ../app/ui.js:1062 msgid "Password is required" msgstr "" -#: ../app/ui.js:1272 +#: ../app/ui.js:1275 msgid "" "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen" msgstr "" @@ -53,3 +53,239 @@ msgstr "" #: ../core/rfb.js:556 msgid "Disconnect timeout" msgstr "" + +#: ../vnc.html:70 +msgid "noVNC encountered an error:" +msgstr "" + +#: ../vnc.html:78 +msgid "Hide/Show the control bar" +msgstr "" + +#: ../vnc.html:85 +msgid "Move/Drag Viewport" +msgstr "" + +#: ../vnc.html:85 +msgid "viewport drag" +msgstr "" + +#: ../vnc.html:91 ../vnc.html:94 ../vnc.html:97 ../vnc.html:100 +msgid "Active Mouse Button" +msgstr "" + +#: ../vnc.html:91 +msgid "No mousebutton" +msgstr "" + +#: ../vnc.html:94 +msgid "Left mousebutton" +msgstr "" + +#: ../vnc.html:97 +msgid "Middle mousebutton" +msgstr "" + +#: ../vnc.html:100 +msgid "Right mousebutton" +msgstr "" + +#: ../vnc.html:103 +msgid "Keyboard" +msgstr "" + +#: ../vnc.html:103 +msgid "Show Keyboard" +msgstr "" + +#: ../vnc.html:110 +msgid "Extra keys" +msgstr "" + +#: ../vnc.html:110 +msgid "Show Extra Keys" +msgstr "" + +#: ../vnc.html:115 +msgid "Ctrl" +msgstr "" + +#: ../vnc.html:115 +msgid "Toggle Ctrl" +msgstr "" + +#: ../vnc.html:118 +msgid "Alt" +msgstr "" + +#: ../vnc.html:118 +msgid "Toggle Alt" +msgstr "" + +#: ../vnc.html:121 +msgid "Send Tab" +msgstr "" + +#: ../vnc.html:121 +msgid "Tab" +msgstr "" + +#: ../vnc.html:124 +msgid "Esc" +msgstr "" + +#: ../vnc.html:124 +msgid "Send Escape" +msgstr "" + +#: ../vnc.html:127 +msgid "Ctrl+Alt+Del" +msgstr "" + +#: ../vnc.html:127 +msgid "Send Ctrl-Alt-Del" +msgstr "" + +#: ../vnc.html:135 +msgid "Shutdown/Reboot" +msgstr "" + +#: ../vnc.html:135 +msgid "Shutdown/Reboot..." +msgstr "" + +#: ../vnc.html:141 +msgid "Power" +msgstr "" + +#: ../vnc.html:143 +msgid "Shutdown" +msgstr "" + +#: ../vnc.html:144 +msgid "Reboot" +msgstr "" + +#: ../vnc.html:145 +msgid "Reset" +msgstr "" + +#: ../vnc.html:150 ../vnc.html:156 +msgid "Clipboard" +msgstr "" + +#: ../vnc.html:160 +msgid "Clear" +msgstr "" + +#: ../vnc.html:166 +msgid "Fullscreen" +msgstr "" + +#: ../vnc.html:171 ../vnc.html:178 +msgid "Settings" +msgstr "" + +#: ../vnc.html:181 +msgid "Encrypt" +msgstr "" + +#: ../vnc.html:184 +msgid "True Color" +msgstr "" + +#: ../vnc.html:187 +msgid "Local Cursor" +msgstr "" + +#: ../vnc.html:190 +msgid "Clip to Window" +msgstr "" + +#: ../vnc.html:193 +msgid "Shared Mode" +msgstr "" + +#: ../vnc.html:196 +msgid "View Only" +msgstr "" + +#: ../vnc.html:200 +msgid "Path:" +msgstr "" + +#: ../vnc.html:204 +msgid "Scaling Mode:" +msgstr "" + +#: ../vnc.html:206 +msgid "None" +msgstr "" + +#: ../vnc.html:207 +msgid "Local Scaling" +msgstr "" + +#: ../vnc.html:208 +msgid "Local Downscaling" +msgstr "" + +#: ../vnc.html:209 +msgid "Remote Resizing" +msgstr "" + +#: ../vnc.html:213 +msgid "Repeater ID:" +msgstr "" + +#: ../vnc.html:219 +msgid "Style:" +msgstr "" + +#: ../vnc.html:221 +msgid "default" +msgstr "" + +#: ../vnc.html:227 +msgid "Logging:" +msgstr "" + +#: ../vnc.html:234 +msgid "Apply" +msgstr "" + +#: ../vnc.html:241 ../vnc.html:271 +msgid "Connect" +msgstr "" + +#: ../vnc.html:244 +msgid "Disconnect" +msgstr "" + +#: ../vnc.html:251 +msgid "Connection" +msgstr "" + +#: ../vnc.html:254 +msgid "Host:" +msgstr "" + +#: ../vnc.html:258 +msgid "Port:" +msgstr "" + +#: ../vnc.html:262 ../vnc.html:290 +msgid "Password:" +msgstr "" + +#: ../vnc.html:266 +msgid "Token:" +msgstr "" + +#: ../vnc.html:294 +msgid "Send Password" +msgstr "" + +#: ../vnc.html:319 +msgid "Canvas not supported." +msgstr "" diff --git a/po/xgettext-html b/po/xgettext-html new file mode 100755 index 00000000..d71822c4 --- /dev/null +++ b/po/xgettext-html @@ -0,0 +1,117 @@ +#!/usr/bin/env node +/* + * xgettext-html: HTML gettext parser + * Copyright (C) 2016 Pierre Ossman + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +var getopt = require('node-getopt'); + +var jsdom = require("jsdom"); +var fs = require("fs"); + +opt = getopt.create([ + ['o' , 'output=FILE' , 'write output to specified file'], + ['h' , 'help' , 'display this help'], +]).bindHelp().parseSystem(); + +var strings = {}; + +function addString(str, location) { + if (str.length == 0) { + return; + } + + if (strings[str] === undefined) { + strings[str] = {} + } + strings[str][location] = null; +} + +// See https://html.spec.whatwg.org/multipage/dom.html#attr-translate +function process(elem, locator, enabled) { + function isAnyOf(searchElement, items) { + return items.indexOf(searchElement) !== -1; + } + + if (elem.hasAttribute("translate")) { + if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { + enabled = true; + } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { + enabled = false; + } + } + + if (enabled) { + if (elem.hasAttribute("abbr") && + elem.tagName === "TH") { + addString(elem.getAttribute("abbr"), locator(elem)); + } + if (elem.hasAttribute("alt") && + isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { + addString(elem.getAttribute("alt"), locator(elem)); + } + if (elem.hasAttribute("download") && + isAnyOf(elem.tagName, ["A", "AREA"])) { + addString(elem.getAttribute("download"), locator(elem)); + } + if (elem.hasAttribute("label") && + isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", + "OPTION", "TRACK"])) { + addString(elem.getAttribute("label"), locator(elem)); + } + if (elem.hasAttribute("placeholder") && + isAnyOf(elem.tagName in ["INPUT", "TEXTAREA"])) { + addString(elem.getAttribute("placeholder"), locator(elem)); + } + if (elem.hasAttribute("title")) { + addString(elem.getAttribute("title"), locator(elem)); + } + if (elem.hasAttribute("value") && + elem.tagName === "INPUT" && + isAnyOf(elem.getAttribute("type"), ["reset", "button"])) { + addString(elem.getAttribute("value"), locator(elem)); + } + } + + for (var i = 0;i < elem.childNodes.length;i++) { + node = elem.childNodes[i]; + if (node.nodeType === node.ELEMENT_NODE) { + process(node, locator, enabled); + } else if (node.nodeType === node.TEXT_NODE && enabled) { + addString(node.data.trim(), locator(node)); + } + } +} + +for (var i = 0;i < opt.argv.length;i++) { + var file; + + fn = opt.argv[i]; + file = fs.readFileSync(fn, "utf8"); + doc = jsdom.jsdom(file); + + locator = function (elem) { + offset = jsdom.nodeLocation(elem).start; + line = file.slice(0, offset).split("\n").length; + return fn + ":" + line; + }; + + process(doc.body, locator, true); +} + +var output = ""; + +for (str in strings) { + output += "#:"; + for (location in strings[str]) { + output += " " + location; + } + output += "\n"; + + output += "msgid " + JSON.stringify(str) + "\n"; + output += "msgstr \"\"\n"; + output += "\n"; +} + +fs.writeFileSync(opt.options.output, output); diff --git a/vnc.html b/vnc.html index ef880d20..85e4ab13 100644 --- a/vnc.html +++ b/vnc.html @@ -79,7 +79,7 @@
-

no
VNC

+

no
VNC

-

no
VNC

+

no
VNC