i18n-js 3.2.1 → 4.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +4 -0
  3. data/.github/FUNDING.yml +3 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  5. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +38 -0
  8. data/.github/dependabot.yml +15 -0
  9. data/.github/workflows/ruby-tests.yml +73 -0
  10. data/.gitignore +13 -7
  11. data/.rubocop.yml +19 -0
  12. data/CHANGELOG.md +45 -386
  13. data/CODE_OF_CONDUCT.md +74 -0
  14. data/CONTRIBUTING.md +79 -0
  15. data/Gemfile +3 -0
  16. data/LICENSE.md +20 -0
  17. data/MIGRATING_FROM_V3_TO_V4.md +191 -0
  18. data/README.md +439 -791
  19. data/Rakefile +10 -20
  20. data/bin/release +81 -0
  21. data/exe/i18n +5 -0
  22. data/i18n-js.gemspec +51 -29
  23. data/lib/guard/i18n-js/templates/Guardfile +10 -0
  24. data/lib/guard/i18n-js/version.rb +13 -0
  25. data/lib/guard/i18n-js.rb +95 -0
  26. data/lib/i18n-js/clean_hash.rb +13 -0
  27. data/lib/i18n-js/cli/check_command.rb +17 -0
  28. data/lib/i18n-js/cli/command.rb +79 -0
  29. data/lib/i18n-js/cli/export_command.rb +95 -0
  30. data/lib/i18n-js/cli/init_command.rb +52 -0
  31. data/lib/i18n-js/cli/lint_scripts_command.rb +157 -0
  32. data/lib/i18n-js/cli/lint_translations_command.rb +155 -0
  33. data/lib/i18n-js/cli/plugins_command.rb +67 -0
  34. data/lib/i18n-js/cli/ui.rb +64 -0
  35. data/lib/i18n-js/cli/version_command.rb +18 -0
  36. data/lib/i18n-js/cli.rb +66 -0
  37. data/lib/i18n-js/embed_fallback_translations_plugin.rb +70 -0
  38. data/lib/i18n-js/export_files_plugin.rb +103 -0
  39. data/lib/i18n-js/lint.js +150645 -0
  40. data/lib/i18n-js/lint.ts +196 -0
  41. data/lib/i18n-js/listen.rb +96 -0
  42. data/lib/i18n-js/plugin.rb +103 -0
  43. data/lib/i18n-js/schema.rb +216 -0
  44. data/lib/i18n-js/sort_hash.rb +12 -0
  45. data/lib/i18n-js/version.rb +5 -0
  46. data/lib/i18n-js.rb +107 -1
  47. data/package.json +5 -20
  48. metadata +153 -181
  49. data/.editorconfig +0 -24
  50. data/.npmignore +0 -27
  51. data/.travis.yml +0 -36
  52. data/Appraisals +0 -32
  53. data/app/assets/javascripts/i18n/filtered.js.erb +0 -23
  54. data/app/assets/javascripts/i18n/shims.js +0 -208
  55. data/app/assets/javascripts/i18n/translations.js +0 -3
  56. data/app/assets/javascripts/i18n.js +0 -1077
  57. data/gemfiles/i18n_0_6.gemfile +0 -7
  58. data/gemfiles/i18n_0_7.gemfile +0 -7
  59. data/gemfiles/i18n_0_8.gemfile +0 -7
  60. data/gemfiles/i18n_0_9.gemfile +0 -7
  61. data/gemfiles/i18n_1_0.gemfile +0 -7
  62. data/gemfiles/i18n_1_1.gemfile +0 -7
  63. data/gemfiles/i18n_1_2.gemfile +0 -7
  64. data/gemfiles/i18n_1_3.gemfile +0 -7
  65. data/gemfiles/i18n_1_4.gemfile +0 -7
  66. data/gemfiles/i18n_1_5.gemfile +0 -7
  67. data/lib/i18n/js/dependencies.rb +0 -59
  68. data/lib/i18n/js/engine.rb +0 -87
  69. data/lib/i18n/js/fallback_locales.rb +0 -70
  70. data/lib/i18n/js/formatters/base.rb +0 -23
  71. data/lib/i18n/js/formatters/js.rb +0 -31
  72. data/lib/i18n/js/formatters/json.rb +0 -13
  73. data/lib/i18n/js/middleware.rb +0 -82
  74. data/lib/i18n/js/private/hash_with_symbol_keys.rb +0 -36
  75. data/lib/i18n/js/segment.rb +0 -75
  76. data/lib/i18n/js/utils.rb +0 -65
  77. data/lib/i18n/js/version.rb +0 -7
  78. data/lib/i18n/js.rb +0 -259
  79. data/lib/rails/generators/i18n/js/config/config_generator.rb +0 -19
  80. data/lib/rails/generators/i18n/js/config/templates/i18n-js.yml +0 -27
  81. data/lib/tasks/export.rake +0 -8
  82. data/spec/fixtures/custom_path.yml +0 -5
  83. data/spec/fixtures/default.yml +0 -5
  84. data/spec/fixtures/erb.yml +0 -5
  85. data/spec/fixtures/except_condition.yml +0 -7
  86. data/spec/fixtures/js_export_dir_custom.yml +0 -7
  87. data/spec/fixtures/js_export_dir_none.yml +0 -6
  88. data/spec/fixtures/js_extend_parent.yml +0 -6
  89. data/spec/fixtures/js_extend_segment.yml +0 -6
  90. data/spec/fixtures/js_file_per_locale.yml +0 -7
  91. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +0 -4
  92. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +0 -6
  93. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +0 -4
  94. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +0 -4
  95. data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +0 -4
  96. data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +0 -4
  97. data/spec/fixtures/js_file_with_namespace_and_pretty_print.yml +0 -7
  98. data/spec/fixtures/js_sort_translation_keys_false.yml +0 -6
  99. data/spec/fixtures/js_sort_translation_keys_true.yml +0 -6
  100. data/spec/fixtures/json_only.yml +0 -18
  101. data/spec/fixtures/locales.yml +0 -95
  102. data/spec/fixtures/merge_plurals.yml +0 -6
  103. data/spec/fixtures/multiple_conditions.yml +0 -7
  104. data/spec/fixtures/multiple_conditions_per_locale.yml +0 -7
  105. data/spec/fixtures/multiple_files.yml +0 -7
  106. data/spec/fixtures/no_config.yml +0 -2
  107. data/spec/fixtures/no_scope.yml +0 -4
  108. data/spec/fixtures/simple_scope.yml +0 -5
  109. data/spec/js/currency.spec.js +0 -62
  110. data/spec/js/current_locale.spec.js +0 -19
  111. data/spec/js/dates.spec.js +0 -265
  112. data/spec/js/defaults.spec.js +0 -31
  113. data/spec/js/extend.spec.js +0 -110
  114. data/spec/js/interpolation.spec.js +0 -124
  115. data/spec/js/jasmine/MIT.LICENSE +0 -20
  116. data/spec/js/jasmine/jasmine-html.js +0 -190
  117. data/spec/js/jasmine/jasmine.css +0 -166
  118. data/spec/js/jasmine/jasmine.js +0 -2476
  119. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  120. data/spec/js/locales.spec.js +0 -31
  121. data/spec/js/localization.spec.js +0 -48
  122. data/spec/js/numbers.spec.js +0 -170
  123. data/spec/js/placeholder.spec.js +0 -24
  124. data/spec/js/pluralization.spec.js +0 -211
  125. data/spec/js/prepare_options.spec.js +0 -41
  126. data/spec/js/require.js +0 -2083
  127. data/spec/js/specs.html +0 -49
  128. data/spec/js/specs_requirejs.html +0 -72
  129. data/spec/js/translate.spec.js +0 -284
  130. data/spec/js/translations.js +0 -163
  131. data/spec/js/utility_functions.spec.js +0 -20
  132. data/spec/ruby/i18n/js/fallback_locales_spec.rb +0 -84
  133. data/spec/ruby/i18n/js/segment_spec.rb +0 -219
  134. data/spec/ruby/i18n/js/utils_spec.rb +0 -106
  135. data/spec/ruby/i18n/js_spec.rb +0 -663
  136. data/spec/spec_helper.rb +0 -79
  137. data/yarn.lock +0 -131
@@ -1,1077 +0,0 @@
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 (typeof message !== "undefined" && message !== null) {
479
- break;
480
- }
481
- }
482
-
483
- if (typeof message === "undefined" || message === null) {
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
- var usedScope = scope;
579
-
580
- var optionsWithoutDefault = this.prepareOptions(options)
581
- delete optionsWithoutDefault.defaultValue
582
-
583
- // Iterate through the translation options until a translation
584
- // or message is found.
585
- var translationFound =
586
- translationOptions.some(function(translationOption) {
587
- if (isSet(translationOption.scope)) {
588
- usedScope = translationOption.scope;
589
- translation = this.lookup(usedScope, optionsWithoutDefault);
590
- } else if (isSet(translationOption.message)) {
591
- translation = lazyEvaluate(translationOption.message, scope);
592
- }
593
-
594
- if (translation !== undefined && translation !== null) {
595
- return true;
596
- }
597
- }, this);
598
-
599
- if (!translationFound) {
600
- return this.missingTranslation(scope, options);
601
- }
602
-
603
- if (typeof(translation) === "string") {
604
- translation = this.interpolate(translation, options);
605
- } else if (isArray(translation)) {
606
- translation = translation.map(function(t) {
607
- return (typeof(t) === "string" ? this.interpolate(t, options) : t);
608
- }, this);
609
- } else if (isObject(translation) && isSet(options.count)) {
610
- translation = this.pluralize(options.count, usedScope, options);
611
- }
612
-
613
- return translation;
614
- };
615
-
616
- // This function interpolates the all variables in the given message.
617
- I18n.interpolate = function(message, options) {
618
- if (message === null) {
619
- return message;
620
- }
621
-
622
- options = options || {};
623
- var matches = message.match(this.placeholder)
624
- , placeholder
625
- , value
626
- , name
627
- , regex
628
- ;
629
-
630
- if (!matches) {
631
- return message;
632
- }
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, result;
659
-
660
- result = this.pluralizationLookup(count, scope, options);
661
- if (typeof result.translations === "undefined" || result.translations == null) {
662
- return this.missingTranslation(scope, options);
663
- }
664
-
665
- if (typeof 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
- }));