i18n-backend-http 0.1.0

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.
@@ -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
+