i18n-js-pika 3.0.0.rc6

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +13 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +52 -0
  6. data/README.md +326 -0
  7. data/Rakefile +13 -0
  8. data/app/assets/javascripts/i18n/filtered.js.erb +2 -0
  9. data/app/assets/javascripts/i18n/shims.js +93 -0
  10. data/app/assets/javascripts/i18n/translations.js +3 -0
  11. data/app/assets/javascripts/i18n.js +690 -0
  12. data/i18n-js.gemspec +27 -0
  13. data/lib/i18n/js/engine.rb +22 -0
  14. data/lib/i18n/js/middleware.rb +59 -0
  15. data/lib/i18n/js/version.rb +12 -0
  16. data/lib/i18n/js.rb +162 -0
  17. data/lib/i18n-js.rb +1 -0
  18. data/lib/tasks/export.rake +8 -0
  19. data/package.json +11 -0
  20. data/spec/fixtures/custom_path.yml +4 -0
  21. data/spec/fixtures/default.yml +4 -0
  22. data/spec/fixtures/js_file_per_locale.yml +3 -0
  23. data/spec/fixtures/locales.yml +76 -0
  24. data/spec/fixtures/multiple_conditions.yml +5 -0
  25. data/spec/fixtures/multiple_files.yml +6 -0
  26. data/spec/fixtures/no_config.yml +2 -0
  27. data/spec/fixtures/no_scope.yml +3 -0
  28. data/spec/fixtures/simple_scope.yml +4 -0
  29. data/spec/i18n_js_spec.rb +139 -0
  30. data/spec/js/currency.spec.js +60 -0
  31. data/spec/js/current_locale.spec.js +19 -0
  32. data/spec/js/dates.spec.js +222 -0
  33. data/spec/js/defaults.spec.js +23 -0
  34. data/spec/js/interpolation.spec.js +28 -0
  35. data/spec/js/jasmine/MIT.LICENSE +20 -0
  36. data/spec/js/jasmine/jasmine-html.js +190 -0
  37. data/spec/js/jasmine/jasmine.css +166 -0
  38. data/spec/js/jasmine/jasmine.js +2476 -0
  39. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  40. data/spec/js/localization.spec.js +41 -0
  41. data/spec/js/numbers.spec.js +142 -0
  42. data/spec/js/placeholder.spec.js +24 -0
  43. data/spec/js/pluralization.spec.js +105 -0
  44. data/spec/js/prepare_options.spec.js +41 -0
  45. data/spec/js/specs.html +46 -0
  46. data/spec/js/translate.spec.js +120 -0
  47. data/spec/js/translations.js +120 -0
  48. data/spec/spec_helper.rb +41 -0
  49. metadata +196 -0
@@ -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
+ ActiveSupport.on_load(:after_initialize, :yield => true) do
8
+ next unless JS.has_asset_pipeline?
9
+ next unless Rails.configuration.assets.compile
10
+
11
+ registry = Sprockets.respond_to?("register_preprocessor") ? Sprockets : Rails.application.assets
12
+
13
+ registry.register_preprocessor "application/javascript", :"i18n-js_dependencies" do |context, source|
14
+ next source unless context.logical_path == "i18n/filtered"
15
+ ::I18n.load_path.each {|path| context.depend_on(File.expand_path(path))}
16
+ source
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ module I18n
2
+ module JS
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ @cache = nil
10
+ verify_locale_files!
11
+ @app.call(env)
12
+ end
13
+
14
+ private
15
+ def cache_path
16
+ @cache_path ||= Rails.root.join("tmp/cache/i18n-js.yml")
17
+ end
18
+
19
+ def cache
20
+ @cache ||= begin
21
+ if cache_path.exist?
22
+ YAML.load_file(cache_path) || {}
23
+ else
24
+ {}
25
+ end
26
+ end
27
+ end
28
+
29
+ # Check if translations should be regenerated.
30
+ # ONLY REGENERATE when these conditions are met:
31
+ #
32
+ # # Cache file doesn't exist
33
+ # # Translations and cache size are different (files were removed/added)
34
+ # # Translation file has been updated
35
+ #
36
+ def verify_locale_files!
37
+ valid_cache = []
38
+ new_cache = {}
39
+
40
+ valid_cache.push cache_path.exist?
41
+ valid_cache.push ::I18n.load_path.uniq.size == cache.size
42
+
43
+ ::I18n.load_path.each do |path|
44
+ changed_at = File.mtime(path).to_i
45
+ valid_cache.push changed_at == cache[path]
46
+ new_cache[path] = changed_at
47
+ end
48
+
49
+ return if valid_cache.all?
50
+
51
+ File.open(cache_path, "w+") do |file|
52
+ file << new_cache.to_yaml
53
+ end
54
+
55
+ ::I18n::JS.export
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,12 @@
1
+ module I18n
2
+ module JS
3
+ module Version
4
+ MAJOR = 3
5
+ MINOR = 0
6
+ PATCH = 0
7
+ # Set to nil for stable release
8
+ BUILD = 'rc6'.freeze
9
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
10
+ end
11
+ end
12
+ end
data/lib/i18n/js.rb ADDED
@@ -0,0 +1,162 @@
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
+ segments = if config? && config[:translations]
80
+ configured_segments
81
+ else
82
+ {"#{export_dir}/translations.js" => translations}
83
+ end
84
+ segments.inject({}) do |hash, (filename, translations)|
85
+ hash[filename] = translations.nil? ? nil : translations.select{|locale,_| I18n.available_locales.include?(locale) }
86
+ hash
87
+ end
88
+ end
89
+
90
+ # Load configuration file for partial exporting and
91
+ # custom output directory
92
+ def self.config
93
+ if config?
94
+ (YAML.load_file(config_file) || {}).with_indifferent_access
95
+ else
96
+ {}
97
+ end
98
+ end
99
+
100
+ # Check if configuration file exist
101
+ def self.config?
102
+ File.file? config_file
103
+ end
104
+
105
+ # Convert translations to JSON string and save file.
106
+ def self.save(translations, file)
107
+ FileUtils.mkdir_p File.dirname(file)
108
+
109
+ File.open(file, "w+") do |f|
110
+ f << %(I18n.translations || (I18n.translations = {});\n)
111
+ translations.each do |locale, translations_for_locale|
112
+ f << %(I18n.translations["#{locale}"] = #{translations_for_locale.to_json};\n);
113
+ end
114
+ end
115
+ end
116
+
117
+ def self.scoped_translations(scopes) # :nodoc:
118
+ result = {}
119
+
120
+ [scopes].flatten.each do |scope|
121
+ deep_merge! result, filter(translations, scope)
122
+ end
123
+
124
+ result
125
+ end
126
+
127
+ # Filter translations according to the specified scope.
128
+ def self.filter(translations, scopes)
129
+ scopes = scopes.split(".") if scopes.is_a?(String)
130
+ scopes = scopes.clone
131
+ scope = scopes.shift
132
+
133
+ if scope == "*"
134
+ results = {}
135
+ translations.each do |scope, translations|
136
+ tmp = scopes.empty? ? translations : filter(translations, scopes)
137
+ results[scope.to_sym] = tmp unless tmp.nil?
138
+ end
139
+ return results
140
+ elsif translations.has_key?(scope.to_sym)
141
+ return {scope.to_sym => scopes.empty? ? translations[scope.to_sym] : filter(translations[scope.to_sym], scopes)}
142
+ end
143
+ nil
144
+ end
145
+
146
+ # Initialize and return translations
147
+ def self.translations
148
+ ::I18n.backend.instance_eval do
149
+ init_translations unless initialized?
150
+ translations
151
+ end
152
+ end
153
+
154
+ def self.deep_merge(target, hash) # :nodoc:
155
+ target.merge(hash, &MERGER)
156
+ end
157
+
158
+ def self.deep_merge!(target, hash) # :nodoc:
159
+ target.merge!(hash, &MERGER)
160
+ end
161
+ end
162
+ end
data/lib/i18n-js.rb ADDED
@@ -0,0 +1 @@
1
+ require "i18n/js"
@@ -0,0 +1,8 @@
1
+ namespace :i18n do
2
+ namespace :js do
3
+ desc "Export translations to JS file(s)"
4
+ task :export => :environment do
5
+ I18n::JS.export
6
+ end
7
+ end
8
+ end
data/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "i18n-js",
3
+ "version": "0.0.0",
4
+ "devDependencies": {
5
+ "jasmine-node": "*"
6
+ },
7
+
8
+ "scripts": {
9
+ "test": "./node_modules/.bin/jasmine-node spec/js"
10
+ }
11
+ }
@@ -0,0 +1,4 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "tmp/i18n-js/all.js"
4
+ only: "*"
@@ -0,0 +1,4 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "tmp/i18n-js/translations.js"
4
+ only: "*"
@@ -0,0 +1,3 @@
1
+ translations:
2
+ - file: "tmp/i18n-js/%{locale}.js"
3
+ only: '*'
@@ -0,0 +1,76 @@
1
+ en:
2
+ number:
3
+ format:
4
+ separator: "."
5
+ delimiter: ","
6
+ precision: 3
7
+ currency:
8
+ format:
9
+ format: "%u%n"
10
+ unit: "$"
11
+ separator: "."
12
+ delimiter: ","
13
+ precision: 2
14
+ date:
15
+ formats:
16
+ default: "%Y-%m-%d"
17
+ short: "%b %d"
18
+ long: "%B %d, %Y"
19
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
20
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
21
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
22
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
23
+ # order: [ :year, :month, :day ]
24
+ time:
25
+ formats:
26
+ default: "%a, %d %b %Y %H:%M:%S %z"
27
+ short: "%d %b %H:%M"
28
+ long: "%B %d, %Y %H:%M"
29
+ am: "am"
30
+ pm: "pm"
31
+ admin:
32
+ show:
33
+ title: "Show"
34
+ note: "more details"
35
+ edit:
36
+ title: "Edit"
37
+
38
+ fr:
39
+ date:
40
+ formats:
41
+ default: "%d/%m/%Y"
42
+ short: "%e %b"
43
+ long: "%e %B %Y"
44
+ long_ordinal: "%e %B %Y"
45
+ only_day: "%e"
46
+ day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
47
+ abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
48
+ month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
49
+ abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
50
+ # order: [ :day, :month, :year ]
51
+ time:
52
+ formats:
53
+ default: "%d %B %Y %H:%M"
54
+ time: "%H:%M"
55
+ short: "%d %b %H:%M"
56
+ long: "%A %d %B %Y %H:%M:%S %Z"
57
+ long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
58
+ only_second: "%S"
59
+ am: 'am'
60
+ pm: 'pm'
61
+ number:
62
+ format:
63
+ precision: 3
64
+ separator: ','
65
+ delimiter: ' '
66
+ currency:
67
+ format:
68
+ unit: '€'
69
+ precision: 2
70
+ format: '%n %u'
71
+ admin:
72
+ show:
73
+ title: "Visualiser"
74
+ note: "plus de détails"
75
+ edit:
76
+ title: "Editer"
@@ -0,0 +1,5 @@
1
+ translations:
2
+ - file: "tmp/i18n-js/bitsnpieces.js"
3
+ only:
4
+ - "*.date.formats"
5
+ - "*.number.currency"
@@ -0,0 +1,6 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "tmp/i18n-js/all.js"
4
+ only: "*"
5
+ - file: "tmp/i18n-js/tudo.js"
6
+ only: "*"
@@ -0,0 +1,2 @@
1
+ #This file has no config
2
+
@@ -0,0 +1,3 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "tmp/i18n-js/no_scope.js"
@@ -0,0 +1,4 @@
1
+ # Find more details about this configuration file at http://github.com/fnando/i18n-js
2
+ translations:
3
+ - file: "tmp/i18n-js/simple_scope.js"
4
+ only: "*.date.formats"
@@ -0,0 +1,139 @@
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
+
95
+ context "with available_locales set" do
96
+ before do
97
+ I18n.stub :available_locales => [:fr]
98
+ end
99
+
100
+ context 'when without config' do
101
+ let(:result){ I18n::JS.translation_segments.values.first }
102
+ it "limits the translations to the available locales" do
103
+ result[:en].should be_nil
104
+ result[:fr].should be_present
105
+ end
106
+ end
107
+
108
+ # Don't know how to test this yet
109
+ context 'when with config'
110
+ end
111
+ end
112
+
113
+ context "general" do
114
+ it "sets export directory" do
115
+ I18n::JS.export_dir.should eql("public/javascripts")
116
+ end
117
+
118
+ it "sets empty hash as configuration when no file is found" do
119
+ I18n::JS.config?.should be_false
120
+ I18n::JS.config.should eql({})
121
+ end
122
+ end
123
+
124
+ context "hash merging" do
125
+ it "performs a deep merge" do
126
+ target = {:a => {:b => 1}}
127
+ result = I18n::JS.deep_merge(target, {:a => {:c => 2}})
128
+
129
+ result[:a].should eql({:b => 1, :c => 2})
130
+ end
131
+
132
+ it "performs a banged deep merge" do
133
+ target = {:a => {:b => 1}}
134
+ I18n::JS.deep_merge!(target, {:a => {:c => 2}})
135
+
136
+ target[:a].should eql({:b => 1, :c => 2})
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,60 @@
1
+ var I18n = require("../../app/assets/javascripts/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
+ });
@@ -0,0 +1,19 @@
1
+ var I18n = require("../../app/assets/javascripts/i18n");
2
+
3
+ describe("Current locale", function(){
4
+ beforeEach(function(){
5
+ I18n.reset();
6
+ });
7
+
8
+ it("returns I18n.locale", function(){
9
+ I18n.locale = "pt-BR";
10
+ expect(I18n.currentLocale()).toEqual("pt-BR");
11
+ });
12
+
13
+ it("returns I18n.defaultLocale", function(){
14
+ I18n.locale = null;
15
+ I18n.defaultLocale = "pt-BR";
16
+
17
+ expect(I18n.currentLocale()).toEqual("pt-BR");
18
+ });
19
+ });