i18n-js 2.1.2 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/Gemfile.lock +23 -31
  2. data/README.md +318 -0
  3. data/Rakefile +6 -6
  4. data/app/assets/javascripts/i18n/translations.js.erb +3 -0
  5. data/i18n-js.gemspec +5 -6
  6. data/lib/i18n-js.rb +1 -177
  7. data/lib/i18n.js +698 -0
  8. data/lib/i18n/js.rb +157 -0
  9. data/lib/i18n/js/engine.rb +22 -0
  10. data/lib/{i18n-js → i18n/js}/middleware.rb +7 -7
  11. data/lib/i18n/js/version.rb +10 -0
  12. data/spec/{resources → fixtures}/custom_path.yml +1 -1
  13. data/spec/{resources → fixtures}/default.yml +1 -1
  14. data/spec/fixtures/js_file_per_locale.yml +3 -0
  15. data/spec/{resources → fixtures}/locales.yml +1 -1
  16. data/spec/fixtures/multiple_conditions.yml +5 -0
  17. data/spec/{resources → fixtures}/multiple_files.yml +3 -3
  18. data/spec/{resources → fixtures}/no_config.yml +0 -0
  19. data/spec/{resources → fixtures}/no_scope.yml +1 -1
  20. data/spec/{resources → fixtures}/simple_scope.yml +1 -1
  21. data/spec/i18n_js_spec.rb +122 -0
  22. data/spec/js/currency.spec.js +60 -0
  23. data/spec/js/current_locale.spec.js +19 -0
  24. data/spec/js/dates.spec.js +218 -0
  25. data/spec/js/defaults.spec.js +23 -0
  26. data/spec/js/interpolation.spec.js +28 -0
  27. data/spec/js/jasmine/MIT.LICENSE +20 -0
  28. data/spec/js/jasmine/jasmine-html.js +190 -0
  29. data/spec/js/jasmine/jasmine.css +166 -0
  30. data/spec/js/jasmine/jasmine.js +2476 -0
  31. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  32. data/spec/js/localization.spec.js +41 -0
  33. data/spec/js/numbers.spec.js +124 -0
  34. data/spec/js/placeholder.spec.js +24 -0
  35. data/spec/js/pluralization.spec.js +105 -0
  36. data/spec/js/prepare_options.spec.js +41 -0
  37. data/spec/js/specs.html +46 -0
  38. data/spec/js/translate.spec.js +115 -0
  39. data/spec/js/translations.js +115 -0
  40. data/spec/spec_helper.rb +36 -14
  41. metadata +115 -69
  42. data/.gitignore +0 -5
  43. data/.rspec +0 -1
  44. data/README.rdoc +0 -305
  45. data/config/i18n-js.yml +0 -22
  46. data/lib/i18n-js/engine.rb +0 -62
  47. data/lib/i18n-js/railtie.rb +0 -13
  48. data/lib/i18n-js/rake.rb +0 -16
  49. data/lib/i18n-js/version.rb +0 -10
  50. data/spec/i18n_spec.js +0 -768
  51. data/spec/i18n_spec.rb +0 -205
  52. data/spec/resources/js_file_per_locale.yml +0 -3
  53. data/spec/resources/multiple_conditions.yml +0 -6
  54. data/vendor/assets/javascripts/i18n.js +0 -450
  55. data/vendor/assets/javascripts/i18n/translations.js.erb +0 -7
@@ -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 SimplesIdeias
2
- module I18n
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
- unless valid_cache.all?
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
- SimplesIdeias::I18n.export!
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
@@ -0,0 +1,10 @@
1
+ module I18n
2
+ module JS
3
+ module Version
4
+ MAJOR = 3
5
+ MINOR = 0
6
+ PATCH = 0
7
+ STRING = "#{MAJOR}.#{MINOR}.#{PATCH}.rc1"
8
+ end
9
+ end
10
+ end
@@ -1,4 +1,4 @@
1
1
  # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
2
  translations:
3
- - file: "public/javascripts/translations/all.js"
3
+ - file: "tmp/i18n-js/all.js"
4
4
  only: "*"
@@ -1,4 +1,4 @@
1
1
  # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
2
  translations:
3
- - file: "public/javascripts/translations.js"
3
+ - file: "tmp/i18n-js/translations.js"
4
4
  only: "*"
@@ -0,0 +1,3 @@
1
+ translations:
2
+ - file: "tmp/i18n-js/%{locale}.js"
3
+ only: '*'
@@ -73,4 +73,4 @@ fr:
73
73
  title: "Visualiser"
74
74
  note: "plus de détails"
75
75
  edit:
76
- title: "Editer"
76
+ title: "Editer"
@@ -0,0 +1,5 @@
1
+ translations:
2
+ - file: "tmp/i18n-js/bitsnpieces.js"
3
+ only:
4
+ - "*.date.formats"
5
+ - "*.number.currency"
@@ -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: "public/javascripts/all.js"
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: "*"
@@ -1,3 +1,3 @@
1
1
  # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
2
  translations:
3
- - file: "public/javascripts/no_scope.js"
3
+ - file: "tmp/i18n-js/no_scope.js"
@@ -1,4 +1,4 @@
1
1
  # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
2
  translations:
3
- - file: "public/javascripts/simple_scope.js"
3
+ - file: "tmp/i18n-js/simple_scope.js"
4
4
  only: "*.date.formats"
@@ -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
+ });