i18n-js 2.1.2 → 3.8.3

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 (127) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +24 -0
  3. data/.github/FUNDING.yml +3 -0
  4. data/.github/workflows/tests.yaml +100 -0
  5. data/.gitignore +6 -4
  6. data/.npmignore +27 -0
  7. data/Appraisals +44 -0
  8. data/CHANGELOG.md +539 -0
  9. data/Gemfile +1 -1
  10. data/README.md +1086 -0
  11. data/Rakefile +19 -7
  12. data/app/assets/javascripts/i18n.js +1095 -0
  13. data/app/assets/javascripts/i18n/filtered.js.erb +23 -0
  14. data/app/assets/javascripts/i18n/shims.js +240 -0
  15. data/app/assets/javascripts/i18n/translations.js +3 -0
  16. data/gemfiles/i18n_0_6.gemfile +7 -0
  17. data/gemfiles/i18n_0_7.gemfile +7 -0
  18. data/gemfiles/i18n_0_8.gemfile +7 -0
  19. data/gemfiles/i18n_0_9.gemfile +7 -0
  20. data/gemfiles/i18n_1_0.gemfile +7 -0
  21. data/gemfiles/i18n_1_1.gemfile +7 -0
  22. data/gemfiles/i18n_1_2.gemfile +7 -0
  23. data/gemfiles/i18n_1_3.gemfile +7 -0
  24. data/gemfiles/i18n_1_4.gemfile +7 -0
  25. data/gemfiles/i18n_1_5.gemfile +7 -0
  26. data/gemfiles/i18n_1_6.gemfile +7 -0
  27. data/gemfiles/i18n_1_7.gemfile +7 -0
  28. data/gemfiles/i18n_1_8.gemfile +7 -0
  29. data/i18n-js.gemspec +12 -9
  30. data/i18njs.png +0 -0
  31. data/lib/i18n-js.rb +1 -177
  32. data/lib/i18n/js.rb +264 -0
  33. data/lib/i18n/js/dependencies.rb +63 -0
  34. data/lib/i18n/js/engine.rb +87 -0
  35. data/lib/i18n/js/fallback_locales.rb +70 -0
  36. data/lib/i18n/js/formatters/base.rb +25 -0
  37. data/lib/i18n/js/formatters/js.rb +39 -0
  38. data/lib/i18n/js/formatters/json.rb +13 -0
  39. data/lib/{i18n-js → i18n/js}/middleware.rb +32 -9
  40. data/lib/i18n/js/private/config_store.rb +31 -0
  41. data/lib/i18n/js/private/hash_with_symbol_keys.rb +36 -0
  42. data/lib/i18n/js/segment.rb +80 -0
  43. data/lib/i18n/js/utils.rb +78 -0
  44. data/lib/i18n/js/version.rb +7 -0
  45. data/lib/rails/generators/i18n/js/config/config_generator.rb +19 -0
  46. data/{config → lib/rails/generators/i18n/js/config/templates}/i18n-js.yml +11 -6
  47. data/lib/tasks/export.rake +8 -0
  48. data/package.json +25 -0
  49. data/spec/fixtures/custom_path.yml +5 -0
  50. data/spec/fixtures/default.yml +5 -0
  51. data/spec/fixtures/erb.yml +5 -0
  52. data/spec/fixtures/except_condition.yml +7 -0
  53. data/spec/fixtures/js_export_dir_custom.yml +7 -0
  54. data/spec/fixtures/js_export_dir_none.yml +6 -0
  55. data/spec/fixtures/js_extend_parent.yml +6 -0
  56. data/spec/fixtures/js_extend_segment.yml +6 -0
  57. data/spec/fixtures/js_file_per_locale.yml +7 -0
  58. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +4 -0
  59. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +6 -0
  60. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +4 -0
  61. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +4 -0
  62. data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +4 -0
  63. data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +4 -0
  64. data/spec/fixtures/js_file_with_namespace_prefix_and_pretty_print.yml +9 -0
  65. data/spec/fixtures/js_sort_translation_keys_false.yml +6 -0
  66. data/spec/fixtures/js_sort_translation_keys_true.yml +6 -0
  67. data/spec/fixtures/json_only.yml +18 -0
  68. data/spec/{resources → fixtures}/locales.yml +58 -1
  69. data/spec/fixtures/merge_plurals.yml +6 -0
  70. data/spec/fixtures/merge_plurals_with_no_overrides.yml +4 -0
  71. data/spec/fixtures/merge_plurals_with_partial_overrides.yml +4 -0
  72. data/spec/fixtures/millions.yml +4 -0
  73. data/spec/fixtures/multiple_conditions.yml +7 -0
  74. data/spec/fixtures/multiple_conditions_per_locale.yml +7 -0
  75. data/spec/fixtures/multiple_files.yml +7 -0
  76. data/spec/{resources → fixtures}/no_config.yml +0 -0
  77. data/spec/fixtures/no_scope.yml +4 -0
  78. data/spec/fixtures/simple_scope.yml +5 -0
  79. data/spec/js/currency.spec.js +62 -0
  80. data/spec/js/current_locale.spec.js +19 -0
  81. data/spec/js/dates.spec.js +276 -0
  82. data/spec/js/defaults.spec.js +31 -0
  83. data/spec/js/extend.spec.js +110 -0
  84. data/spec/js/interpolation.spec.js +124 -0
  85. data/spec/js/jasmine/MIT.LICENSE +20 -0
  86. data/spec/js/jasmine/jasmine-html.js +190 -0
  87. data/spec/js/jasmine/jasmine.css +166 -0
  88. data/spec/js/jasmine/jasmine.js +2476 -0
  89. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  90. data/spec/js/json_parsable.spec.js +14 -0
  91. data/spec/js/locales.spec.js +31 -0
  92. data/spec/js/localization.spec.js +78 -0
  93. data/spec/js/numbers.spec.js +174 -0
  94. data/spec/js/placeholder.spec.js +24 -0
  95. data/spec/js/pluralization.spec.js +219 -0
  96. data/spec/js/prepare_options.spec.js +41 -0
  97. data/spec/js/require.js +2083 -0
  98. data/spec/js/specs.html +49 -0
  99. data/spec/js/specs_requirejs.html +72 -0
  100. data/spec/js/translate.spec.js +304 -0
  101. data/spec/js/translations.js +188 -0
  102. data/spec/js/utility_functions.spec.js +20 -0
  103. data/spec/ruby/i18n/js/fallback_locales_spec.rb +84 -0
  104. data/spec/ruby/i18n/js/segment_spec.rb +261 -0
  105. data/spec/ruby/i18n/js/utils_spec.rb +106 -0
  106. data/spec/ruby/i18n/js_spec.rb +748 -0
  107. data/spec/spec_helper.rb +75 -14
  108. data/yarn.lock +131 -0
  109. metadata +223 -98
  110. data/.rspec +0 -1
  111. data/Gemfile.lock +0 -51
  112. data/README.rdoc +0 -305
  113. data/lib/i18n-js/engine.rb +0 -62
  114. data/lib/i18n-js/railtie.rb +0 -13
  115. data/lib/i18n-js/rake.rb +0 -16
  116. data/lib/i18n-js/version.rb +0 -10
  117. data/spec/i18n_spec.js +0 -768
  118. data/spec/i18n_spec.rb +0 -205
  119. data/spec/resources/custom_path.yml +0 -4
  120. data/spec/resources/default.yml +0 -4
  121. data/spec/resources/js_file_per_locale.yml +0 -3
  122. data/spec/resources/multiple_conditions.yml +0 -6
  123. data/spec/resources/multiple_files.yml +0 -6
  124. data/spec/resources/no_scope.yml +0 -3
  125. data/spec/resources/simple_scope.yml +0 -4
  126. data/vendor/assets/javascripts/i18n.js +0 -450
  127. data/vendor/assets/javascripts/i18n/translations.js.erb +0 -7
@@ -0,0 +1,748 @@
1
+ require "spec_helper"
2
+
3
+ describe I18n::JS do
4
+
5
+ describe '.config_file_path' do
6
+ let(:default_path) { I18n::JS::DEFAULT_CONFIG_PATH }
7
+ let(:new_path) { File.join("tmp", default_path) }
8
+
9
+ subject { described_class.config_file_path }
10
+
11
+ context "when it is not set" do
12
+ it { should eq default_path }
13
+ end
14
+ context "when it is set already" do
15
+ before { described_class.config_file_path = new_path }
16
+
17
+ it { should eq new_path }
18
+ end
19
+ end
20
+
21
+ context "exporting" do
22
+ before do
23
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
24
+ end
25
+
26
+ it "exports messages to default path when configuration file doesn't exist" do
27
+ I18n::JS.export
28
+ file_should_exist "translations.js"
29
+ end
30
+
31
+ it "exports messages using custom output path" do
32
+ set_config "custom_path.yml"
33
+ allow(I18n::JS::Segment).to receive(:new).with("tmp/i18n-js/all.js", translations, {js_extend: true, sort_translation_keys: true, json_only: false}).and_call_original
34
+ allow_any_instance_of(I18n::JS::Segment).to receive(:save!).with(no_args)
35
+ I18n::JS.export
36
+ end
37
+
38
+ it "sets default scope to * when not specified" do
39
+ set_config "no_scope.yml"
40
+ allow(I18n::JS::Segment).to receive(:new).with("tmp/i18n-js/no_scope.js", translations, {js_extend: true, sort_translation_keys: true, json_only: false}).and_call_original
41
+ allow_any_instance_of(I18n::JS::Segment).to receive(:save!).with(no_args)
42
+ I18n::JS.export
43
+ end
44
+
45
+ it "exports to multiple files" do
46
+ set_config "multiple_files.yml"
47
+ I18n::JS.export
48
+
49
+ file_should_exist "all.js"
50
+ file_should_exist "tudo.js"
51
+ end
52
+
53
+ it "ignores an empty config file" do
54
+ set_config "no_config.yml"
55
+ I18n::JS.export
56
+
57
+ file_should_exist "translations.js"
58
+ end
59
+
60
+ it "exports to a JS file per available locale" do
61
+ set_config "js_file_per_locale.yml"
62
+ I18n::JS.export
63
+
64
+ file_should_exist "en.js"
65
+ file_should_exist "fr.js"
66
+
67
+ en_output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "en.js"))
68
+ expect(en_output).to eq(<<EOS
69
+ I18n.translations || (I18n.translations = {});
70
+ I18n.translations["en"] = I18n.extend((I18n.translations["en"] || {}), JSON.parse('{"admin":{"edit":{"title":"Edit"},"show":{"note":"more details","title":"Show"}},"date":{"abbr_day_names":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"abbr_month_names":[null,"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"day_names":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"formats":{"default":"%Y-%m-%d","long":"%B %d, %Y","short":"%b %d"},"month_names":[null,"January","February","March","April","May","June","July","August","September","October","November","December"]}}'));
71
+ EOS
72
+ )
73
+ fr_output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "fr.js"))
74
+ expect(fr_output).to eq(<<EOS
75
+ I18n.translations || (I18n.translations = {});
76
+ I18n.translations["fr"] = I18n.extend((I18n.translations["fr"] || {}), JSON.parse('{"admin":{"edit":{"title":"Editer"},"show":{"note":"plus de détails","title":"Visualiser"}},"date":{"abbr_day_names":["dim","lun","mar","mer","jeu","ven","sam"],"abbr_month_names":[null,"jan.","fév.","mar.","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],"day_names":["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],"formats":{"default":"%d/%m/%Y","long":"%e %B %Y","long_ordinal":"%e %B %Y","only_day":"%e","short":"%e %b"},"month_names":[null,"janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"]}}'));
77
+ EOS
78
+ )
79
+ end
80
+
81
+ it "exports with multiple conditions" do
82
+ set_config "multiple_conditions.yml"
83
+ I18n::JS.export
84
+
85
+ file_should_exist "bitsnpieces.js"
86
+ end
87
+
88
+ it "exports with multiple conditions to a JS file per available locale" do
89
+ allow(::I18n).to receive(:available_locales){ [:en, :fr] }
90
+
91
+ set_config "multiple_conditions_per_locale.yml"
92
+
93
+ result = I18n::JS.translation_segments
94
+ expect(result.map(&:file)).to eql(["tmp/i18n-js/bits.%{locale}.js"])
95
+
96
+ result.map(&:save!)
97
+
98
+ en_output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "bits.en.js"))
99
+ expect(en_output).to eq(<<EOS
100
+ I18n.translations || (I18n.translations = {});
101
+ I18n.translations["en"] = I18n.extend((I18n.translations["en"] || {}), JSON.parse('{"date":{"formats":{"default":"%Y-%m-%d","long":"%B %d, %Y","short":"%b %d"}},"number":{"currency":{"format":{"delimiter":",","format":"%u%n","precision":2,"separator":".","unit":"$"}}}}'));
102
+ EOS
103
+ )
104
+ fr_output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "bits.fr.js"))
105
+ expect(fr_output).to eq(<<EOS
106
+ I18n.translations || (I18n.translations = {});
107
+ I18n.translations["fr"] = I18n.extend((I18n.translations["fr"] || {}), JSON.parse('{"date":{"formats":{"default":"%d/%m/%Y","long":"%e %B %Y","long_ordinal":"%e %B %Y","only_day":"%e","short":"%e %b"}},"number":{"currency":{"format":{"format":"%n %u","precision":2,"unit":"€"}}}}'));
108
+ EOS
109
+ )
110
+ end
111
+
112
+ it "exports as json only" do
113
+ set_config "json_only.yml"
114
+ I18n::JS.export
115
+ en_output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "json_only.en.js"))
116
+ fr_output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "json_only.fr.js"))
117
+ expect(en_output).to eq (<<EOS
118
+ {"en":{"date":{"formats":{"default":"%Y-%m-%d","long":"%B %d, %Y","short":"%b %d"}},"number":{"currency":{"format":{"delimiter":",","format":"%u%n","precision":2,"separator":".","unit":"$"}}}}}
119
+ EOS
120
+ ).chop
121
+ expect(fr_output).to eq (<<EOS
122
+ {"fr":{"date":{"formats":{"default":"%d/%m/%Y","long":"%e %B %Y","long_ordinal":"%e %B %Y","only_day":"%e","short":"%e %b"}},"number":{"currency":{"format":{"format":"%n %u","precision":2,"unit":"€"}}}}}
123
+ EOS
124
+ ).chop
125
+ expect(JSON.parse(en_output).keys.first).to eq 'en'
126
+ end
127
+
128
+ it "exports with :except condition" do
129
+ set_config "except_condition.yml"
130
+ I18n::JS.export
131
+
132
+ file_should_exist "trimmed.js"
133
+ end
134
+
135
+ it "calls .export_i18n_js" do
136
+ allow(described_class).to receive(:export_i18n_js)
137
+ I18n::JS.export
138
+ expect(described_class).to have_received(:export_i18n_js).once
139
+ end
140
+ end
141
+
142
+ context "filters" do
143
+ it "filters translations using scope *.date.formats" do
144
+ result = I18n::JS.filter(translations, "*.date.formats")
145
+ expect(result[:en][:date].keys).to eql([:formats])
146
+ expect(result[:fr][:date].keys).to eql([:formats])
147
+ end
148
+
149
+ it "filters translations using scope [*.date.formats, *.number.currency.format]" do
150
+ result = I18n::JS.scoped_translations(["*.date.formats", "*.number.currency.format"])
151
+ expect(result[:en].keys.collect(&:to_s).sort).to eql(%w[ date number ])
152
+ expect(result[:fr].keys.collect(&:to_s).sort).to eql(%w[ date number ])
153
+ end
154
+
155
+ it "filters translations using multi-star scope" do
156
+ result = I18n::JS.scoped_translations("*.*.formats")
157
+
158
+ expect(result[:en].keys.collect(&:to_s).sort).to eql(%w[ date time ])
159
+ expect(result[:fr].keys.collect(&:to_s).sort).to eql(%w[ date time ])
160
+
161
+ expect(result[:en][:date].keys).to eql([:formats])
162
+ expect(result[:en][:time].keys).to eql([:formats])
163
+
164
+ expect(result[:fr][:date].keys).to eql([:formats])
165
+ expect(result[:fr][:time].keys).to eql([:formats])
166
+ end
167
+
168
+ it "filters translations using alternated stars" do
169
+ result = I18n::JS.scoped_translations("*.admin.*.title")
170
+
171
+ expect(result[:en][:admin].keys.collect(&:to_s).sort).to eql(%w[ edit show ])
172
+ expect(result[:fr][:admin].keys.collect(&:to_s).sort).to eql(%w[ edit show ])
173
+
174
+ expect(result[:en][:admin][:show][:title]).to eql("Show")
175
+ expect(result[:fr][:admin][:show][:title]).to eql("Visualiser")
176
+
177
+ expect(result[:en][:admin][:edit][:title]).to eql("Edit")
178
+ expect(result[:fr][:admin][:edit][:title]).to eql("Editer")
179
+ end
180
+
181
+ describe ".filtered_translations" do
182
+ subject do
183
+ I18n::JS.filtered_translations
184
+ end
185
+
186
+ let!(:old_sort_translation_keys) { I18n::JS.sort_translation_keys? }
187
+ before { I18n::JS.sort_translation_keys = sort_translation_keys_value }
188
+ after { I18n::JS.sort_translation_keys = old_sort_translation_keys }
189
+ before { expect(I18n::JS.sort_translation_keys?).to eq(sort_translation_keys_value) }
190
+
191
+ let(:sorted_hash) do
192
+ {sorted: :hash}
193
+ end
194
+ before do
195
+ allow(I18n::JS::Utils).
196
+ to receive(:deep_key_sort).
197
+ and_return(sorted_hash)
198
+ end
199
+
200
+ shared_examples_for ".filtered_translations" do
201
+ subject do
202
+ I18n::JS.filtered_translations
203
+ end
204
+
205
+ # This example is to prevent the regression from
206
+ # PR https://github.com/fnando/i18n-js/pull/318
207
+ it {should be_a(Hash)}
208
+ # Might need to test the keys... or not
209
+ end
210
+
211
+ context "when translation keys SHOULD be sorted" do
212
+ let(:sort_translation_keys_value) { true }
213
+
214
+ it_behaves_like ".filtered_translations"
215
+ it {should eq(sorted_hash)}
216
+ end
217
+ context "when translation keys should NOT be sorted" do
218
+ let(:sort_translation_keys_value) { false }
219
+
220
+ it_behaves_like ".filtered_translations"
221
+ it {should_not eq(sorted_hash)}
222
+ end
223
+ end
224
+ end
225
+
226
+ context "exceptions" do
227
+ it "does not include scopes listed in the exceptions list" do
228
+ result = I18n::JS.scoped_translations("*", ['de.*', '*.admin', '*.*.currency'])
229
+
230
+ expect(result[:de]).to be_empty
231
+
232
+ expect(result[:en][:admin]).to be_nil
233
+ expect(result[:fr][:admin]).to be_nil
234
+ expect(result[:ja][:admin]).to be_nil
235
+
236
+ expect(result[:en][:number][:currency]).to be_nil
237
+ expect(result[:fr][:number][:currency]).to be_nil
238
+ end
239
+
240
+ it "does not include scopes listed in the exceptions list and respects the 'only' option" do
241
+ result = I18n::JS.scoped_translations("fr.*", ['*.admin', '*.*.currency'])
242
+
243
+ expect(result[:en]).to be_nil
244
+ expect(result[:de]).to be_nil
245
+ expect(result[:ja]).to be_nil
246
+
247
+ expect(result[:fr][:admin]).to be_nil
248
+ expect(result[:fr][:number][:currency]).to be_nil
249
+
250
+ expect(result[:fr][:time][:am]).to be_a(String)
251
+ end
252
+
253
+ it "does exclude absolute scopes listed in the exceptions list" do
254
+ result = I18n::JS.scoped_translations("*", ['de', 'en.admin', 'fr.number.currency'])
255
+
256
+ expect(result[:de]).to be_nil
257
+
258
+ expect(result[:en]).to be_a(Hash)
259
+ expect(result[:en][:admin]).to be_nil
260
+
261
+ expect(result[:fr][:number]).to be_a(Hash)
262
+ expect(result[:fr][:number][:currency]).to be_nil
263
+ end
264
+
265
+ it "does not exclude non-absolute scopes listed in the exceptions list" do
266
+ result = I18n::JS.scoped_translations("*", ['admin', 'currency'])
267
+
268
+ expect(result[:en][:admin]).to be_a(Hash)
269
+ expect(result[:fr][:admin]).to be_a(Hash)
270
+ expect(result[:ja][:admin]).to be_a(Hash)
271
+
272
+ expect(result[:en][:number][:currency]).to be_a(Hash)
273
+ expect(result[:fr][:number][:currency]).to be_a(Hash)
274
+ end
275
+ end
276
+
277
+ context "fallbacks" do
278
+ subject(:translations) do
279
+ I18n::JS.translation_segments.first.translations
280
+ end
281
+
282
+ it "exports without fallback when disabled" do
283
+ set_config "js_file_per_locale_without_fallbacks.yml"
284
+ expect(subject[:fr][:fallback_test]).to eql(nil)
285
+ expect(subject[:fr][:null_test]).to eql(nil)
286
+ expect(subject[:de][:null_test]).to eql(nil)
287
+ end
288
+
289
+ it "exports with default_locale as fallback when enabled" do
290
+ set_config "js_file_per_locale_with_fallbacks_enabled.yml"
291
+ expect(subject[:fr][:fallback_test]).to eql("Success")
292
+ expect(subject[:fr][:null_test]).to eql("fallback for null")
293
+ expect(subject[:de][:null_test]).to eql("fallback for null")
294
+ end
295
+
296
+ it "exports with default_locale as fallback when enabled with :default_locale" do
297
+ set_config "js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml"
298
+ expect(subject[:fr][:fallback_test]).to eql("Success")
299
+ expect(subject[:fr][:null_test]).to eql("fallback for null")
300
+ expect(subject[:de][:null_test]).to eql("fallback for null")
301
+ end
302
+
303
+ it "exports with given locale as fallback" do
304
+ set_config "js_file_per_locale_with_fallbacks_as_locale.yml"
305
+ expect(subject[:fr][:fallback_test]).to eql("Erfolg")
306
+ expect(subject[:fr][:null_test]).to eql(nil)
307
+ expect(subject[:de][:null_test]).to eql(nil)
308
+ end
309
+
310
+ context "when given locale is in `I18n.available_locales` but its translation is missing" do
311
+ subject { translations[:fr][:fallback_test] }
312
+
313
+ let(:new_locale) { :pirate }
314
+ let!(:old_available_locales) { I18n.config.available_locales }
315
+ let!(:new_available_locales) { I18n.config.available_locales + [new_locale] }
316
+ before do
317
+ I18n.config.available_locales = new_available_locales
318
+ set_config "js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml"
319
+ end
320
+ after do
321
+ I18n.config.available_locales = old_available_locales
322
+ end
323
+
324
+ it {should eql(nil)}
325
+ end
326
+
327
+ context "with I18n::Fallbacks enabled" do
328
+ let(:backend_with_fallbacks) { backend_class_with_fallbacks.new }
329
+ let!(:old_backebad) { I18n.backend }
330
+
331
+ before do
332
+ I18n::JS.backend = backend_with_fallbacks
333
+ I18n.fallbacks[:fr] = [:de, :en]
334
+ end
335
+ after { I18n::JS.backend = old_backebad }
336
+
337
+ it "exports with defined locale as fallback when enabled" do
338
+ set_config "js_file_per_locale_with_fallbacks_enabled.yml"
339
+ expect(subject[:fr][:fallback_test]).to eql("Erfolg")
340
+ end
341
+
342
+ it "exports with defined locale as fallback when enabled with :default_locale" do
343
+ set_config "js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml"
344
+ expect(subject[:fr][:fallback_test]).to eql("Success")
345
+ end
346
+
347
+ it "exports with Fallbacks as Hash" do
348
+ set_config "js_file_per_locale_with_fallbacks_as_hash.yml"
349
+ expect(subject[:fr][:fallback_test]).to eql("Erfolg")
350
+ end
351
+ end
352
+ end
353
+
354
+ context "namespace, prefix, suffix, and pretty_print options" do
355
+
356
+ before do
357
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
358
+ set_config "js_file_with_namespace_prefix_and_pretty_print.yml"
359
+ end
360
+
361
+ it "exports with defined locale as fallback when enabled" do
362
+ I18n::JS.export
363
+ file_should_exist "en.js"
364
+ output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "en.js"))
365
+ expect(output).to match(/^#{
366
+ <<EOS
367
+ import random from 'random-library';
368
+ Foo.translations || (Foo.translations = {});
369
+ Foo.translations["en"] = {
370
+ "number": {
371
+ "format": {
372
+ EOS
373
+ }.+#{
374
+ <<EOS
375
+ "edit": {
376
+ "title": "Edit"
377
+ }
378
+ },
379
+ "foo": "Foo",
380
+ "fallback_test": "Success"
381
+ };
382
+ //test
383
+ EOS
384
+ }$/)
385
+ end
386
+ end
387
+
388
+ context "I18n.available_locales" do
389
+
390
+ context "when I18n.available_locales is not set" do
391
+ it "should allow all locales" do
392
+ result = I18n::JS.scoped_translations("*.admin.*.title")
393
+
394
+ expect(result[:en][:admin][:show][:title]).to eql("Show")
395
+ expect(result[:fr][:admin][:show][:title]).to eql("Visualiser")
396
+ expect(result[:ja][:admin][:show][:title]).to eql("Ignore me")
397
+ end
398
+ end
399
+
400
+ context "when I18n.available_locales is set" do
401
+ before { allow(::I18n).to receive(:available_locales){ [:en, :fr] } }
402
+
403
+ it "should ignore non-valid locales" do
404
+ result = I18n::JS.scoped_translations("*.admin.*.title")
405
+
406
+ expect(result[:en][:admin][:show][:title]).to eql("Show")
407
+ expect(result[:fr][:admin][:show][:title]).to eql("Visualiser")
408
+ expect(result.keys.include?(:ja)).to eql(false)
409
+ end
410
+ end
411
+ end
412
+
413
+ context "general" do
414
+ it "sets export directory" do
415
+ expect(I18n::JS::DEFAULT_EXPORT_DIR_PATH).to eql("public/javascripts")
416
+ end
417
+
418
+ it "sets empty hash as configuration when no file is found" do
419
+ expect(I18n::JS.config_file_exists?).to eql(false)
420
+ expect(I18n::JS.config).to eql({})
421
+ end
422
+
423
+ it "executes erb in config file" do
424
+ set_config "erb.yml"
425
+
426
+ config_entry = I18n::JS.config[:translations].first
427
+ expect(config_entry["only"]).to eq("*.date.formats")
428
+ end
429
+ end
430
+
431
+ describe "i18n.js exporting" do
432
+ after { begin described_class.send(:remove_instance_variable, :@export_i18n_js_dir_path); rescue; end }
433
+
434
+ describe ".export_i18n_js with global variable" do
435
+ before do
436
+ allow(FileUtils).to receive(:mkdir_p).and_call_original
437
+ allow(FileUtils).to receive(:cp).and_call_original
438
+
439
+ allow(described_class).to receive(:export_i18n_js_dir_path).and_return(export_i18n_js_dir_path)
440
+ I18n::JS.export_i18n_js
441
+ end
442
+
443
+ context 'when .export_i18n_js_dir_path returns something' do
444
+ let(:export_i18n_js_dir_path) { temp_path }
445
+
446
+ it "does create the folder before copying" do
447
+ expect(FileUtils).to have_received(:mkdir_p).with(export_i18n_js_dir_path).once
448
+ end
449
+ it "does copy the file with FileUtils.cp" do
450
+ expect(FileUtils).to have_received(:cp).once
451
+ end
452
+ it "exports the file" do
453
+ expect(File).to be_file(File.join(I18n::JS.export_i18n_js_dir_path, "i18n.js"))
454
+ end
455
+ end
456
+
457
+ context 'when .export_i18n_js_dir_path is set to nil' do
458
+ let(:export_i18n_js_dir_path) { nil }
459
+
460
+ it "does NOT create the folder before copying" do
461
+ expect(FileUtils).to_not have_received(:mkdir_p)
462
+ end
463
+ it "does NOT copy the file with FileUtils.cp" do
464
+ expect(FileUtils).to_not have_received(:cp)
465
+ end
466
+ end
467
+ end
468
+
469
+ describe ".export_i18n_js with config" do
470
+
471
+ let(:export_action) do
472
+ allow(FileUtils).to receive(:mkdir_p).and_call_original
473
+ allow(FileUtils).to receive(:cp).and_call_original
474
+ I18n::JS.export_i18n_js
475
+ end
476
+
477
+ context 'when :export_i18n_js set in config' do
478
+ before { set_config "js_export_dir_custom.yml"; export_action }
479
+ let(:export_i18n_js_dir_path) { temp_path }
480
+ let(:config_export_path) { "tmp/i18n-js/foo" }
481
+
482
+ it "does create the folder before copying" do
483
+ expect(FileUtils).to have_received(:mkdir_p).with(config_export_path).once
484
+ end
485
+ it "does copy the file with FileUtils.cp" do
486
+ expect(FileUtils).to have_received(:cp).once
487
+ end
488
+ it "exports the file" do
489
+ expect(File).to be_file(File.join(config_export_path, "i18n.js"))
490
+ end
491
+ end
492
+
493
+ context 'when .export_i18n_js_dir_path is set to false' do
494
+ before { set_config "js_export_dir_none.yml"; export_action }
495
+
496
+ it "does NOT create the folder before copying" do
497
+ expect(FileUtils).to_not have_received(:mkdir_p)
498
+ end
499
+
500
+ it "does NOT copy the file with FileUtils.cp" do
501
+ expect(FileUtils).to_not have_received(:cp)
502
+ end
503
+ end
504
+ end
505
+
506
+ describe '.export_i18n_js_dir_path' do
507
+ let(:default_path) { I18n::JS::DEFAULT_EXPORT_DIR_PATH }
508
+ let(:new_path) { File.join("tmp", default_path) }
509
+ after { described_class.send(:remove_instance_variable, :@export_i18n_js_dir_path) }
510
+
511
+ subject { described_class.export_i18n_js_dir_path }
512
+
513
+ context "when it is not set" do
514
+ it { should eq default_path }
515
+ end
516
+ context "when it is set to another path already" do
517
+ before { described_class.export_i18n_js_dir_path = new_path }
518
+
519
+ it { should eq new_path }
520
+ end
521
+ context "when it is set to nil already" do
522
+ before { described_class.export_i18n_js_dir_path = nil }
523
+
524
+ it { should eq :none }
525
+ end
526
+ end
527
+ end
528
+
529
+ describe "translation key sorting" do
530
+
531
+ describe ".sort_translation_keys?" do
532
+ after { described_class.send(:remove_instance_variable, :@sort_translation_keys) }
533
+ subject { described_class.sort_translation_keys? }
534
+
535
+
536
+ context "set with config" do
537
+
538
+ context 'when :sort_translation_keys is not set in config' do
539
+ before :each do
540
+ set_config "default.yml"
541
+ end
542
+
543
+ it { should eq true }
544
+ end
545
+
546
+ context 'when :sort_translation_keys set to true in config' do
547
+ before :each do
548
+ set_config "js_sort_translation_keys_true.yml"
549
+ end
550
+
551
+ it { should eq true }
552
+ end
553
+
554
+ context 'when :sort_translation_keys set to false in config' do
555
+ before :each do
556
+ set_config "js_sort_translation_keys_false.yml"
557
+ end
558
+
559
+ it { should eq false }
560
+ end
561
+ end
562
+
563
+ context 'set by .sort_translation_keys' do
564
+
565
+ context "when it is not set" do
566
+ it { should eq true }
567
+ end
568
+
569
+ context "when it is set to true" do
570
+ before { described_class.sort_translation_keys = true }
571
+
572
+ it { should eq true }
573
+ end
574
+
575
+ context "when it is set to false" do
576
+ before { described_class.sort_translation_keys = false }
577
+
578
+ it { should eq false }
579
+ end
580
+ end
581
+ end
582
+
583
+ context "exporting" do
584
+ subject do
585
+ I18n::JS.export
586
+ file_should_exist "en.js"
587
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "en.js"))
588
+ end
589
+
590
+ before do
591
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
592
+ end
593
+
594
+ context 'sort_translation_keys is true' do
595
+ before :each do
596
+ set_config "js_sort_translation_keys_true.yml"
597
+ end
598
+
599
+ it "exports with the keys sorted" do
600
+ expect(subject).to eq <<EOS
601
+ I18n.translations || (I18n.translations = {});
602
+ I18n.translations["en"] = I18n.extend((I18n.translations["en"] || {}), JSON.parse('{"admin":{"edit":{"title":"Edit"},"show":{"note":"more details","title":"Show"}},"date":{"abbr_day_names":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"abbr_month_names":[null,"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"day_names":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"formats":{"default":"%Y-%m-%d","long":"%B %d, %Y","short":"%b %d"},"month_names":[null,"January","February","March","April","May","June","July","August","September","October","November","December"]},"fallback_test":"Success","foo":"Foo","merge_plurals":{"one":"Apple","other":"Apples"},"merge_plurals_with_no_overrides":{"one":"Apple","other":"Apples","zero":"No Apple"},"merge_plurals_with_partial_overrides":{"one":"Cat","other":"Cats"},"null_test":"fallback for null","number":{"currency":{"format":{"delimiter":",","format":"%u%n","precision":2,"separator":".","unit":"$"}},"format":{"delimiter":",","precision":3,"separator":"."},"human":{"decimal_units":{"units":{"million":"Million"}}}},"time":{"am":"am","formats":{"default":"%a, %d %b %Y %H:%M:%S %z","long":"%B %d, %Y %H:%M","short":"%d %b %H:%M"},"pm":"pm"}}'));
603
+ EOS
604
+ end
605
+ end
606
+ end
607
+ end
608
+
609
+ describe "js_extend option" do
610
+ before do
611
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
612
+ end
613
+
614
+ # https://github.com/fnando/i18n-js/issues/553
615
+ describe "millions" do
616
+ before do
617
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
618
+ end
619
+
620
+ subject do
621
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "millions.js"))
622
+ end
623
+
624
+ it "should correctly export millions" do
625
+ set_config "millions.yml"
626
+ I18n::JS.export
627
+ file_should_exist "millions.js"
628
+
629
+ expect(subject).to eq <<EOS
630
+ I18n.translations || (I18n.translations = {});
631
+ I18n.translations[\"de\"] = I18n.extend((I18n.translations[\"de\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
632
+ I18n.translations[\"en\"] = I18n.extend((I18n.translations[\"en\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
633
+ I18n.translations[\"en-US\"] = I18n.extend((I18n.translations[\"en-US\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
634
+ I18n.translations[\"es\"] = I18n.extend((I18n.translations[\"es\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":{\"one\":\"millón\",\"other\":\"millones\"}}}}}}'));
635
+ I18n.translations[\"fr\"] = I18n.extend((I18n.translations[\"fr\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
636
+ I18n.translations[\"ja\"] = I18n.extend((I18n.translations[\"ja\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
637
+ I18n.translations[\"ru\"] = I18n.extend((I18n.translations[\"ru\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
638
+ EOS
639
+ end
640
+ end
641
+
642
+ it "exports with js_extend option at parent level" do
643
+ set_config "js_extend_parent.yml"
644
+ I18n::JS.export
645
+
646
+ file_should_exist "js_extend_parent.js"
647
+
648
+ output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "js_extend_parent.js"))
649
+ expect(output).to eq(<<EOS
650
+ I18n.translations || (I18n.translations = {});
651
+ I18n.translations[\"en\"] = JSON.parse('{\"date\":{\"formats\":{\"default\":\"%Y-%m-%d\",\"long\":\"%B %d, %Y\",\"short\":\"%b %d\"}}}');
652
+ I18n.translations[\"fr\"] = JSON.parse('{\"date\":{\"formats\":{\"default\":\"%d/%m/%Y\",\"long\":\"%e %B %Y\",\"long_ordinal\":\"%e %B %Y\",\"only_day\":\"%e\",\"short\":\"%e %b\"}}}');
653
+ EOS
654
+ )
655
+ end
656
+
657
+ it "exports with js_extend option at segment level" do
658
+ set_config "js_extend_segment.yml"
659
+ I18n::JS.export
660
+
661
+ file_should_exist "js_extend_segment.js"
662
+
663
+ output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "js_extend_segment.js"))
664
+ expect(output).to eq(<<EOS
665
+ I18n.translations || (I18n.translations = {});
666
+ I18n.translations[\"en\"] = JSON.parse('{\"date\":{\"formats\":{\"default\":\"%Y-%m-%d\",\"long\":\"%B %d, %Y\",\"short\":\"%b %d\"}}}');
667
+ I18n.translations[\"fr\"] = JSON.parse('{\"date\":{\"formats\":{\"default\":\"%d/%m/%Y\",\"long\":\"%e %B %Y\",\"long_ordinal\":\"%e %B %Y\",\"only_day\":\"%e\",\"short\":\"%e %b\"}}}');
668
+ EOS
669
+ )
670
+ end
671
+ end
672
+
673
+ describe "merging plural keys" do
674
+ before do
675
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
676
+ end
677
+
678
+ subject do
679
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "merge_plurals.js"))
680
+ end
681
+
682
+ it "should correctly merge the plural keys" do
683
+ set_config "merge_plurals.yml"
684
+ I18n::JS.export
685
+ file_should_exist "merge_plurals.js"
686
+
687
+ expect(subject).to eq <<EOS
688
+ I18n.translations || (I18n.translations = {});
689
+ I18n.translations[\"en\"] = I18n.extend((I18n.translations[\"en\"] || {}), JSON.parse('{\"merge_plurals\":{\"one\":\"Apple\",\"other\":\"Apples\"}}'));\nI18n.translations[\"fr\"] = I18n.extend((I18n.translations[\"fr\"] || {}), JSON.parse('{\"merge_plurals\":{\"one\":\"Pomme\",\"other\":\"Pommes\",\"zero\":\"Pomme\"}}'));
690
+ EOS
691
+ end
692
+ end
693
+
694
+ describe "merging plural keys, with fallbacks" do
695
+ before do
696
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
697
+ end
698
+
699
+ subject do
700
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "merge_plurals_with_no_overrides.js"))
701
+ end
702
+
703
+ it "should correctly merge the plural keys" do
704
+ set_config "merge_plurals_with_no_overrides.yml"
705
+ I18n::JS.export
706
+ file_should_exist "merge_plurals_with_no_overrides.js"
707
+
708
+ expect(subject).to eq <<EOS
709
+ I18n.translations || (I18n.translations = {});
710
+ I18n.translations[\"de\"] = I18n.extend((I18n.translations[\"de\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
711
+ I18n.translations[\"en\"] = I18n.extend((I18n.translations[\"en\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
712
+ I18n.translations[\"en-US\"] = I18n.extend((I18n.translations[\"en-US\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
713
+ I18n.translations[\"es\"] = I18n.extend((I18n.translations[\"es\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
714
+ I18n.translations[\"fr\"] = I18n.extend((I18n.translations[\"fr\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
715
+ I18n.translations[\"ja\"] = I18n.extend((I18n.translations[\"ja\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
716
+ I18n.translations[\"ru\"] = I18n.extend((I18n.translations[\"ru\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"few\":\"кошек\",\"many\":\"кошка\",\"one\":\"кот\",\"other\":\"кошек\"}}'));
717
+ EOS
718
+ end
719
+ end
720
+
721
+ # see discussion @ https://github.com/fnando/i18n-js/pull/551#issuecomment-543205767
722
+ describe "merging plural keys, with fallbacks, with partial overrides" do
723
+ before do
724
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
725
+ end
726
+
727
+ subject do
728
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "merge_plurals_with_partial_overrides.js"))
729
+ end
730
+
731
+ it "should correctly merge the plural keys" do
732
+ set_config "merge_plurals_with_partial_overrides.yml"
733
+ I18n::JS.export
734
+ file_should_exist "merge_plurals_with_partial_overrides.js"
735
+
736
+ expect(subject).to eq <<EOS
737
+ I18n.translations || (I18n.translations = {});
738
+ I18n.translations[\"de\"] = I18n.extend((I18n.translations[\"de\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
739
+ I18n.translations[\"en\"] = I18n.extend((I18n.translations[\"en\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
740
+ I18n.translations[\"en-US\"] = I18n.extend((I18n.translations[\"en-US\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"few\":null,\"many\":null,\"one\":\"Cat\",\"other\":\"Cats\"}}'));
741
+ I18n.translations[\"es\"] = I18n.extend((I18n.translations[\"es\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
742
+ I18n.translations[\"fr\"] = I18n.extend((I18n.translations[\"fr\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
743
+ I18n.translations[\"ja\"] = I18n.extend((I18n.translations[\"ja\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
744
+ I18n.translations[\"ru\"] = I18n.extend((I18n.translations[\"ru\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
745
+ EOS
746
+ end
747
+ end
748
+ end