123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const parsePropertiesFile = require("devtools/shared/node-properties/node-properties");
- const { sprintf } = require("devtools/shared/sprintfjs/sprintf");
- const propertiesMap = {};
- // We need some special treatment here for webpack.
- //
- // Webpack doesn't always handle dynamic requires in the best way. In
- // particular if it sees an unrestricted dynamic require, it will try
- // to put all the files it can find into the generated pack. (It can
- // also try a bit to parse the expression passed to require, but in
- // our case this doesn't work, because our call below doesn't provide
- // enough information.)
- //
- // Webpack also provides a way around this: require.context. The idea
- // here is to tell webpack some constraints so that it can include
- // fewer files in the pack.
- //
- // Here we introduce new require contexts for each possible locale
- // directory. Then we use the correct context to load the property
- // file. In the webpack case this results in just the locale property
- // files being included in the pack; and in the devtools case this is
- // a wordy no-op.
- const reqShared = require.context("raw!devtools/shared/locales/",
- true, /^.*\.properties$/);
- const reqClient = require.context("raw!devtools/client/locales/",
- true, /^.*\.properties$/);
- const reqGlobal = require.context("raw!toolkit/locales/",
- true, /^.*\.properties$/);
- /**
- * Memoized getter for properties files that ensures a given url is only required and
- * parsed once.
- *
- * @param {String} url
- * The URL of the properties file to parse.
- * @return {Object} parsed properties mapped in an object.
- */
- function getProperties(url) {
- if (!propertiesMap[url]) {
- // See the comment above about webpack and require contexts. Here
- // we take an input like "devtools/shared/locales/debugger.properties"
- // and decide which context require function to use. Despite the
- // string processing here, in the end a string identical to |url|
- // ends up being passed to "require".
- let index = url.lastIndexOf("/");
- // Turn "mumble/locales/resource.properties" => "./resource.properties".
- let baseName = "." + url.substr(index);
- let reqFn;
- if (/^toolkit/.test(url)) {
- reqFn = reqGlobal;
- } else if (/^devtools\/shared/.test(url)) {
- reqFn = reqShared;
- } else {
- reqFn = reqClient;
- }
- propertiesMap[url] = parsePropertiesFile(reqFn(baseName));
- }
- return propertiesMap[url];
- }
- /**
- * Localization convenience methods.
- *
- * @param string stringBundleName
- * The desired string bundle's name.
- */
- function LocalizationHelper(stringBundleName) {
- this.stringBundleName = stringBundleName;
- }
- LocalizationHelper.prototype = {
- /**
- * L10N shortcut function.
- *
- * @param string name
- * @return string
- */
- getStr: function (name) {
- let properties = getProperties(this.stringBundleName);
- if (name in properties) {
- return properties[name];
- }
- throw new Error("No localization found for [" + name + "]");
- },
- /**
- * L10N shortcut function.
- *
- * @param string name
- * @param array args
- * @return string
- */
- getFormatStr: function (name, ...args) {
- return sprintf(this.getStr(name), ...args);
- },
- /**
- * L10N shortcut function for numeric arguments that need to be formatted.
- * All numeric arguments will be fixed to 2 decimals and given a localized
- * decimal separator. Other arguments will be left alone.
- *
- * @param string name
- * @param array args
- * @return string
- */
- getFormatStrWithNumbers: function (name, ...args) {
- let newArgs = args.map(x => {
- return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
- });
- return this.getFormatStr(name, ...newArgs);
- },
- /**
- * Converts a number to a locale-aware string format and keeps a certain
- * number of decimals.
- *
- * @param number number
- * The number to convert.
- * @param number decimals [optional]
- * Total decimals to keep.
- * @return string
- * The localized number as a string.
- */
- numberWithDecimals: function (number, decimals = 0) {
- // If this is an integer, don't do anything special.
- if (number === (number|0)) {
- return number;
- }
- // If this isn't a number (and yes, `isNaN(null)` is false), return zero.
- if (isNaN(number) || number === null) {
- return "0";
- }
- let localized = number.toLocaleString();
- // If no grouping or decimal separators are available, bail out, because
- // padding with zeros at the end of the string won't make sense anymore.
- if (!localized.match(/[^\d]/)) {
- return localized;
- }
- return number.toLocaleString(undefined, {
- maximumFractionDigits: decimals,
- minimumFractionDigits: decimals
- });
- }
- };
- function getPropertiesForNode(node) {
- let bundleEl = node.closest("[data-localization-bundle]");
- if (!bundleEl) {
- return null;
- }
- let propertiesUrl = bundleEl.getAttribute("data-localization-bundle");
- return getProperties(propertiesUrl);
- }
- /**
- * Translate existing markup annotated with data-localization attributes.
- *
- * How to use data-localization in markup:
- *
- * <div data-localization="content=myContent;title=myTitle"/>
- *
- * The data-localization attribute identifies an element as being localizable.
- * The content of the attribute is semi-colon separated list of descriptors.
- * - "title=myTitle" means the "title" attribute should be replaced with the localized
- * string corresponding to the key "myTitle".
- * - "content=myContent" means the text content of the node should be replaced by the
- * string corresponding to "myContent"
- *
- * How to define the localization bundle in markup:
- *
- * <div data-localization-bundle="url/to/my.properties">
- * [...]
- * <div data-localization="content=myContent;title=myTitle"/>
- *
- * Set the data-localization-bundle on an ancestor of the nodes that should be localized.
- *
- * @param {Element} root
- * The root node to use for the localization
- */
- function localizeMarkup(root) {
- let elements = root.querySelectorAll("[data-localization]");
- for (let element of elements) {
- let properties = getPropertiesForNode(element);
- if (!properties) {
- continue;
- }
- let attributes = element.getAttribute("data-localization").split(";");
- for (let attribute of attributes) {
- let [name, value] = attribute.trim().split("=");
- if (name === "content") {
- element.textContent = properties[value];
- } else {
- element.setAttribute(name, properties[value]);
- }
- }
- element.removeAttribute("data-localization");
- }
- }
- const sharedL10N = new LocalizationHelper("devtools/shared/locales/shared.properties");
- /**
- * A helper for having the same interface as LocalizationHelper, but for more
- * than one file. Useful for abstracting l10n string locations.
- */
- function MultiLocalizationHelper(...stringBundleNames) {
- let instances = stringBundleNames.map(bundle => {
- return new LocalizationHelper(bundle);
- });
- // Get all function members of the LocalizationHelper class, making sure we're
- // not executing any potential getters while doing so, and wrap all the
- // methods we've found to work on all given string bundles.
- Object.getOwnPropertyNames(LocalizationHelper.prototype)
- .map(name => ({
- name: name,
- descriptor: Object.getOwnPropertyDescriptor(LocalizationHelper.prototype,
- name)
- }))
- .filter(({ descriptor }) => descriptor.value instanceof Function)
- .forEach(method => {
- this[method.name] = (...args) => {
- for (let l10n of instances) {
- try {
- return method.descriptor.value.apply(l10n, args);
- } catch (e) {
- // Do nothing
- }
- }
- return null;
- };
- });
- }
- exports.LocalizationHelper = LocalizationHelper;
- exports.localizeMarkup = localizeMarkup;
- exports.MultiLocalizationHelper = MultiLocalizationHelper;
- Object.defineProperty(exports, "ELLIPSIS", { get: () => sharedL10N.getStr("ellipsis") });
|