i18n-backend-http 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ script: "bundle exec rake"
2
+ rvm:
3
+ - ree
4
+ - 1.9.2
5
+ - 1.9.3
data/Appraisals ADDED
@@ -0,0 +1,7 @@
1
+ appraise "rails2" do
2
+ gem 'i18n', '~>0.4.0'
3
+ end
4
+
5
+ appraise "rails3" do
6
+ gem 'i18n'
7
+ end
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ gem 'rake'
5
+ gem 'shoulda'
6
+ gem 'vcr'
7
+ gem 'webmock'
8
+ gem 'mocha'
9
+ gem 'appraisal'
10
+ gem 'mynyml-redgreen'
11
+ gem 'json'
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
+ [![Build Status](https://secure.travis-ci.org/grosser/i18n-backend-http.png)](http://travis-ci.org/grosser/i18n-backend-http)
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source :rubygems
4
+
5
+ gem "rake"
6
+ gem "shoulda"
7
+ gem "vcr"
8
+ gem "webmock"
9
+ gem "mocha"
10
+ gem "appraisal"
11
+ gem "mynyml-redgreen"
12
+ gem "json"
13
+ gem "i18n", "~>0.4.0"
14
+
15
+ gemspec :path=>"../"
@@ -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,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source :rubygems
4
+
5
+ gem "rake"
6
+ gem "shoulda"
7
+ gem "vcr"
8
+ gem "webmock"
9
+ gem "mocha"
10
+ gem "appraisal"
11
+ gem "mynyml-redgreen"
12
+ gem "json"
13
+ gem "i18n"
14
+
15
+ gemspec :path=>"../"
@@ -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