ci-18n 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create @coffee-i18n
@@ -0,0 +1,4 @@
1
+
2
+ guard 'coffeescript', :input => 'coffeescripts', :output =>'build/compiled', :bare => false
3
+
4
+ guard 'coffeescript', :input => 'spec/coffeescripts', :output => 'spec/javascripts', :bare => false
@@ -0,0 +1,151 @@
1
+ ci-18n
2
+ ======
3
+
4
+ This javascript library tries to bring the power of the
5
+ [I18n ruby library](https://github.com/svenfuchs/i18n.git)
6
+ to javascript.
7
+
8
+ Currently this library is on active development, so use it with caution.
9
+
10
+ Installation
11
+ ------
12
+
13
+ This library has been though to be used inside a [Ruby on Rails][rails]
14
+ application, namely rails 3.1 and its new asset pipeline.
15
+
16
+ [rails]: http://www.rubyonrails.org
17
+
18
+ ### Installation within rails 3.1
19
+
20
+ It's as simple as running in your rails root:
21
+
22
+ echo 'gem "ci-18n"' >> Gemfile
23
+
24
+ Then you should simply include the `ci-18n` js file where you want.
25
+ Namely you could include it inside your `application.js` or externalize
26
+ it. Note that running `rake assets:precompile` will compile it.
27
+
28
+ ### Installation without rails 3.1
29
+
30
+ Download the file stored [here][download_link] and put it near
31
+ your other javascripts.
32
+
33
+ [download_link]: https://github.com/mcollina/ci-18n/raw/master/build/ci-18n.min.js
34
+
35
+ General usage
36
+ --------------------
37
+
38
+ ### Translation files loading
39
+
40
+ There are two possible strategies for loading the translation files:
41
+
42
+ 1. include +all of them+ inside your `application.js` for faster loading,
43
+ and in this case you should add `I18n.autosetup("en")` to your main
44
+ js file (if en is your default language);
45
+ 2. autoload it at runtime from the browser, and in this case you have
46
+ to:
47
+ * publish your translation files under the `/locales` folder.
48
+ * add `I18.autoloadAndSetup({ path: "/locales", default: "en" })` if
49
+ en is your default language.
50
+
51
+ ### Translation file syntax
52
+
53
+ I18n.addLanguage("en", { hello: "world", another: "aaa" });
54
+
55
+ For translating date and times, it follows the very same rules of the
56
+ I18n ruby gem, and you might want to use the same data once ported to
57
+ javascript, like so:
58
+
59
+ I18n.addLanguage("de", {
60
+ date: {
61
+ formats: {
62
+ "default": "%d.%m.%Y",
63
+ short: "%d. %b",
64
+ long: "%d. %B %Y"
65
+ },
66
+ day_names: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
67
+ abbr_day_names: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
68
+ month_names: ["Januar", "Februar", "März", "April", "Mai",
69
+ "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember", null],
70
+ abbr_month_names: ["Jan", "Feb", "Mar", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]
71
+ },
72
+ time: {
73
+ formats: {
74
+ "default": "%a, %d. %b %Y %H:%M:%S %z",
75
+ short: "%d. %b %H:%M",
76
+ long: "%d. %B %Y %H:%M"
77
+ },
78
+ am: 'am',
79
+ pm: 'pm'
80
+ }
81
+ });
82
+
83
+ ### Translation & Localization
84
+
85
+ Really, if you are familiar with the ruby [I18n][i18n] library, it just works
86
+ the same.
87
+
88
+ You can translate with:
89
+ $i18n.t("hello.world")
90
+
91
+ If you want to localize a datetime:
92
+ $i18n.l(new Date(), format: "default", type: "datetime")
93
+
94
+ or if you want to localize just a date:
95
+ $i18n.l(new Date(), format: "default", type: "date")
96
+
97
+ [i18n]: https://github.com/svenfuchs/i18n
98
+
99
+ TODO
100
+ ----
101
+
102
+ * Testing on all major browser.
103
+ * Add some documentation.
104
+ * More documentation.
105
+ * Examples.
106
+ * Build a website.
107
+ * Build a translation repository like the one for I18n.
108
+ * Use of the rails asset pipeline to build a langs.js with all language
109
+ translations.
110
+ * Simplify autoloading.
111
+ * Clean up the development environment.
112
+
113
+ Development Environment
114
+ ------
115
+
116
+ Currently the build uses heavily some ruby gems, so run:
117
+
118
+ bundle install
119
+
120
+ This library is built with [CoffeeScript](https://github.com/jashkenas/coffee-script),
121
+ so you need to install it before doing everything else.
122
+
123
+ As it's built with CoffeeScript, we need to rebuild the sources before each test run,
124
+ and for that purpose start [Guard](https://github.com/guard/guard) with
125
+ the command:
126
+
127
+ guard
128
+
129
+ To build everything, hit
130
+
131
+ CTRL-\
132
+
133
+ To run the specs in the browser, launch the command
134
+
135
+ rake jasmine
136
+
137
+ and head to http://localhost:8888.
138
+
139
+ Finally, to build the js version to use in your code, run:
140
+
141
+ rake minify
142
+
143
+ That generates two files under the build/ directory, ci-18n.js and ci-18n.min.js.
144
+
145
+ Development
146
+ -----
147
+
148
+ * Source hosted at GitHub.
149
+ * Report Issues/Questions/Feature requests on GitHub Issues.
150
+
151
+ Pull requests are very welcome! Make sure your patches are well tested. Please create a topic branch for every separate change you make.
@@ -0,0 +1,47 @@
1
+
2
+ require 'active_support/concern'
3
+ require 'jasmine'
4
+ load 'jasmine/tasks/jasmine.rake'
5
+ require 'rake/minify'
6
+
7
+ Rake::Minify.new do
8
+ group("build/ci-18n.min.js") do
9
+ add("coffeescripts/I18n.coffee")
10
+ add("javascripts/sprintf-0.7-beta1.js")
11
+ end
12
+
13
+ group("build/ci-18n.js") do
14
+ add("coffeescripts/I18n.coffee", :minify => false)
15
+ add("javascripts/sprintf-0.7-beta1.js", :minify => false)
16
+ end
17
+ end
18
+
19
+ task :build_vendor => :minify do
20
+ dir = File.dirname(__FILE__) + "/vendor/assets/javascripts"
21
+ FileUtils.mkdir_p(dir)
22
+ FileUtils.cp("build/ci-18n.js", dir)
23
+ FileUtils.cp("build/ci-18n.min.js", dir)
24
+ end
25
+
26
+ require 'jeweler'
27
+ Jeweler::Tasks.new do |gem|
28
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
29
+ gem.name = "ci-18n"
30
+ gem.homepage = "http://github.com/mcollina/ci-18n"
31
+ gem.license = "MIT"
32
+ gem.summary = %Q{A localization library for javascript files in ruby on rails.}
33
+ gem.description = %Q{A localization library for javascript files in ruby on rails.}
34
+ gem.email = "hello@matteocollina.com"
35
+ gem.authors = ["Matteo Collina"]
36
+ gem.add_development_dependency "bundler", "~> 1.0.0"
37
+ gem.add_development_dependency 'jeweler', '>= 0'
38
+ gem.add_development_dependency 'coffee-script', '~> 2.2.0'
39
+ gem.add_development_dependency 'jasmine', '~> 1.0'
40
+ gem.add_development_dependency 'guard', '~> 0.3.1'
41
+ gem.add_development_dependency 'guard-coffeescript', '~> 0.2.0'
42
+ gem.add_development_dependency 'guard-livereload', '~> 0.1.9'
43
+ gem.add_development_dependency 'rake-minify', '~> 0.3'
44
+ end
45
+ Jeweler::RubygemsDotOrgTasks.new
46
+
47
+ task :build => :build_vendor
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
File without changes
@@ -0,0 +1,464 @@
1
+ (function() {
2
+ var I18n;
3
+ I18n = (function() {
4
+ var innerLookup;
5
+ function I18n(currentLocale, defaultLocale) {
6
+ if (currentLocale == null) {
7
+ currentLocale = {};
8
+ }
9
+ if (defaultLocale == null) {
10
+ defaultLocale = void 0;
11
+ }
12
+ if (typeof currentLocale === 'string') {
13
+ this.localeString = currentLocale;
14
+ } else {
15
+ this.localeVal = currentLocale;
16
+ }
17
+ if (typeof defaultLocale === 'string') {
18
+ this.defaultString = defaultLocale;
19
+ } else if (!(defaultLocale != null)) {
20
+ this.defaultString = I18n.getDefaultLanguage();
21
+ } else {
22
+ this.defaultVal = defaultLocale || {};
23
+ }
24
+ }
25
+ I18n.prototype.locale = function() {
26
+ return this.localeVal || (this.localeVal = I18n.language(this.localeString));
27
+ };
28
+ I18n.prototype.defaultLocale = function() {
29
+ return this.defaultVal || (this.defaultVal = I18n.language(this.defaultString));
30
+ };
31
+ innerLookup = function(locale, keywordList) {
32
+ var keyword, _i, _len;
33
+ for (_i = 0, _len = keywordList.length; _i < _len; _i++) {
34
+ keyword = keywordList[_i];
35
+ if (locale == null) {
36
+ break;
37
+ }
38
+ locale = locale[keyword];
39
+ }
40
+ return locale;
41
+ };
42
+ I18n.prototype.translate = function(keywordList, options) {
43
+ var lookup;
44
+ if (options == null) {
45
+ options = {};
46
+ }
47
+ keywordList = I18n.normalizeKeys(keywordList, options);
48
+ lookup = innerLookup(this.locale(), keywordList) || options["default"] || innerLookup(this.defaultLocale(), keywordList);
49
+ delete options.scope;
50
+ delete options["default"];
51
+ return I18n.interpolate(lookup, options);
52
+ };
53
+ I18n.prototype.localize = function(date, options) {
54
+ var match, matches, regexp, replacement_builder, string, _i, _len;
55
+ if (!(date instanceof Date)) {
56
+ throw "Argument Error: " + date + " is not localizable";
57
+ }
58
+ regexp = /%([a-z]|%)/ig;
59
+ string = options.format;
60
+ matches = string.match(regexp);
61
+ if (matches == null) {
62
+ if (options.type == null) {
63
+ throw "Argument Error: missing type";
64
+ }
65
+ if (options.type === "datetime") {
66
+ options.type = "time";
67
+ }
68
+ string = this.translate("" + options.type + ".formats." + options.format);
69
+ if (string != null) {
70
+ matches = string.match(regexp);
71
+ }
72
+ }
73
+ if (matches == null) {
74
+ throw "Argument Error: no such format";
75
+ }
76
+ for (_i = 0, _len = matches.length; _i < _len; _i++) {
77
+ match = matches[_i];
78
+ match = match.slice(-1);
79
+ replacement_builder = I18n.strftime[match];
80
+ if (replacement_builder != null) {
81
+ string = string.replace("%" + match, replacement_builder(date, this));
82
+ }
83
+ }
84
+ return string;
85
+ };
86
+ I18n.prototype.t = I18n.prototype.translate;
87
+ I18n.prototype.l = I18n.prototype.localize;
88
+ return I18n;
89
+ })();
90
+ I18n.normalizeKeys = function(keywords, options) {
91
+ var keyword, splitted_keywords, _i, _len, _ref;
92
+ if (keywords == null) {
93
+ keywords = [];
94
+ }
95
+ if (options == null) {
96
+ options = {
97
+ scope: []
98
+ };
99
+ }
100
+ if (keywords instanceof Array) {
101
+ return keywords;
102
+ }
103
+ splitted_keywords = [];
104
+ _ref = keywords.split(".");
105
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
106
+ keyword = _ref[_i];
107
+ if ((keyword != null) && keyword !== '') {
108
+ splitted_keywords.push(keyword);
109
+ }
110
+ }
111
+ return I18n.normalizeKeys(options.scope).concat(splitted_keywords);
112
+ };
113
+ (function() {
114
+ var interpolate_basic, interpolate_sprintf;
115
+ interpolate_basic = function(string, option, value) {
116
+ var new_string;
117
+ new_string = string.replace(RegExp("%{" + option + "}", "g"), value);
118
+ if (string === new_string) {
119
+ return;
120
+ }
121
+ return new_string;
122
+ };
123
+ interpolate_sprintf = function(string, option, value) {
124
+ var match, regexp, result;
125
+ regexp = RegExp("%<" + option + ">(.*?\\d*\\.?\\d*[bBdiouxXeEfgGcps])");
126
+ match = string.match(regexp);
127
+ if (match == null) {
128
+ return;
129
+ }
130
+ result = sprintf("%(keyword)" + match[1], {
131
+ keyword: value
132
+ });
133
+ return string.replace(match[0], result);
134
+ };
135
+ return I18n.interpolate = function(string, options) {
136
+ var new_string, option, value;
137
+ if (options == null) {
138
+ options = {};
139
+ }
140
+ if (!(string != null)) {
141
+ return string;
142
+ }
143
+ for (option in options) {
144
+ value = options[option];
145
+ new_string = interpolate_basic(string, option, value);
146
+ new_string || (new_string = interpolate_sprintf(string, option, value));
147
+ if (new_string == null) {
148
+ throw new Error("Missing placeholder for keyword \"" + option + "\"");
149
+ }
150
+ string = new_string;
151
+ }
152
+ return string;
153
+ };
154
+ })();
155
+ I18n.strftime = {
156
+ 'd': function(date) {
157
+ return ('0' + date.getDate()).slice(-2);
158
+ },
159
+ 'b': function(date, i18n) {
160
+ return i18n.t("date.abbr_month_names")[date.getMonth()];
161
+ },
162
+ 'B': function(date, i18n) {
163
+ return i18n.t("date.month_names")[date.getMonth()];
164
+ },
165
+ 'a': function(date, i18n) {
166
+ return i18n.t("date.abbr_day_names")[date.getDay()];
167
+ },
168
+ 'A': function(date, i18n) {
169
+ return i18n.t("date.day_names")[date.getDay()];
170
+ },
171
+ 'Y': function(date) {
172
+ return date.getFullYear();
173
+ },
174
+ 'm': function(date) {
175
+ return ('0' + (date.getMonth() + 1)).slice(-2);
176
+ },
177
+ 'H': function(date) {
178
+ return ('0' + (date.getHours())).slice(-2);
179
+ },
180
+ 'M': function(date) {
181
+ return ('0' + (date.getMinutes())).slice(-2);
182
+ },
183
+ 'S': function(date) {
184
+ return ('0' + (date.getSeconds())).slice(-2);
185
+ },
186
+ 'z': function(date) {
187
+ var tz_offset;
188
+ tz_offset = date.getTimezoneOffset();
189
+ return (tz_offset > 0 && '-' || '+') + ('0' + (tz_offset / 60)).slice(-2) + ('0' + (tz_offset % 60)).slice(-2);
190
+ },
191
+ 'p': function(date, i18n) {
192
+ return i18n.t("time")[date.getHours() >= 12 && 'pm' || 'am'];
193
+ },
194
+ 'e': function(date) {
195
+ return date.getDate();
196
+ },
197
+ 'I': function(date) {
198
+ return ('0' + (date.getHours() % 12)).slice(-2);
199
+ },
200
+ 'j': function(date) {
201
+ return (((date.getTime() - new Date("Jan 1 " + date.getFullYear()).getTime()) / (1000 * 60 * 60 * 24) + 1) + '').split(/\./)[0];
202
+ },
203
+ 'k': function(date) {
204
+ return date.getHours();
205
+ },
206
+ 'l': function(date) {
207
+ return date.getHours() % 12;
208
+ },
209
+ 'w': function(date) {
210
+ return date.getDay();
211
+ },
212
+ 'y': function(date) {
213
+ return ("" + (date.getYear())).slice(-2);
214
+ },
215
+ '%': function() {
216
+ return '%';
217
+ }
218
+ };
219
+ (function() {
220
+ var defaultLanguage, languages;
221
+ languages = {};
222
+ defaultLanguage = void 0;
223
+ I18n.addLanguage = function(name, lang) {
224
+ return languages[name] = lang;
225
+ };
226
+ I18n.clearLanguages = function() {
227
+ return languages = {};
228
+ };
229
+ I18n.language = function(name) {
230
+ return languages[name];
231
+ };
232
+ I18n.setDefaultLanguage = function(name) {
233
+ return defaultLanguage = name;
234
+ };
235
+ return I18n.getDefaultLanguage = function() {
236
+ return defaultLanguage;
237
+ };
238
+ })();
239
+ I18n.load = function(path, lang) {
240
+ var script, url;
241
+ url = "" + path + "/" + lang + ".js";
242
+ script = document.createElement('script');
243
+ script.setAttribute('src', url);
244
+ return document.getElementsByTagName('head')[0].appendChild(script);
245
+ };
246
+ I18n.detectLanguage = function(navigator) {
247
+ var name, _i, _len, _ref;
248
+ _ref = ["language", "browserLanguage"];
249
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
250
+ name = _ref[_i];
251
+ if (navigator[name] != null) {
252
+ return navigator[name];
253
+ }
254
+ }
255
+ };
256
+ I18n.autoloadAndSetup = function(options) {
257
+ var lang, langsToLoad, _i, _len;
258
+ if (options.language == null) {
259
+ options.language = I18n.detectLanguage(window.navigator);
260
+ }
261
+ langsToLoad = [options.language];
262
+ if (options["default"] != null) {
263
+ langsToLoad.push(options["default"]);
264
+ }
265
+ for (_i = 0, _len = langsToLoad.length; _i < _len; _i++) {
266
+ lang = langsToLoad[_i];
267
+ I18n.load(options.path, lang);
268
+ }
269
+ return I18n.setup(options.language, options["default"]);
270
+ };
271
+ I18n.setup = function(locale, defaultLocale) {
272
+ return window.$i18n = new I18n(locale, defaultLocale);
273
+ };
274
+ I18n.autosetup = function(defaultLocale) {
275
+ var locale;
276
+ locale = I18n.detectLanguage(window.navigator);
277
+ return I18n.setup(locale, defaultLocale);
278
+ };
279
+ window.I18n = I18n;
280
+ }).call(this);
281
+
282
+ /**
283
+ sprintf() for JavaScript 0.7-beta1
284
+ http://www.diveintojavascript.com/projects/javascript-sprintf
285
+
286
+ Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com>
287
+ All rights reserved.
288
+
289
+ Redistribution and use in source and binary forms, with or without
290
+ modification, are permitted provided that the following conditions are met:
291
+ * Redistributions of source code must retain the above copyright
292
+ notice, this list of conditions and the following disclaimer.
293
+ * Redistributions in binary form must reproduce the above copyright
294
+ notice, this list of conditions and the following disclaimer in the
295
+ documentation and/or other materials provided with the distribution.
296
+ * Neither the name of sprintf() for JavaScript nor the
297
+ names of its contributors may be used to endorse or promote products
298
+ derived from this software without specific prior written permission.
299
+
300
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
301
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
302
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
303
+ DISCLAIMED. IN NO EVENT SHALL Alexandru Marasteanu BE LIABLE FOR ANY
304
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
305
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
306
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
307
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
308
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
309
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
310
+
311
+
312
+ Changelog:
313
+ 2010.09.06 - 0.7-beta1
314
+ - features: vsprintf, support for named placeholders
315
+ - enhancements: format cache, reduced global namespace pollution
316
+
317
+ 2010.05.22 - 0.6:
318
+ - reverted to 0.4 and fixed the bug regarding the sign of the number 0
319
+ Note:
320
+ Thanks to Raphael Pigulla <raph (at] n3rd [dot) org> (http://www.n3rd.org/)
321
+ who warned me about a bug in 0.5, I discovered that the last update was
322
+ a regress. I appologize for that.
323
+
324
+ 2010.05.09 - 0.5:
325
+ - bug fix: 0 is now preceeded with a + sign
326
+ - bug fix: the sign was not at the right position on padded results (Kamal Abdali)
327
+ - switched from GPL to BSD license
328
+
329
+ 2007.10.21 - 0.4:
330
+ - unit test and patch (David Baird)
331
+
332
+ 2007.09.17 - 0.3:
333
+ - bug fix: no longer throws exception on empty paramenters (Hans Pufal)
334
+
335
+ 2007.09.11 - 0.2:
336
+ - feature: added argument swapping
337
+
338
+ 2007.04.03 - 0.1:
339
+ - initial release
340
+ **/
341
+
342
+ var sprintf = (function() {
343
+ function get_type(variable) {
344
+ return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
345
+ }
346
+ function str_repeat(input, multiplier) {
347
+ for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */}
348
+ return output.join('');
349
+ }
350
+
351
+ var str_format = function() {
352
+ if (!str_format.cache.hasOwnProperty(arguments[0])) {
353
+ str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
354
+ }
355
+ return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
356
+ };
357
+
358
+ str_format.format = function(parse_tree, argv) {
359
+ var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
360
+ for (i = 0; i < tree_length; i++) {
361
+ node_type = get_type(parse_tree[i]);
362
+ if (node_type === 'string') {
363
+ output.push(parse_tree[i]);
364
+ }
365
+ else if (node_type === 'array') {
366
+ match = parse_tree[i]; // convenience purposes only
367
+ if (match[2]) { // keyword argument
368
+ arg = argv[cursor];
369
+ for (k = 0; k < match[2].length; k++) {
370
+ if (!arg.hasOwnProperty(match[2][k])) {
371
+ throw(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
372
+ }
373
+ arg = arg[match[2][k]];
374
+ }
375
+ }
376
+ else if (match[1]) { // positional argument (explicit)
377
+ arg = argv[match[1]];
378
+ }
379
+ else { // positional argument (implicit)
380
+ arg = argv[cursor++];
381
+ }
382
+
383
+ if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
384
+ throw(sprintf('[sprintf] expecting number but found %s', get_type(arg)));
385
+ }
386
+ switch (match[8]) {
387
+ case 'b': arg = arg.toString(2); break;
388
+ case 'c': arg = String.fromCharCode(arg); break;
389
+ case 'd': arg = parseInt(arg, 10); break;
390
+ case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
391
+ case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
392
+ case 'o': arg = arg.toString(8); break;
393
+ case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
394
+ case 'u': arg = Math.abs(arg); break;
395
+ case 'x': arg = arg.toString(16); break;
396
+ case 'X': arg = arg.toString(16).toUpperCase(); break;
397
+ }
398
+ arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
399
+ pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
400
+ pad_length = match[6] - String(arg).length;
401
+ pad = match[6] ? str_repeat(pad_character, pad_length) : '';
402
+ output.push(match[5] ? arg + pad : pad + arg);
403
+ }
404
+ }
405
+ return output.join('');
406
+ };
407
+
408
+ str_format.cache = {};
409
+
410
+ str_format.parse = function(fmt) {
411
+ var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
412
+ while (_fmt) {
413
+ if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
414
+ parse_tree.push(match[0]);
415
+ }
416
+ else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
417
+ parse_tree.push('%');
418
+ }
419
+ else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
420
+ if (match[2]) {
421
+ arg_names |= 1;
422
+ var field_list = [], replacement_field = match[2], field_match = [];
423
+ if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
424
+ field_list.push(field_match[1]);
425
+ while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
426
+ if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
427
+ field_list.push(field_match[1]);
428
+ }
429
+ else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
430
+ field_list.push(field_match[1]);
431
+ }
432
+ else {
433
+ throw('[sprintf] huh?');
434
+ }
435
+ }
436
+ }
437
+ else {
438
+ throw('[sprintf] huh?');
439
+ }
440
+ match[2] = field_list;
441
+ }
442
+ else {
443
+ arg_names |= 2;
444
+ }
445
+ if (arg_names === 3) {
446
+ throw('[sprintf] mixing positional and named placeholders is not (yet) supported');
447
+ }
448
+ parse_tree.push(match);
449
+ }
450
+ else {
451
+ throw('[sprintf] huh?');
452
+ }
453
+ _fmt = _fmt.substring(match[0].length);
454
+ }
455
+ return parse_tree;
456
+ };
457
+
458
+ return str_format;
459
+ })();
460
+
461
+ var vsprintf = function(fmt, argv) {
462
+ argv.unshift(fmt);
463
+ return sprintf.apply(null, argv);
464
+ };