// Instantiate the object var I18n = I18n || {}; // Set default locale to english I18n.defaultLocale = "en"; // Set current locale to null I18n.locale = I18n.locale || null; I18n.lookup = function(scope, options) { var translations = this.prepareOptions(I18n.translations); var messages = translations[I18n.currentLocale()]; options = this.prepareOptions(options); if (!messages) { return; } if (typeof(scope) == "object") { scope = scope.join("."); } if (options.scope) { scope = options.scope.toString() + "." + scope; } scope = scope.split("."); while (scope.length > 0) { var currentScope = scope.shift(); messages = messages[currentScope]; if (!messages) { break; } } if (!messages && options.defaultValue != null && options.defaultValue != undefined) { 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 = {}; var opts; var count = arguments.length; for (var i = 0; i < count; i++) { opts = arguments[i]; if (!opts) { continue; } for (var key in opts) { if (options[key] == undefined || options[key] == null) { options[key] = opts[key]; } } } return options; }; I18n.interpolate = function(message, options) { options = options || {}; var regex = /\{\{(.*?)\}\}/gm; var matches = message.match(regex); if (!matches) { return message; } var placeholder, value, name; for (var i = 0; placeholder = matches[i]; i++) { name = placeholder.replace(/\{\{(.*?)\}\}/gm, "$1"); value = options[name]; if (options[name] == null || options[name] == undefined) { value = "[missing " + placeholder + " value]"; } 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); 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(err) { 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); default: if (scope.match(/^(date|time)/)) { return this.toTime(scope, value); } else { return value.toString(); } } }; I18n.parseDate = function(d) { var matches, date; var year, month, day, hour, min, sec = null; if (matches = d.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ |T](\d{2}):(\d{2}):(\d{2}))?(Z)?/)) { // date/time strings: yyyy-mm-dd hh:mm:ss or yyyy-mm-dd or yyyy-mm-ddThh:mm:ssZ for (var i = 1; i <= 6; i++) { matches[i] = matches[i] == undefined? 0 : parseInt(matches[i], 10); } // month starts on 0 matches[2] = matches[2] - 1; if (matches[7]) { date = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6])); } else if (!isNaN(matches[4])) { date = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]); } else { date = new Date(matches[1], matches[2], matches[3]); } } else if (typeof(d) == "number") { // UNIX timestamp date = new Date(); date.setTime(d); } else { // an arbitrary javascript string date = new Date(); date.setTime(Date.parse(d)); } return date; }; I18n.toTime = function(scope, d) { var date = this.parseDate(d); var 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(); } var weekDay = date.getDay(); var day = date.getDate(); var year = date.getFullYear(); var month = date.getMonth() + 1; var hour = date.getHours(); var hour12 = hour; var meridian = hour > 12? "PM" : "AM"; var secs = date.getSeconds(); var mins = date.getMinutes(); var offset = date.getTimezoneOffset(); var absOffsetHours = Math.floor(Math.abs(offset / 60)); var absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60); var timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? '0' + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? '0' + absOffsetMinutes : absOffsetMinutes); if (hour12 > 12) { hour12 = 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("%-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", 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: ','} ); var string = number.toFixed(options["precision"]).toString(); var parts = string.split("."); number = parts[0]; var precision = parts[1]; var n = []; while (number.length > 0) { n.unshift(number.substr(Math.max(0, number.length - 3), 3)); number = number.substr(0, number.length -3); } var formattedNumber = n.join(options["delimiter"]); if (options["precision"] > 0) { formattedNumber += options["separator"] + parts[1]; } 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.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.pluralize = function(count, scope, options) { var translation = this.lookup(scope, options); var message; options = options || {}; options["count"] = count.toString(); switch(Math.abs(count)) { case 0: message = translation["zero"] || translation["none"] || translation["other"] || this.missingTranslation(scope, "zero"); break; case 1: message = translation["one"] || this.missingTranslation(scope, "one");; break; default: message = translation["other"] || this.missingTranslation(scope, "other");; } return this.interpolate(message, options); }; I18n.missingTranslation = function() { var message = '[missing "' + this.currentLocale(); var count = arguments.length; for (var i = 0; i < count; i++) { message += "." + arguments[i]; } message += '" translation]'; return message; }; I18n.currentLocale = function() { return (I18n.locale || I18n.defaultLocale); }; // shortcuts I18n.t = I18n.translate; I18n.l = I18n.localize; I18n.p = I18n.pluralize;