]>
Commit | Line | Data |
---|---|---|
6d6f0db0 SR |
1 | /* |
2 | * noVNC: HTML5 VNC client | |
84586c0f | 3 | * Copyright (C) 2018 The noVNC Authors |
6d6f0db0 SR |
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 | ||
0e4808bf JD |
13 | export class Localizer { |
14 | constructor() { | |
15 | // Currently configured language | |
16 | this.language = 'en'; | |
6d6f0db0 | 17 | |
0e4808bf JD |
18 | // Current dictionary of translations |
19 | this.dictionary = undefined; | |
20 | } | |
6d6f0db0 | 21 | |
6d6f0db0 | 22 | // Configure suitable language based on user preferences |
0e4808bf | 23 | setup(supportedLanguages) { |
6d6f0db0 SR |
24 | this.language = 'en'; // Default: US English |
25 | ||
26 | /* | |
27 | * Navigator.languages only available in Chrome (32+) and FireFox (32+) | |
28 | * Fall back to navigator.language for other browsers | |
29 | */ | |
2b5f94fa | 30 | let userLanguages; |
6d6f0db0 SR |
31 | if (typeof window.navigator.languages == 'object') { |
32 | userLanguages = window.navigator.languages; | |
33 | } else { | |
34 | userLanguages = [navigator.language || navigator.userLanguage]; | |
35 | } | |
36 | ||
2b5f94fa JD |
37 | for (let i = 0;i < userLanguages.length;i++) { |
38 | const userLang = userLanguages[i] | |
39 | .toLowerCase() | |
40 | .replace("_", "-") | |
41 | .split("-"); | |
6d6f0db0 SR |
42 | |
43 | // Built-in default? | |
44 | if ((userLang[0] === 'en') && | |
45 | ((userLang[1] === undefined) || (userLang[1] === 'us'))) { | |
46 | return; | |
47 | } | |
48 | ||
49 | // First pass: perfect match | |
2b5f94fa JD |
50 | for (let j = 0; j < supportedLanguages.length; j++) { |
51 | const supLang = supportedLanguages[j] | |
52 | .toLowerCase() | |
53 | .replace("_", "-") | |
54 | .split("-"); | |
6d6f0db0 | 55 | |
426a8c92 | 56 | if (userLang[0] !== supLang[0]) { |
6d6f0db0 | 57 | continue; |
426a8c92 PO |
58 | } |
59 | if (userLang[1] !== supLang[1]) { | |
6d6f0db0 | 60 | continue; |
426a8c92 | 61 | } |
6d6f0db0 SR |
62 | |
63 | this.language = supportedLanguages[j]; | |
64 | return; | |
65 | } | |
66 | ||
67 | // Second pass: fallback | |
2b5f94fa JD |
68 | for (let j = 0;j < supportedLanguages.length;j++) { |
69 | const supLang = supportedLanguages[j] | |
70 | .toLowerCase() | |
71 | .replace("_", "-") | |
72 | .split("-"); | |
6d6f0db0 | 73 | |
426a8c92 | 74 | if (userLang[0] !== supLang[0]) { |
6d6f0db0 | 75 | continue; |
426a8c92 PO |
76 | } |
77 | if (supLang[1] !== undefined) { | |
6d6f0db0 | 78 | continue; |
426a8c92 | 79 | } |
6d6f0db0 SR |
80 | |
81 | this.language = supportedLanguages[j]; | |
82 | return; | |
83 | } | |
84 | } | |
0e4808bf | 85 | } |
6d6f0db0 SR |
86 | |
87 | // Retrieve localised text | |
0e4808bf | 88 | get(id) { |
6d6f0db0 SR |
89 | if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { |
90 | return this.dictionary[id]; | |
91 | } else { | |
92 | return id; | |
93 | } | |
0e4808bf | 94 | } |
6d6f0db0 SR |
95 | |
96 | // Traverses the DOM and translates relevant fields | |
97 | // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate | |
0e4808bf | 98 | translateDOM() { |
2b5f94fa | 99 | const self = this; |
0e4808bf | 100 | |
6d6f0db0 SR |
101 | function process(elem, enabled) { |
102 | function isAnyOf(searchElement, items) { | |
103 | return items.indexOf(searchElement) !== -1; | |
104 | } | |
105 | ||
106 | function translateAttribute(elem, attr) { | |
2b5f94fa | 107 | const str = self.get(elem.getAttribute(attr)); |
6d6f0db0 SR |
108 | elem.setAttribute(attr, str); |
109 | } | |
110 | ||
111 | function translateTextNode(node) { | |
2b5f94fa | 112 | const str = self.get(node.data.trim()); |
6d6f0db0 SR |
113 | node.data = str; |
114 | } | |
115 | ||
116 | if (elem.hasAttribute("translate")) { | |
117 | if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { | |
118 | enabled = true; | |
119 | } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { | |
120 | enabled = false; | |
121 | } | |
122 | } | |
123 | ||
124 | if (enabled) { | |
125 | if (elem.hasAttribute("abbr") && | |
126 | elem.tagName === "TH") { | |
127 | translateAttribute(elem, "abbr"); | |
128 | } | |
129 | if (elem.hasAttribute("alt") && | |
130 | isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { | |
131 | translateAttribute(elem, "alt"); | |
132 | } | |
133 | if (elem.hasAttribute("download") && | |
134 | isAnyOf(elem.tagName, ["A", "AREA"])) { | |
135 | translateAttribute(elem, "download"); | |
136 | } | |
137 | if (elem.hasAttribute("label") && | |
138 | isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", | |
7b536961 | 139 | "OPTION", "TRACK"])) { |
6d6f0db0 SR |
140 | translateAttribute(elem, "label"); |
141 | } | |
142 | // FIXME: Should update "lang" | |
143 | if (elem.hasAttribute("placeholder") && | |
144 | isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) { | |
145 | translateAttribute(elem, "placeholder"); | |
146 | } | |
147 | if (elem.hasAttribute("title")) { | |
148 | translateAttribute(elem, "title"); | |
149 | } | |
150 | if (elem.hasAttribute("value") && | |
151 | elem.tagName === "INPUT" && | |
86b9c473 | 152 | isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) { |
6d6f0db0 SR |
153 | translateAttribute(elem, "value"); |
154 | } | |
155 | } | |
156 | ||
2b5f94fa JD |
157 | for (let i = 0; i < elem.childNodes.length; i++) { |
158 | const node = elem.childNodes[i]; | |
6d6f0db0 SR |
159 | if (node.nodeType === node.ELEMENT_NODE) { |
160 | process(node, enabled); | |
161 | } else if (node.nodeType === node.TEXT_NODE && enabled) { | |
162 | translateTextNode(node); | |
163 | } | |
164 | } | |
165 | } | |
166 | ||
167 | process(document.body, true); | |
0e4808bf JD |
168 | } |
169 | } | |
6d6f0db0 | 170 | |
2b5f94fa | 171 | export const l10n = new Localizer(); |
6d6f0db0 | 172 | export default l10n.get.bind(l10n); |