socialcast-i18n-js 4.0.0.rc1

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 (66) hide show
  1. data/.gitignore +13 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +2 -0
  4. data/README.md +324 -0
  5. data/README.rdoc +320 -0
  6. data/Rakefile +13 -0
  7. data/app/assets/javascripts/i18n/translations.js.erb +6 -0
  8. data/app/assets/javascripts/i18n.js +661 -0
  9. data/config/i18n-js.yml +22 -0
  10. data/i18n-js.gemspec +28 -0
  11. data/lib/i18n/js/engine.rb +6 -0
  12. data/lib/i18n/js/file_dependency_processor.rb +17 -0
  13. data/lib/i18n/js/railtie.rb +17 -0
  14. data/lib/i18n/js/translator.rb +56 -0
  15. data/lib/i18n/js/version.rb +10 -0
  16. data/lib/i18n-js/engine.rb +63 -0
  17. data/lib/i18n-js/middleware.rb +59 -0
  18. data/lib/i18n-js/railtie.rb +13 -0
  19. data/lib/i18n-js/rake.rb +16 -0
  20. data/lib/i18n-js/version.rb +10 -0
  21. data/lib/i18n-js.rb +22 -0
  22. data/lib/tasks/i18n-js.rake +17 -0
  23. data/spec/file_dependency_processor_spec.rb +32 -0
  24. data/spec/fixtures/custom_path.yml +2 -0
  25. data/spec/fixtures/default.yml +2 -0
  26. data/spec/fixtures/js_file_per_locale.yml +1 -0
  27. data/spec/fixtures/locales.yml +76 -0
  28. data/spec/fixtures/multiple_conditions.yml +3 -0
  29. data/spec/fixtures/no_config.yml +2 -0
  30. data/spec/fixtures/simple_scope.yml +2 -0
  31. data/spec/i18n_js_spec.rb +11 -0
  32. data/spec/i18n_spec.js +820 -0
  33. data/spec/i18n_spec.rb +205 -0
  34. data/spec/js/currency.spec.js +60 -0
  35. data/spec/js/current_locale.spec.js +19 -0
  36. data/spec/js/dates.spec.js +218 -0
  37. data/spec/js/defaults.spec.js +23 -0
  38. data/spec/js/interpolation.spec.js +28 -0
  39. data/spec/js/jasmine/MIT.LICENSE +20 -0
  40. data/spec/js/jasmine/jasmine-html.js +190 -0
  41. data/spec/js/jasmine/jasmine.css +166 -0
  42. data/spec/js/jasmine/jasmine.js +2476 -0
  43. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  44. data/spec/js/localization.spec.js +41 -0
  45. data/spec/js/numbers.spec.js +124 -0
  46. data/spec/js/placeholder.spec.js +24 -0
  47. data/spec/js/pluralization.spec.js +105 -0
  48. data/spec/js/prepare_options.spec.js +41 -0
  49. data/spec/js/specs.html +46 -0
  50. data/spec/js/translate.spec.js +119 -0
  51. data/spec/js/translations.js +115 -0
  52. data/spec/resources/custom_path.yml +4 -0
  53. data/spec/resources/default.yml +4 -0
  54. data/spec/resources/js_file_per_locale.yml +3 -0
  55. data/spec/resources/locales.yml +76 -0
  56. data/spec/resources/multiple_conditions.yml +6 -0
  57. data/spec/resources/multiple_files.yml +6 -0
  58. data/spec/resources/no_config.yml +2 -0
  59. data/spec/resources/no_scope.yml +3 -0
  60. data/spec/resources/simple_scope.yml +4 -0
  61. data/spec/spec_helper.rb +40 -0
  62. data/spec/translator_spec.rb +45 -0
  63. data/vendor/assets/javascripts/i18n/translations.js.erb +9 -0
  64. data/vendor/assets/javascripts/i18n.js +531 -0
  65. data/vendor/assets/javascripts/underscore.js +1059 -0
  66. metadata +240 -0
@@ -0,0 +1,531 @@
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 && scope.length > 0) {
116
+ currentScope = scope.shift();
117
+ messages = messages[currentScope];
118
+ }
119
+
120
+ if (!messages) {
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 && 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") {
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
+ I18n.toNumber = function(number, options) {
360
+ options = this.prepareOptions(
361
+ options,
362
+ this.lookup("number.format"),
363
+ {precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false}
364
+ );
365
+
366
+ var negative = number < 0
367
+ , string = Math.abs(number).toFixed(options.precision).toString()
368
+ , parts = string.split(".")
369
+ , precision
370
+ , buffer = []
371
+ , formattedNumber
372
+ ;
373
+
374
+ number = parts[0];
375
+ precision = parts[1];
376
+
377
+ while (number.length > 0) {
378
+ buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
379
+ number = number.substr(0, number.length -3);
380
+ }
381
+
382
+ formattedNumber = buffer.join(options.delimiter);
383
+
384
+ if (options.precision > 0) {
385
+ formattedNumber += options.separator + parts[1];
386
+ }
387
+
388
+ if (negative) {
389
+ formattedNumber = "-" + formattedNumber;
390
+ }
391
+
392
+ if (options.strip_insignificant_zeros) {
393
+ var regex = {
394
+ separator: new RegExp(options.separator.replace(/\./, "\\.") + "$")
395
+ , zeros: /0+$/
396
+ };
397
+
398
+ formattedNumber = formattedNumber
399
+ .replace(regex.zeros, "")
400
+ .replace(regex.separator, "")
401
+ ;
402
+ }
403
+
404
+ return formattedNumber;
405
+ };
406
+
407
+ I18n.toCurrency = function(number, options) {
408
+ options = this.prepareOptions(
409
+ options,
410
+ this.lookup("number.currency.format"),
411
+ this.lookup("number.format"),
412
+ {unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."}
413
+ );
414
+
415
+ number = this.toNumber(number, options);
416
+ number = options.format
417
+ .replace("%u", options.unit)
418
+ .replace("%n", number)
419
+ ;
420
+
421
+ return number;
422
+ };
423
+
424
+ I18n.toHumanSize = function(number, options) {
425
+ var kb = 1024
426
+ , size = number
427
+ , iterations = 0
428
+ , unit
429
+ , precision
430
+ ;
431
+
432
+ while (size >= kb && iterations < 4) {
433
+ size = size / kb;
434
+ iterations += 1;
435
+ }
436
+
437
+ if (iterations === 0) {
438
+ unit = this.t("number.human.storage_units.units.byte", {count: size});
439
+ precision = 0;
440
+ } else {
441
+ unit = this.t("number.human.storage_units.units." + [null, "kb", "mb", "gb", "tb"][iterations]);
442
+ precision = (size - Math.floor(size) === 0) ? 0 : 1;
443
+ }
444
+
445
+ options = this.prepareOptions(
446
+ options,
447
+ {precision: precision, format: "%n%u", delimiter: ""}
448
+ );
449
+
450
+ number = this.toNumber(size, options);
451
+ number = options.format
452
+ .replace("%u", unit)
453
+ .replace("%n", number)
454
+ ;
455
+
456
+ return number;
457
+ };
458
+
459
+ I18n.toPercentage = function(number, options) {
460
+ options = this.prepareOptions(
461
+ options,
462
+ this.lookup("number.percentage.format"),
463
+ this.lookup("number.format"),
464
+ {precision: 3, separator: ".", delimiter: ""}
465
+ );
466
+
467
+ number = this.toNumber(number, options);
468
+ return number + "%";
469
+ };
470
+
471
+ I18n.pluralizer = function(locale) {
472
+ pluralizer = this.pluralizationRules[locale];
473
+ if (pluralizer !== undefined) return pluralizer;
474
+ return this.pluralizationRules["en"];
475
+ };
476
+
477
+ I18n.findAndTranslateValidNode = function(keys, translation) {
478
+ for (i = 0; i < keys.length; i++) {
479
+ key = keys[i];
480
+ if (this.isValidNode(translation, key)) return translation[key];
481
+ }
482
+ return null;
483
+ };
484
+
485
+ I18n.pluralize = function(count, scope, options) {
486
+ var translation;
487
+
488
+ try {
489
+ translation = this.lookup(scope, options);
490
+ } catch (error) {}
491
+
492
+ if (!translation) {
493
+ return this.missingTranslation(scope);
494
+ }
495
+
496
+ var message;
497
+ options = this.prepareOptions(options);
498
+ options.count = count.toString();
499
+
500
+ pluralizer = this.pluralizer(this.currentLocale());
501
+ key = pluralizer(Math.abs(count));
502
+ keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key];
503
+
504
+ message = this.findAndTranslateValidNode(keys, translation);
505
+ if (message == null) message = this.missingTranslation(scope, keys[0]);
506
+
507
+ return this.interpolate(message, options);
508
+ };
509
+
510
+ I18n.missingTranslation = function() {
511
+ var message = '[missing "' + this.currentLocale()
512
+ , count = arguments.length
513
+ ;
514
+
515
+ for (var i = 0; i < count; i++) {
516
+ message += "." + arguments[i];
517
+ }
518
+
519
+ message += '" translation]';
520
+
521
+ return message;
522
+ };
523
+
524
+ I18n.currentLocale = function() {
525
+ return (I18n.locale || I18n.defaultLocale);
526
+ };
527
+
528
+ // shortcuts
529
+ I18n.t = I18n.translate;
530
+ I18n.l = I18n.localize;
531
+ I18n.p = I18n.pluralize;