main
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);