main
Raw Download raw file
  1/*
  2 * noVNC: HTML5 VNC client
  3 * Copyright (C) 2018 The noVNC authors
  4 * Licensed under MPL 2.0 (see LICENSE.txt)
  5 *
  6 * See README.md for usage and integration instructions.
  7 */
  8
  9/*
 10 * Localization utilities
 11 */
 12
 13export class Localizer {
 14    constructor() {
 15        // Currently configured language
 16        this.language = 'en';
 17
 18        // Current dictionary of translations
 19        this._dictionary = undefined;
 20    }
 21
 22    // Configure suitable language based on user preferences
 23    async setup(supportedLanguages, baseURL) {
 24        this.language = 'en'; // Default: US English
 25        this._dictionary = undefined;
 26
 27        this._setupLanguage(supportedLanguages);
 28        await this._setupDictionary(baseURL);
 29    }
 30
 31    _setupLanguage(supportedLanguages) {
 32        /*
 33         * Navigator.languages only available in Chrome (32+) and FireFox (32+)
 34         * Fall back to navigator.language for other browsers
 35         */
 36        let userLanguages;
 37        if (typeof window.navigator.languages == 'object') {
 38            userLanguages = window.navigator.languages;
 39        } else {
 40            userLanguages = [navigator.language || navigator.userLanguage];
 41        }
 42
 43        for (let i = 0;i < userLanguages.length;i++) {
 44            const userLang = userLanguages[i]
 45                .toLowerCase()
 46                .replace("_", "-")
 47                .split("-");
 48
 49            // First pass: perfect match
 50            for (let j = 0; j < supportedLanguages.length; j++) {
 51                const supLang = supportedLanguages[j]
 52                    .toLowerCase()
 53                    .replace("_", "-")
 54                    .split("-");
 55
 56                if (userLang[0] !== supLang[0]) {
 57                    continue;
 58                }
 59                if (userLang[1] !== supLang[1]) {
 60                    continue;
 61                }
 62
 63                this.language = supportedLanguages[j];
 64                return;
 65            }
 66
 67            // Second pass: English fallback
 68            if (userLang[0] === 'en') {
 69                return;
 70            }
 71
 72            // Third pass pass: other fallback
 73            for (let j = 0;j < supportedLanguages.length;j++) {
 74                const supLang = supportedLanguages[j]
 75                    .toLowerCase()
 76                    .replace("_", "-")
 77                    .split("-");
 78
 79                if (userLang[0] !== supLang[0]) {
 80                    continue;
 81                }
 82                if (supLang[1] !== undefined) {
 83                    continue;
 84                }
 85
 86                this.language = supportedLanguages[j];
 87                return;
 88            }
 89        }
 90    }
 91
 92    async _setupDictionary(baseURL) {
 93        if (baseURL) {
 94            if (!baseURL.endsWith("/")) {
 95                baseURL = baseURL + "/";
 96            }
 97        } else {
 98            baseURL = "";
 99        }
100
101        if (this.language === "en") {
102            return;
103        }
104
105        let response = await fetch(baseURL + this.language + ".json");
106        if (!response.ok) {
107            throw Error("" + response.status + " " + response.statusText);
108        }
109
110        this._dictionary = await response.json();
111    }
112
113    // Retrieve localised text
114    get(id) {
115        if (typeof this._dictionary !== 'undefined' &&
116            this._dictionary[id]) {
117            return this._dictionary[id];
118        } else {
119            return id;
120        }
121    }
122
123    // Traverses the DOM and translates relevant fields
124    // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate
125    translateDOM() {
126        const self = this;
127
128        function process(elem, enabled) {
129            function isAnyOf(searchElement, items) {
130                return items.indexOf(searchElement) !== -1;
131            }
132
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);
138            }
139
140            function translateAttribute(elem, attr) {
141                const str = translateString(elem.getAttribute(attr));
142                elem.setAttribute(attr, str);
143            }
144
145            function translateTextNode(node) {
146                const str = translateString(node.data);
147                node.data = str;
148            }
149
150            if (elem.hasAttribute("translate")) {
151                if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) {
152                    enabled = true;
153                } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) {
154                    enabled = false;
155                }
156            }
157
158            if (enabled) {
159                if (elem.hasAttribute("abbr") &&
160                    elem.tagName === "TH") {
161                    translateAttribute(elem, "abbr");
162                }
163                if (elem.hasAttribute("alt") &&
164                    isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) {
165                    translateAttribute(elem, "alt");
166                }
167                if (elem.hasAttribute("download") &&
168                    isAnyOf(elem.tagName, ["A", "AREA"])) {
169                    translateAttribute(elem, "download");
170                }
171                if (elem.hasAttribute("label") &&
172                    isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP",
173                                           "OPTION", "TRACK"])) {
174                    translateAttribute(elem, "label");
175                }
176                // FIXME: Should update "lang"
177                if (elem.hasAttribute("placeholder") &&
178                    isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) {
179                    translateAttribute(elem, "placeholder");
180                }
181                if (elem.hasAttribute("title")) {
182                    translateAttribute(elem, "title");
183                }
184                if (elem.hasAttribute("value") &&
185                    elem.tagName === "INPUT" &&
186                    isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) {
187                    translateAttribute(elem, "value");
188                }
189            }
190
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);
197                }
198            }
199        }
200
201        process(document.body, true);
202    }
203}
204
205export const l10n = new Localizer();
206export default l10n.get.bind(l10n);