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.
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
+ });