i18n-js 2.1.2 → 3.9.2

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 (130) 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 +106 -0
  5. data/.gitignore +6 -4
  6. data/.npmignore +27 -0
  7. data/Appraisals +52 -0
  8. data/CHANGELOG.md +575 -0
  9. data/Gemfile +1 -1
  10. data/README.md +1099 -0
  11. data/Rakefile +19 -7
  12. data/app/assets/javascripts/i18n/filtered.js.erb +23 -0
  13. data/app/assets/javascripts/i18n/shims.js +240 -0
  14. data/app/assets/javascripts/i18n/translations.js +3 -0
  15. data/app/assets/javascripts/i18n.js +1095 -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_10.gemfile +7 -0
  23. data/gemfiles/i18n_1_2.gemfile +7 -0
  24. data/gemfiles/i18n_1_3.gemfile +7 -0
  25. data/gemfiles/i18n_1_4.gemfile +7 -0
  26. data/gemfiles/i18n_1_5.gemfile +7 -0
  27. data/gemfiles/i18n_1_6.gemfile +7 -0
  28. data/gemfiles/i18n_1_7.gemfile +7 -0
  29. data/gemfiles/i18n_1_8.gemfile +7 -0
  30. data/gemfiles/i18n_1_9.gemfile +7 -0
  31. data/i18n-js.gemspec +13 -10
  32. data/i18njs.png +0 -0
  33. data/lib/i18n/js/dependencies.rb +67 -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 +81 -0
  43. data/lib/i18n/js/utils.rb +91 -0
  44. data/lib/i18n/js/version.rb +7 -0
  45. data/lib/i18n/js.rb +274 -0
  46. data/lib/i18n-js.rb +1 -177
  47. data/lib/rails/generators/i18n/js/config/config_generator.rb +19 -0
  48. data/{config → lib/rails/generators/i18n/js/config/templates}/i18n-js.yml +11 -6
  49. data/lib/tasks/export.rake +8 -0
  50. data/package.json +25 -0
  51. data/spec/fixtures/custom_path.yml +5 -0
  52. data/spec/fixtures/default.yml +5 -0
  53. data/spec/fixtures/erb.yml +5 -0
  54. data/spec/fixtures/except_condition.yml +7 -0
  55. data/spec/fixtures/js_available_locales_custom.yml +1 -0
  56. data/spec/fixtures/js_export_dir_custom.yml +7 -0
  57. data/spec/fixtures/js_export_dir_none.yml +6 -0
  58. data/spec/fixtures/js_extend_parent.yml +6 -0
  59. data/spec/fixtures/js_extend_segment.yml +6 -0
  60. data/spec/fixtures/js_file_per_locale.yml +7 -0
  61. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +4 -0
  62. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +6 -0
  63. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +4 -0
  64. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +4 -0
  65. data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +4 -0
  66. data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +4 -0
  67. data/spec/fixtures/js_file_with_namespace_prefix_and_pretty_print.yml +9 -0
  68. data/spec/fixtures/js_sort_translation_keys_false.yml +6 -0
  69. data/spec/fixtures/js_sort_translation_keys_true.yml +6 -0
  70. data/spec/fixtures/json_only.yml +18 -0
  71. data/spec/{resources → fixtures}/locales.yml +58 -1
  72. data/spec/fixtures/merge_plurals.yml +6 -0
  73. data/spec/fixtures/merge_plurals_with_no_overrides.yml +4 -0
  74. data/spec/fixtures/merge_plurals_with_partial_overrides.yml +4 -0
  75. data/spec/fixtures/millions.yml +4 -0
  76. data/spec/fixtures/multiple_conditions.yml +7 -0
  77. data/spec/fixtures/multiple_conditions_per_locale.yml +7 -0
  78. data/spec/fixtures/multiple_files.yml +7 -0
  79. data/spec/{resources → fixtures}/no_config.yml +0 -0
  80. data/spec/fixtures/no_scope.yml +4 -0
  81. data/spec/fixtures/simple_scope.yml +5 -0
  82. data/spec/js/currency.spec.js +62 -0
  83. data/spec/js/current_locale.spec.js +19 -0
  84. data/spec/js/dates.spec.js +276 -0
  85. data/spec/js/defaults.spec.js +31 -0
  86. data/spec/js/extend.spec.js +110 -0
  87. data/spec/js/interpolation.spec.js +124 -0
  88. data/spec/js/jasmine/MIT.LICENSE +20 -0
  89. data/spec/js/jasmine/jasmine-html.js +190 -0
  90. data/spec/js/jasmine/jasmine.css +166 -0
  91. data/spec/js/jasmine/jasmine.js +2476 -0
  92. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  93. data/spec/js/json_parsable.spec.js +14 -0
  94. data/spec/js/locales.spec.js +31 -0
  95. data/spec/js/localization.spec.js +78 -0
  96. data/spec/js/numbers.spec.js +174 -0
  97. data/spec/js/placeholder.spec.js +24 -0
  98. data/spec/js/pluralization.spec.js +228 -0
  99. data/spec/js/prepare_options.spec.js +41 -0
  100. data/spec/js/require.js +2083 -0
  101. data/spec/js/specs.html +49 -0
  102. data/spec/js/specs_requirejs.html +72 -0
  103. data/spec/js/translate.spec.js +304 -0
  104. data/spec/js/translations.js +188 -0
  105. data/spec/js/utility_functions.spec.js +20 -0
  106. data/spec/ruby/i18n/js/fallback_locales_spec.rb +84 -0
  107. data/spec/ruby/i18n/js/segment_spec.rb +286 -0
  108. data/spec/ruby/i18n/js/utils_spec.rb +138 -0
  109. data/spec/ruby/i18n/js_spec.rb +797 -0
  110. data/spec/spec_helper.rb +75 -14
  111. data/yarn.lock +138 -0
  112. metadata +225 -96
  113. data/.rspec +0 -1
  114. data/Gemfile.lock +0 -51
  115. data/README.rdoc +0 -305
  116. data/lib/i18n-js/engine.rb +0 -62
  117. data/lib/i18n-js/railtie.rb +0 -13
  118. data/lib/i18n-js/rake.rb +0 -16
  119. data/lib/i18n-js/version.rb +0 -10
  120. data/spec/i18n_spec.js +0 -768
  121. data/spec/i18n_spec.rb +0 -205
  122. data/spec/resources/custom_path.yml +0 -4
  123. data/spec/resources/default.yml +0 -4
  124. data/spec/resources/js_file_per_locale.yml +0 -3
  125. data/spec/resources/multiple_conditions.yml +0 -6
  126. data/spec/resources/multiple_files.yml +0 -6
  127. data/spec/resources/no_scope.yml +0 -3
  128. data/spec/resources/simple_scope.yml +0 -4
  129. data/vendor/assets/javascripts/i18n/translations.js.erb +0 -7
  130. data/vendor/assets/javascripts/i18n.js +0 -450
@@ -0,0 +1,797 @@
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(:available_locales) { %i[fr pirate] }
314
+
315
+ before do
316
+ allow(::I18n).to receive(:available_locales).and_return(available_locales)
317
+ set_config 'js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml'
318
+ end
319
+
320
+ it { should eql(nil) }
321
+ end
322
+
323
+ context 'when given locale is in `.js_available_locales` but its translation is missing' do
324
+ subject { translations[:fr][:fallback_test] }
325
+
326
+ let(:available_locales) { %i[fr pirate] }
327
+
328
+ before do
329
+ allow(described_class).to receive(:js_available_locales).and_return(available_locales)
330
+ set_config 'js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml'
331
+ end
332
+
333
+ it { should eql(nil) }
334
+ end
335
+
336
+ context "with I18n::Fallbacks enabled" do
337
+ let(:backend_with_fallbacks) { backend_class_with_fallbacks.new }
338
+ let!(:old_backebad) { I18n.backend }
339
+
340
+ before do
341
+ I18n::JS.backend = backend_with_fallbacks
342
+ I18n.fallbacks[:fr] = [:de, :en]
343
+ end
344
+ after { I18n::JS.backend = old_backebad }
345
+
346
+ it "exports with defined locale as fallback when enabled" do
347
+ set_config "js_file_per_locale_with_fallbacks_enabled.yml"
348
+ expect(subject[:fr][:fallback_test]).to eql("Erfolg")
349
+ end
350
+
351
+ it "exports with defined locale as fallback when enabled with :default_locale" do
352
+ set_config "js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml"
353
+ expect(subject[:fr][:fallback_test]).to eql("Success")
354
+ end
355
+
356
+ it "exports with Fallbacks as Hash" do
357
+ set_config "js_file_per_locale_with_fallbacks_as_hash.yml"
358
+ expect(subject[:fr][:fallback_test]).to eql("Erfolg")
359
+ end
360
+ end
361
+ end
362
+
363
+ context "namespace, prefix, suffix, and pretty_print options" do
364
+
365
+ before do
366
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
367
+ set_config "js_file_with_namespace_prefix_and_pretty_print.yml"
368
+ end
369
+
370
+ it "exports with defined locale as fallback when enabled" do
371
+ I18n::JS.export
372
+ file_should_exist "en.js"
373
+ output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "en.js"))
374
+ expect(output).to match(/^#{
375
+ <<EOS
376
+ import random from 'random-library';
377
+ Foo.translations || (Foo.translations = {});
378
+ Foo.translations["en"] = {
379
+ "number": {
380
+ "format": {
381
+ EOS
382
+ }.+#{
383
+ <<EOS
384
+ "edit": {
385
+ "title": "Edit"
386
+ }
387
+ },
388
+ "foo": "Foo",
389
+ "fallback_test": "Success"
390
+ };
391
+ //test
392
+ EOS
393
+ }$/)
394
+ end
395
+ end
396
+
397
+ describe '.js_available_locales' do
398
+ subject { described_class.js_available_locales }
399
+
400
+ let(:results) { described_class.scoped_translations('*.admin.*.title') }
401
+ let(:result) { ->(locale) { results[locale][:admin][:show][:title] } }
402
+
403
+ context 'when I18n.available_locales is not set' do
404
+ it { expect(subject).to eq ::I18n.available_locales }
405
+
406
+ it 'should allow all locales' do
407
+ expect(result.call(:en)).to eql('Show')
408
+ expect(result.call(:fr)).to eql('Visualiser')
409
+ expect(result.call(:ja)).to eql('Ignore me')
410
+ end
411
+ end
412
+
413
+ context 'when I18n.available_locales is set' do
414
+ let(:available_locales) { %i[en fr] }
415
+
416
+ before { allow(::I18n).to receive(:available_locales).and_return(available_locales) }
417
+
418
+ it { expect(subject).to eq available_locales }
419
+
420
+ it 'should ignore non-valid locales' do
421
+ expect(result.call(:en)).to eql('Show')
422
+ expect(result.call(:fr)).to eql('Visualiser')
423
+ expect(results).not_to include(:ja)
424
+ end
425
+
426
+ context 'when :js_available_locales set in config' do
427
+ before { set_config 'js_available_locales_custom.yml' }
428
+
429
+ it { expect(subject).to eq %i[en foo] }
430
+
431
+ it 'should ignore non-valid locales' do
432
+ expect(result.call(:en)).to eql('Show')
433
+ expect(results).not_to include(:fr, :ja)
434
+ end
435
+ end
436
+ end
437
+ end
438
+
439
+ context 'I18n.available_locales' do
440
+ let(:results) { described_class.scoped_translations('*.admin.*.title') }
441
+ let(:result) { ->(locale) { results[locale][:admin][:show][:title] } }
442
+
443
+ context 'when I18n.available_locales is not set' do
444
+ it 'should allow all locales' do
445
+ expect(result.call(:en)).to eql('Show')
446
+ expect(result.call(:fr)).to eql('Visualiser')
447
+ expect(result.call(:ja)).to eql('Ignore me')
448
+ end
449
+ end
450
+
451
+ context 'when I18n.available_locales is set' do
452
+ before { allow(::I18n).to receive(:available_locales){ [:en, :fr] } }
453
+
454
+ it 'should ignore non-valid locales' do
455
+ expect(result.call(:en)).to eql('Show')
456
+ expect(result.call(:fr)).to eql('Visualiser')
457
+ expect(results).not_to include(:ja)
458
+ end
459
+ end
460
+ end
461
+
462
+ context "general" do
463
+ it "sets export directory" do
464
+ expect(I18n::JS::DEFAULT_EXPORT_DIR_PATH).to eql("public/javascripts")
465
+ end
466
+
467
+ it "sets empty hash as configuration when no file is found" do
468
+ expect(I18n::JS.config_file_exists?).to eql(false)
469
+ expect(I18n::JS.config).to eql({})
470
+ end
471
+
472
+ it "executes erb in config file" do
473
+ set_config "erb.yml"
474
+
475
+ config_entry = I18n::JS.config[:translations].first
476
+ expect(config_entry["only"]).to eq("*.date.formats")
477
+ end
478
+ end
479
+
480
+ describe "i18n.js exporting" do
481
+ after { begin described_class.send(:remove_instance_variable, :@export_i18n_js_dir_path); rescue; end }
482
+
483
+ describe ".export_i18n_js with global variable" do
484
+ before do
485
+ allow(FileUtils).to receive(:mkdir_p).and_call_original
486
+ allow(FileUtils).to receive(:cp).and_call_original
487
+
488
+ allow(described_class).to receive(:export_i18n_js_dir_path).and_return(export_i18n_js_dir_path)
489
+ I18n::JS.export_i18n_js
490
+ end
491
+
492
+ context 'when .export_i18n_js_dir_path returns something' do
493
+ let(:export_i18n_js_dir_path) { temp_path }
494
+
495
+ it "does create the folder before copying" do
496
+ expect(FileUtils).to have_received(:mkdir_p).with(export_i18n_js_dir_path).once
497
+ end
498
+ it "does copy the file with FileUtils.cp" do
499
+ expect(FileUtils).to have_received(:cp).once
500
+ end
501
+ it "exports the file" do
502
+ expect(File).to be_file(File.join(I18n::JS.export_i18n_js_dir_path, "i18n.js"))
503
+ end
504
+ end
505
+
506
+ context 'when .export_i18n_js_dir_path is set to nil' do
507
+ let(:export_i18n_js_dir_path) { nil }
508
+
509
+ it "does NOT create the folder before copying" do
510
+ expect(FileUtils).to_not have_received(:mkdir_p)
511
+ end
512
+ it "does NOT copy the file with FileUtils.cp" do
513
+ expect(FileUtils).to_not have_received(:cp)
514
+ end
515
+ end
516
+ end
517
+
518
+ describe ".export_i18n_js with config" do
519
+
520
+ let(:export_action) do
521
+ allow(FileUtils).to receive(:mkdir_p).and_call_original
522
+ allow(FileUtils).to receive(:cp).and_call_original
523
+ I18n::JS.export_i18n_js
524
+ end
525
+
526
+ context 'when :export_i18n_js set in config' do
527
+ before { set_config "js_export_dir_custom.yml"; export_action }
528
+ let(:export_i18n_js_dir_path) { temp_path }
529
+ let(:config_export_path) { "tmp/i18n-js/foo" }
530
+
531
+ it "does create the folder before copying" do
532
+ expect(FileUtils).to have_received(:mkdir_p).with(config_export_path).once
533
+ end
534
+ it "does copy the file with FileUtils.cp" do
535
+ expect(FileUtils).to have_received(:cp).once
536
+ end
537
+ it "exports the file" do
538
+ expect(File).to be_file(File.join(config_export_path, "i18n.js"))
539
+ end
540
+ end
541
+
542
+ context 'when .export_i18n_js_dir_path is set to false' do
543
+ before { set_config "js_export_dir_none.yml"; export_action }
544
+
545
+ it "does NOT create the folder before copying" do
546
+ expect(FileUtils).to_not have_received(:mkdir_p)
547
+ end
548
+
549
+ it "does NOT copy the file with FileUtils.cp" do
550
+ expect(FileUtils).to_not have_received(:cp)
551
+ end
552
+ end
553
+ end
554
+
555
+ describe '.export_i18n_js_dir_path' do
556
+ let(:default_path) { I18n::JS::DEFAULT_EXPORT_DIR_PATH }
557
+ let(:new_path) { File.join("tmp", default_path) }
558
+ after { described_class.send(:remove_instance_variable, :@export_i18n_js_dir_path) }
559
+
560
+ subject { described_class.export_i18n_js_dir_path }
561
+
562
+ context "when it is not set" do
563
+ it { should eq default_path }
564
+ end
565
+ context "when it is set to another path already" do
566
+ before { described_class.export_i18n_js_dir_path = new_path }
567
+
568
+ it { should eq new_path }
569
+ end
570
+ context "when it is set to nil already" do
571
+ before { described_class.export_i18n_js_dir_path = nil }
572
+
573
+ it { should eq :none }
574
+ end
575
+ end
576
+ end
577
+
578
+ describe "translation key sorting" do
579
+
580
+ describe ".sort_translation_keys?" do
581
+ after { described_class.send(:remove_instance_variable, :@sort_translation_keys) }
582
+ subject { described_class.sort_translation_keys? }
583
+
584
+
585
+ context "set with config" do
586
+
587
+ context 'when :sort_translation_keys is not set in config' do
588
+ before :each do
589
+ set_config "default.yml"
590
+ end
591
+
592
+ it { should eq true }
593
+ end
594
+
595
+ context 'when :sort_translation_keys set to true in config' do
596
+ before :each do
597
+ set_config "js_sort_translation_keys_true.yml"
598
+ end
599
+
600
+ it { should eq true }
601
+ end
602
+
603
+ context 'when :sort_translation_keys set to false in config' do
604
+ before :each do
605
+ set_config "js_sort_translation_keys_false.yml"
606
+ end
607
+
608
+ it { should eq false }
609
+ end
610
+ end
611
+
612
+ context 'set by .sort_translation_keys' do
613
+
614
+ context "when it is not set" do
615
+ it { should eq true }
616
+ end
617
+
618
+ context "when it is set to true" do
619
+ before { described_class.sort_translation_keys = true }
620
+
621
+ it { should eq true }
622
+ end
623
+
624
+ context "when it is set to false" do
625
+ before { described_class.sort_translation_keys = false }
626
+
627
+ it { should eq false }
628
+ end
629
+ end
630
+ end
631
+
632
+ context "exporting" do
633
+ subject do
634
+ I18n::JS.export
635
+ file_should_exist "en.js"
636
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "en.js"))
637
+ end
638
+
639
+ before do
640
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
641
+ end
642
+
643
+ context 'sort_translation_keys is true' do
644
+ before :each do
645
+ set_config "js_sort_translation_keys_true.yml"
646
+ end
647
+
648
+ it "exports with the keys sorted" do
649
+ expect(subject).to eq <<EOS
650
+ I18n.translations || (I18n.translations = {});
651
+ 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"}}'));
652
+ EOS
653
+ end
654
+ end
655
+ end
656
+ end
657
+
658
+ describe "js_extend option" do
659
+ before do
660
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
661
+ end
662
+
663
+ # https://github.com/fnando/i18n-js/issues/553
664
+ describe "millions" do
665
+ before do
666
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
667
+ end
668
+
669
+ subject do
670
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "millions.js"))
671
+ end
672
+
673
+ it "should correctly export millions" do
674
+ set_config "millions.yml"
675
+ I18n::JS.export
676
+ file_should_exist "millions.js"
677
+
678
+ expect(subject).to eq <<EOS
679
+ I18n.translations || (I18n.translations = {});
680
+ I18n.translations[\"de\"] = I18n.extend((I18n.translations[\"de\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
681
+ I18n.translations[\"en\"] = I18n.extend((I18n.translations[\"en\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
682
+ I18n.translations[\"en-US\"] = I18n.extend((I18n.translations[\"en-US\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
683
+ I18n.translations[\"es\"] = I18n.extend((I18n.translations[\"es\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":{\"one\":\"millón\",\"other\":\"millones\"}}}}}}'));
684
+ I18n.translations[\"fr\"] = I18n.extend((I18n.translations[\"fr\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
685
+ I18n.translations[\"ja\"] = I18n.extend((I18n.translations[\"ja\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
686
+ I18n.translations[\"ru\"] = I18n.extend((I18n.translations[\"ru\"] || {}), JSON.parse('{\"number\":{\"human\":{\"decimal_units\":{\"units\":{\"million\":\"Million\"}}}}}'));
687
+ EOS
688
+ end
689
+ end
690
+
691
+ it "exports with js_extend option at parent level" do
692
+ set_config "js_extend_parent.yml"
693
+ I18n::JS.export
694
+
695
+ file_should_exist "js_extend_parent.js"
696
+
697
+ output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "js_extend_parent.js"))
698
+ expect(output).to eq(<<EOS
699
+ I18n.translations || (I18n.translations = {});
700
+ I18n.translations[\"en\"] = JSON.parse('{\"date\":{\"formats\":{\"default\":\"%Y-%m-%d\",\"long\":\"%B %d, %Y\",\"short\":\"%b %d\"}}}');
701
+ 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\"}}}');
702
+ EOS
703
+ )
704
+ end
705
+
706
+ it "exports with js_extend option at segment level" do
707
+ set_config "js_extend_segment.yml"
708
+ I18n::JS.export
709
+
710
+ file_should_exist "js_extend_segment.js"
711
+
712
+ output = File.read(File.join(I18n::JS.export_i18n_js_dir_path, "js_extend_segment.js"))
713
+ expect(output).to eq(<<EOS
714
+ I18n.translations || (I18n.translations = {});
715
+ I18n.translations[\"en\"] = JSON.parse('{\"date\":{\"formats\":{\"default\":\"%Y-%m-%d\",\"long\":\"%B %d, %Y\",\"short\":\"%b %d\"}}}');
716
+ 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\"}}}');
717
+ EOS
718
+ )
719
+ end
720
+ end
721
+
722
+ describe "merging plural keys" 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.js"))
729
+ end
730
+
731
+ it "should correctly merge the plural keys" do
732
+ set_config "merge_plurals.yml"
733
+ I18n::JS.export
734
+ file_should_exist "merge_plurals.js"
735
+
736
+ expect(subject).to eq <<EOS
737
+ I18n.translations || (I18n.translations = {});
738
+ 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\"}}'));
739
+ EOS
740
+ end
741
+ end
742
+
743
+ describe "merging plural keys, with fallbacks" do
744
+ before do
745
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
746
+ end
747
+
748
+ subject do
749
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "merge_plurals_with_no_overrides.js"))
750
+ end
751
+
752
+ it "should correctly merge the plural keys" do
753
+ set_config "merge_plurals_with_no_overrides.yml"
754
+ I18n::JS.export
755
+ file_should_exist "merge_plurals_with_no_overrides.js"
756
+
757
+ expect(subject).to eq <<EOS
758
+ I18n.translations || (I18n.translations = {});
759
+ I18n.translations[\"de\"] = I18n.extend((I18n.translations[\"de\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
760
+ I18n.translations[\"en\"] = I18n.extend((I18n.translations[\"en\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
761
+ I18n.translations[\"en-US\"] = I18n.extend((I18n.translations[\"en-US\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
762
+ I18n.translations[\"es\"] = I18n.extend((I18n.translations[\"es\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
763
+ I18n.translations[\"fr\"] = I18n.extend((I18n.translations[\"fr\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
764
+ I18n.translations[\"ja\"] = I18n.extend((I18n.translations[\"ja\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"one\":\"Apple\",\"other\":\"Apples\",\"zero\":\"No Apple\"}}'));
765
+ I18n.translations[\"ru\"] = I18n.extend((I18n.translations[\"ru\"] || {}), JSON.parse('{\"merge_plurals_with_no_overrides\":{\"few\":\"кошек\",\"many\":\"кошка\",\"one\":\"кот\",\"other\":\"кошек\"}}'));
766
+ EOS
767
+ end
768
+ end
769
+
770
+ # see discussion @ https://github.com/fnando/i18n-js/pull/551#issuecomment-543205767
771
+ describe "merging plural keys, with fallbacks, with partial overrides" do
772
+ before do
773
+ stub_const('I18n::JS::DEFAULT_EXPORT_DIR_PATH', temp_path)
774
+ end
775
+
776
+ subject do
777
+ File.read(File.join(I18n::JS.export_i18n_js_dir_path, "merge_plurals_with_partial_overrides.js"))
778
+ end
779
+
780
+ it "should correctly merge the plural keys" do
781
+ set_config "merge_plurals_with_partial_overrides.yml"
782
+ I18n::JS.export
783
+ file_should_exist "merge_plurals_with_partial_overrides.js"
784
+
785
+ expect(subject).to eq <<EOS
786
+ I18n.translations || (I18n.translations = {});
787
+ I18n.translations[\"de\"] = I18n.extend((I18n.translations[\"de\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
788
+ I18n.translations[\"en\"] = I18n.extend((I18n.translations[\"en\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
789
+ 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\"}}'));
790
+ I18n.translations[\"es\"] = I18n.extend((I18n.translations[\"es\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
791
+ I18n.translations[\"fr\"] = I18n.extend((I18n.translations[\"fr\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
792
+ I18n.translations[\"ja\"] = I18n.extend((I18n.translations[\"ja\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
793
+ I18n.translations[\"ru\"] = I18n.extend((I18n.translations[\"ru\"] || {}), JSON.parse('{\"merge_plurals_with_partial_overrides\":{\"one\":\"Cat\",\"other\":\"Cats\"}}'));
794
+ EOS
795
+ end
796
+ end
797
+ end