l10n.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const parsePropertiesFile = require("devtools/shared/node-properties/node-properties");
  6. const { sprintf } = require("devtools/shared/sprintfjs/sprintf");
  7. const propertiesMap = {};
  8. // We need some special treatment here for webpack.
  9. //
  10. // Webpack doesn't always handle dynamic requires in the best way. In
  11. // particular if it sees an unrestricted dynamic require, it will try
  12. // to put all the files it can find into the generated pack. (It can
  13. // also try a bit to parse the expression passed to require, but in
  14. // our case this doesn't work, because our call below doesn't provide
  15. // enough information.)
  16. //
  17. // Webpack also provides a way around this: require.context. The idea
  18. // here is to tell webpack some constraints so that it can include
  19. // fewer files in the pack.
  20. //
  21. // Here we introduce new require contexts for each possible locale
  22. // directory. Then we use the correct context to load the property
  23. // file. In the webpack case this results in just the locale property
  24. // files being included in the pack; and in the devtools case this is
  25. // a wordy no-op.
  26. const reqShared = require.context("raw!devtools/shared/locales/",
  27. true, /^.*\.properties$/);
  28. const reqClient = require.context("raw!devtools/client/locales/",
  29. true, /^.*\.properties$/);
  30. const reqGlobal = require.context("raw!toolkit/locales/",
  31. true, /^.*\.properties$/);
  32. /**
  33. * Memoized getter for properties files that ensures a given url is only required and
  34. * parsed once.
  35. *
  36. * @param {String} url
  37. * The URL of the properties file to parse.
  38. * @return {Object} parsed properties mapped in an object.
  39. */
  40. function getProperties(url) {
  41. if (!propertiesMap[url]) {
  42. // See the comment above about webpack and require contexts. Here
  43. // we take an input like "devtools/shared/locales/debugger.properties"
  44. // and decide which context require function to use. Despite the
  45. // string processing here, in the end a string identical to |url|
  46. // ends up being passed to "require".
  47. let index = url.lastIndexOf("/");
  48. // Turn "mumble/locales/resource.properties" => "./resource.properties".
  49. let baseName = "." + url.substr(index);
  50. let reqFn;
  51. if (/^toolkit/.test(url)) {
  52. reqFn = reqGlobal;
  53. } else if (/^devtools\/shared/.test(url)) {
  54. reqFn = reqShared;
  55. } else {
  56. reqFn = reqClient;
  57. }
  58. propertiesMap[url] = parsePropertiesFile(reqFn(baseName));
  59. }
  60. return propertiesMap[url];
  61. }
  62. /**
  63. * Localization convenience methods.
  64. *
  65. * @param string stringBundleName
  66. * The desired string bundle's name.
  67. */
  68. function LocalizationHelper(stringBundleName) {
  69. this.stringBundleName = stringBundleName;
  70. }
  71. LocalizationHelper.prototype = {
  72. /**
  73. * L10N shortcut function.
  74. *
  75. * @param string name
  76. * @return string
  77. */
  78. getStr: function (name) {
  79. let properties = getProperties(this.stringBundleName);
  80. if (name in properties) {
  81. return properties[name];
  82. }
  83. throw new Error("No localization found for [" + name + "]");
  84. },
  85. /**
  86. * L10N shortcut function.
  87. *
  88. * @param string name
  89. * @param array args
  90. * @return string
  91. */
  92. getFormatStr: function (name, ...args) {
  93. return sprintf(this.getStr(name), ...args);
  94. },
  95. /**
  96. * L10N shortcut function for numeric arguments that need to be formatted.
  97. * All numeric arguments will be fixed to 2 decimals and given a localized
  98. * decimal separator. Other arguments will be left alone.
  99. *
  100. * @param string name
  101. * @param array args
  102. * @return string
  103. */
  104. getFormatStrWithNumbers: function (name, ...args) {
  105. let newArgs = args.map(x => {
  106. return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
  107. });
  108. return this.getFormatStr(name, ...newArgs);
  109. },
  110. /**
  111. * Converts a number to a locale-aware string format and keeps a certain
  112. * number of decimals.
  113. *
  114. * @param number number
  115. * The number to convert.
  116. * @param number decimals [optional]
  117. * Total decimals to keep.
  118. * @return string
  119. * The localized number as a string.
  120. */
  121. numberWithDecimals: function (number, decimals = 0) {
  122. // If this is an integer, don't do anything special.
  123. if (number === (number|0)) {
  124. return number;
  125. }
  126. // If this isn't a number (and yes, `isNaN(null)` is false), return zero.
  127. if (isNaN(number) || number === null) {
  128. return "0";
  129. }
  130. let localized = number.toLocaleString();
  131. // If no grouping or decimal separators are available, bail out, because
  132. // padding with zeros at the end of the string won't make sense anymore.
  133. if (!localized.match(/[^\d]/)) {
  134. return localized;
  135. }
  136. return number.toLocaleString(undefined, {
  137. maximumFractionDigits: decimals,
  138. minimumFractionDigits: decimals
  139. });
  140. }
  141. };
  142. function getPropertiesForNode(node) {
  143. let bundleEl = node.closest("[data-localization-bundle]");
  144. if (!bundleEl) {
  145. return null;
  146. }
  147. let propertiesUrl = bundleEl.getAttribute("data-localization-bundle");
  148. return getProperties(propertiesUrl);
  149. }
  150. /**
  151. * Translate existing markup annotated with data-localization attributes.
  152. *
  153. * How to use data-localization in markup:
  154. *
  155. * <div data-localization="content=myContent;title=myTitle"/>
  156. *
  157. * The data-localization attribute identifies an element as being localizable.
  158. * The content of the attribute is semi-colon separated list of descriptors.
  159. * - "title=myTitle" means the "title" attribute should be replaced with the localized
  160. * string corresponding to the key "myTitle".
  161. * - "content=myContent" means the text content of the node should be replaced by the
  162. * string corresponding to "myContent"
  163. *
  164. * How to define the localization bundle in markup:
  165. *
  166. * <div data-localization-bundle="url/to/my.properties">
  167. * [...]
  168. * <div data-localization="content=myContent;title=myTitle"/>
  169. *
  170. * Set the data-localization-bundle on an ancestor of the nodes that should be localized.
  171. *
  172. * @param {Element} root
  173. * The root node to use for the localization
  174. */
  175. function localizeMarkup(root) {
  176. let elements = root.querySelectorAll("[data-localization]");
  177. for (let element of elements) {
  178. let properties = getPropertiesForNode(element);
  179. if (!properties) {
  180. continue;
  181. }
  182. let attributes = element.getAttribute("data-localization").split(";");
  183. for (let attribute of attributes) {
  184. let [name, value] = attribute.trim().split("=");
  185. if (name === "content") {
  186. element.textContent = properties[value];
  187. } else {
  188. element.setAttribute(name, properties[value]);
  189. }
  190. }
  191. element.removeAttribute("data-localization");
  192. }
  193. }
  194. const sharedL10N = new LocalizationHelper("devtools/shared/locales/shared.properties");
  195. /**
  196. * A helper for having the same interface as LocalizationHelper, but for more
  197. * than one file. Useful for abstracting l10n string locations.
  198. */
  199. function MultiLocalizationHelper(...stringBundleNames) {
  200. let instances = stringBundleNames.map(bundle => {
  201. return new LocalizationHelper(bundle);
  202. });
  203. // Get all function members of the LocalizationHelper class, making sure we're
  204. // not executing any potential getters while doing so, and wrap all the
  205. // methods we've found to work on all given string bundles.
  206. Object.getOwnPropertyNames(LocalizationHelper.prototype)
  207. .map(name => ({
  208. name: name,
  209. descriptor: Object.getOwnPropertyDescriptor(LocalizationHelper.prototype,
  210. name)
  211. }))
  212. .filter(({ descriptor }) => descriptor.value instanceof Function)
  213. .forEach(method => {
  214. this[method.name] = (...args) => {
  215. for (let l10n of instances) {
  216. try {
  217. return method.descriptor.value.apply(l10n, args);
  218. } catch (e) {
  219. // Do nothing
  220. }
  221. }
  222. return null;
  223. };
  224. });
  225. }
  226. exports.LocalizationHelper = LocalizationHelper;
  227. exports.localizeMarkup = localizeMarkup;
  228. exports.MultiLocalizationHelper = MultiLocalizationHelper;
  229. Object.defineProperty(exports, "ELLIPSIS", { get: () => sharedL10N.getStr("ellipsis") });