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
data/.travis.yml
ADDED
data/Appraisals
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
i18n-backend-http (0.1.0)
|
5
|
+
faraday
|
6
|
+
gem_of_thrones
|
7
|
+
i18n
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
addressable (2.2.8)
|
13
|
+
appraisal (0.4.1)
|
14
|
+
bundler
|
15
|
+
rake
|
16
|
+
crack (0.3.1)
|
17
|
+
faraday (0.8.0)
|
18
|
+
multipart-post (~> 1.1)
|
19
|
+
gem_of_thrones (0.2.1)
|
20
|
+
i18n (0.6.0)
|
21
|
+
json (1.7.1)
|
22
|
+
metaclass (0.0.1)
|
23
|
+
mocha (0.11.4)
|
24
|
+
metaclass (~> 0.0.1)
|
25
|
+
multipart-post (1.1.5)
|
26
|
+
mynyml-redgreen (0.7.1)
|
27
|
+
term-ansicolor (>= 1.0.4)
|
28
|
+
rake (0.9.2)
|
29
|
+
shoulda (3.0.1)
|
30
|
+
shoulda-context (~> 1.0.0)
|
31
|
+
shoulda-matchers (~> 1.0.0)
|
32
|
+
shoulda-context (1.0.0)
|
33
|
+
shoulda-matchers (1.0.0)
|
34
|
+
term-ansicolor (1.0.7)
|
35
|
+
vcr (2.1.1)
|
36
|
+
webmock (1.8.6)
|
37
|
+
addressable (>= 2.2.7)
|
38
|
+
crack (>= 0.1.7)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
appraisal
|
45
|
+
i18n-backend-http!
|
46
|
+
json
|
47
|
+
mocha
|
48
|
+
mynyml-redgreen
|
49
|
+
rake
|
50
|
+
shoulda
|
51
|
+
vcr
|
52
|
+
webmock
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'appraisal'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
task :default do
|
5
|
+
sh "bundle exec rake appraisal:install && bundle exec rake appraisal test"
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'rake/testtask'
|
9
|
+
Rake::TestTask.new(:test) do |test|
|
10
|
+
test.libs << 'lib'
|
11
|
+
test.pattern = 'test/**/*_test.rb'
|
12
|
+
test.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
# extracted from https://github.com/grosser/project_template
|
16
|
+
rule /^version:bump:.*/ do |t|
|
17
|
+
sh "git status | grep 'nothing to commit'" # ensure we are not dirty
|
18
|
+
index = ['major', 'minor','patch'].index(t.name.split(':').last)
|
19
|
+
file = 'lib/i18n/backend/http/version.rb'
|
20
|
+
|
21
|
+
version_file = File.read(file)
|
22
|
+
old_version, *version_parts = version_file.match(/(\d+)\.(\d+)\.(\d+)/).to_a
|
23
|
+
version_parts[index] = version_parts[index].to_i + 1
|
24
|
+
version_parts[2] = 0 if index < 2 # remove patch for minor
|
25
|
+
version_parts[1] = 0 if index < 1 # remove minor for major
|
26
|
+
new_version = version_parts * '.'
|
27
|
+
File.open(file,'w'){|f| f.write(version_file.sub(old_version, new_version)) }
|
28
|
+
|
29
|
+
sh "bundle && git add #{file} Gemfile.lock && git commit -m 'bump version to #{new_version}'"
|
30
|
+
end
|
data/Readme.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
Rails I18n Backend for Http APIs with etag-aware distributed background polling and lru-memory+[memcache] caching.<br/>
|
2
|
+
Very few request, always up to date + fast.
|
3
|
+
|
4
|
+
Install
|
5
|
+
=======
|
6
|
+
|
7
|
+
gem install i18n-backend-http
|
8
|
+
Or
|
9
|
+
|
10
|
+
rails plugin install git://github.com/grosser/i18n-backend-http.git
|
11
|
+
|
12
|
+
Usage
|
13
|
+
=====
|
14
|
+
|
15
|
+
```Ruby
|
16
|
+
class MyBackend < I18n::Backend::Http
|
17
|
+
def initialize(options={})
|
18
|
+
super({
|
19
|
+
:host => "https://api.host.com",
|
20
|
+
:cache => Rails.cache,
|
21
|
+
:http_open_timeout => 5, # default: 1
|
22
|
+
:http_read_timeout => 5, # default: 1
|
23
|
+
# :exception_handler => lambda{|e| Rails.logger.error e },
|
24
|
+
# :memory_cache_size => ??, # default: 10 locales
|
25
|
+
}.merge(options))
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_response(body)
|
29
|
+
JSON.load(body)["translations"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def path(locale)
|
33
|
+
"/path/to/api/#{locale}.json"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
I18n.backend = MyBackend.new
|
38
|
+
```
|
39
|
+
|
40
|
+
### Polling
|
41
|
+
Tries to update all used translations every 10 minutes (using ETag and :cache), can be stopped via `I18n.backend.stop_polling`.<br/>
|
42
|
+
If a :cache is given, all backends pick one master to do the polling, all others refresh from :cache
|
43
|
+
|
44
|
+
```Ruby
|
45
|
+
I18n.backend = MyBackend.new(:polling_interval => 30.minutes, :cache => Rails.cache)
|
46
|
+
|
47
|
+
I18n.t('some.key') == "Old value"
|
48
|
+
# change in backend + wait 30 minutes
|
49
|
+
I18n.t('some.key') == "New value"
|
50
|
+
```
|
51
|
+
|
52
|
+
### :cache
|
53
|
+
If you pass `:cache => Rails.cache`, translations will be loaded from cache and updated in the cache.<br/>
|
54
|
+
The cache **MUST** support :unless_exist, so [gem_of_thrones](https://github.com/grosser/gem_of_thrones) can do its job,<br/>
|
55
|
+
MemCacheStore + LibMemCacheStore + ActiveSupport::Cache::MemoryStore (edge) work.
|
56
|
+
|
57
|
+
### Exceptions
|
58
|
+
To handle http exceptions provide e.g. `:exception_handler => lambda{|e| puts e }` (prints to stderr by default).
|
59
|
+
|
60
|
+
### Limited memory cache
|
61
|
+
The backend stores the 10 least recently used locales in memory, if you want to mess with this `:memory_cache_size => 100`
|
62
|
+
|
63
|
+
### Fallback
|
64
|
+
If the http backend is down, it does not translate, but also does not constantly try to query -> your app is untranslated but not down.</br>
|
65
|
+
You should either use :default for all I18n.t or use a Chain, so when http is down e.g. english is used.
|
66
|
+
|
67
|
+
```Ruby
|
68
|
+
I18n.backend = I18n::Backend::Chain.new(
|
69
|
+
MyBackend.new(options),
|
70
|
+
I18n::Backend::Simple.new
|
71
|
+
)
|
72
|
+
```
|
73
|
+
|
74
|
+
TODO
|
75
|
+
====
|
76
|
+
- available_locales is not implemented, since we did not need it
|
77
|
+
- `reload` -> all caches should be cleared
|
78
|
+
|
79
|
+
Author
|
80
|
+
======
|
81
|
+
[Michael Grosser](http://grosser.it)<br/>
|
82
|
+
michael@grosser.it<br/>
|
83
|
+
License: MIT<br/>
|
84
|
+
[](http://travis-ci.org/grosser/i18n-backend-http)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/mgrosser/code/tools/i18n-backend-http
|
3
|
+
specs:
|
4
|
+
i18n-backend-http (0.0.1)
|
5
|
+
faraday
|
6
|
+
gem_of_thrones
|
7
|
+
i18n
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
addressable (2.2.8)
|
13
|
+
appraisal (0.4.1)
|
14
|
+
bundler
|
15
|
+
rake
|
16
|
+
crack (0.3.1)
|
17
|
+
faraday (0.8.0)
|
18
|
+
multipart-post (~> 1.1)
|
19
|
+
gem_of_thrones (0.2.1)
|
20
|
+
i18n (0.4.2)
|
21
|
+
json (1.7.1)
|
22
|
+
metaclass (0.0.1)
|
23
|
+
mocha (0.11.4)
|
24
|
+
metaclass (~> 0.0.1)
|
25
|
+
multipart-post (1.1.5)
|
26
|
+
mynyml-redgreen (0.7.1)
|
27
|
+
term-ansicolor (>= 1.0.4)
|
28
|
+
rake (0.9.2.2)
|
29
|
+
shoulda (3.0.1)
|
30
|
+
shoulda-context (~> 1.0.0)
|
31
|
+
shoulda-matchers (~> 1.0.0)
|
32
|
+
shoulda-context (1.0.0)
|
33
|
+
shoulda-matchers (1.0.0)
|
34
|
+
term-ansicolor (1.0.7)
|
35
|
+
vcr (2.1.1)
|
36
|
+
webmock (1.8.7)
|
37
|
+
addressable (>= 2.2.7)
|
38
|
+
crack (>= 0.1.7)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
appraisal
|
45
|
+
i18n (~> 0.4.0)
|
46
|
+
i18n-backend-http!
|
47
|
+
json
|
48
|
+
mocha
|
49
|
+
mynyml-redgreen
|
50
|
+
rake
|
51
|
+
shoulda
|
52
|
+
vcr
|
53
|
+
webmock
|
@@ -0,0 +1,53 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/mgrosser/code/tools/i18n-backend-http
|
3
|
+
specs:
|
4
|
+
i18n-backend-http (0.0.1)
|
5
|
+
faraday
|
6
|
+
gem_of_thrones
|
7
|
+
i18n
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
addressable (2.2.8)
|
13
|
+
appraisal (0.4.1)
|
14
|
+
bundler
|
15
|
+
rake
|
16
|
+
crack (0.3.1)
|
17
|
+
faraday (0.8.0)
|
18
|
+
multipart-post (~> 1.1)
|
19
|
+
gem_of_thrones (0.2.1)
|
20
|
+
i18n (0.6.0)
|
21
|
+
json (1.7.1)
|
22
|
+
metaclass (0.0.1)
|
23
|
+
mocha (0.11.4)
|
24
|
+
metaclass (~> 0.0.1)
|
25
|
+
multipart-post (1.1.5)
|
26
|
+
mynyml-redgreen (0.7.1)
|
27
|
+
term-ansicolor (>= 1.0.4)
|
28
|
+
rake (0.9.2.2)
|
29
|
+
shoulda (3.0.1)
|
30
|
+
shoulda-context (~> 1.0.0)
|
31
|
+
shoulda-matchers (~> 1.0.0)
|
32
|
+
shoulda-context (1.0.0)
|
33
|
+
shoulda-matchers (1.0.0)
|
34
|
+
term-ansicolor (1.0.7)
|
35
|
+
vcr (2.1.1)
|
36
|
+
webmock (1.8.7)
|
37
|
+
addressable (>= 2.2.7)
|
38
|
+
crack (>= 0.1.7)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
appraisal
|
45
|
+
i18n
|
46
|
+
i18n-backend-http!
|
47
|
+
json
|
48
|
+
mocha
|
49
|
+
mynyml-redgreen
|
50
|
+
rake
|
51
|
+
shoulda
|
52
|
+
vcr
|
53
|
+
webmock
|
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
name = "i18n-backend-http"
|
3
|
+
require "i18n/backend/http/version"
|
4
|
+
|
5
|
+
Gem::Specification.new(name, I18n::Backend::Http::VERSION) do |s|
|
6
|
+
s.summary = "Rails I18n Backend for Http APIs with etag-aware background polling and memory+[memcache] caching"
|
7
|
+
s.authors = ["Michael Grosser"]
|
8
|
+
s.email = "michael@grosser.it"
|
9
|
+
s.homepage = "http://github.com/grosser/#{name}"
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.license = 'MIT'
|
12
|
+
s.add_runtime_dependency "i18n"
|
13
|
+
s.add_runtime_dependency "gem_of_thrones"
|
14
|
+
s.add_runtime_dependency "faraday"
|
15
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
require 'i18n/backend/transliterator'
|
3
|
+
require 'i18n/backend/base'
|
4
|
+
require 'gem_of_thrones'
|
5
|
+
require 'i18n/backend/http/version'
|
6
|
+
require 'i18n/backend/http/etag_http_client'
|
7
|
+
require 'i18n/backend/http/null_cache'
|
8
|
+
require 'i18n/backend/http/lru_cache'
|
9
|
+
|
10
|
+
module I18n
|
11
|
+
module Backend
|
12
|
+
class Http
|
13
|
+
include ::I18n::Backend::Base
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
@options = {
|
17
|
+
:http_open_timeout => 1,
|
18
|
+
:http_read_timeout => 1,
|
19
|
+
:polling_interval => 10*60,
|
20
|
+
:cache => NullCache.new,
|
21
|
+
:poll => true,
|
22
|
+
:exception_handler => lambda{|e| $stderr.puts e },
|
23
|
+
:memory_cache_size => 10,
|
24
|
+
}.merge(options)
|
25
|
+
|
26
|
+
@http_client = EtagHttpClient.new(@options)
|
27
|
+
@translations = LRUCache.new(@options[:memory_cache_size])
|
28
|
+
start_polling if @options[:poll]
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop_polling
|
32
|
+
@stop_polling = true
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def start_polling
|
38
|
+
Thread.new do
|
39
|
+
until @stop_polling
|
40
|
+
sleep(@options[:polling_interval])
|
41
|
+
update_caches
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def lookup(locale, key, scope = [], options = {})
|
47
|
+
key = ::I18n.normalize_keys(locale, key, scope, options[:separator])[1..-1].join('.')
|
48
|
+
translations(locale)[key]
|
49
|
+
end
|
50
|
+
|
51
|
+
def translations(locale)
|
52
|
+
@translations[locale] ||= (
|
53
|
+
translations_from_cache(locale) ||
|
54
|
+
download_and_cache_translations(locale)
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_caches
|
59
|
+
@translations.keys.each do |locale|
|
60
|
+
if @options[:cache].is_a?(NullCache)
|
61
|
+
download_and_cache_translations(locale)
|
62
|
+
else
|
63
|
+
locked_update_cache(locale)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def locked_update_cache(locale)
|
69
|
+
@aspirants ||= {}
|
70
|
+
aspirant = @aspirants[locale] ||= GemOfThrones.new(
|
71
|
+
:cache => @options[:cache],
|
72
|
+
:timeout => (@options[:polling_interval] * 3).ceil,
|
73
|
+
:cache_key => "i18n/backend/http/locked_update_caches/#{locale}"
|
74
|
+
)
|
75
|
+
if aspirant.rise_to_power
|
76
|
+
download_and_cache_translations(locale)
|
77
|
+
else
|
78
|
+
update_memory_cache_from_cache(locale)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def update_memory_cache_from_cache(locale)
|
83
|
+
@translations[locale] = translations_from_cache(locale)
|
84
|
+
end
|
85
|
+
|
86
|
+
def translations_from_cache(locale)
|
87
|
+
@options[:cache].read(cache_key(locale))
|
88
|
+
end
|
89
|
+
|
90
|
+
def cache_key(locale)
|
91
|
+
"i18n/backend/http/translations/#{locale}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def download_and_cache_translations(locale)
|
95
|
+
@http_client.download(path(locale)) do |result|
|
96
|
+
translations = parse_response(result)
|
97
|
+
@options[:cache].write(cache_key(locale), translations)
|
98
|
+
@translations[locale] = translations
|
99
|
+
end
|
100
|
+
rescue => e
|
101
|
+
@options[:exception_handler].call(e)
|
102
|
+
@translations[locale] = {} # do not write distributed cache
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_response(body)
|
106
|
+
raise "implement parse_response"
|
107
|
+
end
|
108
|
+
|
109
|
+
def path(locale)
|
110
|
+
raise "implement path"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|