mirror of
https://github.com/discourse/discourse.git
synced 2024-12-17 13:23:44 +08:00
728845d008
This patch sets I18n.defaultLocale in the Discourse.start() script block (it was formerly always 'en') to SiteSetting.default_locale, and patches translate() to perform fallback to defaultLocale followed by english. Additionally, when enable_verbose_localization() is called, no fallbacks will be performed. It also memoizes the file loading operations in JsLocaleHelper and strips out translations from the fallbacks that are also present in a prefered language, to minimize file size.
557 lines
14 KiB
JavaScript
557 lines
14 KiB
JavaScript
/*global I18n:true */
|
|
|
|
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
|
|
if (!Array.prototype.indexOf) {
|
|
Array.prototype.indexOf = function (searchElement, fromIndex) {
|
|
if ( this === undefined || this === null ) {
|
|
throw new TypeError( '"this" is null or not defined' );
|
|
}
|
|
|
|
var length = this.length >>> 0; // Hack to convert object.length to a UInt32
|
|
|
|
fromIndex = +fromIndex || 0;
|
|
|
|
if (Math.abs(fromIndex) === Infinity) {
|
|
fromIndex = 0;
|
|
}
|
|
|
|
if (fromIndex < 0) {
|
|
fromIndex += length;
|
|
if (fromIndex < 0) {
|
|
fromIndex = 0;
|
|
}
|
|
}
|
|
|
|
for (;fromIndex < length; fromIndex++) {
|
|
if (this[fromIndex] === searchElement) {
|
|
return fromIndex;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
}
|
|
|
|
// Instantiate the object
|
|
var I18n = I18n || {};
|
|
|
|
// Set default locale to english
|
|
I18n.defaultLocale = "en";
|
|
|
|
// Set default handling of translation fallbacks to false
|
|
I18n.fallbacks = false;
|
|
|
|
// Set default separator
|
|
I18n.defaultSeparator = ".";
|
|
|
|
// Set current locale to null
|
|
I18n.locale = null;
|
|
|
|
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
|
|
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
|
|
|
|
I18n.fallbackRules = {};
|
|
|
|
I18n.noFallbacks = false;
|
|
|
|
I18n.pluralizationRules = {
|
|
en: function(n) {
|
|
return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other";
|
|
},
|
|
"zh_CN": function(n) {
|
|
return n === 0 ? ["zero", "none", "other"] : "other";
|
|
},
|
|
"zh_TW": function(n) {
|
|
return n === 0 ? ["zero", "none", "other"] : "other";
|
|
},
|
|
"ko": function(n) {
|
|
return n === 0 ? ["zero", "none", "other"] : "other";
|
|
}
|
|
};
|
|
|
|
I18n.getFallbacks = function(locale) {
|
|
if (locale === I18n.defaultLocale) {
|
|
return [];
|
|
} else if (!I18n.fallbackRules[locale]) {
|
|
var rules = [],
|
|
components = locale.split("-");
|
|
|
|
for (var l = 1; l < components.length; l++) {
|
|
rules.push(components.slice(0, l).join("-"));
|
|
}
|
|
|
|
rules.push(I18n.defaultLocale);
|
|
|
|
I18n.fallbackRules[locale] = rules;
|
|
}
|
|
|
|
return I18n.fallbackRules[locale];
|
|
};
|
|
|
|
I18n.isValidNode = function(obj, node, undefined) {
|
|
return obj[node] !== null && obj[node] !== undefined;
|
|
};
|
|
|
|
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.defaultSeparator);
|
|
}
|
|
|
|
if (options.scope) {
|
|
scope = options.scope.toString() + this.defaultSeparator + scope;
|
|
}
|
|
|
|
scope = scope.split(this.defaultSeparator);
|
|
|
|
while (messages && scope.length > 0) {
|
|
currentScope = scope.shift();
|
|
messages = messages[currentScope];
|
|
}
|
|
|
|
if (!messages) {
|
|
if (I18n.fallbacks) {
|
|
var fallbacks = this.getFallbacks(locale);
|
|
for (var fallback = 0; fallback < fallbacks.length; fallbacks++) {
|
|
messages = I18n.lookup(lookupInitialScope, this.prepareOptions({locale: fallbacks[fallback]}, options));
|
|
if (messages) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!messages && this.isValidNode(options, "defaultValue")) {
|
|
messages = 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);
|
|
// Fallback to the default locale
|
|
if (!translation && this.currentLocale() !== this.defaultLocale && !this.noFallbacks) {
|
|
options.locale = this.defaultLocale;
|
|
translation = this.lookup(scope, options);
|
|
}
|
|
if (!translation && this.currentLocale() !== 'en' && !this.noFallbacks) {
|
|
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.localize = function(scope, value) {
|
|
switch (scope) {
|
|
case "currency":
|
|
return this.toCurrency(value);
|
|
case "number":
|
|
scope = this.lookup("number.format");
|
|
return this.toNumber(value, scope);
|
|
case "percentage":
|
|
return this.toPercentage(value);
|
|
default:
|
|
if (scope.match(/^(date|time)/)) {
|
|
return this.toTime(scope, value);
|
|
} else {
|
|
return value.toString();
|
|
}
|
|
}
|
|
};
|
|
|
|
I18n.parseDate = function(date) {
|
|
var matches, convertedDate;
|
|
|
|
// we have a date, so just return it.
|
|
if (typeof date === "object") {
|
|
return date;
|
|
}
|
|
|
|
// it matches the following formats:
|
|
// yyyy-mm-dd
|
|
// yyyy-mm-dd[ T]hh:mm::ss
|
|
// yyyy-mm-dd[ T]hh:mm::ss
|
|
// yyyy-mm-dd[ T]hh:mm::ssZ
|
|
// yyyy-mm-dd[ T]hh:mm::ss+0000
|
|
//
|
|
matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);
|
|
|
|
if (matches) {
|
|
for (var i = 1; i <= 6; i++) {
|
|
matches[i] = parseInt(matches[i], 10) || 0;
|
|
}
|
|
|
|
// month starts on 0
|
|
matches[2] -= 1;
|
|
|
|
if (matches[7]) {
|
|
convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]));
|
|
} else {
|
|
convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]);
|
|
}
|
|
} else if (typeof date === "number") {
|
|
// UNIX timestamp
|
|
convertedDate = new Date();
|
|
convertedDate.setTime(date);
|
|
} else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
|
|
// a valid javascript format with timezone info
|
|
convertedDate = new Date();
|
|
convertedDate.setTime(Date.parse(date));
|
|
} else {
|
|
// an arbitrary javascript string
|
|
convertedDate = new Date();
|
|
convertedDate.setTime(Date.parse(date));
|
|
}
|
|
|
|
return convertedDate;
|
|
};
|
|
|
|
I18n.toTime = function(scope, d) {
|
|
var date = this.parseDate(d),
|
|
format = this.lookup(scope);
|
|
|
|
if (date.toString().match(/invalid/i)) {
|
|
return date.toString();
|
|
}
|
|
|
|
if (!format) {
|
|
return date.toString();
|
|
}
|
|
|
|
return this.strftime(date, format);
|
|
};
|
|
|
|
I18n.strftime = function(date, format) {
|
|
var options = this.lookup("date");
|
|
|
|
if (!options) {
|
|
return date.toString();
|
|
}
|
|
|
|
options.meridian = options.meridian || ["AM", "PM"];
|
|
|
|
var weekDay = date.getDay(),
|
|
day = date.getDate(),
|
|
year = date.getFullYear(),
|
|
month = date.getMonth() + 1,
|
|
hour = date.getHours(),
|
|
hour12 = hour,
|
|
meridian = hour > 11 ? 1 : 0,
|
|
secs = date.getSeconds(),
|
|
mins = date.getMinutes(),
|
|
offset = date.getTimezoneOffset(),
|
|
absOffsetHours = Math.floor(Math.abs(offset / 60)),
|
|
absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60),
|
|
timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes);
|
|
|
|
if (hour12 > 12) {
|
|
hour12 = hour12 - 12;
|
|
} else if (hour12 === 0) {
|
|
hour12 = 12;
|
|
}
|
|
|
|
var padding = function(n) {
|
|
var s = "0" + n.toString();
|
|
return s.substr(s.length - 2);
|
|
};
|
|
|
|
var f = format;
|
|
f = f.replace("%a", options.abbr_day_names[weekDay]);
|
|
f = f.replace("%A", options.day_names[weekDay]);
|
|
f = f.replace("%b", options.abbr_month_names[month]);
|
|
f = f.replace("%B", options.month_names[month]);
|
|
f = f.replace("%d", padding(day));
|
|
f = f.replace("%e", day);
|
|
f = f.replace("%-d", day);
|
|
f = f.replace("%H", padding(hour));
|
|
f = f.replace("%-H", hour);
|
|
f = f.replace("%I", padding(hour12));
|
|
f = f.replace("%-I", hour12);
|
|
f = f.replace("%m", padding(month));
|
|
f = f.replace("%-m", month);
|
|
f = f.replace("%M", padding(mins));
|
|
f = f.replace("%-M", mins);
|
|
f = f.replace("%p", options.meridian[meridian]);
|
|
f = f.replace("%S", padding(secs));
|
|
f = f.replace("%-S", secs);
|
|
f = f.replace("%w", weekDay);
|
|
f = f.replace("%y", padding(year));
|
|
f = f.replace("%-y", padding(year).replace(/^0+/, ""));
|
|
f = f.replace("%Y", year);
|
|
f = f.replace("%z", timezoneoffset);
|
|
|
|
return f;
|
|
};
|
|
|
|
I18n.toNumber = function(number, options) {
|
|
options = this.prepareOptions(
|
|
options,
|
|
this.lookup("number.format"),
|
|
{precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false}
|
|
);
|
|
|
|
var negative = number < 0,
|
|
string = Math.abs(number).toFixed(options.precision).toString(),
|
|
parts = string.split("."),
|
|
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.toCurrency = function(number, options) {
|
|
options = this.prepareOptions(
|
|
options,
|
|
this.lookup("number.currency.format"),
|
|
this.lookup("number.format"),
|
|
{unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."}
|
|
);
|
|
|
|
number = this.toNumber(number, options);
|
|
number = options.format
|
|
.replace("%u", options.unit)
|
|
.replace("%n", number)
|
|
;
|
|
|
|
return number;
|
|
};
|
|
|
|
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.toPercentage = function(number, options) {
|
|
options = this.prepareOptions(
|
|
options,
|
|
this.lookup("number.percentage.format"),
|
|
this.lookup("number.format"),
|
|
{precision: 3, separator: ".", delimiter: ""}
|
|
);
|
|
|
|
number = this.toNumber(number, options);
|
|
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() + "." + scope;
|
|
if (key) { message += "." + key; }
|
|
return message + ']';
|
|
};
|
|
|
|
I18n.currentLocale = function() {
|
|
return (I18n.locale || I18n.defaultLocale);
|
|
};
|
|
|
|
// shortcuts
|
|
I18n.t = I18n.translate;
|
|
I18n.l = I18n.localize;
|
|
I18n.p = I18n.pluralize;
|
|
|
|
I18n.enable_verbose_localization = 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 (!_.isEmpty(value)) {
|
|
message += ", parameters: " + JSON.stringify(value);
|
|
}
|
|
Em.Logger.info(message);
|
|
}
|
|
return t.apply(I18n, [scope, value]) + " (t" + current + ")";
|
|
};
|
|
};
|
|
|
|
|
|
I18n.verbose_localization_session = function(){
|
|
sessionStorage.setItem("verbose_localization", "true");
|
|
I18n.enable_verbose_localization();
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
if(sessionStorage && sessionStorage.getItem("verbose_localization")) {
|
|
I18n.enable_verbose_localization();
|
|
}
|
|
} catch(e){
|
|
// we don't care really, can happen if cookies disabled
|
|
}
|