2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2018 The noVNC Authors
4 * Licensed under MPL 2.0 (see LICENSE.txt)
6 * See README.md for usage and integration instructions.
10 * Localization Utilities
13export class Localizer {
15 // Currently configured language
18 // Current dictionary of translations
19 this._dictionary = undefined;
22 // Configure suitable language based on user preferences
23 async setup(supportedLanguages, baseURL) {
24 this.language = 'en'; // Default: US English
25 this._dictionary = undefined;
27 this._setupLanguage(supportedLanguages);
28 await this._setupDictionary(baseURL);
31 _setupLanguage(supportedLanguages) {
33 * Navigator.languages only available in Chrome (32+) and FireFox (32+)
34 * Fall back to navigator.language for other browsers
37 if (typeof window.navigator.languages == 'object') {
38 userLanguages = window.navigator.languages;
40 userLanguages = [navigator.language || navigator.userLanguage];
43 for (let i = 0;i < userLanguages.length;i++) {
44 const userLang = userLanguages[i]
49 // First pass: perfect match
50 for (let j = 0; j < supportedLanguages.length; j++) {
51 const supLang = supportedLanguages[j]
56 if (userLang[0] !== supLang[0]) {
59 if (userLang[1] !== supLang[1]) {
63 this.language = supportedLanguages[j];
67 // Second pass: English fallback
68 if (userLang[0] === 'en') {
72 // Third pass pass: other fallback
73 for (let j = 0;j < supportedLanguages.length;j++) {
74 const supLang = supportedLanguages[j]
79 if (userLang[0] !== supLang[0]) {
82 if (supLang[1] !== undefined) {
86 this.language = supportedLanguages[j];
92 async _setupDictionary(baseURL) {
94 if (!baseURL.endsWith("/")) {
95 baseURL = baseURL + "/";
101 if (this.language === "en") {
105 let response = await fetch(baseURL + this.language + ".json");
107 throw Error("" + response.status + " " + response.statusText);
110 this._dictionary = await response.json();
113 // Retrieve localised text
115 if (typeof this._dictionary !== 'undefined' &&
116 this._dictionary[id]) {
117 return this._dictionary[id];
123 // Traverses the DOM and translates relevant fields
124 // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate
128 function process(elem, enabled) {
129 function isAnyOf(searchElement, items) {
130 return items.indexOf(searchElement) !== -1;
133 function translateString(str) {
134 // We assume surrounding whitespace, and whitespace around line
135 // breaks is just for source formatting
136 str = str.split("\n").map(s => s.trim()).join(" ").trim();
137 return self.get(str);
140 function translateAttribute(elem, attr) {
141 const str = translateString(elem.getAttribute(attr));
142 elem.setAttribute(attr, str);
145 function translateTextNode(node) {
146 const str = translateString(node.data);
150 if (elem.hasAttribute("translate")) {
151 if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) {
153 } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) {
159 if (elem.hasAttribute("abbr") &&
160 elem.tagName === "TH") {
161 translateAttribute(elem, "abbr");
163 if (elem.hasAttribute("alt") &&
164 isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) {
165 translateAttribute(elem, "alt");
167 if (elem.hasAttribute("download") &&
168 isAnyOf(elem.tagName, ["A", "AREA"])) {
169 translateAttribute(elem, "download");
171 if (elem.hasAttribute("label") &&
172 isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP",
173 "OPTION", "TRACK"])) {
174 translateAttribute(elem, "label");
176 // FIXME: Should update "lang"
177 if (elem.hasAttribute("placeholder") &&
178 isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) {
179 translateAttribute(elem, "placeholder");
181 if (elem.hasAttribute("title")) {
182 translateAttribute(elem, "title");
184 if (elem.hasAttribute("value") &&
185 elem.tagName === "INPUT" &&
186 isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) {
187 translateAttribute(elem, "value");
191 for (let i = 0; i < elem.childNodes.length; i++) {
192 const node = elem.childNodes[i];
193 if (node.nodeType === node.ELEMENT_NODE) {
194 process(node, enabled);
195 } else if (node.nodeType === node.TEXT_NODE && enabled) {
196 translateTextNode(node);
201 process(document.body, true);
205export const l10n = new Localizer();
206export default l10n.get.bind(l10n);