localization.js 6.5 KB

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