blob: 8f2f9280858c152761931c158b6e9ae2626ccc12 [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* MessageManager class handles internationalized strings.
*
* Note: chrome.i18n isn't sufficient because...
* 1. There's a bug in chrome that makes it unavailable in iframes:
* https://crbug.com/130200
* 2. The client code may not be packaged in a Chrome extension.
* 3. The client code may be part of a library packaged in a third-party
* Chrome extension.
*
* @param {Array} languages List of languages to load, in the order they
* should be loaded. Newer messages replace older ones. 'en' is
* automatically added as the first language if it is not already present.
*/
lib.MessageManager = function(languages) {
this.languages_ = languages.map((el) => el.replace(/-/g, '_'));
if (this.languages_.indexOf('en') == -1)
this.languages_.unshift('en');
this.messages = {};
};
/**
* Add message definitions to the message manager.
*
* This takes an object of the same format of a Chrome messages.json file. See
* <https://developer.chrome.com/extensions/i18n-messages>.
*/
lib.MessageManager.prototype.addMessages = function(defs) {
for (var key in defs) {
var def = defs[key];
if (!def.placeholders) {
this.messages[key] = def.message;
} else {
// Replace "$NAME$" placeholders with "$1", etc.
this.messages[key] = def.message.replace(
/\$([a-z][^\s\$]+)\$/ig,
function(m, name) {
return defs[key].placeholders[name.toLowerCase()].content;
});
}
}
};
/**
* Load the first available language message bundle.
*
* @param {string} pattern A url pattern containing a "$1" where the locale
* name should go.
* @param {function(Array,Array)} onComplete Function to be called when loading
* is complete. The two arrays are the list of successful and failed
* locale names. If the first parameter is length 0, no locales were
* loaded.
*/
lib.MessageManager.prototype.findAndLoadMessages = function(
pattern, onComplete) {
var languages = this.languages_.concat();
var loaded = [];
var failed = [];
function onLanguageComplete(state) {
if (state) {
loaded = languages.shift();
} else {
failed = languages.shift();
}
if (languages.length) {
tryNextLanguage();
} else {
onComplete(loaded, failed);
}
}
var tryNextLanguage = function() {
this.loadMessages(this.replaceReferences(pattern, languages),
onLanguageComplete.bind(this, true),
onLanguageComplete.bind(this, false));
}.bind(this);
tryNextLanguage();
};
/**
* Load messages from a messages.json file.
*/
lib.MessageManager.prototype.loadMessages = function(
url, onSuccess, opt_onError) {
var xhr = new XMLHttpRequest();
xhr.onload = () => {
this.addMessages(JSON.parse(xhr.responseText));
onSuccess();
};
if (opt_onError)
xhr.onerror = () => opt_onError(xhr);
xhr.open('GET', url);
xhr.send();
};
/**
* Per-instance copy of replaceReferences.
*/
lib.MessageManager.prototype.replaceReferences = lib.i18n.replaceReferences;
/**
* Get a message by name, optionally replacing arguments too.
*
* @param {string} msgname String containing the name of the message to get.
* @param {Array} opt_args Optional array containing the argument values.
* @param {string} opt_default Optional value to return if the msgname is not
* found. Returns the message name by default.
*/
lib.MessageManager.prototype.get = function(msgname, opt_args, opt_default) {
// First try the integrated browser getMessage. We prefer that over any
// registered messages as only the browser supports translations.
let message = lib.i18n.getMessage(msgname, opt_args);
if (message)
return message;
// Look it up in the registered cache next.
message = this.messages[msgname];
if (!message) {
console.warn('Unknown message: ' + msgname);
message = opt_default === undefined ? msgname : opt_default;
// Register the message with the default to avoid multiple warnings.
this.messages[msgname] = message;
}
return this.replaceReferences(message, opt_args);
};
/**
* Process all of the "i18n" html attributes found in a given dom fragment.
*
* The real work happens in processI18nAttribute.
*/
lib.MessageManager.prototype.processI18nAttributes = function(dom) {
var nodes = dom.querySelectorAll('[i18n]');
for (var i = 0; i < nodes.length; i++)
this.processI18nAttribute(nodes[i]);
};
/**
* Process the "i18n" attribute in the specified node.
*
* The i18n attribute should contain a JSON object. The keys are taken to
* be attribute names, and the values are message names.
*
* If the JSON object has a "_" (underscore) key, its value is used as the
* textContent of the element.
*
* Message names can refer to other attributes on the same element with by
* prefixing with a dollar sign. For example...
*
* <button id='send-button'
* i18n='{"aria-label": "$id", "_": "SEND_BUTTON_LABEL"}'
* ></button>
*
* The aria-label message name will be computed as "SEND_BUTTON_ARIA_LABEL".
* Notice that the "id" attribute was appended to the target attribute, and
* the result converted to UPPER_AND_UNDER style.
*/
lib.MessageManager.prototype.processI18nAttribute = function(node) {
// Convert the "lower-and-dashes" attribute names into
// "UPPER_AND_UNDER" style.
const thunk = (str) => str.replace(/-/g, '_').toUpperCase();
var i18n = node.getAttribute('i18n');
if (!i18n)
return;
try {
i18n = JSON.parse(i18n);
} catch (ex) {
console.error('Can\'t parse ' + node.tagName + '#' + node.id + ': ' + i18n);
throw ex;
}
// Load all the messages specified in the i18n attributes.
for (var key in i18n) {
// The node attribute we'll be setting.
var attr = key;
var msgname = i18n[key];
// For "=foo", re-use the referenced message name.
if (msgname.startsWith('=')) {
key = msgname.substr(1);
msgname = i18n[key];
}
// For "$foo", calculate the message name.
if (msgname.startsWith('$'))
msgname = thunk(node.getAttribute(msgname.substr(1)) + '_' + key);
// Finally load the message.
var msg = this.get(msgname);
if (attr == '_')
node.textContent = msg;
else
node.setAttribute(attr, msg);
}
};