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.
- data/.travis.yml +5 -0
- data/Appraisals +7 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +52 -0
- data/Rakefile +30 -0
- data/Readme.md +84 -0
- data/gemfiles/rails2.gemfile +15 -0
- data/gemfiles/rails2.gemfile.lock +53 -0
- data/gemfiles/rails3.gemfile +15 -0
- data/gemfiles/rails3.gemfile.lock +53 -0
- data/i18n-backend-http.gemspec +15 -0
- data/lib/i18n/backend/http.rb +114 -0
- data/lib/i18n/backend/http/etag_http_client.rb +33 -0
- data/lib/i18n/backend/http/lru_cache.rb +40 -0
- data/lib/i18n/backend/http/null_cache.rb +19 -0
- data/lib/i18n/backend/http/version.rb +7 -0
- data/test/fixtures/static/testing.yml +15 -0
- data/test/fixtures/vcr/error.yml +47 -0
- data/test/fixtures/vcr/matching_etag.yml +12985 -0
- data/test/fixtures/vcr/multiple_locales.yml +85 -0
- data/test/fixtures/vcr/simple.yml +12894 -0
- data/test/i18n/backend/http_test.rb +356 -0
- data/test/test_helper.rb +18 -0
- metadata +128 -0
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|
+
|