discourse/app/assets/javascripts/locales/i18n.js
Régis Hanol a2c04be718 FIX: eradicate I18n fallback issues 💣
FIX: client's translation overrides were not working when the current locale was missing a key
FIX: ExtraLocalesController.show was not properly handling multiple translations
FIX: JsLocaleHelper#output_locale was not properly handling multiple translations

FIX: ExtraLocalesController.show's spec which was randomly failing
FIX: JsLocaleHelper#output_locale was muting cached translations hashes

REFACTOR: move 'enableVerboseLocalization' to the 'localization' initializer
REFACTOR: remove unused I18n.js methods (getFallbacks, localize, parseDate, toTime, strftime, toCurrency, toPercentage)
REFACTOR: remove all I18n.pluralizationRules and instead use MessageFormat's pluralization rules

TEST: add tests for localization initializer
TEST: add tests for I18n.js
2017-02-24 11:31:21 +01:00

292 lines
7.1 KiB
JavaScript

/*global I18n:true */
// Instantiate the object
var I18n = I18n || {};
// Set default locale to english
I18n.defaultLocale = "en";
// Set default pluralization rule
I18n.pluralizationRules = {
en: function(n) {
return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other";
}
};
// Set current locale to null
I18n.locale = null;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
I18n.SEPARATOR = ".";
I18n.noFallbacks = false;
I18n.isValidNode = function(obj, node, undefined) {
return obj[node] !== null && obj[node] !== undefined;
};
function checkExtras(origScope, sep, extras) {
if (!extras || extras.length === 0) { return; }
for (var i = 0; i < extras.length; i++) {
var messages = extras[i];
scope = origScope.split(sep);
if (scope[0] === 'js') { scope.shift(); }
while (messages && scope.length > 0) {
currentScope = scope.shift();
messages = messages[currentScope];
}
if (messages !== undefined) { return messages; }
}
}
I18n.lookup = function(scope, options) {
options = options || {};
var lookupInitialScope = scope,
translations = this.prepareOptions(I18n.translations),
locale = options.locale || I18n.currentLocale(),
messages = translations[locale] || {},
currentScope;
options = this.prepareOptions(options);
if (typeof scope === "object") {
scope = scope.join(this.SEPARATOR);
}
if (options.scope) {
scope = options.scope.toString() + this.SEPARATOR + scope;
}
var origScope = "" + scope;
scope = origScope.split(this.SEPARATOR);
while (messages && scope.length > 0) {
currentScope = scope.shift();
messages = messages[currentScope];
}
messages = messages || checkExtras(origScope, this.SEPARATOR, this.extras) || options.defaultValue;
return messages;
};
// Merge serveral hash options, checking if value is set before
// overwriting any value. The precedence is from left to right.
//
// I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
// #=> {name: "John Doe", role: "user"}
//
I18n.prepareOptions = function() {
var options = {},
opts,
count = arguments.length;
for (var i = 0; i < count; i++) {
opts = arguments[i];
if (!opts) {
continue;
}
for (var key in opts) {
if (!this.isValidNode(options, key)) {
options[key] = opts[key];
}
}
}
return options;
};
I18n.interpolate = function(message, options) {
options = this.prepareOptions(options);
var matches = message.match(this.PLACEHOLDER),
placeholder,
value,
name;
if (!matches) { return message; }
for (var i = 0; placeholder = matches[i]; i++) {
name = placeholder.replace(this.PLACEHOLDER, "$1");
value = options[name];
if (!this.isValidNode(options, name)) {
value = "[missing " + placeholder + " value]";
}
var regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
message = message.replace(regex, value);
}
return message;
};
I18n.translate = function(scope, options) {
options = this.prepareOptions(options);
var translation = this.lookup(scope, options);
if (!this.noFallbacks) {
if (!translation && this.currentLocale() !== this.defaultLocale) {
options.locale = this.defaultLocale;
translation = this.lookup(scope, options);
}
if (!translation && this.currentLocale() !== 'en') {
options.locale = 'en';
translation = this.lookup(scope, options);
}
}
try {
if (typeof translation === "object") {
if (typeof options.count === "number") {
return this.pluralize(options.count, scope, options);
} else {
return translation;
}
} else {
return this.interpolate(translation, options);
}
} catch (error) {
return this.missingTranslation(scope);
}
};
I18n.toNumber = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.format"),
{precision: 3, separator: this.SEPARATOR, delimiter: ",", strip_insignificant_zeros: false}
);
var negative = number < 0,
string = Math.abs(number).toFixed(options.precision).toString(),
parts = string.split(this.SEPARATOR),
precision,
buffer = [],
formattedNumber;
number = parts[0];
precision = parts[1];
while (number.length > 0) {
buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
number = number.substr(0, number.length -3);
}
formattedNumber = buffer.join(options.delimiter);
if (options.precision > 0) {
formattedNumber += options.separator + parts[1];
}
if (negative) {
formattedNumber = "-" + formattedNumber;
}
if (options.strip_insignificant_zeros) {
var regex = {
separator: new RegExp(options.separator.replace(/\./, "\\.") + "$"),
zeros: /0+$/
};
formattedNumber = formattedNumber
.replace(regex.zeros, "")
.replace(regex.separator, "")
;
}
return formattedNumber;
};
I18n.toHumanSize = function(number, options) {
var kb = 1024,
size = number,
iterations = 0,
unit,
precision;
while (size >= kb && iterations < 4) {
size = size / kb;
iterations += 1;
}
if (iterations === 0) {
unit = this.t("number.human.storage_units.units.byte", {count: size});
precision = 0;
} else {
unit = this.t("number.human.storage_units.units." + [null, "kb", "mb", "gb", "tb"][iterations]);
precision = (size - Math.floor(size) === 0) ? 0 : 1;
}
options = this.prepareOptions(
options,
{precision: precision, format: "%n%u", delimiter: ""}
);
number = this.toNumber(size, options);
number = options.format
.replace("%u", unit)
.replace("%n", number)
;
return number;
};
I18n.pluralizer = function(locale) {
var pluralizer = this.pluralizationRules[locale];
if (pluralizer !== undefined) return pluralizer;
return this.pluralizationRules["en"];
};
I18n.findAndTranslateValidNode = function(keys, translation) {
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (this.isValidNode(translation, key)) return translation[key];
}
return null;
};
I18n.pluralize = function(count, scope, options) {
var translation;
try { translation = this.lookup(scope, options); } catch (error) {}
if (!translation) { return this.missingTranslation(scope); }
options = this.prepareOptions(options);
options.count = count.toString();
var pluralizer = this.pluralizer(this.currentLocale());
var key = pluralizer(Math.abs(count));
var keys = ((typeof key === "object") && (key instanceof Array)) ? key : [key];
var message = this.findAndTranslateValidNode(keys, translation);
if (message == null) message = this.missingTranslation(scope, keys[0]);
return this.interpolate(message, options);
};
I18n.missingTranslation = function(scope, key) {
var message = '[' + this.currentLocale() + this.SEPARATOR + scope;
if (key) { message += this.SEPARATOR + key; }
return message + ']';
};
I18n.currentLocale = function() {
return I18n.locale || I18n.defaultLocale;
};
// shortcuts
I18n.t = I18n.translate;