i18n-backend-http 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,356 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('../../../test_helper', __FILE__)
3
+
4
+ class I18nBackendHttpTest < Test::Unit::TestCase
5
+ class SimpleCache
6
+ def initialize
7
+ @cache = {}
8
+ end
9
+
10
+ def read(key)
11
+ @cache[key]
12
+ end
13
+
14
+ def write(key, value, options={})
15
+ if not options[:unless_exist] or not @cache[key]
16
+ @cache[key] = value
17
+ end
18
+ end
19
+
20
+ def fetch(key, options={})
21
+ result = read(key)
22
+ return result if result
23
+ result = yield
24
+ write(key, result)
25
+ end
26
+ end
27
+
28
+ class ZenEnd < ::I18n::Backend::Http
29
+ def initialize(options={})
30
+ super({
31
+ :host => "https://support.zendesk.com",
32
+ }.merge(options))
33
+ end
34
+
35
+ # TODO remove conversion
36
+ def lookup(locale, key, scope = [], options = {})
37
+ locale = locale.to_s[/\d+/].to_i # we only care about the id en-EN-x-ID, everything else could cause duplicate caches
38
+ super(locale, key, scope, options)
39
+ end
40
+
41
+ def parse_response(body)
42
+ JSON.load(body)["locale"]["translations"]
43
+ end
44
+
45
+ def path(locale)
46
+ "/api/v2/locales/#{locale}.json?include=translations"
47
+ end
48
+ end
49
+
50
+ def silence_backend
51
+ $stderr.stubs(:puts)
52
+ end
53
+
54
+ def with_local_available
55
+ VCR.use_cassette("simple") do
56
+ yield
57
+ end
58
+ end
59
+
60
+ def with_error
61
+ VCR.use_cassette("error") do
62
+ I18n.locale = "de_DE-x-888888888"
63
+ yield
64
+ end
65
+ end
66
+
67
+ def add_local_change
68
+ ::I18n.backend.send(:translations, 8)[@key] = "OLD"
69
+ end
70
+
71
+ def update_caches
72
+ I18n.backend.send(:update_caches)
73
+ end
74
+
75
+ context "I18n::Backend::Http" do
76
+ setup do
77
+ @existing_key = "txt.modal.welcome.browsers.best_support"
78
+ @missing_key = "txt.blublublub"
79
+ Thread.list.each {|thread| thread.exit unless thread == Thread.current } # stop all polling threads
80
+ end
81
+
82
+ teardown do
83
+ I18n.backend && I18n.backend.respond_to?(:stop_polling) && I18n.backend.stop_polling
84
+ end
85
+
86
+ context "#translate" do
87
+ setup do
88
+ I18n.locale = "de_DE-x-8"
89
+ I18n.backend = ZenEnd.new
90
+ end
91
+
92
+ should "translate via api" do
93
+ with_local_available do
94
+ assert_equal "Am besten", I18n.t(@existing_key)
95
+ end
96
+ end
97
+
98
+ should "translate via api and :scope" do
99
+ with_local_available do
100
+ assert_equal "Am besten", I18n.t("browsers.best_support", :scope => "txt.modal.welcome")
101
+ end
102
+ end
103
+
104
+ should "translate via :default" do
105
+ with_local_available do
106
+ assert_equal "XXX", I18n.t("txt.blublublub", :default => "XXX")
107
+ end
108
+ end
109
+
110
+ should "caches the locale" do
111
+ with_local_available do
112
+ assert_equal "Am besten", I18n.t(@existing_key)
113
+ JSON.expects(:load).never
114
+ assert_equal "By group", I18n.t("txt.admin.helpers.rules_helper.by_group_label")
115
+ end
116
+ end
117
+
118
+ should "fail when key is unknown" do
119
+ with_local_available do
120
+ assert_equal "translation missing: de_DE-x-8.#{@missing_key}", I18n.t(@missing_key).gsub(', ', '.')
121
+ end
122
+ end
123
+
124
+ should "fail when I mess up the host" do
125
+ silence_backend
126
+ I18n.backend = ZenEnd.new(:host => "https://MUAHAHAHAHA.com")
127
+ VCR.use_cassette("invalid_host") do
128
+ assert_equal "translation missing: de_DE-x-8.#{@missing_key}", I18n.t(@missing_key).gsub(', ', '.')
129
+ end
130
+ end
131
+
132
+ should "fail with invalid locale" do
133
+ silence_backend
134
+ with_error do
135
+ assert_equal "translation missing: #{I18n.locale}.#{@existing_key}", I18n.t(@existing_key).gsub(', ', '.')
136
+ end
137
+ end
138
+
139
+ should "call :exception_handler when error occurs" do
140
+ exception = nil
141
+ I18n.backend = ZenEnd.new(:exception_handler => lambda{|e|
142
+ exception = e
143
+ })
144
+ $stderr.expects(:puts).never
145
+
146
+ with_error do
147
+ I18n.t(@existing_key).gsub(', ', '.')
148
+ end
149
+
150
+ assert_equal exception.class, RuntimeError
151
+ end
152
+
153
+ should "keep :memory_cache_size items in memory cache" do
154
+ I18n.backend = ZenEnd.new(:memory_cache_size => 1)
155
+
156
+ VCR.use_cassette("multiple_locales") do
157
+ assert_equal "Am besten", I18n.t(@existing_key)
158
+ I18n.locale = "es-ES-x-2"
159
+ assert_equal "Mejor", I18n.t(@existing_key)
160
+ end
161
+
162
+ assert_equal "Mejor", I18n.t(@existing_key) # still in memory
163
+
164
+ I18n.locale = "de-DE-x-8"
165
+ I18n.backend.expects(:download_and_cache_translations).returns({})
166
+ I18n.t(@existing_key) # dropped from memory
167
+ end
168
+
169
+ # FIXME how to simulate http timeouts !?
170
+ #should "fails when api is slower then set timeout" do
171
+ # Timeout.timeout 0.8 do
172
+ # assert_equal "translation missing: de_DE-x-8.#{@missing_key}", I18n.t(@missing_key).gsub(', ', '.')
173
+ # end
174
+ #end
175
+
176
+ context "with cache" do
177
+ setup do
178
+ @cache = SimpleCache.new
179
+ I18n.backend = ZenEnd.new(:cache => @cache)
180
+ end
181
+
182
+ should "loads translations from cache" do
183
+ @cache.write "i18n/backend/http/translations/8", {"foo" => "bar"}
184
+ assert_equal "bar", I18n.t("foo")
185
+ end
186
+
187
+ should "downloads translations on cache miss" do
188
+ with_local_available do
189
+ assert_equal "Am besten", I18n.t(@existing_key)
190
+ end
191
+ assert @cache.read("i18n/backend/http/translations/8")
192
+ end
193
+
194
+ should "not store invalid responses in cache" do
195
+ silence_backend
196
+ with_error do
197
+ assert_equal "translation missing: #{I18n.locale}.#{@existing_key}", I18n.t(@existing_key).gsub(', ', '.')
198
+ end
199
+ assert !@cache.read("i18n/backend/http/translations/#{I18n.locale.to_s[/\d+/]}")
200
+ end
201
+
202
+ should "use the memory cache before the cache" do
203
+ @cache.write "i18n/backend/http/translations/8", {"foo" => "bar"}
204
+ assert_equal "bar", I18n.t("foo")
205
+ @cache.write "i18n/backend/http/translations/8", {"foo" => "baZZZ"}
206
+ assert_equal "bar", I18n.t("foo")
207
+ end
208
+ end
209
+ end
210
+
211
+ context "#start_polling" do
212
+ should "not start polling when poll => false is given" do
213
+ I18n.locale = "de_DE-x-8"
214
+ I18n.backend = ZenEnd.new(:poll => false, :polling_interval => 0.2)
215
+ sleep 0.1
216
+ I18n.backend.expects(:update_caches).never
217
+ I18n.backend.stop_polling
218
+ sleep 0.5
219
+ end
220
+
221
+ should "update_caches" do
222
+ I18n.locale = "de_DE-x-8"
223
+ I18n.backend = ZenEnd.new(:polling_interval => 0.2)
224
+ I18n.backend.expects(:update_caches).twice
225
+ sleep 0.5
226
+ end
227
+
228
+ should "stop when calling stop_polling" do
229
+ I18n.locale = "de_DE-x-8"
230
+ I18n.backend = ZenEnd.new(:polling_interval => 0.2)
231
+ sleep 0.1
232
+ I18n.backend.expects(:update_caches).once
233
+ I18n.backend.stop_polling
234
+ sleep 0.5
235
+ end
236
+ end
237
+
238
+ context "#update_caches_via_api" do
239
+ setup do
240
+ I18n.locale = "de_DE-x-8"
241
+ I18n.backend = ZenEnd.new
242
+ @key = @existing_key
243
+ end
244
+
245
+ should "update translations" do
246
+ # init it
247
+ with_local_available do
248
+ assert_equal "Am besten", I18n.t(@key)
249
+ end
250
+
251
+ # add a change
252
+ add_local_change
253
+ assert_equal "OLD", I18n.t(@key)
254
+
255
+ # update
256
+ with_local_available do
257
+ update_caches
258
+ end
259
+
260
+ assert_equal "Am besten", I18n.t(@key)
261
+ end
262
+
263
+ should "not update if api did not change" do
264
+ VCR.use_cassette("matching_etag") do
265
+ assert_equal "Am besten", I18n.t(@key) # initial request
266
+
267
+ # add a change
268
+ add_local_change
269
+
270
+ # update -> not modified!
271
+ update_caches
272
+ assert_equal "OLD", I18n.t(@key)
273
+ end
274
+ end
275
+
276
+ context "with cache" do
277
+ setup do
278
+ @key = @existing_key
279
+ @cache = SimpleCache.new
280
+ I18n.backend = ZenEnd.new(:cache => @cache)
281
+ end
282
+
283
+ should "update cache" do
284
+ # init it via cache
285
+ @cache.write "i18n/backend/http/translations/8", {@key => "bar"}
286
+ assert_equal "bar", I18n.t(@key)
287
+
288
+ # add a change
289
+ add_local_change
290
+ assert_equal "OLD", I18n.t(@key)
291
+
292
+ # update via api
293
+ with_local_available do
294
+ update_caches
295
+ end
296
+
297
+ assert_equal "Am besten", I18n.t(@key)
298
+
299
+ # loading from cache should have new translations
300
+ I18n.backend = ZenEnd.new(:cache => @cache)
301
+ assert_equal "Am besten", I18n.t(@key)
302
+ end
303
+
304
+ should "pick one server to be the master" do
305
+ @cache.write "i18n/backend/http/translations/8", {@key => "bar"}
306
+ ZenEnd.any_instance.expects(:download_and_cache_translations).twice
307
+ 4.times{
308
+ backend = ZenEnd.new(:polling_interval => 0.3, :cache => @cache)
309
+ assert_equal "bar", backend.translate("de-DE-x-8", @key)
310
+ }
311
+ sleep 0.7
312
+ end
313
+
314
+ should "update all translations known by all clients" do
315
+ VCR.use_cassette("multiple_locales") do
316
+ @cache.write "i18n/backend/http/translations/8", {@key => "bar"}
317
+ @cache.write "i18n/backend/http/translations/2", {@key => "bar"}
318
+
319
+ a = ZenEnd.new(:polling_interval => 0.3, :cache => @cache)
320
+ b = ZenEnd.new(:polling_interval => 0.3, :cache => @cache)
321
+
322
+ assert_equal "bar", a.translate("de-DE-x-8", @key)
323
+ assert_equal "bar", b.translate("es-Es-x-2", @key)
324
+
325
+ sleep 0.4 # to refresh vcr: a.stop_polling; b.stop_polling; sleep 10
326
+
327
+ assert_equal "Am besten", a.translate("de-DE-x-8", @key)
328
+ assert_equal "Mejor", b.translate("es-Es-x-2", @key)
329
+ end
330
+ end
331
+
332
+ should "updates translations from cache if its a slave" do
333
+ VCR.use_cassette("matching_etag") do
334
+ @cache.write "i18n/backend/http/translations/8", {@key => "bar"}
335
+ backends = Array.new(4)
336
+
337
+ backends = backends.map do
338
+ ZenEnd.new(:polling_interval => 0.3, :cache => @cache)
339
+ end
340
+
341
+ translate = lambda{
342
+ backends.map do |backend|
343
+ backend.translate(I18n.locale, @key)
344
+ end
345
+ }
346
+ assert_equal ["bar", "bar", "bar", "bar"], translate.call
347
+
348
+ sleep 0.8
349
+
350
+ assert_equal ["Am besten", "Am besten", "Am besten", "Am besten"], translate.call
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,18 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+
4
+ require 'test/unit'
5
+ require 'shoulda'
6
+ require 'mocha'
7
+ require 'vcr'
8
+ require 'redgreen'
9
+
10
+ VCR.configure do |c|
11
+ c.cassette_library_dir = 'test/fixtures/vcr'
12
+ c.hook_into :webmock
13
+ end
14
+
15
+ $LOAD_PATH.unshift 'lib'
16
+ require 'i18n/backend/http'
17
+ require 'i18n/backend/simple' # is used when I18n first starts
18
+ require 'json'
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: i18n-backend-http
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Michael Grosser
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-05-18 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: i18n
22
+ version_requirements: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ hash: 3
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ requirement: *id001
33
+ prerelease: false
34
+ - !ruby/object:Gem::Dependency
35
+ name: gem_of_thrones
36
+ version_requirements: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ hash: 3
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ type: :runtime
46
+ requirement: *id002
47
+ prerelease: false
48
+ - !ruby/object:Gem::Dependency
49
+ name: faraday
50
+ version_requirements: &id003 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ type: :runtime
60
+ requirement: *id003
61
+ prerelease: false
62
+ description:
63
+ email: michael@grosser.it
64
+ executables: []
65
+
66
+ extensions: []
67
+
68
+ extra_rdoc_files: []
69
+
70
+ files:
71
+ - .travis.yml
72
+ - Appraisals
73
+ - Gemfile
74
+ - Gemfile.lock
75
+ - Rakefile
76
+ - Readme.md
77
+ - gemfiles/rails2.gemfile
78
+ - gemfiles/rails2.gemfile.lock
79
+ - gemfiles/rails3.gemfile
80
+ - gemfiles/rails3.gemfile.lock
81
+ - i18n-backend-http.gemspec
82
+ - lib/i18n/backend/http.rb
83
+ - lib/i18n/backend/http/etag_http_client.rb
84
+ - lib/i18n/backend/http/lru_cache.rb
85
+ - lib/i18n/backend/http/null_cache.rb
86
+ - lib/i18n/backend/http/version.rb
87
+ - test/fixtures/static/testing.yml
88
+ - test/fixtures/vcr/error.yml
89
+ - test/fixtures/vcr/matching_etag.yml
90
+ - test/fixtures/vcr/multiple_locales.yml
91
+ - test/fixtures/vcr/simple.yml
92
+ - test/i18n/backend/http_test.rb
93
+ - test/test_helper.rb
94
+ homepage: http://github.com/grosser/i18n-backend-http
95
+ licenses:
96
+ - MIT
97
+ post_install_message:
98
+ rdoc_options: []
99
+
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ hash: 3
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ hash: 3
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ requirements: []
121
+
122
+ rubyforge_project:
123
+ rubygems_version: 1.8.24
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: Rails I18n Backend for Http APIs with etag-aware background polling and memory+[memcache] caching
127
+ test_files: []
128
+