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 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