i18n-js 2.1.2 → 3.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +24 -0
  3. data/.gitignore +5 -4
  4. data/.npmignore +27 -0
  5. data/.travis.yml +37 -0
  6. data/Appraisals +20 -0
  7. data/CHANGELOG.md +354 -0
  8. data/Gemfile +1 -1
  9. data/README.md +872 -0
  10. data/Rakefile +19 -7
  11. data/app/assets/javascripts/i18n/filtered.js.erb +23 -0
  12. data/app/assets/javascripts/i18n/shims.js +208 -0
  13. data/app/assets/javascripts/i18n/translations.js +3 -0
  14. data/app/assets/javascripts/i18n.js +1077 -0
  15. data/gemfiles/i18n_0_6.gemfile +7 -0
  16. data/gemfiles/i18n_0_7.gemfile +7 -0
  17. data/gemfiles/i18n_0_8.gemfile +7 -0
  18. data/gemfiles/i18n_0_9.gemfile +7 -0
  19. data/gemfiles/i18n_1_0.gemfile +7 -0
  20. data/i18n-js.gemspec +11 -9
  21. data/lib/i18n/js/dependencies.rb +59 -0
  22. data/lib/i18n/js/engine.rb +87 -0
  23. data/lib/i18n/js/fallback_locales.rb +70 -0
  24. data/lib/{i18n-js → i18n/js}/middleware.rb +32 -9
  25. data/lib/i18n/js/private/hash_with_symbol_keys.rb +36 -0
  26. data/lib/i18n/js/segment.rb +88 -0
  27. data/lib/i18n/js/utils.rb +52 -0
  28. data/lib/i18n/js/version.rb +7 -0
  29. data/lib/i18n/js.rb +242 -0
  30. data/lib/i18n-js.rb +1 -177
  31. data/lib/rails/generators/i18n/js/config/config_generator.rb +19 -0
  32. data/{config → lib/rails/generators/i18n/js/config/templates}/i18n-js.yml +11 -6
  33. data/lib/tasks/export.rake +8 -0
  34. data/package.json +25 -0
  35. data/spec/fixtures/custom_path.yml +5 -0
  36. data/spec/fixtures/default.yml +5 -0
  37. data/spec/fixtures/erb.yml +5 -0
  38. data/spec/fixtures/except_condition.yml +7 -0
  39. data/spec/fixtures/js_export_dir_custom.yml +7 -0
  40. data/spec/fixtures/js_export_dir_none.yml +6 -0
  41. data/spec/fixtures/js_extend_parent.yml +6 -0
  42. data/spec/fixtures/js_extend_segment.yml +6 -0
  43. data/spec/fixtures/js_file_per_locale.yml +7 -0
  44. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +4 -0
  45. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +6 -0
  46. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +4 -0
  47. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +4 -0
  48. data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +4 -0
  49. data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +4 -0
  50. data/spec/fixtures/js_file_with_namespace_and_pretty_print.yml +7 -0
  51. data/spec/fixtures/js_sort_translation_keys_false.yml +6 -0
  52. data/spec/fixtures/js_sort_translation_keys_true.yml +6 -0
  53. data/spec/{resources → fixtures}/locales.yml +13 -1
  54. data/spec/fixtures/multiple_conditions.yml +7 -0
  55. data/spec/fixtures/multiple_conditions_per_locale.yml +7 -0
  56. data/spec/fixtures/multiple_files.yml +7 -0
  57. data/spec/{resources → fixtures}/no_config.yml +0 -0
  58. data/spec/fixtures/no_scope.yml +4 -0
  59. data/spec/fixtures/simple_scope.yml +5 -0
  60. data/spec/js/currency.spec.js +62 -0
  61. data/spec/js/current_locale.spec.js +19 -0
  62. data/spec/js/dates.spec.js +265 -0
  63. data/spec/js/defaults.spec.js +31 -0
  64. data/spec/js/extend.spec.js +110 -0
  65. data/spec/js/interpolation.spec.js +124 -0
  66. data/spec/js/jasmine/MIT.LICENSE +20 -0
  67. data/spec/js/jasmine/jasmine-html.js +190 -0
  68. data/spec/js/jasmine/jasmine.css +166 -0
  69. data/spec/js/jasmine/jasmine.js +2476 -0
  70. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  71. data/spec/js/locales.spec.js +31 -0
  72. data/spec/js/localization.spec.js +48 -0
  73. data/spec/js/numbers.spec.js +170 -0
  74. data/spec/js/placeholder.spec.js +24 -0
  75. data/spec/js/pluralization.spec.js +211 -0
  76. data/spec/js/prepare_options.spec.js +41 -0
  77. data/spec/js/require.js +2083 -0
  78. data/spec/js/specs.html +49 -0
  79. data/spec/js/specs_requirejs.html +72 -0
  80. data/spec/js/translate.spec.js +277 -0
  81. data/spec/js/translations.js +164 -0
  82. data/spec/js/utility_functions.spec.js +20 -0
  83. data/spec/ruby/i18n/js/fallback_locales_spec.rb +84 -0
  84. data/spec/ruby/i18n/js/segment_spec.rb +157 -0
  85. data/spec/ruby/i18n/js/utils_spec.rb +106 -0
  86. data/spec/ruby/i18n/js_spec.rb +627 -0
  87. data/spec/spec_helper.rb +55 -14
  88. data/yarn.lock +131 -0
  89. metadata +188 -96
  90. data/.rspec +0 -1
  91. data/Gemfile.lock +0 -51
  92. data/README.rdoc +0 -305
  93. data/lib/i18n-js/engine.rb +0 -62
  94. data/lib/i18n-js/railtie.rb +0 -13
  95. data/lib/i18n-js/rake.rb +0 -16
  96. data/lib/i18n-js/version.rb +0 -10
  97. data/spec/i18n_spec.js +0 -768
  98. data/spec/i18n_spec.rb +0 -205
  99. data/spec/resources/custom_path.yml +0 -4
  100. data/spec/resources/default.yml +0 -4
  101. data/spec/resources/js_file_per_locale.yml +0 -3
  102. data/spec/resources/multiple_conditions.yml +0 -6
  103. data/spec/resources/multiple_files.yml +0 -6
  104. data/spec/resources/no_scope.yml +0 -3
  105. data/spec/resources/simple_scope.yml +0 -4
  106. data/vendor/assets/javascripts/i18n/translations.js.erb +0 -7
  107. data/vendor/assets/javascripts/i18n.js +0 -450
@@ -0,0 +1,1077 @@
1
+ // I18n.js
2
+ // =======
3
+ //
4
+ // This small library provides the Rails I18n API on the Javascript.
5
+ // You don't actually have to use Rails (or even Ruby) to use I18n.js.
6
+ // Just make sure you export all translations in an object like this:
7
+ //
8
+ // I18n.translations.en = {
9
+ // hello: "Hello World"
10
+ // };
11
+ //
12
+ // See tests for specific formatting like numbers and dates.
13
+ //
14
+
15
+ // Using UMD pattern from
16
+ // https://github.com/umdjs/umd#regular-module
17
+ // `returnExports.js` version
18
+ ;(function (root, factory) {
19
+ if (typeof define === 'function' && define.amd) {
20
+ // AMD. Register as an anonymous module.
21
+ define("i18n", function(){ return factory(root);});
22
+ } else if (typeof module === 'object' && module.exports) {
23
+ // Node. Does not work with strict CommonJS, but
24
+ // only CommonJS-like environments that support module.exports,
25
+ // like Node.
26
+ module.exports = factory(root);
27
+ } else {
28
+ // Browser globals (root is window)
29
+ root.I18n = factory(root);
30
+ }
31
+ }(this, function(global) {
32
+ "use strict";
33
+
34
+ // Use previously defined object if exists in current scope
35
+ var I18n = global && global.I18n || {};
36
+
37
+ // Just cache the Array#slice function.
38
+ var slice = Array.prototype.slice;
39
+
40
+ // Apply number padding.
41
+ var padding = function(number) {
42
+ return ("0" + number.toString()).substr(-2);
43
+ };
44
+
45
+ // Improved toFixed number rounding function with support for unprecise floating points
46
+ // JavaScript's standard toFixed function does not round certain numbers correctly (for example 0.105 with precision 2).
47
+ var toFixed = function(number, precision) {
48
+ return decimalAdjust('round', number, -precision).toFixed(precision);
49
+ };
50
+
51
+ // Is a given variable an object?
52
+ // Borrowed from Underscore.js
53
+ var isObject = function(obj) {
54
+ var type = typeof obj;
55
+ return type === 'function' || type === 'object'
56
+ };
57
+
58
+ var isFunction = function(func) {
59
+ var type = typeof func;
60
+ return type === 'function'
61
+ };
62
+
63
+ // Check if value is different than undefined and null;
64
+ var isSet = function(value) {
65
+ return typeof(value) !== 'undefined' && value !== null;
66
+ };
67
+
68
+ // Is a given value an array?
69
+ // Borrowed from Underscore.js
70
+ var isArray = function(val) {
71
+ if (Array.isArray) {
72
+ return Array.isArray(val);
73
+ }
74
+ return Object.prototype.toString.call(val) === '[object Array]';
75
+ };
76
+
77
+ var isString = function(val) {
78
+ return typeof val === 'string' || Object.prototype.toString.call(val) === '[object String]';
79
+ };
80
+
81
+ var isNumber = function(val) {
82
+ return typeof val === 'number' || Object.prototype.toString.call(val) === '[object Number]';
83
+ };
84
+
85
+ var isBoolean = function(val) {
86
+ return val === true || val === false;
87
+ };
88
+
89
+ var isNull = function(val) {
90
+ return val === null;
91
+ };
92
+
93
+ var decimalAdjust = function(type, value, exp) {
94
+ // If the exp is undefined or zero...
95
+ if (typeof exp === 'undefined' || +exp === 0) {
96
+ return Math[type](value);
97
+ }
98
+ value = +value;
99
+ exp = +exp;
100
+ // If the value is not a number or the exp is not an integer...
101
+ if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
102
+ return NaN;
103
+ }
104
+ // Shift
105
+ value = value.toString().split('e');
106
+ value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
107
+ // Shift back
108
+ value = value.toString().split('e');
109
+ return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
110
+ }
111
+
112
+ var lazyEvaluate = function(message, scope) {
113
+ if (isFunction(message)) {
114
+ return message(scope);
115
+ } else {
116
+ return message;
117
+ }
118
+ }
119
+
120
+ var merge = function (dest, obj) {
121
+ var key, value;
122
+ for (key in obj) if (obj.hasOwnProperty(key)) {
123
+ value = obj[key];
124
+ if (isString(value) || isNumber(value) || isBoolean(value) || isArray(value) || isNull(value)) {
125
+ dest[key] = value;
126
+ } else {
127
+ if (dest[key] == null) dest[key] = {};
128
+ merge(dest[key], value);
129
+ }
130
+ }
131
+ return dest;
132
+ };
133
+
134
+ // Set default days/months translations.
135
+ var DATE = {
136
+ day_names: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
137
+ , abbr_day_names: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
138
+ , month_names: [null, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
139
+ , abbr_month_names: [null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
140
+ , meridian: ["AM", "PM"]
141
+ };
142
+
143
+ // Set default number format.
144
+ var NUMBER_FORMAT = {
145
+ precision: 3
146
+ , separator: "."
147
+ , delimiter: ","
148
+ , strip_insignificant_zeros: false
149
+ };
150
+
151
+ // Set default currency format.
152
+ var CURRENCY_FORMAT = {
153
+ unit: "$"
154
+ , precision: 2
155
+ , format: "%u%n"
156
+ , sign_first: true
157
+ , delimiter: ","
158
+ , separator: "."
159
+ };
160
+
161
+ // Set default percentage format.
162
+ var PERCENTAGE_FORMAT = {
163
+ unit: "%"
164
+ , precision: 3
165
+ , format: "%n%u"
166
+ , separator: "."
167
+ , delimiter: ""
168
+ };
169
+
170
+ // Set default size units.
171
+ var SIZE_UNITS = [null, "kb", "mb", "gb", "tb"];
172
+
173
+ // Other default options
174
+ var DEFAULT_OPTIONS = {
175
+ // Set default locale. This locale will be used when fallback is enabled and
176
+ // the translation doesn't exist in a particular locale.
177
+ defaultLocale: "en"
178
+ // Set the current locale to `en`.
179
+ , locale: "en"
180
+ // Set the translation key separator.
181
+ , defaultSeparator: "."
182
+ // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
183
+ , placeholder: /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm
184
+ // Set if engine should fallback to the default locale when a translation
185
+ // is missing.
186
+ , fallbacks: false
187
+ // Set the default translation object.
188
+ , translations: {}
189
+ // Set missing translation behavior. 'message' will display a message
190
+ // that the translation is missing, 'guess' will try to guess the string
191
+ , missingBehaviour: 'message'
192
+ // if you use missingBehaviour with 'message', but want to know that the
193
+ // string is actually missing for testing purposes, you can prefix the
194
+ // guessed string by setting the value here. By default, no prefix!
195
+ , missingTranslationPrefix: ''
196
+ };
197
+
198
+ // Set default locale. This locale will be used when fallback is enabled and
199
+ // the translation doesn't exist in a particular locale.
200
+ I18n.reset = function() {
201
+ var key;
202
+ for (key in DEFAULT_OPTIONS) {
203
+ this[key] = DEFAULT_OPTIONS[key];
204
+ }
205
+ };
206
+
207
+ // Much like `reset`, but only assign options if not already assigned
208
+ I18n.initializeOptions = function() {
209
+ var key;
210
+ for (key in DEFAULT_OPTIONS) if (!isSet(this[key])) {
211
+ this[key] = DEFAULT_OPTIONS[key];
212
+ }
213
+ };
214
+ I18n.initializeOptions();
215
+
216
+ // Return a list of all locales that must be tried before returning the
217
+ // missing translation message. By default, this will consider the inline option,
218
+ // current locale and fallback locale.
219
+ //
220
+ // I18n.locales.get("de-DE");
221
+ // // ["de-DE", "de", "en"]
222
+ //
223
+ // You can define custom rules for any locale. Just make sure you return a array
224
+ // containing all locales.
225
+ //
226
+ // // Default the Wookie locale to English.
227
+ // I18n.locales["wk"] = function(locale) {
228
+ // return ["en"];
229
+ // };
230
+ //
231
+ I18n.locales = {};
232
+
233
+ // Retrieve locales based on inline locale, current locale or default to
234
+ // I18n's detection.
235
+ I18n.locales.get = function(locale) {
236
+ var result = this[locale] || this[I18n.locale] || this["default"];
237
+
238
+ if (isFunction(result)) {
239
+ result = result(locale);
240
+ }
241
+
242
+ if (isArray(result) === false) {
243
+ result = [result];
244
+ }
245
+
246
+ return result;
247
+ };
248
+
249
+ // The default locale list.
250
+ I18n.locales["default"] = function(locale) {
251
+ var locales = []
252
+ , list = []
253
+ ;
254
+
255
+ // Handle the inline locale option that can be provided to
256
+ // the `I18n.t` options.
257
+ if (locale) {
258
+ locales.push(locale);
259
+ }
260
+
261
+ // Add the current locale to the list.
262
+ if (!locale && I18n.locale) {
263
+ locales.push(I18n.locale);
264
+ }
265
+
266
+ // Add the default locale if fallback strategy is enabled.
267
+ if (I18n.fallbacks && I18n.defaultLocale) {
268
+ locales.push(I18n.defaultLocale);
269
+ }
270
+
271
+ // Locale code format 1:
272
+ // According to RFC4646 (http://www.ietf.org/rfc/rfc4646.txt)
273
+ // language codes for Traditional Chinese should be `zh-Hant`
274
+ //
275
+ // But due to backward compatibility
276
+ // We use older version of IETF language tag
277
+ // @see http://www.w3.org/TR/html401/struct/dirlang.html
278
+ // @see http://en.wikipedia.org/wiki/IETF_language_tag
279
+ //
280
+ // Format: `language-code = primary-code ( "-" subcode )*`
281
+ //
282
+ // primary-code uses ISO639-1
283
+ // @see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
284
+ // @see http://www.iso.org/iso/home/standards/language_codes.htm
285
+ //
286
+ // subcode uses ISO 3166-1 alpha-2
287
+ // @see http://en.wikipedia.org/wiki/ISO_3166
288
+ // @see http://www.iso.org/iso/country_codes.htm
289
+ //
290
+ // @note
291
+ // subcode can be in upper case or lower case
292
+ // defining it in upper case is a convention only
293
+
294
+
295
+ // Locale code format 2:
296
+ // Format: `code = primary-code ( "-" region-code )*`
297
+ // primary-code uses ISO 639-1
298
+ // script-code uses ISO 15924
299
+ // region-code uses ISO 3166-1 alpha-2
300
+ // Example: zh-Hant-TW, en-HK, zh-Hant-CN
301
+ //
302
+ // It is similar to RFC4646 (or actually the same),
303
+ // but seems to be limited to language, script, region
304
+
305
+ // Compute each locale with its country code.
306
+ // So this will return an array containing
307
+ // `de-DE` and `de`
308
+ // or
309
+ // `zh-hans-tw`, `zh-hans`, `zh`
310
+ // locales.
311
+ locales.forEach(function(locale) {
312
+ var localeParts = locale.split("-");
313
+ var firstFallback = null;
314
+ var secondFallback = null;
315
+ if (localeParts.length === 3) {
316
+ firstFallback = [
317
+ localeParts[0],
318
+ localeParts[1]
319
+ ].join("-");
320
+ secondFallback = localeParts[0];
321
+ }
322
+ else if (localeParts.length === 2) {
323
+ firstFallback = localeParts[0];
324
+ }
325
+
326
+ if (list.indexOf(locale) === -1) {
327
+ list.push(locale);
328
+ }
329
+
330
+ if (! I18n.fallbacks) {
331
+ return;
332
+ }
333
+
334
+ [
335
+ firstFallback,
336
+ secondFallback
337
+ ].forEach(function(nullableFallbackLocale) {
338
+ // We don't want null values
339
+ if (typeof nullableFallbackLocale === "undefined") { return; }
340
+ if (nullableFallbackLocale === null) { return; }
341
+ // We don't want duplicate values
342
+ //
343
+ // Comparing with `locale` first is faster than
344
+ // checking whether value's presence in the list
345
+ if (nullableFallbackLocale === locale) { return; }
346
+ if (list.indexOf(nullableFallbackLocale) !== -1) { return; }
347
+
348
+ list.push(nullableFallbackLocale);
349
+ });
350
+ });
351
+
352
+ // No locales set? English it is.
353
+ if (!locales.length) {
354
+ locales.push("en");
355
+ }
356
+
357
+ return list;
358
+ };
359
+
360
+ // Hold pluralization rules.
361
+ I18n.pluralization = {};
362
+
363
+ // Return the pluralizer for a specific locale.
364
+ // If no specify locale is found, then I18n's default will be used.
365
+ I18n.pluralization.get = function(locale) {
366
+ return this[locale] || this[I18n.locale] || this["default"];
367
+ };
368
+
369
+ // The default pluralizer rule.
370
+ // It detects the `zero`, `one`, and `other` scopes.
371
+ I18n.pluralization["default"] = function(count) {
372
+ switch (count) {
373
+ case 0: return ["zero", "other"];
374
+ case 1: return ["one"];
375
+ default: return ["other"];
376
+ }
377
+ };
378
+
379
+ // Return current locale. If no locale has been set, then
380
+ // the current locale will be the default locale.
381
+ I18n.currentLocale = function() {
382
+ return this.locale || this.defaultLocale;
383
+ };
384
+
385
+ // Check if value is different than undefined and null;
386
+ I18n.isSet = isSet;
387
+
388
+ // Find and process the translation using the provided scope and options.
389
+ // This is used internally by some functions and should not be used as an
390
+ // public API.
391
+ I18n.lookup = function(scope, options) {
392
+ options = options || {}
393
+
394
+ var locales = this.locales.get(options.locale).slice()
395
+ , locale
396
+ , scopes
397
+ , fullScope
398
+ , translations
399
+ ;
400
+
401
+ fullScope = this.getFullScope(scope, options);
402
+
403
+ while (locales.length) {
404
+ locale = locales.shift();
405
+ scopes = fullScope.split(this.defaultSeparator);
406
+ translations = this.translations[locale];
407
+
408
+ if (!translations) {
409
+ continue;
410
+ }
411
+ while (scopes.length) {
412
+ translations = translations[scopes.shift()];
413
+
414
+ if (translations === undefined || translations === null) {
415
+ break;
416
+ }
417
+ }
418
+
419
+ if (translations !== undefined && translations !== null) {
420
+ return translations;
421
+ }
422
+ }
423
+
424
+ if (isSet(options.defaultValue)) {
425
+ return lazyEvaluate(options.defaultValue, scope);
426
+ }
427
+ };
428
+
429
+ // lookup pluralization rule key into translations
430
+ I18n.pluralizationLookupWithoutFallback = function(count, locale, translations) {
431
+ var pluralizer = this.pluralization.get(locale)
432
+ , pluralizerKeys = pluralizer(count)
433
+ , pluralizerKey
434
+ , message;
435
+
436
+ if (isObject(translations)) {
437
+ while (pluralizerKeys.length) {
438
+ pluralizerKey = pluralizerKeys.shift();
439
+ if (isSet(translations[pluralizerKey])) {
440
+ message = translations[pluralizerKey];
441
+ break;
442
+ }
443
+ }
444
+ }
445
+
446
+ return message;
447
+ };
448
+
449
+ // Lookup dedicated to pluralization
450
+ I18n.pluralizationLookup = function(count, scope, options) {
451
+ options = options || {}
452
+ var locales = this.locales.get(options.locale).slice()
453
+ , locale
454
+ , scopes
455
+ , translations
456
+ , message
457
+ ;
458
+ scope = this.getFullScope(scope, options);
459
+
460
+ while (locales.length) {
461
+ locale = locales.shift();
462
+ scopes = scope.split(this.defaultSeparator);
463
+ translations = this.translations[locale];
464
+
465
+ if (!translations) {
466
+ continue;
467
+ }
468
+
469
+ while (scopes.length) {
470
+ translations = translations[scopes.shift()];
471
+ if (!isObject(translations)) {
472
+ break;
473
+ }
474
+ if (scopes.length == 0) {
475
+ message = this.pluralizationLookupWithoutFallback(count, locale, translations);
476
+ }
477
+ }
478
+ if (message != null && message != undefined) {
479
+ break;
480
+ }
481
+ }
482
+
483
+ if (message == null || message == undefined) {
484
+ if (isSet(options.defaultValue)) {
485
+ if (isObject(options.defaultValue)) {
486
+ message = this.pluralizationLookupWithoutFallback(count, options.locale, options.defaultValue);
487
+ } else {
488
+ message = options.defaultValue;
489
+ }
490
+ translations = options.defaultValue;
491
+ }
492
+ }
493
+
494
+ return { message: message, translations: translations };
495
+ };
496
+
497
+ // Rails changed the way the meridian is stored.
498
+ // It started with `date.meridian` returning an array,
499
+ // then it switched to `time.am` and `time.pm`.
500
+ // This function abstracts this difference and returns
501
+ // the correct meridian or the default value when none is provided.
502
+ I18n.meridian = function() {
503
+ var time = this.lookup("time");
504
+ var date = this.lookup("date");
505
+
506
+ if (time && time.am && time.pm) {
507
+ return [time.am, time.pm];
508
+ } else if (date && date.meridian) {
509
+ return date.meridian;
510
+ } else {
511
+ return DATE.meridian;
512
+ }
513
+ };
514
+
515
+ // Merge serveral hash options, checking if value is set before
516
+ // overwriting any value. The precedence is from left to right.
517
+ //
518
+ // I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
519
+ // #=> {name: "John Doe", role: "user"}
520
+ //
521
+ I18n.prepareOptions = function() {
522
+ var args = slice.call(arguments)
523
+ , options = {}
524
+ , subject
525
+ ;
526
+
527
+ while (args.length) {
528
+ subject = args.shift();
529
+
530
+ if (typeof(subject) != "object") {
531
+ continue;
532
+ }
533
+
534
+ for (var attr in subject) {
535
+ if (!subject.hasOwnProperty(attr)) {
536
+ continue;
537
+ }
538
+
539
+ if (isSet(options[attr])) {
540
+ continue;
541
+ }
542
+
543
+ options[attr] = subject[attr];
544
+ }
545
+ }
546
+
547
+ return options;
548
+ };
549
+
550
+ // Generate a list of translation options for default fallbacks.
551
+ // `defaultValue` is also deleted from options as it is returned as part of
552
+ // the translationOptions array.
553
+ I18n.createTranslationOptions = function(scope, options) {
554
+ var translationOptions = [{scope: scope}];
555
+
556
+ // Defaults should be an array of hashes containing either
557
+ // fallback scopes or messages
558
+ if (isSet(options.defaults)) {
559
+ translationOptions = translationOptions.concat(options.defaults);
560
+ }
561
+
562
+ // Maintain support for defaultValue. Since it is always a message
563
+ // insert it in to the translation options as such.
564
+ if (isSet(options.defaultValue)) {
565
+ translationOptions.push({ message: options.defaultValue });
566
+ }
567
+
568
+ return translationOptions;
569
+ };
570
+
571
+ // Translate the given scope with the provided options.
572
+ I18n.translate = function(scope, options) {
573
+ options = options || {}
574
+
575
+ var translationOptions = this.createTranslationOptions(scope, options);
576
+
577
+ var translation;
578
+
579
+ var optionsWithoutDefault = this.prepareOptions(options)
580
+ delete optionsWithoutDefault.defaultValue
581
+
582
+ // Iterate through the translation options until a translation
583
+ // or message is found.
584
+ var translationFound =
585
+ translationOptions.some(function(translationOption) {
586
+ if (isSet(translationOption.scope)) {
587
+ translation = this.lookup(translationOption.scope, optionsWithoutDefault);
588
+ } else if (isSet(translationOption.message)) {
589
+ translation = lazyEvaluate(translationOption.message, scope);
590
+ }
591
+
592
+ if (translation !== undefined && translation !== null) {
593
+ return true;
594
+ }
595
+ }, this);
596
+
597
+ if (!translationFound) {
598
+ return this.missingTranslation(scope, options);
599
+ }
600
+
601
+ if (typeof(translation) === "string") {
602
+ translation = this.interpolate(translation, options);
603
+ } else if (isArray(translation)) {
604
+ translation = translation.map(function(t) {
605
+ return (typeof(t) === "string" ? this.interpolate(t, options) : t);
606
+ }, this);
607
+ } else if (isObject(translation) && isSet(options.count)) {
608
+ translation = this.pluralize(options.count, scope, options);
609
+ }
610
+
611
+ return translation;
612
+ };
613
+
614
+ // This function interpolates the all variables in the given message.
615
+ I18n.interpolate = function(message, options) {
616
+ if (message === null) {
617
+ return message;
618
+ }
619
+
620
+ options = options || {}
621
+ var matches = message.match(this.placeholder)
622
+ , placeholder
623
+ , value
624
+ , name
625
+ , regex
626
+ ;
627
+
628
+ if (!matches) {
629
+ return message;
630
+ }
631
+
632
+ var value;
633
+
634
+ while (matches.length) {
635
+ placeholder = matches.shift();
636
+ name = placeholder.replace(this.placeholder, "$1");
637
+
638
+ if (isSet(options[name])) {
639
+ value = options[name].toString().replace(/\$/gm, "_#$#_");
640
+ } else if (name in options) {
641
+ value = this.nullPlaceholder(placeholder, message, options);
642
+ } else {
643
+ value = this.missingPlaceholder(placeholder, message, options);
644
+ }
645
+
646
+ regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
647
+ message = message.replace(regex, value);
648
+ }
649
+
650
+ return message.replace(/_#\$#_/g, "$");
651
+ };
652
+
653
+ // Pluralize the given scope using the `count` value.
654
+ // The pluralized translation may have other placeholders,
655
+ // which will be retrieved from `options`.
656
+ I18n.pluralize = function(count, scope, options) {
657
+ options = this.prepareOptions({count: String(count)}, options)
658
+ var pluralizer, message, result;
659
+
660
+ result = this.pluralizationLookup(count, scope, options);
661
+ if (result.translations == undefined || result.translations == null) {
662
+ return this.missingTranslation(scope, options);
663
+ }
664
+
665
+ if (result.message != undefined && result.message != null) {
666
+ return this.interpolate(result.message, options);
667
+ }
668
+ else {
669
+ pluralizer = this.pluralization.get(options.locale);
670
+ return this.missingTranslation(scope + '.' + pluralizer(count)[0], options);
671
+ }
672
+ };
673
+
674
+ // Return a missing translation message for the given parameters.
675
+ I18n.missingTranslation = function(scope, options) {
676
+ //guess intended string
677
+ if(this.missingBehaviour == 'guess'){
678
+ //get only the last portion of the scope
679
+ var s = scope.split('.').slice(-1)[0];
680
+ //replace underscore with space && camelcase with space and lowercase letter
681
+ return (this.missingTranslationPrefix.length > 0 ? this.missingTranslationPrefix : '') +
682
+ s.replace('_',' ').replace(/([a-z])([A-Z])/g,
683
+ function(match, p1, p2) {return p1 + ' ' + p2.toLowerCase()} );
684
+ }
685
+
686
+ var localeForTranslation = (options != null && options.locale != null) ? options.locale : this.currentLocale();
687
+ var fullScope = this.getFullScope(scope, options);
688
+ var fullScopeWithLocale = [localeForTranslation, fullScope].join(this.defaultSeparator);
689
+
690
+ return '[missing "' + fullScopeWithLocale + '" translation]';
691
+ };
692
+
693
+ // Return a missing placeholder message for given parameters
694
+ I18n.missingPlaceholder = function(placeholder, message, options) {
695
+ return "[missing " + placeholder + " value]";
696
+ };
697
+
698
+ I18n.nullPlaceholder = function() {
699
+ return I18n.missingPlaceholder.apply(I18n, arguments);
700
+ };
701
+
702
+ // Format number using localization rules.
703
+ // The options will be retrieved from the `number.format` scope.
704
+ // If this isn't present, then the following options will be used:
705
+ //
706
+ // - `precision`: `3`
707
+ // - `separator`: `"."`
708
+ // - `delimiter`: `","`
709
+ // - `strip_insignificant_zeros`: `false`
710
+ //
711
+ // You can also override these options by providing the `options` argument.
712
+ //
713
+ I18n.toNumber = function(number, options) {
714
+ options = this.prepareOptions(
715
+ options
716
+ , this.lookup("number.format")
717
+ , NUMBER_FORMAT
718
+ );
719
+
720
+ var negative = number < 0
721
+ , string = toFixed(Math.abs(number), options.precision).toString()
722
+ , parts = string.split(".")
723
+ , precision
724
+ , buffer = []
725
+ , formattedNumber
726
+ , format = options.format || "%n"
727
+ , sign = negative ? "-" : ""
728
+ ;
729
+
730
+ number = parts[0];
731
+ precision = parts[1];
732
+
733
+ while (number.length > 0) {
734
+ buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
735
+ number = number.substr(0, number.length -3);
736
+ }
737
+
738
+ formattedNumber = buffer.join(options.delimiter);
739
+
740
+ if (options.strip_insignificant_zeros && precision) {
741
+ precision = precision.replace(/0+$/, "");
742
+ }
743
+
744
+ if (options.precision > 0 && precision) {
745
+ formattedNumber += options.separator + precision;
746
+ }
747
+
748
+ if (options.sign_first) {
749
+ format = "%s" + format;
750
+ }
751
+ else {
752
+ format = format.replace("%n", "%s%n");
753
+ }
754
+
755
+ formattedNumber = format
756
+ .replace("%u", options.unit)
757
+ .replace("%n", formattedNumber)
758
+ .replace("%s", sign)
759
+ ;
760
+
761
+ return formattedNumber;
762
+ };
763
+
764
+ // Format currency with localization rules.
765
+ // The options will be retrieved from the `number.currency.format` and
766
+ // `number.format` scopes, in that order.
767
+ //
768
+ // Any missing option will be retrieved from the `I18n.toNumber` defaults and
769
+ // the following options:
770
+ //
771
+ // - `unit`: `"$"`
772
+ // - `precision`: `2`
773
+ // - `format`: `"%u%n"`
774
+ // - `delimiter`: `","`
775
+ // - `separator`: `"."`
776
+ //
777
+ // You can also override these options by providing the `options` argument.
778
+ //
779
+ I18n.toCurrency = function(number, options) {
780
+ options = this.prepareOptions(
781
+ options
782
+ , this.lookup("number.currency.format")
783
+ , this.lookup("number.format")
784
+ , CURRENCY_FORMAT
785
+ );
786
+
787
+ return this.toNumber(number, options);
788
+ };
789
+
790
+ // Localize several values.
791
+ // You can provide the following scopes: `currency`, `number`, or `percentage`.
792
+ // If you provide a scope that matches the `/^(date|time)/` regular expression
793
+ // then the `value` will be converted by using the `I18n.toTime` function.
794
+ //
795
+ // It will default to the value's `toString` function.
796
+ //
797
+ I18n.localize = function(scope, value, options) {
798
+ options || (options = {});
799
+
800
+ switch (scope) {
801
+ case "currency":
802
+ return this.toCurrency(value);
803
+ case "number":
804
+ scope = this.lookup("number.format");
805
+ return this.toNumber(value, scope);
806
+ case "percentage":
807
+ return this.toPercentage(value);
808
+ default:
809
+ var localizedValue;
810
+
811
+ if (scope.match(/^(date|time)/)) {
812
+ localizedValue = this.toTime(scope, value);
813
+ } else {
814
+ localizedValue = value.toString();
815
+ }
816
+
817
+ return this.interpolate(localizedValue, options);
818
+ }
819
+ };
820
+
821
+ // Parse a given `date` string into a JavaScript Date object.
822
+ // This function is time zone aware.
823
+ //
824
+ // The following string formats are recognized:
825
+ //
826
+ // yyyy-mm-dd
827
+ // yyyy-mm-dd[ T]hh:mm::ss
828
+ // yyyy-mm-dd[ T]hh:mm::ss
829
+ // yyyy-mm-dd[ T]hh:mm::ssZ
830
+ // yyyy-mm-dd[ T]hh:mm::ss+0000
831
+ // yyyy-mm-dd[ T]hh:mm::ss+00:00
832
+ // yyyy-mm-dd[ T]hh:mm::ss.123Z
833
+ //
834
+ I18n.parseDate = function(date) {
835
+ var matches, convertedDate, fraction;
836
+ // we have a date, so just return it.
837
+ if (typeof(date) == "object") {
838
+ return date;
839
+ };
840
+
841
+ matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})([\.,]\d{1,3})?)?(Z|\+00:?00)?/);
842
+
843
+ if (matches) {
844
+ for (var i = 1; i <= 6; i++) {
845
+ matches[i] = parseInt(matches[i], 10) || 0;
846
+ }
847
+
848
+ // month starts on 0
849
+ matches[2] -= 1;
850
+
851
+ fraction = matches[7] ? 1000 * ("0" + matches[7]) : null;
852
+
853
+ if (matches[8]) {
854
+ convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction));
855
+ } else {
856
+ convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction);
857
+ }
858
+ } else if (typeof(date) == "number") {
859
+ // UNIX timestamp
860
+ convertedDate = new Date();
861
+ convertedDate.setTime(date);
862
+ } else if (date.match(/([A-Z][a-z]{2}) ([A-Z][a-z]{2}) (\d+) (\d+:\d+:\d+) ([+-]\d+) (\d+)/)) {
863
+ // This format `Wed Jul 20 13:03:39 +0000 2011` is parsed by
864
+ // webkit/firefox, but not by IE, so we must parse it manually.
865
+ convertedDate = new Date();
866
+ convertedDate.setTime(Date.parse([
867
+ RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$6, RegExp.$4, RegExp.$5
868
+ ].join(" ")));
869
+ } else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
870
+ // a valid javascript format with timezone info
871
+ convertedDate = new Date();
872
+ convertedDate.setTime(Date.parse(date));
873
+ } else {
874
+ // an arbitrary javascript string
875
+ convertedDate = new Date();
876
+ convertedDate.setTime(Date.parse(date));
877
+ }
878
+
879
+ return convertedDate;
880
+ };
881
+
882
+ // Formats time according to the directives in the given format string.
883
+ // The directives begins with a percent (%) character. Any text not listed as a
884
+ // directive will be passed through to the output string.
885
+ //
886
+ // The accepted formats are:
887
+ //
888
+ // %a - The abbreviated weekday name (Sun)
889
+ // %A - The full weekday name (Sunday)
890
+ // %b - The abbreviated month name (Jan)
891
+ // %B - The full month name (January)
892
+ // %c - The preferred local date and time representation
893
+ // %d - Day of the month (01..31)
894
+ // %-d - Day of the month (1..31)
895
+ // %H - Hour of the day, 24-hour clock (00..23)
896
+ // %-H - Hour of the day, 24-hour clock (0..23)
897
+ // %I - Hour of the day, 12-hour clock (01..12)
898
+ // %-I - Hour of the day, 12-hour clock (1..12)
899
+ // %m - Month of the year (01..12)
900
+ // %-m - Month of the year (1..12)
901
+ // %M - Minute of the hour (00..59)
902
+ // %-M - Minute of the hour (0..59)
903
+ // %p - Meridian indicator (AM or PM)
904
+ // %S - Second of the minute (00..60)
905
+ // %-S - Second of the minute (0..60)
906
+ // %w - Day of the week (Sunday is 0, 0..6)
907
+ // %y - Year without a century (00..99)
908
+ // %-y - Year without a century (0..99)
909
+ // %Y - Year with century
910
+ // %z - Timezone offset (+0545)
911
+ //
912
+ I18n.strftime = function(date, format) {
913
+ var options = this.lookup("date")
914
+ , meridianOptions = I18n.meridian()
915
+ ;
916
+
917
+ if (!options) {
918
+ options = {};
919
+ }
920
+
921
+ options = this.prepareOptions(options, DATE);
922
+
923
+ if (isNaN(date.getTime())) {
924
+ throw new Error('I18n.strftime() requires a valid date object, but received an invalid date.');
925
+ }
926
+
927
+ var weekDay = date.getDay()
928
+ , day = date.getDate()
929
+ , year = date.getFullYear()
930
+ , month = date.getMonth() + 1
931
+ , hour = date.getHours()
932
+ , hour12 = hour
933
+ , meridian = hour > 11 ? 1 : 0
934
+ , secs = date.getSeconds()
935
+ , mins = date.getMinutes()
936
+ , offset = date.getTimezoneOffset()
937
+ , absOffsetHours = Math.floor(Math.abs(offset / 60))
938
+ , absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60)
939
+ , timezoneoffset = (offset > 0 ? "-" : "+") +
940
+ (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) +
941
+ (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes)
942
+ ;
943
+
944
+ if (hour12 > 12) {
945
+ hour12 = hour12 - 12;
946
+ } else if (hour12 === 0) {
947
+ hour12 = 12;
948
+ }
949
+
950
+ format = format.replace("%a", options.abbr_day_names[weekDay]);
951
+ format = format.replace("%A", options.day_names[weekDay]);
952
+ format = format.replace("%b", options.abbr_month_names[month]);
953
+ format = format.replace("%B", options.month_names[month]);
954
+ format = format.replace("%d", padding(day));
955
+ format = format.replace("%e", day);
956
+ format = format.replace("%-d", day);
957
+ format = format.replace("%H", padding(hour));
958
+ format = format.replace("%-H", hour);
959
+ format = format.replace("%I", padding(hour12));
960
+ format = format.replace("%-I", hour12);
961
+ format = format.replace("%m", padding(month));
962
+ format = format.replace("%-m", month);
963
+ format = format.replace("%M", padding(mins));
964
+ format = format.replace("%-M", mins);
965
+ format = format.replace("%p", meridianOptions[meridian]);
966
+ format = format.replace("%S", padding(secs));
967
+ format = format.replace("%-S", secs);
968
+ format = format.replace("%w", weekDay);
969
+ format = format.replace("%y", padding(year));
970
+ format = format.replace("%-y", padding(year).replace(/^0+/, ""));
971
+ format = format.replace("%Y", year);
972
+ format = format.replace("%z", timezoneoffset);
973
+
974
+ return format;
975
+ };
976
+
977
+ // Convert the given dateString into a formatted date.
978
+ I18n.toTime = function(scope, dateString) {
979
+ var date = this.parseDate(dateString)
980
+ , format = this.lookup(scope)
981
+ ;
982
+
983
+ if (date.toString().match(/invalid/i)) {
984
+ return date.toString();
985
+ }
986
+
987
+ if (!format) {
988
+ return date.toString();
989
+ }
990
+
991
+ return this.strftime(date, format);
992
+ };
993
+
994
+ // Convert a number into a formatted percentage value.
995
+ I18n.toPercentage = function(number, options) {
996
+ options = this.prepareOptions(
997
+ options
998
+ , this.lookup("number.percentage.format")
999
+ , this.lookup("number.format")
1000
+ , PERCENTAGE_FORMAT
1001
+ );
1002
+
1003
+ return this.toNumber(number, options);
1004
+ };
1005
+
1006
+ // Convert a number into a readable size representation.
1007
+ I18n.toHumanSize = function(number, options) {
1008
+ var kb = 1024
1009
+ , size = number
1010
+ , iterations = 0
1011
+ , unit
1012
+ , precision
1013
+ ;
1014
+
1015
+ while (size >= kb && iterations < 4) {
1016
+ size = size / kb;
1017
+ iterations += 1;
1018
+ }
1019
+
1020
+ if (iterations === 0) {
1021
+ unit = this.t("number.human.storage_units.units.byte", {count: size});
1022
+ precision = 0;
1023
+ } else {
1024
+ unit = this.t("number.human.storage_units.units." + SIZE_UNITS[iterations]);
1025
+ precision = (size - Math.floor(size) === 0) ? 0 : 1;
1026
+ }
1027
+
1028
+ options = this.prepareOptions(
1029
+ options
1030
+ , {unit: unit, precision: precision, format: "%n%u", delimiter: ""}
1031
+ );
1032
+
1033
+ return this.toNumber(size, options);
1034
+ };
1035
+
1036
+ I18n.getFullScope = function(scope, options) {
1037
+ options = options || {}
1038
+
1039
+ // Deal with the scope as an array.
1040
+ if (isArray(scope)) {
1041
+ scope = scope.join(this.defaultSeparator);
1042
+ }
1043
+
1044
+ // Deal with the scope option provided through the second argument.
1045
+ //
1046
+ // I18n.t('hello', {scope: 'greetings'});
1047
+ //
1048
+ if (options.scope) {
1049
+ scope = [options.scope, scope].join(this.defaultSeparator);
1050
+ }
1051
+
1052
+ return scope;
1053
+ };
1054
+ /**
1055
+ * Merge obj1 with obj2 (shallow merge), without modifying inputs
1056
+ * @param {Object} obj1
1057
+ * @param {Object} obj2
1058
+ * @returns {Object} Merged values of obj1 and obj2
1059
+ *
1060
+ * In order to support ES3, `Object.prototype.hasOwnProperty.call` is used
1061
+ * Idea is from:
1062
+ * https://stackoverflow.com/questions/8157700/object-has-no-hasownproperty-method-i-e-its-undefined-ie8
1063
+ */
1064
+ I18n.extend = function ( obj1, obj2 ) {
1065
+ if (typeof(obj1) === "undefined" && typeof(obj2) === "undefined") {
1066
+ return {};
1067
+ }
1068
+ return merge(obj1, obj2);
1069
+ };
1070
+
1071
+ // Set aliases, so we can save some typing.
1072
+ I18n.t = I18n.translate;
1073
+ I18n.l = I18n.localize;
1074
+ I18n.p = I18n.pluralize;
1075
+
1076
+ return I18n;
1077
+ }));