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/. */
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);
16 // Create and memoize a formatter for the provided decimals
17 formatter = Intl.NumberFormat(undefined, {
18 maximumFractionDigits: decimals,
19 minimumFractionDigits: decimals,
21 numberFormatters.set(decimals, formatter);
28 * Memoized getter for properties files that ensures a given url is only required and
32 * The URL of the properties file to parse.
33 * @return {Object} parsed properties mapped in an object.
35 function getProperties(url) {
36 if (!propertiesMap[url]) {
38 let isNodeEnv = false;
40 // eslint-disable-next-line no-undef
41 isNodeEnv = process?.release?.name == "node";
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) +
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" });
62 propertiesFile = require("raw!" + url);
65 propertiesMap[url] = parsePropertiesFile(propertiesFile);
68 return propertiesMap[url];
72 * Localization convenience methods.
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.
79 function LocalizationHelper(stringBundleName, strict = false) {
80 this.stringBundleName = stringBundleName;
84 LocalizationHelper.prototype = {
86 * L10N shortcut function.
92 const properties = getProperties(this.stringBundleName);
93 if (name in properties) {
94 return properties[name];
98 throw new Error("No localization found for [" + name + "]");
101 console.error("No localization found for [" + name + "]");
106 * L10N shortcut function.
112 getFormatStr(name, ...args) {
113 return sprintf(this.getStr(name), ...args);
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.
125 getFormatStrWithNumbers(name, ...args) {
126 const newArgs = args.map(x => {
127 return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
130 return this.getFormatStr(name, ...newArgs);
134 * Converts a number to a locale-aware string format and keeps a certain
135 * number of decimals.
137 * @param number number
138 * The number to convert.
139 * @param number decimals [optional]
140 * Total decimals to keep.
142 * The localized number as a string.
144 numberWithDecimals(number, decimals = 0) {
145 // Do not show decimals for integers.
146 if (number === (number | 0)) {
147 return getNumberFormatter(0).format(number);
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);
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);
170 function getPropertiesForNode(node) {
171 const bundleEl = node.closest("[data-localization-bundle]");
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">
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
205 function localizeMarkup(root) {
206 const elements = root.querySelectorAll("[data-localization]");
207 for (const element of elements) {
208 const properties = getPropertiesForNode(element);
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];
219 element.setAttribute(name, properties[value]);
223 element.removeAttribute("data-localization");
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.
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);
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)
248 descriptor: Object.getOwnPropertyDescriptor(
249 LocalizationHelper.prototype,
253 .filter(({ descriptor }) => descriptor.value instanceof Function)
255 this[method.name] = (...args) => {
256 for (const l10n of instances) {
258 return method.descriptor.value.apply(l10n, args);
268 exports.LocalizationHelper = LocalizationHelper;
269 exports.localizeMarkup = localizeMarkup;
270 exports.MultiLocalizationHelper = MultiLocalizationHelper;
271 Object.defineProperty(exports, "ELLIPSIS", {
272 get: () => sharedL10N.getStr("ellipsis"),