ci-18n 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -0
- data/Guardfile +4 -0
- data/README.md +151 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/build/.gitkeep +0 -0
- data/build/ci-18n.js +464 -0
- data/build/ci-18n.min.js +52 -0
- data/ci-18n.gemspec +81 -0
- data/coffeescripts/I18n.coffee +220 -0
- data/javascripts/sprintf-0.7-beta1.js +183 -0
- data/lib/ci-18n.rb +1 -0
- data/lib/ci18n.rb +17 -0
- data/spec/coffeescripts/autoloadSpec.coffee +122 -0
- data/spec/coffeescripts/helpers/SpecHelper.coffee +6 -0
- data/spec/coffeescripts/languageRepositorySpec.coffee +34 -0
- data/spec/coffeescripts/localizeSpec.coffee +139 -0
- data/spec/coffeescripts/translateSpec.coffee +74 -0
- data/spec/coffeescripts/utilsSpec.coffee +61 -0
- data/spec/javascripts/support/jasmine.yml +74 -0
- data/spec/javascripts/support/jasmine_config.rb +23 -0
- data/spec/javascripts/support/jasmine_runner.rb +32 -0
- metadata +211 -0
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create @coffee-i18n
|
data/Guardfile
ADDED
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/build/.gitkeep
ADDED
File without changes
|
data/build/ci-18n.js
ADDED
@@ -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
|
+
};
|