i18n-backend-jargon 0.2.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 23ce2a5f4c297d66c0132207381587951f95399c
4
+ data.tar.gz: 6262386043b006bebfcbde5cb7dab809ad736b3f
5
+ SHA512:
6
+ metadata.gz: cc9fbb888f0c66417878cdae746ca71592f45530a3abc63f158075fc91b66da1636515492cde7c3476b910c38f7da8ad9588e5a16ceb806369acfa49b160984a
7
+ data.tar.gz: e43273ddbd12a2d59aba075473976bde7abaaf610b2698f9312ef8037618ed7eb0006bd24f26eaf75a9d01f57b88c7cf960150240de04f4b3324a2bf0664bad6
@@ -0,0 +1,202 @@
1
+ require 'i18n'
2
+ require 'i18n/backend/transliterator'
3
+ require 'i18n/backend/base'
4
+ require 'gem_of_thrones'
5
+ require 'i18n/backend/jargon/version'
6
+ require 'i18n/backend/jargon/etag_http_client'
7
+ require 'i18n/backend/jargon/null_cache'
8
+
9
+ module I18n
10
+ module Backend
11
+ class Jargon
12
+ include ::I18n::Backend::Base
13
+
14
+ def initialize(options)
15
+ @config = {
16
+ host: 'http://localhost',
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
+ raise ArgumentError if @config[:uuid].nil?
26
+ end
27
+
28
+ def initialized?
29
+ @initialized ||= false
30
+ end
31
+
32
+ def stop_polling
33
+ @stop_polling = true
34
+ end
35
+
36
+ def reload!
37
+ @initialized = false
38
+ @translations = nil
39
+ end
40
+
41
+ def available_locales
42
+ @available_locales ||= download_available_locales
43
+ end
44
+
45
+ def locale_path(locale)
46
+ localization_path + "/#{locale}"
47
+ end
48
+
49
+ def localization_path
50
+ "api/uuid/#{@config[:uuid]}"
51
+ end
52
+
53
+ def translate(locale, key, options = {})
54
+ raise InvalidLocale.new(locale) unless locale
55
+ entry = key && lookup(locale, key, options[:scope], options)
56
+
57
+ if options.empty?
58
+ entry = resolve(locale, key, entry, options)
59
+ else
60
+ count, default = options.values_at(:count, :default)
61
+ values = options.except(*RESERVED_KEYS)
62
+ entry = entry.nil? && default ?
63
+ default(locale, key, default, options) : resolve(locale, key, entry, options)
64
+ end
65
+
66
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options)) if entry.nil?
67
+ entry = entry.dup if entry.is_a?(String)
68
+
69
+ entry = pluralize(locale, entry, count) if count
70
+ entry = interpolate(locale, entry, values) if values
71
+ entry
72
+ end
73
+
74
+ protected
75
+
76
+ def download_available_locales
77
+ init_translations unless initialized?
78
+ download_localization
79
+ end
80
+
81
+ def init_translations
82
+ @http_client = EtagHttpClient.new(@config)
83
+ @translations = {}
84
+ start_polling if @config[:poll]
85
+ @initialized = true
86
+ end
87
+
88
+ def start_polling
89
+ Thread.new do
90
+ until @stop_polling
91
+ sleep(@config[:polling_interval])
92
+ update_caches
93
+ end
94
+ end
95
+ end
96
+
97
+ def lookup(locale, key, scope = [], options = {})
98
+ init_translations unless initialized?
99
+ key = ::I18n.normalize_keys(locale, key, scope, options[:separator])[1..-1].join('.')
100
+ lookup_key translations(locale), key
101
+ end
102
+
103
+ def resolve(locale, object, subject, options = {})
104
+ return subject if options[:resolve] == false
105
+ result = catch(:exception) do
106
+ case subject
107
+ when Symbol
108
+ I18n.translate(subject, options.merge(:locale => locale, :throw => true))
109
+ when Proc
110
+ date_or_time = options.delete(:object) || object
111
+ resolve(locale, object, subject.call(date_or_time, options))
112
+ else
113
+ subject
114
+ end
115
+ end
116
+ result unless result.is_a?(MissingTranslation)
117
+ end
118
+
119
+ def translations(locale)
120
+ translations_from_cache(locale)
121
+ @translations[locale]
122
+ end
123
+
124
+ def update_caches
125
+ @translations.keys.each do |locale|
126
+ if @config[:cache].is_a?(NullCache)
127
+ download_and_cache_translations(locale)
128
+ else
129
+ locked_update_cache(locale)
130
+ end
131
+ end
132
+ end
133
+
134
+ def locked_update_cache(locale)
135
+ @aspirants ||= {}
136
+ aspirant = @aspirants[locale] ||= GemOfThrones.new(
137
+ :cache => @config[:cache],
138
+ :timeout => (@config[:polling_interval] * 3).ceil,
139
+ :cache_key => "i18n/backend/http/locked_update_caches/#{locale}"
140
+ )
141
+ if aspirant.rise_to_power
142
+ download_and_cache_translations(locale)
143
+ else
144
+ update_memory_cache_from_cache(locale)
145
+ end
146
+ end
147
+
148
+ def update_memory_cache_from_cache(locale)
149
+ @translations[locale] = translations_from_cache(locale)
150
+ end
151
+
152
+ def translations_from_cache(locale)
153
+ translations = @config[:cache].read(cache_key(locale))
154
+ if translations.nil?
155
+ download_and_cache_translations(locale)
156
+ else
157
+ @translations[locale] = translations
158
+ end
159
+ end
160
+
161
+ def cache_key(locale)
162
+ "i18n/backend/http/translations/#{locale}"
163
+ end
164
+
165
+ def download_and_cache_translations(locale)
166
+ @http_client.download(locale_path(locale)) do |result|
167
+ translations = parse_locale(result)
168
+ @config[:cache].write(cache_key(locale), translations)
169
+ @translations[locale] = translations
170
+ end
171
+ rescue => e
172
+ @config[:exception_handler].call(e)
173
+ @translations[locale] = {} # do not write distributed cache
174
+ end
175
+
176
+ def download_localization
177
+ @http_client.download(localization_path) do |result|
178
+ parse_localization(result)
179
+ end
180
+ end
181
+
182
+ def parse_locale(body)
183
+ j = JSON.parse(JSON.parse(body)['locale']['json'])
184
+ flat_hash(j).map{ |k,v| [k.sub(/\.$/, ''), v] }.to_h
185
+ end
186
+
187
+ def parse_localization(body)
188
+ JSON.load(body)['localization']['available_locales']
189
+ end
190
+
191
+ def flat_hash(hash, k = '')
192
+ return {k => hash} unless hash.is_a?(Hash)
193
+ hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k +v[0].to_s + '.') }
194
+ end
195
+
196
+ # hook for extension with other resolution method
197
+ def lookup_key(translations, key)
198
+ translations[key]
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,33 @@
1
+ require 'faraday'
2
+
3
+ module I18n
4
+ module Backend
5
+ class Jargon
6
+ class EtagHttpClient
7
+ def initialize(options)
8
+ @options = options
9
+ @etags = {}
10
+ end
11
+
12
+ def download(path)
13
+ @client ||= Faraday.new(@options[:host])
14
+ response = @client.get(path) do |request|
15
+ request.headers["If-None-Match"] = @etags[path] if @etags[path]
16
+ request.headers["Accept"] = 'application/json'
17
+ request.options[:timeout] = @options[:http_read_timeout]
18
+ request.options[:open_timeout] = @options[:http_open_timeout]
19
+ end
20
+
21
+ @etags[path] = response['ETag']
22
+
23
+ case response.status
24
+ when 200 then yield response.body
25
+ when 304
26
+ else
27
+ raise "Failed request: #{response.inspect}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ module I18n
2
+ module Backend
3
+ class Jargon
4
+ class NullCache
5
+ def fetch(*args)
6
+ yield
7
+ end
8
+
9
+ def read(key)
10
+ end
11
+
12
+ def write(key, value)
13
+ value
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module I18n
2
+ module Backend
3
+ class Jargon
4
+ VERSION = Version = "0.2.3"
5
+ end
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: i18n-backend-jargon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.3
5
+ platform: ruby
6
+ authors:
7
+ - Michael Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: i18n
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: gem_of_thrones
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email: michael@grosser.it
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/i18n/backend/jargon.rb
62
+ - lib/i18n/backend/jargon/etag_http_client.rb
63
+ - lib/i18n/backend/jargon/null_cache.rb
64
+ - lib/i18n/backend/jargon/version.rb
65
+ homepage: http://github.com/grosser/
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.2.2
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Rails I18n Backend for Http APIs with etag-aware background polling and memory+[memcache]
89
+ caching
90
+ test_files: []