Bug 1839316: part 5) Guard the "fetchpriority" attribute behind a pref. r=kershaw...
[gecko.git] / devtools / shared / l10n.js
blob6f7b0773fbd91debbb9ec21a50bda03b2bac6ac9
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";
6 const parsePropertiesFile = require("devtools/shared/node-properties/node-properties");
7 const { sprintf } = require("devtools/shared/sprintfjs/sprintf");
9 const propertiesMap = {};
11 // Map used to memoize Number formatters.
12 const numberFormatters = new Map();
13 const getNumberFormatter = function (decimals) {
14   let formatter = numberFormatters.get(decimals);
15   if (!formatter) {
16     // Create and memoize a formatter for the provided decimals
17     formatter = Intl.NumberFormat(undefined, {
18       maximumFractionDigits: decimals,
19       minimumFractionDigits: decimals,
20     });
21     numberFormatters.set(decimals, formatter);
22   }
24   return formatter;
27 /**
28  * Memoized getter for properties files that ensures a given url is only required and
29  * parsed once.
30  *
31  * @param {String} url
32  *        The URL of the properties file to parse.
33  * @return {Object} parsed properties mapped in an object.
34  */
35 function getProperties(url) {
36   if (!propertiesMap[url]) {
37     let propertiesFile;
38     let isNodeEnv = false;
39     try {
40       // eslint-disable-next-line no-undef
41       isNodeEnv = process?.release?.name == "node";
42     } catch (e) {}
44     if (isNodeEnv) {
45       // In Node environment (e.g. when running jest test), we need to prepend the en-US
46       // to the filename in order to have the actual location of the file in source.
47       const lastDelimIndex = url.lastIndexOf("/");
48       const defaultLocaleUrl =
49         url.substring(0, lastDelimIndex) +
50         "/en-US" +
51         url.substring(lastDelimIndex);
53       const path = require("path");
54       // eslint-disable-next-line no-undef
55       const rootPath = path.join(__dirname, "../../");
56       const absoluteUrl = path.join(rootPath, defaultLocaleUrl);
57       const { readFileSync } = require("fs");
58       // In Node environment we directly use readFileSync to get the file content instead
59       // of relying on custom raw loader, like we do in regular environment.
60       propertiesFile = readFileSync(absoluteUrl, { encoding: "utf8" });
61     } else {
62       propertiesFile = require("raw!" + url);
63     }
65     propertiesMap[url] = parsePropertiesFile(propertiesFile);
66   }
68   return propertiesMap[url];
71 /**
72  * Localization convenience methods.
73  *
74  * @param string stringBundleName
75  *        The desired string bundle's name.
76  * @param boolean strict
77  *        (legacy) pass true to force the helper to throw if the l10n id cannot be found.
78  */
79 function LocalizationHelper(stringBundleName, strict = false) {
80   this.stringBundleName = stringBundleName;
81   this.strict = strict;
84 LocalizationHelper.prototype = {
85   /**
86    * L10N shortcut function.
87    *
88    * @param string name
89    * @return string
90    */
91   getStr(name) {
92     const properties = getProperties(this.stringBundleName);
93     if (name in properties) {
94       return properties[name];
95     }
97     if (this.strict) {
98       throw new Error("No localization found for [" + name + "]");
99     }
101     console.error("No localization found for [" + name + "]");
102     return name;
103   },
105   /**
106    * L10N shortcut function.
107    *
108    * @param string name
109    * @param array args
110    * @return string
111    */
112   getFormatStr(name, ...args) {
113     return sprintf(this.getStr(name), ...args);
114   },
116   /**
117    * L10N shortcut function for numeric arguments that need to be formatted.
118    * All numeric arguments will be fixed to 2 decimals and given a localized
119    * decimal separator. Other arguments will be left alone.
120    *
121    * @param string name
122    * @param array args
123    * @return string
124    */
125   getFormatStrWithNumbers(name, ...args) {
126     const newArgs = args.map(x => {
127       return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
128     });
130     return this.getFormatStr(name, ...newArgs);
131   },
133   /**
134    * Converts a number to a locale-aware string format and keeps a certain
135    * number of decimals.
136    *
137    * @param number number
138    *        The number to convert.
139    * @param number decimals [optional]
140    *        Total decimals to keep.
141    * @return string
142    *         The localized number as a string.
143    */
144   numberWithDecimals(number, decimals = 0) {
145     // Do not show decimals for integers.
146     if (number === (number | 0)) {
147       return getNumberFormatter(0).format(number);
148     }
150     // If this isn't a number (and yes, `isNaN(null)` is false), return zero.
151     if (isNaN(number) || number === null) {
152       return getNumberFormatter(0).format(0);
153     }
155     // Localize the number using a memoized Intl.NumberFormat formatter.
156     const localized = getNumberFormatter(decimals).format(number);
158     // Convert the localized number to a number again.
159     const localizedNumber = localized * 1;
160     // Check if this number is now equal to an integer.
161     if (localizedNumber === (localizedNumber | 0)) {
162       // If it is, remove the fraction part.
163       return getNumberFormatter(0).format(localizedNumber);
164     }
166     return localized;
167   },
170 function getPropertiesForNode(node) {
171   const bundleEl = node.closest("[data-localization-bundle]");
172   if (!bundleEl) {
173     return null;
174   }
176   const propertiesUrl = bundleEl.getAttribute("data-localization-bundle");
177   return getProperties(propertiesUrl);
181  * Translate existing markup annotated with data-localization attributes.
183  * How to use data-localization in markup:
185  *   <div data-localization="content=myContent;title=myTitle"/>
187  * The data-localization attribute identifies an element as being localizable.
188  * The content of the attribute is semi-colon separated list of descriptors.
189  * - "title=myTitle" means the "title" attribute should be replaced with the localized
190  *   string corresponding to the key "myTitle".
191  * - "content=myContent" means the text content of the node should be replaced by the
192  *   string corresponding to "myContent"
194  * How to define the localization bundle in markup:
196  *   <div data-localization-bundle="url/to/my.properties">
197  *     [...]
198  *       <div data-localization="content=myContent;title=myTitle"/>
200  * Set the data-localization-bundle on an ancestor of the nodes that should be localized.
202  * @param {Element} root
203  *        The root node to use for the localization
204  */
205 function localizeMarkup(root) {
206   const elements = root.querySelectorAll("[data-localization]");
207   for (const element of elements) {
208     const properties = getPropertiesForNode(element);
209     if (!properties) {
210       continue;
211     }
213     const attributes = element.getAttribute("data-localization").split(";");
214     for (const attribute of attributes) {
215       const [name, value] = attribute.trim().split("=");
216       if (name === "content") {
217         element.textContent = properties[value];
218       } else {
219         element.setAttribute(name, properties[value]);
220       }
221     }
223     element.removeAttribute("data-localization");
224   }
227 const sharedL10N = new LocalizationHelper(
228   "devtools/shared/locales/shared.properties"
232  * A helper for having the same interface as LocalizationHelper, but for more
233  * than one file. Useful for abstracting l10n string locations.
234  */
235 function MultiLocalizationHelper(...stringBundleNames) {
236   const instances = stringBundleNames.map(bundle => {
237     // Use strict = true because the MultiLocalizationHelper logic relies on try/catch
238     // around the underlying LocalizationHelper APIs.
239     return new LocalizationHelper(bundle, true);
240   });
242   // Get all function members of the LocalizationHelper class, making sure we're
243   // not executing any potential getters while doing so, and wrap all the
244   // methods we've found to work on all given string bundles.
245   Object.getOwnPropertyNames(LocalizationHelper.prototype)
246     .map(name => ({
247       name,
248       descriptor: Object.getOwnPropertyDescriptor(
249         LocalizationHelper.prototype,
250         name
251       ),
252     }))
253     .filter(({ descriptor }) => descriptor.value instanceof Function)
254     .forEach(method => {
255       this[method.name] = (...args) => {
256         for (const l10n of instances) {
257           try {
258             return method.descriptor.value.apply(l10n, args);
259           } catch (e) {
260             // Do nothing
261           }
262         }
263         return null;
264       };
265     });
268 exports.LocalizationHelper = LocalizationHelper;
269 exports.localizeMarkup = localizeMarkup;
270 exports.MultiLocalizationHelper = MultiLocalizationHelper;
271 Object.defineProperty(exports, "ELLIPSIS", {
272   get: () => sharedL10N.getStr("ellipsis"),