mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 06:43:44 +08:00
9a656e18e9
Allow for a default translation string to be returned when a translation cannot be found. Useful in contexts where there is a known fallback, such as custom emoji group strings.
351 lines
8.8 KiB
JavaScript
351 lines
8.8 KiB
JavaScript
// Instantiate the object
|
|
var I18n = I18n || {};
|
|
|
|
// Set default locale to english
|
|
I18n.defaultLocale = "en";
|
|
|
|
// Set default pluralization rule
|
|
I18n.pluralizationRules = {
|
|
en(n) {
|
|
return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other";
|
|
},
|
|
};
|
|
|
|
// Set current locale to null
|
|
I18n.locale = null;
|
|
I18n.fallbackLocale = 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;
|
|
};
|
|
|
|
I18n.lookup = function (scope, options) {
|
|
options = options || {};
|
|
|
|
var 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 originalScope = scope;
|
|
scope = scope.split(this.SEPARATOR);
|
|
|
|
if (scope.length > 0 && scope[0] !== "js") {
|
|
scope.unshift("js");
|
|
}
|
|
|
|
while (messages && scope.length > 0) {
|
|
currentScope = scope.shift();
|
|
messages = messages[currentScope];
|
|
}
|
|
|
|
if (messages === undefined && this.extras && this.extras[locale]) {
|
|
messages = this.extras[locale];
|
|
scope = originalScope.split(this.SEPARATOR);
|
|
|
|
while (messages && scope.length > 0) {
|
|
currentScope = scope.shift();
|
|
messages = messages[currentScope];
|
|
}
|
|
}
|
|
|
|
if (messages === undefined) {
|
|
messages = options.defaultValue;
|
|
}
|
|
|
|
return messages;
|
|
};
|
|
|
|
// Merge several 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");
|
|
|
|
if (typeof options[name] === "string") {
|
|
// The dollar sign (`$`) is a special replace pattern, and `$&` inserts
|
|
// the matched string. Thus dollars signs need to be escaped with the
|
|
// special pattern `$$`, which inserts a single `$`.
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
|
|
value = options[name].replace(/\$/g, "$$$$");
|
|
} else {
|
|
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);
|
|
options.needsPluralization = typeof options.count === "number";
|
|
options.ignoreMissing = !this.noFallbacks;
|
|
|
|
var translation = this.findTranslation(scope, options);
|
|
|
|
if (!this.noFallbacks) {
|
|
if (!translation && this.fallbackLocale) {
|
|
options.locale = this.fallbackLocale;
|
|
translation = this.findTranslation(scope, options);
|
|
}
|
|
|
|
options.ignoreMissing = false;
|
|
|
|
if (!translation && this.currentLocale() !== this.defaultLocale) {
|
|
options.locale = this.defaultLocale;
|
|
translation = this.findTranslation(scope, options);
|
|
}
|
|
|
|
if (!translation && this.currentLocale() !== "en") {
|
|
options.locale = "en";
|
|
translation = this.findTranslation(scope, options);
|
|
}
|
|
}
|
|
|
|
try {
|
|
return this.interpolate(translation, options);
|
|
} catch (error) {
|
|
return (
|
|
options.translatedFallback ||
|
|
this.missingTranslation(scope, null, options)
|
|
);
|
|
}
|
|
};
|
|
|
|
I18n.findTranslation = function (scope, options) {
|
|
var translation = this.lookup(scope, options);
|
|
|
|
if (translation && options.needsPluralization) {
|
|
translation = this.pluralize(translation, scope, options);
|
|
}
|
|
|
|
return translation;
|
|
};
|
|
|
|
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),
|
|
buffer = [],
|
|
formattedNumber;
|
|
|
|
number = parts[0];
|
|
|
|
while (number.length > 0) {
|
|
var pos = Math.max(0, number.length - 3);
|
|
buffer.unshift(number.slice(pos, pos + 3));
|
|
number = number.slice(0, -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: this.t("number.human.storage_units.format"),
|
|
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 (translation, scope, options) {
|
|
if (typeof translation !== "object") return translation;
|
|
|
|
options = this.prepareOptions(options);
|
|
var count = options.count.toString();
|
|
|
|
var pluralizer = this.pluralizer(options.locale || 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 || options.ignoreMissing) {
|
|
return message;
|
|
}
|
|
|
|
return this.missingTranslation(scope, keys[0]);
|
|
};
|
|
|
|
I18n.missingTranslation = function (scope, key, options) {
|
|
var message = "[" + this.currentLocale() + this.SEPARATOR + scope;
|
|
|
|
if (key) {
|
|
message += this.SEPARATOR + key;
|
|
}
|
|
|
|
if (options && options.hasOwnProperty("count")) {
|
|
message += " count=" + JSON.stringify(options.count);
|
|
}
|
|
|
|
return message + "]";
|
|
};
|
|
|
|
I18n.currentLocale = function () {
|
|
return I18n.locale || I18n.defaultLocale;
|
|
};
|
|
|
|
I18n.enableVerboseLocalization = function () {
|
|
var counter = 0;
|
|
var keys = {};
|
|
var t = I18n.t;
|
|
|
|
I18n.noFallbacks = true;
|
|
|
|
I18n.t = I18n.translate = function (scope, value) {
|
|
var current = keys[scope];
|
|
if (!current) {
|
|
current = keys[scope] = ++counter;
|
|
var message = "Translation #" + current + ": " + scope;
|
|
if (value && Object.keys(value).length > 0) {
|
|
message += ", parameters: " + JSON.stringify(value);
|
|
}
|
|
// eslint-disable-next-line no-console
|
|
console.info(message);
|
|
}
|
|
return t.apply(I18n, [scope, value]) + " (#" + current + ")";
|
|
};
|
|
};
|
|
|
|
I18n.enableVerboseLocalizationSession = function () {
|
|
sessionStorage.setItem("verbose_localization", "true");
|
|
I18n.enableVerboseLocalization();
|
|
|
|
return "Verbose localization is enabled. Close the browser tab to turn it off. Reload the page to see the translation keys.";
|
|
};
|
|
|
|
// shortcuts
|
|
I18n.t = I18n.translate;
|