i18n-js 2.1.2 → 3.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.
- data/Gemfile.lock +23 -31
- data/README.md +318 -0
- data/Rakefile +6 -6
- data/app/assets/javascripts/i18n/translations.js.erb +3 -0
- data/i18n-js.gemspec +5 -6
- data/lib/i18n-js.rb +1 -177
- data/lib/i18n.js +698 -0
- data/lib/i18n/js.rb +157 -0
- data/lib/i18n/js/engine.rb +22 -0
- data/lib/{i18n-js → i18n/js}/middleware.rb +7 -7
- data/lib/i18n/js/version.rb +10 -0
- data/spec/{resources → fixtures}/custom_path.yml +1 -1
- data/spec/{resources → fixtures}/default.yml +1 -1
- data/spec/fixtures/js_file_per_locale.yml +3 -0
- data/spec/{resources → fixtures}/locales.yml +1 -1
- data/spec/fixtures/multiple_conditions.yml +5 -0
- data/spec/{resources → fixtures}/multiple_files.yml +3 -3
- data/spec/{resources → fixtures}/no_config.yml +0 -0
- data/spec/{resources → fixtures}/no_scope.yml +1 -1
- data/spec/{resources → fixtures}/simple_scope.yml +1 -1
- data/spec/i18n_js_spec.rb +122 -0
- data/spec/js/currency.spec.js +60 -0
- data/spec/js/current_locale.spec.js +19 -0
- data/spec/js/dates.spec.js +218 -0
- data/spec/js/defaults.spec.js +23 -0
- data/spec/js/interpolation.spec.js +28 -0
- data/spec/js/jasmine/MIT.LICENSE +20 -0
- data/spec/js/jasmine/jasmine-html.js +190 -0
- data/spec/js/jasmine/jasmine.css +166 -0
- data/spec/js/jasmine/jasmine.js +2476 -0
- data/spec/js/jasmine/jasmine_favicon.png +0 -0
- data/spec/js/localization.spec.js +41 -0
- data/spec/js/numbers.spec.js +124 -0
- data/spec/js/placeholder.spec.js +24 -0
- data/spec/js/pluralization.spec.js +105 -0
- data/spec/js/prepare_options.spec.js +41 -0
- data/spec/js/specs.html +46 -0
- data/spec/js/translate.spec.js +115 -0
- data/spec/js/translations.js +115 -0
- data/spec/spec_helper.rb +36 -14
- metadata +115 -69
- data/.gitignore +0 -5
- data/.rspec +0 -1
- data/README.rdoc +0 -305
- data/config/i18n-js.yml +0 -22
- data/lib/i18n-js/engine.rb +0 -62
- data/lib/i18n-js/railtie.rb +0 -13
- data/lib/i18n-js/rake.rb +0 -16
- data/lib/i18n-js/version.rb +0 -10
- data/spec/i18n_spec.js +0 -768
- data/spec/i18n_spec.rb +0 -205
- data/spec/resources/js_file_per_locale.yml +0 -3
- data/spec/resources/multiple_conditions.yml +0 -6
- data/vendor/assets/javascripts/i18n.js +0 -450
- data/vendor/assets/javascripts/i18n/translations.js.erb +0 -7
data/lib/i18n/js.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
require "i18n"
|
2
|
+
require "FileUtils" unless defined?(FileUtils)
|
3
|
+
|
4
|
+
module I18n
|
5
|
+
module JS
|
6
|
+
if defined?(Rails)
|
7
|
+
require "i18n/js/middleware"
|
8
|
+
require "i18n/js/engine"
|
9
|
+
end
|
10
|
+
|
11
|
+
# deep_merge by Stefan Rusterholz, see <http://www.ruby-forum.com/topic/142809>.
|
12
|
+
MERGER = proc do |key, v1, v2|
|
13
|
+
Hash === v1 && Hash === v2 ? v1.merge(v2, &MERGER) : v2
|
14
|
+
end
|
15
|
+
|
16
|
+
# Detect if Rails app has asset pipeline support.
|
17
|
+
#
|
18
|
+
def self.has_asset_pipeline?
|
19
|
+
Rails.configuration.respond_to?(:assets) && Rails.configuration.assets.enabled
|
20
|
+
end
|
21
|
+
|
22
|
+
# The configuration file. This defaults to the `config/i18n-js.yml` file.
|
23
|
+
#
|
24
|
+
def self.config_file
|
25
|
+
@config_file ||= "config/i18n-js.yml"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Export translations to JavaScript, considering settings
|
29
|
+
# from configuration file
|
30
|
+
def self.export
|
31
|
+
translation_segments.each do |filename, translations|
|
32
|
+
save(translations, filename)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.segments_per_locale(pattern, scope)
|
37
|
+
I18n.available_locales.each_with_object({}) do |locale, segments|
|
38
|
+
result = scoped_translations("#{locale}.#{scope}")
|
39
|
+
next if result.empty?
|
40
|
+
|
41
|
+
segment_name = ::I18n.interpolate(pattern,{:locale => locale})
|
42
|
+
segments[segment_name] = result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.segment_for_scope(scope)
|
47
|
+
if scope == "*"
|
48
|
+
translations
|
49
|
+
else
|
50
|
+
scoped_translations(scope)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.configured_segments
|
55
|
+
config[:translations].each_with_object({}) do |options, segments|
|
56
|
+
options.reverse_merge!(:only => "*")
|
57
|
+
if options[:file] =~ ::I18n::INTERPOLATION_PATTERN
|
58
|
+
segments.merge!(segments_per_locale(options[:file], options[:only]))
|
59
|
+
else
|
60
|
+
result = segment_for_scope(options[:only])
|
61
|
+
segments[options[:file]] = result unless result.empty?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.export_dir
|
67
|
+
"public/javascripts"
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.filtered_translations
|
71
|
+
{}.tap do |result|
|
72
|
+
translation_segments.each do |filename, translations|
|
73
|
+
deep_merge!(result, translations)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.translation_segments
|
79
|
+
if config? && config[:translations]
|
80
|
+
configured_segments
|
81
|
+
else
|
82
|
+
{"#{export_dir}/translations.js" => translations}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Load configuration file for partial exporting and
|
87
|
+
# custom output directory
|
88
|
+
def self.config
|
89
|
+
if config?
|
90
|
+
(YAML.load_file(config_file) || {}).with_indifferent_access
|
91
|
+
else
|
92
|
+
{}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Check if configuration file exist
|
97
|
+
def self.config?
|
98
|
+
File.file? config_file
|
99
|
+
end
|
100
|
+
|
101
|
+
# Convert translations to JSON string and save file.
|
102
|
+
def self.save(translations, file)
|
103
|
+
FileUtils.mkdir_p File.dirname(file)
|
104
|
+
|
105
|
+
File.open(file, "w+") do |f|
|
106
|
+
f << %(I18n.translations = );
|
107
|
+
f << translations.to_json
|
108
|
+
f << %(;)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.scoped_translations(scopes) # :nodoc:
|
113
|
+
result = {}
|
114
|
+
|
115
|
+
[scopes].flatten.each do |scope|
|
116
|
+
deep_merge! result, filter(translations, scope)
|
117
|
+
end
|
118
|
+
|
119
|
+
result
|
120
|
+
end
|
121
|
+
|
122
|
+
# Filter translations according to the specified scope.
|
123
|
+
def self.filter(translations, scopes)
|
124
|
+
scopes = scopes.split(".") if scopes.is_a?(String)
|
125
|
+
scopes = scopes.clone
|
126
|
+
scope = scopes.shift
|
127
|
+
|
128
|
+
if scope == "*"
|
129
|
+
results = {}
|
130
|
+
translations.each do |scope, translations|
|
131
|
+
tmp = scopes.empty? ? translations : filter(translations, scopes)
|
132
|
+
results[scope.to_sym] = tmp unless tmp.nil?
|
133
|
+
end
|
134
|
+
return results
|
135
|
+
elsif translations.has_key?(scope.to_sym)
|
136
|
+
return {scope.to_sym => scopes.empty? ? translations[scope.to_sym] : filter(translations[scope.to_sym], scopes)}
|
137
|
+
end
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
141
|
+
# Initialize and return translations
|
142
|
+
def self.translations
|
143
|
+
::I18n.backend.instance_eval do
|
144
|
+
init_translations unless initialized?
|
145
|
+
translations
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.deep_merge(target, hash) # :nodoc:
|
150
|
+
target.merge(hash, &MERGER)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.deep_merge!(target, hash) # :nodoc:
|
154
|
+
target.merge!(hash, &MERGER)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "i18n/js"
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module JS
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
initializer :after => "sprockets.environment" do
|
7
|
+
path = File.expand_path("../../..", __FILE__)
|
8
|
+
::Rails.configuration.assets.paths.unshift(path) if JS.has_asset_pipeline?
|
9
|
+
end
|
10
|
+
|
11
|
+
ActiveSupport.on_load(:after_initialize, :yield => true) do
|
12
|
+
next unless JS.has_asset_pipeline?
|
13
|
+
|
14
|
+
Rails.application.assets.register_preprocessor "application/javascript", :"i18n-js_dependencies" do |context, source|
|
15
|
+
next source unless context.logical_path == "i18n/translations"
|
16
|
+
::I18n.load_path.each {|path| context.depend_on(path)}
|
17
|
+
source
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
module
|
2
|
-
module
|
1
|
+
module I18n
|
2
|
+
module JS
|
3
3
|
class Middleware
|
4
4
|
def initialize(app)
|
5
5
|
@app = app
|
@@ -46,13 +46,13 @@ module SimplesIdeias
|
|
46
46
|
new_cache[path] = changed_at
|
47
47
|
end
|
48
48
|
|
49
|
-
|
50
|
-
File.open(cache_path, "w+") do |file|
|
51
|
-
file << new_cache.to_yaml
|
52
|
-
end
|
49
|
+
return if valid_cache.all?
|
53
50
|
|
54
|
-
|
51
|
+
File.open(cache_path, "w+") do |file|
|
52
|
+
file << new_cache.to_yaml
|
55
53
|
end
|
54
|
+
|
55
|
+
::I18n::JS.export
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# Find more details about this configuration file at http://github.com/fnando/i18n-js
|
2
2
|
translations:
|
3
|
-
- file: "
|
3
|
+
- file: "tmp/i18n-js/all.js"
|
4
|
+
only: "*"
|
5
|
+
- file: "tmp/i18n-js/tudo.js"
|
4
6
|
only: "*"
|
5
|
-
- file: "public/javascripts/tudo.js"
|
6
|
-
only: "*"
|
File without changes
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe I18n::JS do
|
4
|
+
context "exporting" do
|
5
|
+
before do
|
6
|
+
I18n::JS.stub :export_dir => temp_path
|
7
|
+
end
|
8
|
+
|
9
|
+
it "exports messages to default path when configuration file doesn't exist" do
|
10
|
+
I18n::JS.export
|
11
|
+
file_should_exist "translations.js"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "exports messages using custom output path" do
|
15
|
+
set_config "custom_path.yml"
|
16
|
+
I18n::JS.should_receive(:save).with(translations, "tmp/i18n-js/all.js")
|
17
|
+
I18n::JS.export
|
18
|
+
end
|
19
|
+
|
20
|
+
it "sets default scope to * when not specified" do
|
21
|
+
set_config "no_scope.yml"
|
22
|
+
I18n::JS.should_receive(:save).with(translations, "tmp/i18n-js/no_scope.js")
|
23
|
+
I18n::JS.export
|
24
|
+
end
|
25
|
+
|
26
|
+
it "exports to multiple files" do
|
27
|
+
set_config "multiple_files.yml"
|
28
|
+
I18n::JS.export
|
29
|
+
|
30
|
+
file_should_exist "all.js"
|
31
|
+
file_should_exist "tudo.js"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "ignores an empty config file" do
|
35
|
+
set_config "no_config.yml"
|
36
|
+
I18n::JS.export
|
37
|
+
|
38
|
+
file_should_exist "translations.js"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "exports to a JS file per available locale" do
|
42
|
+
set_config "js_file_per_locale.yml"
|
43
|
+
I18n::JS.export
|
44
|
+
|
45
|
+
file_should_exist "en.js"
|
46
|
+
end
|
47
|
+
|
48
|
+
it "exports with multiple conditions" do
|
49
|
+
set_config "multiple_conditions.yml"
|
50
|
+
I18n::JS.export
|
51
|
+
|
52
|
+
file_should_exist "bitsnpieces.js"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "filters" do
|
57
|
+
it "filters translations using scope *.date.formats" do
|
58
|
+
result = I18n::JS.filter(translations, "*.date.formats")
|
59
|
+
result[:en][:date].keys.should eql([:formats])
|
60
|
+
result[:fr][:date].keys.should eql([:formats])
|
61
|
+
end
|
62
|
+
|
63
|
+
it "filters translations using scope [*.date.formats, *.number.currency.format]" do
|
64
|
+
result = I18n::JS.scoped_translations(["*.date.formats", "*.number.currency.format"])
|
65
|
+
result[:en].keys.collect(&:to_s).sort.should eql(%w[ date number ])
|
66
|
+
result[:fr].keys.collect(&:to_s).sort.should eql(%w[ date number ])
|
67
|
+
end
|
68
|
+
|
69
|
+
it "filters translations using multi-star scope" do
|
70
|
+
result = I18n::JS.scoped_translations("*.*.formats")
|
71
|
+
|
72
|
+
result[:en].keys.collect(&:to_s).sort.should eql(%w[ date time ])
|
73
|
+
result[:fr].keys.collect(&:to_s).sort.should eql(%w[ date time ])
|
74
|
+
|
75
|
+
result[:en][:date].keys.should eql([:formats])
|
76
|
+
result[:en][:time].keys.should eql([:formats])
|
77
|
+
|
78
|
+
result[:fr][:date].keys.should eql([:formats])
|
79
|
+
result[:fr][:time].keys.should eql([:formats])
|
80
|
+
end
|
81
|
+
|
82
|
+
it "filters translations using alternated stars" do
|
83
|
+
result = I18n::JS.scoped_translations("*.admin.*.title")
|
84
|
+
|
85
|
+
result[:en][:admin].keys.collect(&:to_s).sort.should eql(%w[ edit show ])
|
86
|
+
result[:fr][:admin].keys.collect(&:to_s).sort.should eql(%w[ edit show ])
|
87
|
+
|
88
|
+
result[:en][:admin][:show][:title].should eql("Show")
|
89
|
+
result[:fr][:admin][:show][:title].should eql("Visualiser")
|
90
|
+
|
91
|
+
result[:en][:admin][:edit][:title].should eql("Edit")
|
92
|
+
result[:fr][:admin][:edit][:title].should eql("Editer")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "general" do
|
97
|
+
it "sets export directory" do
|
98
|
+
I18n::JS.export_dir.should eql("public/javascripts")
|
99
|
+
end
|
100
|
+
|
101
|
+
it "sets empty hash as configuration when no file is found" do
|
102
|
+
I18n::JS.config?.should be_false
|
103
|
+
I18n::JS.config.should eql({})
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "hash merging" do
|
108
|
+
it "performs a deep merge" do
|
109
|
+
target = {:a => {:b => 1}}
|
110
|
+
result = I18n::JS.deep_merge(target, {:a => {:c => 2}})
|
111
|
+
|
112
|
+
result[:a].should eql({:b => 1, :c => 2})
|
113
|
+
end
|
114
|
+
|
115
|
+
it "performs a banged deep merge" do
|
116
|
+
target = {:a => {:b => 1}}
|
117
|
+
I18n::JS.deep_merge!(target, {:a => {:c => 2}})
|
118
|
+
|
119
|
+
target[:a].should eql({:b => 1, :c => 2})
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
var I18n = require("../../lib/i18n")
|
2
|
+
, Translations = require("./translations")
|
3
|
+
;
|
4
|
+
|
5
|
+
describe("Currency", function(){
|
6
|
+
var actual, expected;
|
7
|
+
|
8
|
+
beforeEach(function() {
|
9
|
+
I18n.reset();
|
10
|
+
I18n.translations = Translations();
|
11
|
+
});
|
12
|
+
|
13
|
+
it("formats currency with default settings", function(){
|
14
|
+
expect(I18n.toCurrency(100.99)).toEqual("$100.99");
|
15
|
+
expect(I18n.toCurrency(1000.99)).toEqual("$1,000.99");
|
16
|
+
});
|
17
|
+
|
18
|
+
it("formats currency with custom settings", function(){
|
19
|
+
I18n.translations.en.number = {
|
20
|
+
currency: {
|
21
|
+
format: {
|
22
|
+
format: "%n %u",
|
23
|
+
unit: "USD",
|
24
|
+
delimiter: ".",
|
25
|
+
separator: ",",
|
26
|
+
precision: 2
|
27
|
+
}
|
28
|
+
}
|
29
|
+
};
|
30
|
+
|
31
|
+
expect(I18n.toCurrency(12)).toEqual("12,00 USD");
|
32
|
+
expect(I18n.toCurrency(123)).toEqual("123,00 USD");
|
33
|
+
expect(I18n.toCurrency(1234.56)).toEqual("1.234,56 USD");
|
34
|
+
});
|
35
|
+
|
36
|
+
it("formats currency with custom settings and partial overriding", function(){
|
37
|
+
I18n.translations.en.number = {
|
38
|
+
currency: {
|
39
|
+
format: {
|
40
|
+
format: "%n %u",
|
41
|
+
unit: "USD",
|
42
|
+
delimiter: ".",
|
43
|
+
separator: ",",
|
44
|
+
precision: 2
|
45
|
+
}
|
46
|
+
}
|
47
|
+
};
|
48
|
+
|
49
|
+
expect(I18n.toCurrency(12, {precision: 0})).toEqual("12 USD");
|
50
|
+
expect(I18n.toCurrency(123, {unit: "bucks"})).toEqual("123,00 bucks");
|
51
|
+
});
|
52
|
+
|
53
|
+
it("formats currency with some custom options that should be merged with default options", function(){
|
54
|
+
expect(I18n.toCurrency(1234, {precision: 0})).toEqual("$1,234");
|
55
|
+
expect(I18n.toCurrency(1234, {unit: "º"})).toEqual("º1,234.00");
|
56
|
+
expect(I18n.toCurrency(1234, {separator: "-"})).toEqual("$1,234-00");
|
57
|
+
expect(I18n.toCurrency(1234, {delimiter: "-"})).toEqual("$1-234.00");
|
58
|
+
expect(I18n.toCurrency(1234, {format: "%u %n"})).toEqual("$ 1,234.00");
|
59
|
+
});
|
60
|
+
});
|