mlanett-i18n-js 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/spec/i18n_spec.rb ADDED
@@ -0,0 +1,205 @@
1
+ require "spec_helper"
2
+
3
+ if File.basename(Rails.root) != "tmp"
4
+ abort <<-TXT
5
+ \e[31;5m
6
+ WARNING: That will remove your project!
7
+ Please go to #{File.expand_path(File.dirname(__FILE__) + "/..")} and run `rake spec`\e[0m
8
+ TXT
9
+ end
10
+
11
+ describe SimplesIdeias::I18n do
12
+ before do
13
+ # Remove temporary directory if already present
14
+ FileUtils.rm_r(Rails.root) if File.exist?(Rails.root)
15
+
16
+ # Create temporary directory to test the files generation
17
+ %w( config public/javascripts ).each do |path|
18
+ FileUtils.mkdir_p Rails.root.join(path)
19
+ end
20
+
21
+ # Overwrite defaut locales path to use fixtures
22
+ I18n.load_path = [File.dirname(__FILE__) + "/resources/locales.yml"]
23
+ end
24
+
25
+ after do
26
+ # Remove temporary directory
27
+ FileUtils.rm_r(Rails.root)
28
+ end
29
+
30
+ it "copies the configuration file" do
31
+ File.should_not be_file(SimplesIdeias::I18n.config_file)
32
+ SimplesIdeias::I18n.setup!
33
+ File.should be_file(SimplesIdeias::I18n.config_file)
34
+ end
35
+
36
+ it "keeps existing configuration file" do
37
+ File.open(SimplesIdeias::I18n.config_file, "w+") {|f| f << "ORIGINAL"}
38
+ SimplesIdeias::I18n.setup!
39
+
40
+ File.read(SimplesIdeias::I18n.config_file).should == "ORIGINAL"
41
+ end
42
+
43
+ it "copies JavaScript library" do
44
+ path = Rails.root.join("public/javascripts/i18n.js")
45
+
46
+ File.should_not be_file(path)
47
+ SimplesIdeias::I18n.setup!
48
+ File.should be_file(path)
49
+ end
50
+
51
+ it "loads configuration file" do
52
+ set_config "default.yml"
53
+ SimplesIdeias::I18n.setup!
54
+
55
+ SimplesIdeias::I18n.config?.should be_true
56
+ SimplesIdeias::I18n.config.should be_kind_of(HashWithIndifferentAccess)
57
+ SimplesIdeias::I18n.config.should_not be_empty
58
+ end
59
+
60
+ it "sets empty hash as configuration when no file is found" do
61
+ SimplesIdeias::I18n.config?.should be_false
62
+ SimplesIdeias::I18n.config.should == {}
63
+ end
64
+
65
+ it "exports messages to default path when configuration file doesn't exist" do
66
+ SimplesIdeias::I18n.export!
67
+ Rails.root.join(SimplesIdeias::I18n.export_dir, "translations.js").should be_file
68
+ end
69
+
70
+ it "exports messages using custom output path" do
71
+ set_config "custom_path.yml"
72
+ SimplesIdeias::I18n.should_receive(:save).with(translations, "public/javascripts/translations/all.js")
73
+ SimplesIdeias::I18n.export!
74
+ end
75
+
76
+ it "sets default scope to * when not specified" do
77
+ set_config "no_scope.yml"
78
+ SimplesIdeias::I18n.should_receive(:save).with(translations, "public/javascripts/no_scope.js")
79
+ SimplesIdeias::I18n.export!
80
+ end
81
+
82
+ it "exports to multiple files" do
83
+ set_config "multiple_files.yml"
84
+ SimplesIdeias::I18n.export!
85
+
86
+ File.should be_file(Rails.root.join("public/javascripts/all.js"))
87
+ File.should be_file(Rails.root.join("public/javascripts/tudo.js"))
88
+ end
89
+
90
+ it "ignores an empty config file" do
91
+ set_config "no_config.yml"
92
+ SimplesIdeias::I18n.export!
93
+ Rails.root.join(SimplesIdeias::I18n.export_dir, "translations.js").should be_file
94
+ end
95
+
96
+ it "exports to a JS file per available locale" do
97
+ set_config "js_file_per_locale.yml"
98
+ SimplesIdeias::I18n.export!
99
+
100
+ File.should be_file(Rails.root.join("public/javascripts/i18n/en.js"))
101
+ end
102
+
103
+ it "exports with multiple conditions" do
104
+ set_config "multiple_conditions.yml"
105
+ SimplesIdeias::I18n.export!
106
+ File.should be_file(Rails.root.join("public/javascripts/bitsnpieces.js"))
107
+ end
108
+
109
+ it "filters translations using scope *.date.formats" do
110
+ result = SimplesIdeias::I18n.filter(translations, "*.date.formats")
111
+ result[:en][:date].keys.should == [:formats]
112
+ result[:fr][:date].keys.should == [:formats]
113
+ end
114
+
115
+ it "filters translations using scope [*.date.formats, *.number.currency.format]" do
116
+ result = SimplesIdeias::I18n.scoped_translations(["*.date.formats", "*.number.currency.format"])
117
+ result[:en].keys.collect(&:to_s).sort.should == %w[ date number ]
118
+ result[:fr].keys.collect(&:to_s).sort.should == %w[ date number ]
119
+ end
120
+
121
+ it "filters translations using multi-star scope" do
122
+ result = SimplesIdeias::I18n.scoped_translations("*.*.formats")
123
+
124
+ result[:en].keys.collect(&:to_s).sort.should == %w[ date time ]
125
+ result[:fr].keys.collect(&:to_s).sort.should == %w[ date time ]
126
+
127
+ result[:en][:date].keys.should == [:formats]
128
+ result[:en][:time].keys.should == [:formats]
129
+
130
+ result[:fr][:date].keys.should == [:formats]
131
+ result[:fr][:time].keys.should == [:formats]
132
+ end
133
+
134
+ it "filters translations using alternated stars" do
135
+ result = SimplesIdeias::I18n.scoped_translations("*.admin.*.title")
136
+
137
+ result[:en][:admin].keys.collect(&:to_s).sort.should == %w[ edit show ]
138
+ result[:fr][:admin].keys.collect(&:to_s).sort.should == %w[ edit show ]
139
+
140
+ result[:en][:admin][:show][:title].should == "Show"
141
+ result[:fr][:admin][:show][:title].should == "Visualiser"
142
+
143
+ result[:en][:admin][:edit][:title].should == "Edit"
144
+ result[:fr][:admin][:edit][:title].should == "Editer"
145
+ end
146
+
147
+ it "performs a deep merge" do
148
+ target = {:a => {:b => 1}}
149
+ result = SimplesIdeias::I18n.deep_merge(target, {:a => {:c => 2}})
150
+
151
+ result[:a].should == {:b => 1, :c => 2}
152
+ end
153
+
154
+ it "performs a banged deep merge" do
155
+ target = {:a => {:b => 1}}
156
+ SimplesIdeias::I18n.deep_merge!(target, {:a => {:c => 2}})
157
+
158
+ target[:a].should == {:b => 1, :c => 2}
159
+ end
160
+
161
+ it "updates the javascript library" do
162
+ FakeWeb.register_uri(:get, "https://raw.github.com/fnando/i18n-js/master/vendor/assets/javascripts/i18n.js", :body => "UPDATED")
163
+
164
+ SimplesIdeias::I18n.setup!
165
+ SimplesIdeias::I18n.update!
166
+ File.read(SimplesIdeias::I18n.javascript_file).should == "UPDATED"
167
+ end
168
+
169
+ describe "#export_dir" do
170
+ it "detects asset pipeline support" do
171
+ SimplesIdeias::I18n.stub :has_asset_pipeline? => true
172
+ SimplesIdeias::I18n.export_dir == "vendor/assets/javascripts"
173
+ end
174
+
175
+ it "detects older Rails" do
176
+ SimplesIdeias::I18n.stub :has_asset_pipeline? => false
177
+ SimplesIdeias::I18n.export_dir.to_s.should == "public/javascripts"
178
+ end
179
+ end
180
+
181
+ describe "#has_asset_pipeline?" do
182
+ it "detects support" do
183
+ Rails.stub_chain(:configuration, :assets, :enabled => true)
184
+ SimplesIdeias::I18n.should have_asset_pipeline
185
+ end
186
+
187
+ it "skips support" do
188
+ SimplesIdeias::I18n.should_not have_asset_pipeline
189
+ end
190
+ end
191
+
192
+ private
193
+ # Set the configuration as the current one
194
+ def set_config(path)
195
+ config = HashWithIndifferentAccess.new(YAML.load_file(File.dirname(__FILE__) + "/resources/#{path}"))
196
+ SimplesIdeias::I18n.stub(:config? => true)
197
+ SimplesIdeias::I18n.stub(:config => config)
198
+ end
199
+
200
+ # Shortcut to SimplesIdeias::I18n.translations
201
+ def translations
202
+ SimplesIdeias::I18n.translations
203
+ end
204
+ end
205
+
@@ -0,0 +1,4 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "public/javascripts/translations/all.js"
4
+ only: "*"
@@ -0,0 +1,4 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "public/javascripts/translations.js"
4
+ only: "*"
@@ -0,0 +1,3 @@
1
+ translations:
2
+ - file: "public/javascripts/i18n/%{locale}.js"
3
+ only: '*'
@@ -0,0 +1,76 @@
1
+ en:
2
+ number:
3
+ format:
4
+ separator: "."
5
+ delimiter: ","
6
+ precision: 3
7
+ currency:
8
+ format:
9
+ format: "%u%n"
10
+ unit: "$"
11
+ separator: "."
12
+ delimiter: ","
13
+ precision: 2
14
+ date:
15
+ formats:
16
+ default: "%Y-%m-%d"
17
+ short: "%b %d"
18
+ long: "%B %d, %Y"
19
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
20
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
21
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
22
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
23
+ # order: [ :year, :month, :day ]
24
+ time:
25
+ formats:
26
+ default: "%a, %d %b %Y %H:%M:%S %z"
27
+ short: "%d %b %H:%M"
28
+ long: "%B %d, %Y %H:%M"
29
+ am: "am"
30
+ pm: "pm"
31
+ admin:
32
+ show:
33
+ title: "Show"
34
+ note: "more details"
35
+ edit:
36
+ title: "Edit"
37
+
38
+ fr:
39
+ date:
40
+ formats:
41
+ default: "%d/%m/%Y"
42
+ short: "%e %b"
43
+ long: "%e %B %Y"
44
+ long_ordinal: "%e %B %Y"
45
+ only_day: "%e"
46
+ day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
47
+ abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
48
+ month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
49
+ abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
50
+ # order: [ :day, :month, :year ]
51
+ time:
52
+ formats:
53
+ default: "%d %B %Y %H:%M"
54
+ time: "%H:%M"
55
+ short: "%d %b %H:%M"
56
+ long: "%A %d %B %Y %H:%M:%S %Z"
57
+ long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
58
+ only_second: "%S"
59
+ am: 'am'
60
+ pm: 'pm'
61
+ number:
62
+ format:
63
+ precision: 3
64
+ separator: ','
65
+ delimiter: ' '
66
+ currency:
67
+ format:
68
+ unit: '€'
69
+ precision: 2
70
+ format: '%n %u'
71
+ admin:
72
+ show:
73
+ title: "Visualiser"
74
+ note: "plus de détails"
75
+ edit:
76
+ title: "Editer"
@@ -0,0 +1,6 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "public/javascripts/bitsnpieces.js"
4
+ only:
5
+ - "*.date.formats"
6
+ - "*.number.currency"
@@ -0,0 +1,6 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "public/javascripts/all.js"
4
+ only: "*"
5
+ - file: "public/javascripts/tudo.js"
6
+ only: "*"
@@ -0,0 +1,2 @@
1
+ #This file has no config
2
+
@@ -0,0 +1,3 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "public/javascripts/no_scope.js"
@@ -0,0 +1,4 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "public/javascripts/simple_scope.js"
4
+ only: "*.date.formats"
@@ -0,0 +1,19 @@
1
+ require "active_support/all"
2
+ require "active_support/version"
3
+ require "active_support/test_case"
4
+ require "ostruct"
5
+ require "pathname"
6
+ require "i18n"
7
+ require "json"
8
+ require "fakeweb"
9
+
10
+ FakeWeb.allow_net_connect = false
11
+
12
+ # Stub Rails.root, so we don"t need to load the whole Rails environment.
13
+ # Be careful! The specified folder will be removed!
14
+ Rails = OpenStruct.new({
15
+ :root => Pathname.new(File.dirname(__FILE__) + "/tmp"),
16
+ :version => "0"
17
+ })
18
+
19
+ require "i18n-js"
@@ -0,0 +1,706 @@
1
+ // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
2
+ if (!Array.prototype.indexOf) {
3
+ Array.prototype.indexOf = function(searchElement /*, fromIndex */) {
4
+ "use strict";
5
+
6
+ if (this === void 0 || this === null) {
7
+ throw new TypeError();
8
+ }
9
+
10
+ var t = Object(this);
11
+ var len = t.length >>> 0;
12
+
13
+ if (len === 0) {
14
+ return -1;
15
+ }
16
+
17
+ var n = 0;
18
+ if (arguments.length > 0) {
19
+ n = Number(arguments[1]);
20
+ if (n !== n) { // shortcut for verifying if it's NaN
21
+ n = 0;
22
+ } else if (n !== 0 && n !== (Infinity) && n !== -(Infinity)) {
23
+ n = (n > 0 || -1) * Math.floor(Math.abs(n));
24
+ }
25
+ }
26
+
27
+ if (n >= len) {
28
+ return -1;
29
+ }
30
+
31
+ var k = n >= 0
32
+ ? n
33
+ : Math.max(len - Math.abs(n), 0);
34
+
35
+ for (; k < len; k++) {
36
+ if (k in t && t[k] === searchElement) {
37
+ return k;
38
+ }
39
+ }
40
+
41
+ return -1;
42
+ };
43
+ }
44
+
45
+ // Instantiate the object
46
+ var I18n = I18n || {};
47
+
48
+ // Set default locale to english
49
+ I18n.defaultLocale = "en";
50
+
51
+ // Set default handling of translation fallbacks to false
52
+ I18n.fallbacks = false;
53
+
54
+ // Set default separator
55
+ I18n.defaultSeparator = ".";
56
+
57
+ // Set current locale to null
58
+ I18n.locale = null;
59
+
60
+ // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
61
+ I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
62
+
63
+ I18n.fallbackRules = {
64
+ };
65
+
66
+ I18n.pluralizationRules = {
67
+ en: function (n) {
68
+ return n == 0 ? ["zero", "none", "other"] : n == 1 ? "one" : "other";
69
+ }
70
+ };
71
+
72
+ I18n.getFallbacks = function(locale) {
73
+ if (locale === I18n.defaultLocale) {
74
+ return [];
75
+ } else if (!I18n.fallbackRules[locale]) {
76
+ var rules = []
77
+ , components = locale.split("-");
78
+
79
+ for (var l = 1; l < components.length; l++) {
80
+ rules.push(components.slice(0, l).join("-"));
81
+ }
82
+
83
+ rules.push(I18n.defaultLocale);
84
+
85
+ I18n.fallbackRules[locale] = rules;
86
+ }
87
+
88
+ return I18n.fallbackRules[locale];
89
+ }
90
+
91
+ I18n.isValidNode = function(obj, node, undefined) {
92
+ return obj[node] !== null && obj[node] !== undefined;
93
+ };
94
+
95
+ I18n.lookup = function(scope, options) {
96
+ var options = options || {}
97
+ , lookupInitialScope = scope
98
+ , translations = this.prepareOptions(I18n.translations)
99
+ , locale = options.locale || I18n.currentLocale()
100
+ , messages = translations[locale] || {}
101
+ , options = this.prepareOptions(options)
102
+ , currentScope
103
+ ;
104
+
105
+ if (typeof(scope) == "object") {
106
+ scope = scope.join(this.defaultSeparator);
107
+ }
108
+
109
+ if (options.scope) {
110
+ scope = options.scope.toString() + this.defaultSeparator + scope;
111
+ }
112
+
113
+ scope = scope.split(this.defaultSeparator);
114
+
115
+ while (messages !== undefined && scope.length > 0) {
116
+ currentScope = scope.shift();
117
+ messages = messages[currentScope];
118
+ }
119
+
120
+ if (messages === undefined) {
121
+ if (I18n.fallbacks) {
122
+ var fallbacks = this.getFallbacks(locale);
123
+ for (var fallback = 0; fallback < fallbacks.length; fallbacks++) {
124
+ messages = I18n.lookup(lookupInitialScope, this.prepareOptions({locale: fallbacks[fallback]}, options));
125
+ if (messages) {
126
+ break;
127
+ }
128
+ }
129
+ }
130
+
131
+ if (messages === undefined && this.isValidNode(options, "defaultValue")) {
132
+ messages = options.defaultValue;
133
+ }
134
+ }
135
+
136
+ return messages;
137
+ };
138
+
139
+ // Merge serveral hash options, checking if value is set before
140
+ // overwriting any value. The precedence is from left to right.
141
+ //
142
+ // I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
143
+ // #=> {name: "John Doe", role: "user"}
144
+ //
145
+ I18n.prepareOptions = function() {
146
+ var options = {}
147
+ , opts
148
+ , count = arguments.length
149
+ ;
150
+
151
+ for (var i = 0; i < count; i++) {
152
+ opts = arguments[i];
153
+
154
+ if (!opts) {
155
+ continue;
156
+ }
157
+
158
+ for (var key in opts) {
159
+ if (!this.isValidNode(options, key)) {
160
+ options[key] = opts[key];
161
+ }
162
+ }
163
+ }
164
+
165
+ return options;
166
+ };
167
+
168
+ I18n.interpolate = function(message, options) {
169
+ options = this.prepareOptions(options);
170
+ var matches = message.match(this.PLACEHOLDER)
171
+ , placeholder
172
+ , value
173
+ , name
174
+ ;
175
+
176
+ if (!matches) {
177
+ return message;
178
+ }
179
+
180
+ for (var i = 0; placeholder = matches[i]; i++) {
181
+ name = placeholder.replace(this.PLACEHOLDER, "$1");
182
+
183
+ value = options[name];
184
+
185
+ if (!this.isValidNode(options, name)) {
186
+ value = "[missing " + placeholder + " value]";
187
+ }
188
+
189
+ regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
190
+ message = message.replace(regex, value);
191
+ }
192
+
193
+ return message;
194
+ };
195
+
196
+ I18n.translate = function(scope, options) {
197
+ options = this.prepareOptions(options);
198
+ var translation = this.lookup(scope, options);
199
+
200
+ try {
201
+ if (typeof(translation) == "object" || typeof(translation) == "number" || typeof(translation) == "boolean") {
202
+ if (typeof(options.count) == "number") {
203
+ return this.pluralize(options.count, scope, options);
204
+ } else {
205
+ return translation;
206
+ }
207
+ } else {
208
+ return this.interpolate(translation, options);
209
+ }
210
+ } catch(err) {
211
+ return this.missingTranslation(scope);
212
+ }
213
+ };
214
+
215
+ I18n.localize = function(scope, value) {
216
+ switch (scope) {
217
+ case "currency":
218
+ return this.toCurrency(value);
219
+ case "number":
220
+ scope = this.lookup("number.format");
221
+ return this.toNumber(value, scope);
222
+ case "percentage":
223
+ return this.toPercentage(value);
224
+ default:
225
+ if (scope.match(/^(date|time)/)) {
226
+ return this.toTime(scope, value);
227
+ } else {
228
+ return value.toString();
229
+ }
230
+ }
231
+ };
232
+
233
+ I18n.parseDate = function(date) {
234
+ var matches, convertedDate;
235
+
236
+ // we have a date, so just return it.
237
+ if (typeof(date) == "object") {
238
+ return date;
239
+ };
240
+
241
+ // it matches the following formats:
242
+ // yyyy-mm-dd
243
+ // yyyy-mm-dd[ T]hh:mm::ss
244
+ // yyyy-mm-dd[ T]hh:mm::ss
245
+ // yyyy-mm-dd[ T]hh:mm::ssZ
246
+ // yyyy-mm-dd[ T]hh:mm::ss+0000
247
+ //
248
+ matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);
249
+
250
+ if (matches) {
251
+ for (var i = 1; i <= 6; i++) {
252
+ matches[i] = parseInt(matches[i], 10) || 0;
253
+ }
254
+
255
+ // month starts on 0
256
+ matches[2] -= 1;
257
+
258
+ if (matches[7]) {
259
+ convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]));
260
+ } else {
261
+ convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]);
262
+ }
263
+ } else if (typeof(date) == "number") {
264
+ // UNIX timestamp
265
+ convertedDate = new Date();
266
+ convertedDate.setTime(date);
267
+ } else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
268
+ // a valid javascript format with timezone info
269
+ convertedDate = new Date();
270
+ convertedDate.setTime(Date.parse(date))
271
+ } else {
272
+ // an arbitrary javascript string
273
+ convertedDate = new Date();
274
+ convertedDate.setTime(Date.parse(date));
275
+ }
276
+
277
+ return convertedDate;
278
+ };
279
+
280
+ I18n.toTime = function(scope, d) {
281
+ var date = this.parseDate(d)
282
+ , format = this.lookup(scope)
283
+ ;
284
+
285
+ if (date.toString().match(/invalid/i)) {
286
+ return date.toString();
287
+ }
288
+
289
+ if (!format) {
290
+ return date.toString();
291
+ }
292
+
293
+ return this.strftime(date, format);
294
+ };
295
+
296
+ I18n.strftime = function(date, format) {
297
+ var options = this.lookup("date");
298
+
299
+ if (!options) {
300
+ return date.toString();
301
+ }
302
+
303
+ options.meridian = options.meridian || ["AM", "PM"];
304
+
305
+ var weekDay = date.getDay()
306
+ , day = date.getDate()
307
+ , year = date.getFullYear()
308
+ , month = date.getMonth() + 1
309
+ , hour = date.getHours()
310
+ , hour12 = hour
311
+ , meridian = hour > 11 ? 1 : 0
312
+ , secs = date.getSeconds()
313
+ , mins = date.getMinutes()
314
+ , offset = date.getTimezoneOffset()
315
+ , absOffsetHours = Math.floor(Math.abs(offset / 60))
316
+ , absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60)
317
+ , timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes)
318
+ ;
319
+
320
+ if (hour12 > 12) {
321
+ hour12 = hour12 - 12;
322
+ } else if (hour12 === 0) {
323
+ hour12 = 12;
324
+ }
325
+
326
+ var padding = function(n) {
327
+ var s = "0" + n.toString();
328
+ return s.substr(s.length - 2);
329
+ };
330
+
331
+ var f = format;
332
+ f = f.replace("%a", options.abbr_day_names[weekDay]);
333
+ f = f.replace("%A", options.day_names[weekDay]);
334
+ f = f.replace("%b", options.abbr_month_names[month]);
335
+ f = f.replace("%B", options.month_names[month]);
336
+ f = f.replace("%d", padding(day));
337
+ f = f.replace("%e", day);
338
+ f = f.replace("%-d", day);
339
+ f = f.replace("%H", padding(hour));
340
+ f = f.replace("%-H", hour);
341
+ f = f.replace("%I", padding(hour12));
342
+ f = f.replace("%-I", hour12);
343
+ f = f.replace("%m", padding(month));
344
+ f = f.replace("%-m", month);
345
+ f = f.replace("%M", padding(mins));
346
+ f = f.replace("%-M", mins);
347
+ f = f.replace("%p", options.meridian[meridian]);
348
+ f = f.replace("%S", padding(secs));
349
+ f = f.replace("%-S", secs);
350
+ f = f.replace("%w", weekDay);
351
+ f = f.replace("%y", padding(year));
352
+ f = f.replace("%-y", padding(year).replace(/^0+/, ""));
353
+ f = f.replace("%Y", year);
354
+ f = f.replace("%z", timezoneoffset);
355
+
356
+ return f;
357
+ };
358
+
359
+ // Converts Ruby date and time format specifiers to jquery datepicker and timepicker formats
360
+ // Ruby: http://www.ruby-doc.org/core-1.9.3/Time.html#method-i-strftime
361
+ // Datepicker: http://docs.jquery.com/UI/Datepicker/formatDate
362
+ // Timepicker: http://trentrichardson.com/examples/timepicker/
363
+ I18n.rbToJsDateFormat = function(scope, options) {
364
+ var f = this.lookup(scope, options);
365
+
366
+ if(f == null) {
367
+ return "";
368
+ }
369
+
370
+ /* Date and Time Formats */
371
+ f = f.replace(/%[\^]?a/, "D");
372
+ f = f.replace(/%[\^]?A/, "DD");
373
+ f = f.replace(/%[\^]?b/, "M");
374
+ f = f.replace(/%[\^]?B/, "MM");
375
+ f = f.replace("%d", "dd");
376
+ f = f.replace("%-d", "d");
377
+ f = f.replace("%e", "d");
378
+ f = f.replace("%H", "HH");
379
+ f = f.replace("%-H", "H");
380
+ f = f.replace("%k", "H");
381
+ f = f.replace("%I", "hh");
382
+ f = f.replace("%-I", "h");
383
+ f = f.replace("%L", "l");
384
+ f = f.replace("%l", "h");
385
+ f = f.replace("%m", "mm");
386
+ f = f.replace("%-m", "m");
387
+ f = f.replace("%M", "mm");
388
+ f = f.replace("%-M", "m");
389
+ f = f.replace("%N", ""); // Not supported
390
+ f = f.replace("%p", "TT");
391
+ f = f.replace("%P", "tt");
392
+ f = f.replace("%S", "ss");
393
+ f = f.replace("%-S", "s");
394
+ f = f.replace("%u", ""); // Not supported
395
+ f = f.replace("%w", ""); // Not supported
396
+ f = f.replace("%s", ""); // Not supported
397
+ f = f.replace("%y", "y");
398
+ f = f.replace("%-y", "y");
399
+ f = f.replace("%Y", "yy");
400
+ f = f.replace(/%(:{0,2}z|Z)/, "z");
401
+
402
+ /* Combinations */
403
+ f = f.replace("%c", "D M d HH:mm:ss yy");
404
+ f = f.replace("%D", "mm:dd:yy");
405
+ f = f.replace("%F", "yy-mm-dd");
406
+ f = f.replace("%v", "d-M-yy");
407
+ f = f.replace("%x", "mm:dd:yy");
408
+ f = f.replace("%X", "HH:mm:ss");
409
+ f = f.replace("%r", "hh:mm:ss TT");
410
+ f = f.replace("%R", "HH:mm");
411
+ f = f.replace("%T", "HH:mm:ss");
412
+
413
+ /* Literal Strings */
414
+ f = f.replace("%n", "\n");
415
+ f = f.replace("%t", "\t");
416
+ f = f.replace("%%", "%");
417
+
418
+ return f;
419
+ }
420
+
421
+ // Converts Ruby date and time format specifiers to Moment format
422
+ // Ruby: http://www.ruby-doc.org/core-1.9.3/Time.html#method-i-strftime
423
+ // Moment: http://momentjs.com/docs/#/displaying/format/
424
+ I18n.rbToMomentDateFormat = function(scope, options) {
425
+ var f = this.lookup(scope, options);
426
+
427
+ if (f === null) {
428
+ return "";
429
+ }
430
+
431
+ /* Need to escape all string literals first with [literal string] */
432
+ f = f.replace(/(\b[^%\w][A-Za-z0-9 \t\/]+)/g, "[$1]");
433
+
434
+ /* Date and Time Formats */
435
+ f = f.replace(/%[\^]?a/, "ddd");
436
+ f = f.replace(/%[\^]?A/, "dddd");
437
+ f = f.replace(/%[\^]?b/, "MMM");
438
+ f = f.replace(/%[\^]?B/, "MMMM");
439
+ f = f.replace("%d", "DD");
440
+ f = f.replace("%-d", "D");
441
+ f = f.replace("%e", "D");
442
+ f = f.replace("%H", "HH");
443
+ f = f.replace("%-H", "H");
444
+ f = f.replace("%k", "H");
445
+ f = f.replace("%I", "hh");
446
+ f = f.replace("%-I", "h");
447
+ f = f.replace("%L", "SSS");
448
+ f = f.replace("%l", "h");
449
+ f = f.replace("%m", "MM");
450
+ f = f.replace("%-m", "M");
451
+ f = f.replace("%M", "mm");
452
+ f = f.replace("%-M", "m");
453
+ f = f.replace("%N", ""); // Not supported
454
+ f = f.replace("%p", "A");
455
+ f = f.replace("%P", "a");
456
+ f = f.replace("%S", "ss");
457
+ f = f.replace("%-S", "s");
458
+ f = f.replace("%u", ""); // Not supported
459
+ f = f.replace("%w", "d");
460
+ f = f.replace("%s", "X");
461
+ f = f.replace("%y", "YY");
462
+ f = f.replace("%-y", "YY");
463
+ f = f.replace("%Y", "YYYY");
464
+ f = f.replace(/%(:{0,2}z|Z)/, "ZZ");
465
+
466
+ /* Combinations */
467
+ f = f.replace("%c", "ddd MMM D HH:mm:ss YYYY");
468
+ f = f.replace("%D", "MM/DD/YY");
469
+ f = f.replace("%F", "YY-MM-DD");
470
+ f = f.replace("%v", "D-MMM-YYYY");
471
+ f = f.replace("%x", "MM/DD/YY");
472
+ f = f.replace("%X", "HH:mm:ss");
473
+ f = f.replace("%r", "hh:mm:ss A");
474
+ f = f.replace("%R", "HH:mm");
475
+ f = f.replace("%T", "HH:mm:ss");
476
+
477
+ /* Literal Strings */
478
+ f = f.replace("%n", "\n");
479
+ f = f.replace("%t", "\t");
480
+ f = f.replace("%%", "%");
481
+
482
+ return f;
483
+ }
484
+
485
+ // Converts Ruby date and time format specifiers to D3 format
486
+ // Ruby: http://www.ruby-doc.org/core-1.9.3/Time.html#method-i-strftime
487
+ // D3: https://github.com/mbostock/d3/wiki/Time-Formatting
488
+ I18n.rbToD3DateFormat = function(scope, options) {
489
+ var f = this.lookup(scope, options);
490
+
491
+ if (f === null) {
492
+ return "";
493
+ }
494
+
495
+ /*
496
+ Fortunately a vast majority of the codes are the same between
497
+ Ruby and D3 so we only need to include the differences here
498
+ */
499
+
500
+ /* Date and Time Formats */
501
+ f = f.replace(/%[\^]?a/, "%a");
502
+ f = f.replace(/%[\^]?A/, "%A");
503
+ f = f.replace(/%[\^]?b/, "%b");
504
+ f = f.replace(/%[\^]?B/, "%B");
505
+ f = f.replace("%g", ""); // Not supported
506
+ f = f.replace("%G", ""); // Not supported
507
+ f = f.replace("%h", "%b");
508
+ f = f.replace("%k", "%_H");
509
+ f = f.replace("%l", "%_I");
510
+ f = f.replace("%N", ""); // Not supported
511
+ f = f.replace("%P", "%p");
512
+ f = f.replace("%u", ""); // Not supported
513
+ f = f.replace("%w", "d");
514
+ f = f.replace("%s", ""); // Not supported
515
+ f = f.replace(/%(:{0,2}z|Z)/, "%Z");
516
+
517
+ /* Combinations */
518
+ f = f.replace("%D", "%m/%d/%y");
519
+ f = f.replace("%F", "%Y-%m-%d");
520
+ f = f.replace("%v", "%e-%b-%Y");
521
+ f = f.replace("%x", "%m/%d/%y");
522
+ f = f.replace("%X", "%H:%M:%S");
523
+ f = f.replace("%r", "%I:%M:%S %p");
524
+ f = f.replace("%R", "%H:%M");
525
+ f = f.replace("%T", "%H:%M:%S");
526
+
527
+ /* Literal Strings */
528
+ f = f.replace("%n", "\n");
529
+ f = f.replace("%t", "\t");
530
+
531
+ return f;
532
+ }
533
+
534
+ I18n.toNumber = function(number, options) {
535
+ options = this.prepareOptions(
536
+ options,
537
+ this.lookup("number.format"),
538
+ {precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false}
539
+ );
540
+
541
+ var negative = number < 0
542
+ , string = Math.abs(number).toFixed(options.precision).toString()
543
+ , parts = string.split(".")
544
+ , precision
545
+ , buffer = []
546
+ , formattedNumber
547
+ ;
548
+
549
+ number = parts[0];
550
+ precision = parts[1];
551
+
552
+ while (number.length > 0) {
553
+ buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
554
+ number = number.substr(0, number.length -3);
555
+ }
556
+
557
+ formattedNumber = buffer.join(options.delimiter);
558
+
559
+ if (options.precision > 0) {
560
+ formattedNumber += options.separator + parts[1];
561
+ }
562
+
563
+ if (negative) {
564
+ formattedNumber = "-" + formattedNumber;
565
+ }
566
+
567
+ if (options.strip_insignificant_zeros) {
568
+ var regex = {
569
+ separator: new RegExp(options.separator.replace(/\./, "\\.") + "$")
570
+ , zeros: /0+$/
571
+ };
572
+
573
+ formattedNumber = formattedNumber
574
+ .replace(regex.zeros, "")
575
+ .replace(regex.separator, "")
576
+ ;
577
+ }
578
+
579
+ return formattedNumber;
580
+ };
581
+
582
+ I18n.toCurrency = function(number, options) {
583
+ options = this.prepareOptions(
584
+ options,
585
+ this.lookup("number.currency.format"),
586
+ this.lookup("number.format"),
587
+ {unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."}
588
+ );
589
+
590
+ number = this.toNumber(number, options);
591
+ number = options.format
592
+ .replace("%u", options.unit)
593
+ .replace("%n", number)
594
+ ;
595
+
596
+ return number;
597
+ };
598
+
599
+ I18n.toHumanSize = function(number, options) {
600
+ var kb = 1024
601
+ , size = number
602
+ , iterations = 0
603
+ , unit
604
+ , precision
605
+ ;
606
+
607
+ while (size >= kb && iterations < 4) {
608
+ size = size / kb;
609
+ iterations += 1;
610
+ }
611
+
612
+ if (iterations === 0) {
613
+ unit = this.t("number.human.storage_units.units.byte", {count: size});
614
+ precision = 0;
615
+ } else {
616
+ unit = this.t("number.human.storage_units.units." + [null, "kb", "mb", "gb", "tb"][iterations]);
617
+ precision = (size - Math.floor(size) === 0) ? 0 : 1;
618
+ }
619
+
620
+ options = this.prepareOptions(
621
+ options,
622
+ {precision: precision, format: "%n%u", delimiter: ""}
623
+ );
624
+
625
+ number = this.toNumber(size, options);
626
+ number = options.format
627
+ .replace("%u", unit)
628
+ .replace("%n", number)
629
+ ;
630
+
631
+ return number;
632
+ };
633
+
634
+ I18n.toPercentage = function(number, options) {
635
+ options = this.prepareOptions(
636
+ options,
637
+ this.lookup("number.percentage.format"),
638
+ this.lookup("number.format"),
639
+ {precision: 3, separator: ".", delimiter: ""}
640
+ );
641
+
642
+ number = this.toNumber(number, options);
643
+ return number + "%";
644
+ };
645
+
646
+ I18n.pluralizer = function(locale) {
647
+ pluralizer = this.pluralizationRules[locale];
648
+ if (pluralizer !== undefined) return pluralizer;
649
+ return this.pluralizationRules["en"];
650
+ };
651
+
652
+ I18n.findAndTranslateValidNode = function(keys, translation) {
653
+ for (i = 0; i < keys.length; i++) {
654
+ key = keys[i];
655
+ if (this.isValidNode(translation, key)) return translation[key];
656
+ }
657
+ return null;
658
+ };
659
+
660
+ I18n.pluralize = function(count, scope, options) {
661
+ var translation;
662
+
663
+ try {
664
+ translation = this.lookup(scope, options);
665
+ } catch (error) {}
666
+
667
+ if (!translation) {
668
+ return this.missingTranslation(scope);
669
+ }
670
+
671
+ var message;
672
+ options = this.prepareOptions(options);
673
+ options.count = count.toString();
674
+
675
+ pluralizer = this.pluralizer(this.currentLocale());
676
+ key = pluralizer(Math.abs(count));
677
+ keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key];
678
+
679
+ message = this.findAndTranslateValidNode(keys, translation);
680
+ if (message == null) message = this.missingTranslation(scope, keys[0]);
681
+
682
+ return this.interpolate(message, options);
683
+ };
684
+
685
+ I18n.missingTranslation = function() {
686
+ var message = '[missing "' + this.currentLocale()
687
+ , count = arguments.length
688
+ ;
689
+
690
+ for (var i = 0; i < count; i++) {
691
+ message += "." + arguments[i];
692
+ }
693
+
694
+ message += '" translation]';
695
+
696
+ return message;
697
+ };
698
+
699
+ I18n.currentLocale = function() {
700
+ return (I18n.locale || I18n.defaultLocale);
701
+ };
702
+
703
+ // shortcuts
704
+ I18n.t = I18n.translate;
705
+ I18n.l = I18n.localize;
706
+ I18n.p = I18n.pluralize;