copy_tuner_client 0.0.1
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/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Appraisals +15 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +161 -0
- data/README.md +4 -0
- data/Rakefile +28 -0
- data/copy_tuner_client.gemspec +33 -0
- data/features/rails.feature +270 -0
- data/features/step_definitions/copycopter_server_steps.rb +64 -0
- data/features/step_definitions/rails_steps.rb +172 -0
- data/features/support/env.rb +11 -0
- data/features/support/rails_server.rb +124 -0
- data/gemfiles/2.3.gemfile +7 -0
- data/gemfiles/2.3.gemfile.lock +105 -0
- data/gemfiles/3.0.gemfile +7 -0
- data/gemfiles/3.0.gemfile.lock +147 -0
- data/gemfiles/3.1.gemfile +11 -0
- data/gemfiles/3.1.gemfile.lock +191 -0
- data/init.rb +1 -0
- data/lib/copy_tuner_client/cache.rb +144 -0
- data/lib/copy_tuner_client/client.rb +136 -0
- data/lib/copy_tuner_client/configuration.rb +224 -0
- data/lib/copy_tuner_client/errors.rb +12 -0
- data/lib/copy_tuner_client/i18n_backend.rb +92 -0
- data/lib/copy_tuner_client/poller.rb +44 -0
- data/lib/copy_tuner_client/prefixed_logger.rb +45 -0
- data/lib/copy_tuner_client/process_guard.rb +92 -0
- data/lib/copy_tuner_client/rails.rb +21 -0
- data/lib/copy_tuner_client/railtie.rb +12 -0
- data/lib/copy_tuner_client/request_sync.rb +39 -0
- data/lib/copy_tuner_client/version.rb +7 -0
- data/lib/copy_tuner_client.rb +75 -0
- data/lib/tasks/copy_tuner_client_tasks.rake +20 -0
- data/spec/copy_tuner_client/cache_spec.rb +273 -0
- data/spec/copy_tuner_client/client_spec.rb +236 -0
- data/spec/copy_tuner_client/configuration_spec.rb +305 -0
- data/spec/copy_tuner_client/i18n_backend_spec.rb +157 -0
- data/spec/copy_tuner_client/poller_spec.rb +108 -0
- data/spec/copy_tuner_client/prefixed_logger_spec.rb +37 -0
- data/spec/copy_tuner_client/process_guard_spec.rb +118 -0
- data/spec/copy_tuner_client/request_sync_spec.rb +47 -0
- data/spec/copy_tuner_client_spec.rb +19 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/client_spec_helpers.rb +8 -0
- data/spec/support/defines_constants.rb +44 -0
- data/spec/support/fake_client.rb +53 -0
- data/spec/support/fake_copy_tuner_app.rb +175 -0
- data/spec/support/fake_html_safe_string.rb +20 -0
- data/spec/support/fake_logger.rb +68 -0
- data/spec/support/fake_passenger.rb +27 -0
- data/spec/support/fake_resque_job.rb +18 -0
- data/spec/support/fake_unicorn.rb +13 -0
- data/spec/support/middleware_stack.rb +13 -0
- data/spec/support/writing_cache.rb +17 -0
- data/tmp/projects.json +1 -0
- metadata +389 -0
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'copy_tuner_client/errors'
|
4
|
+
|
5
|
+
module CopyTunerClient
|
6
|
+
# Communicates with the CopyTuner server. This class is used to actually
|
7
|
+
# download and upload blurbs, as well as issuing deploys.
|
8
|
+
#
|
9
|
+
# A client is usually instantiated when {Configuration#apply} is called, and
|
10
|
+
# the application will not need to interact with it directly.
|
11
|
+
class Client
|
12
|
+
# These errors will be rescued when connecting CopyTuner.
|
13
|
+
HTTP_ERRORS = [Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
14
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
15
|
+
Net::ProtocolError, SocketError, OpenSSL::SSL::SSLError,
|
16
|
+
Errno::ECONNREFUSED]
|
17
|
+
|
18
|
+
# Usually instantiated from {Configuration#apply}. Copies options.
|
19
|
+
# @param options [Hash]
|
20
|
+
# @option options [String] :api_key API key of the project to connect to
|
21
|
+
# @option options [Fixnum] :port the port to connect to
|
22
|
+
# @option options [Boolean] :public whether to download draft or published content
|
23
|
+
# @option options [Fixnum] :http_read_timeout how long to wait before timing out when reading data from the socket
|
24
|
+
# @option options [Fixnum] :http_open_timeout how long to wait before timing out when opening the socket
|
25
|
+
# @option options [Boolean] :secure whether to use SSL
|
26
|
+
# @option options [Logger] :logger where to log transactions
|
27
|
+
# @option options [String] :ca_file path to root certificate file for ssl verification
|
28
|
+
def initialize(options)
|
29
|
+
[:api_key, :host, :port, :public, :http_read_timeout,
|
30
|
+
:http_open_timeout, :secure, :logger, :ca_file].each do |option|
|
31
|
+
instance_variable_set "@#{option}", options[option]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Downloads all blurbs for the given api_key.
|
36
|
+
#
|
37
|
+
# If the +public+ option was set to +true+, this will use published blurbs.
|
38
|
+
# Otherwise, draft content is fetched.
|
39
|
+
#
|
40
|
+
# The client tracks ETags between download requests, and will return
|
41
|
+
# without yielding anything if the server returns a not modified response.
|
42
|
+
#
|
43
|
+
# @yield [Hash] downloaded blurbs
|
44
|
+
# @raise [ConnectionError] if the connection fails
|
45
|
+
def download
|
46
|
+
connect do |http|
|
47
|
+
request = Net::HTTP::Get.new(uri(download_resource))
|
48
|
+
request['If-None-Match'] = @etag
|
49
|
+
response = http.request(request)
|
50
|
+
|
51
|
+
if check response
|
52
|
+
log 'Downloaded translations'
|
53
|
+
yield JSON.parse(response.body)
|
54
|
+
else
|
55
|
+
log 'No new translations'
|
56
|
+
end
|
57
|
+
|
58
|
+
@etag = response['ETag']
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Uploads the given hash of blurbs as draft content.
|
63
|
+
# @param data [Hash] the blurbs to upload
|
64
|
+
# @raise [ConnectionError] if the connection fails
|
65
|
+
def upload(data)
|
66
|
+
connect do |http|
|
67
|
+
response = http.post(uri('draft_blurbs'), data.to_json, 'Content-Type' => 'application/json')
|
68
|
+
check response
|
69
|
+
log 'Uploaded missing translations'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Issues a deploy, marking all draft content as published for this project.
|
74
|
+
# @raise [ConnectionError] if the connection fails
|
75
|
+
def deploy
|
76
|
+
connect do |http|
|
77
|
+
response = http.post(uri('deploys'), '')
|
78
|
+
check response
|
79
|
+
log 'Deployed'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
attr_reader :host, :port, :api_key, :http_read_timeout,
|
86
|
+
:http_open_timeout, :secure, :logger, :ca_file
|
87
|
+
|
88
|
+
def public?
|
89
|
+
@public
|
90
|
+
end
|
91
|
+
|
92
|
+
def uri(resource)
|
93
|
+
"/api/v2/projects/#{api_key}/#{resource}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def download_resource
|
97
|
+
if public?
|
98
|
+
'published_blurbs'
|
99
|
+
else
|
100
|
+
'draft_blurbs'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def connect
|
105
|
+
http = Net::HTTP.new(host, port)
|
106
|
+
http.open_timeout = http_open_timeout
|
107
|
+
http.read_timeout = http_read_timeout
|
108
|
+
http.use_ssl = secure
|
109
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
110
|
+
http.ca_file = ca_file
|
111
|
+
|
112
|
+
begin
|
113
|
+
yield http
|
114
|
+
rescue *HTTP_ERRORS => exception
|
115
|
+
raise ConnectionError, "#{exception.class.name}: #{exception.message}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def check(response)
|
120
|
+
case response
|
121
|
+
when Net::HTTPNotFound
|
122
|
+
raise InvalidApiKey, "Invalid API key: #{api_key}"
|
123
|
+
when Net::HTTPNotModified
|
124
|
+
false
|
125
|
+
when Net::HTTPSuccess
|
126
|
+
true
|
127
|
+
else
|
128
|
+
raise ConnectionError, "#{response.code}: #{response.body}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def log(message)
|
133
|
+
logger.info message
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'copy_tuner_client/i18n_backend'
|
3
|
+
require 'copy_tuner_client/client'
|
4
|
+
require 'copy_tuner_client/cache'
|
5
|
+
require 'copy_tuner_client/process_guard'
|
6
|
+
require 'copy_tuner_client/poller'
|
7
|
+
require 'copy_tuner_client/prefixed_logger'
|
8
|
+
require 'copy_tuner_client/request_sync'
|
9
|
+
|
10
|
+
module CopyTunerClient
|
11
|
+
# Used to set up and modify settings for the client.
|
12
|
+
class Configuration
|
13
|
+
|
14
|
+
# These options will be present in the Hash returned by {#to_hash}.
|
15
|
+
OPTIONS = [:api_key, :development_environments, :environment_name, :host,
|
16
|
+
:http_open_timeout, :http_read_timeout, :client_name, :client_url,
|
17
|
+
:client_version, :port, :protocol, :proxy_host, :proxy_pass,
|
18
|
+
:proxy_port, :proxy_user, :secure, :polling_delay, :logger,
|
19
|
+
:framework, :middleware, :ca_file].freeze
|
20
|
+
|
21
|
+
# @return [String] The API key for your project, found on the project edit form.
|
22
|
+
attr_accessor :api_key
|
23
|
+
|
24
|
+
# @return [String] The host to connect to (defaults to +copy-tuner.com+).
|
25
|
+
attr_accessor :host
|
26
|
+
|
27
|
+
# @return [Fixnum] The port on which your CopyTuner server runs (defaults to +443+ for secure connections, +80+ for insecure connections).
|
28
|
+
attr_accessor :port
|
29
|
+
|
30
|
+
# @return [Boolean] +true+ for https connections, +false+ for http connections.
|
31
|
+
attr_accessor :secure
|
32
|
+
|
33
|
+
# @return [Fixnum] The HTTP open timeout in seconds (defaults to +2+).
|
34
|
+
attr_accessor :http_open_timeout
|
35
|
+
|
36
|
+
# @return [Fixnum] The HTTP read timeout in seconds (defaults to +5+).
|
37
|
+
attr_accessor :http_read_timeout
|
38
|
+
|
39
|
+
# @return [String, NilClass] The hostname of your proxy server (if using a proxy)
|
40
|
+
attr_accessor :proxy_host
|
41
|
+
|
42
|
+
# @return [String, Fixnum] The port of your proxy server (if using a proxy)
|
43
|
+
attr_accessor :proxy_port
|
44
|
+
|
45
|
+
# @return [String, NilClass] The username to use when logging into your proxy server (if using a proxy)
|
46
|
+
attr_accessor :proxy_user
|
47
|
+
|
48
|
+
# @return [String, NilClass] The password to use when logging into your proxy server (if using a proxy)
|
49
|
+
attr_accessor :proxy_pass
|
50
|
+
|
51
|
+
# @return [Array<String>] A list of environments in which content should be editable
|
52
|
+
attr_accessor :development_environments
|
53
|
+
|
54
|
+
# @return [Array<String>] A list of environments in which the server should not be contacted
|
55
|
+
attr_accessor :test_environments
|
56
|
+
|
57
|
+
# @return [String] The name of the environment the application is running in
|
58
|
+
attr_accessor :environment_name
|
59
|
+
|
60
|
+
# @return [String] The name of the client library being used to send notifications (defaults to +CopyTuner Client+)
|
61
|
+
attr_accessor :client_name
|
62
|
+
|
63
|
+
# @return [String, NilClass] The framework notifications are being sent from, if any (such as +Rails 2.3.9+)
|
64
|
+
attr_accessor :framework
|
65
|
+
|
66
|
+
# @return [String] The version of the client library being used to send notifications (such as +1.0.2+)
|
67
|
+
attr_accessor :client_version
|
68
|
+
|
69
|
+
# @return [String] The url of the client library being used
|
70
|
+
attr_accessor :client_url
|
71
|
+
|
72
|
+
# @return [Integer] The time, in seconds, in between each sync to the server. Defaults to +300+.
|
73
|
+
attr_accessor :polling_delay
|
74
|
+
|
75
|
+
# @return [Logger] Where to log messages. Must respond to same interface as Logger.
|
76
|
+
attr_reader :logger
|
77
|
+
|
78
|
+
# @return the middleware stack, if any, which should respond to +use+
|
79
|
+
attr_accessor :middleware
|
80
|
+
|
81
|
+
# @return [String] the path to a root certificate file used to verify ssl sessions. Default's to the root certificate file for copy-tuner.com.
|
82
|
+
attr_accessor :ca_file
|
83
|
+
|
84
|
+
# @return [Cache] instance used internally to synchronize changes.
|
85
|
+
attr_accessor :cache
|
86
|
+
|
87
|
+
# @return [Client] instance used to communicate with a CopyTuner Server.
|
88
|
+
attr_accessor :client
|
89
|
+
|
90
|
+
alias_method :secure?, :secure
|
91
|
+
|
92
|
+
# Instantiated from {CopyTunerClient.configure}. Sets defaults.
|
93
|
+
def initialize
|
94
|
+
self.client_name = 'CopyTuner Client'
|
95
|
+
self.client_url = 'https://rubygems.org/gems/copy_tuner_client'
|
96
|
+
self.client_version = VERSION
|
97
|
+
self.development_environments = %w(development staging)
|
98
|
+
self.host = 'copy-tuner.com'
|
99
|
+
self.http_open_timeout = 2
|
100
|
+
self.http_read_timeout = 5
|
101
|
+
self.logger = Logger.new($stdout)
|
102
|
+
self.polling_delay = 300
|
103
|
+
self.secure = false
|
104
|
+
self.test_environments = %w(test cucumber)
|
105
|
+
@applied = false
|
106
|
+
end
|
107
|
+
|
108
|
+
# Allows config options to be read like a hash
|
109
|
+
#
|
110
|
+
# @param [Symbol] option Key for a given attribute
|
111
|
+
# @return [Object] the given attribute
|
112
|
+
def [](option)
|
113
|
+
send(option)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a hash of all configurable options
|
117
|
+
# @return [Hash] configuration attributes
|
118
|
+
def to_hash
|
119
|
+
base_options = { :public => public? }
|
120
|
+
|
121
|
+
OPTIONS.inject(base_options) do |hash, option|
|
122
|
+
hash.merge option.to_sym => send(option)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns a hash of all configurable options merged with +hash+
|
127
|
+
#
|
128
|
+
# @param [Hash] hash A set of configuration options that will take precedence over the defaults
|
129
|
+
# @return [Hash] the merged configuration hash
|
130
|
+
def merge(hash)
|
131
|
+
to_hash.merge hash
|
132
|
+
end
|
133
|
+
|
134
|
+
# Determines if the published or draft content will be used
|
135
|
+
# @return [Boolean] Returns +false+ if in a development or test
|
136
|
+
# environment, +true+ otherwise.
|
137
|
+
def public?
|
138
|
+
!(development_environments + test_environments).include?(environment_name)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Determines if the content will be editable
|
142
|
+
# @return [Boolean] Returns +true+ if in a development environment, +false+ otherwise.
|
143
|
+
def development?
|
144
|
+
development_environments.include? environment_name
|
145
|
+
end
|
146
|
+
|
147
|
+
# Determines if the content will fetched from the server
|
148
|
+
# @return [Boolean] Returns +true+ if in a test environment, +false+ otherwise.
|
149
|
+
def test?
|
150
|
+
test_environments.include? environment_name
|
151
|
+
end
|
152
|
+
|
153
|
+
# Determines if the configuration has been applied (internal)
|
154
|
+
# @return [Boolean] Returns +true+ if applied, +false+ otherwise.
|
155
|
+
def applied?
|
156
|
+
@applied
|
157
|
+
end
|
158
|
+
|
159
|
+
# Applies the configuration (internal).
|
160
|
+
#
|
161
|
+
# Called automatically when {CopyTunerClient.configure} is called in the application.
|
162
|
+
#
|
163
|
+
# This creates the {I18nBackend} and puts them together.
|
164
|
+
#
|
165
|
+
# When {#test?} returns +false+, the poller will be started.
|
166
|
+
def apply
|
167
|
+
self.client ||= Client.new(to_hash)
|
168
|
+
self.cache ||= Cache.new(client, to_hash)
|
169
|
+
poller = Poller.new(cache, to_hash)
|
170
|
+
process_guard = ProcessGuard.new(cache, poller, to_hash)
|
171
|
+
I18n.backend = I18nBackend.new(cache)
|
172
|
+
|
173
|
+
if middleware && development?
|
174
|
+
middleware.use RequestSync, :cache => cache
|
175
|
+
end
|
176
|
+
|
177
|
+
@applied = true
|
178
|
+
logger.info "Client #{VERSION} ready"
|
179
|
+
logger.info "Environment Info: #{environment_info}"
|
180
|
+
|
181
|
+
unless test?
|
182
|
+
process_guard.start
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def port
|
187
|
+
@port || default_port
|
188
|
+
end
|
189
|
+
|
190
|
+
# The protocol that should be used when generating URLs to CopyTuner.
|
191
|
+
# @return [String] +https+ if {#secure?} returns +true+, +http+ otherwise.
|
192
|
+
def protocol
|
193
|
+
if secure?
|
194
|
+
'https'
|
195
|
+
else
|
196
|
+
'http'
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# For logging/debugging (internal).
|
201
|
+
# @return [String] a description of the environment in which this configuration was built.
|
202
|
+
def environment_info
|
203
|
+
parts = ["Ruby: #{RUBY_VERSION}", framework, "Env: #{environment_name}"]
|
204
|
+
parts.compact.map { |part| "[#{part}]" }.join(" ")
|
205
|
+
end
|
206
|
+
|
207
|
+
# Wraps the given logger in a PrefixedLogger. This way, CopyTunerClient
|
208
|
+
# log messages are recognizable.
|
209
|
+
# @param original_logger [Logger] the upstream logger to use, which must respond to the standard +Logger+ severity methods.
|
210
|
+
def logger=(original_logger)
|
211
|
+
@logger = PrefixedLogger.new("** [CopyTuner]", original_logger)
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def default_port
|
217
|
+
if secure?
|
218
|
+
443
|
219
|
+
else
|
220
|
+
80
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module CopyTunerClient
|
2
|
+
# Raised when an error occurs while contacting the CopyTuner server. This is
|
3
|
+
# raised by {Client} and generally rescued by {Cache}. The application will
|
4
|
+
# not encounter this error. Polling will continue even if this error is raised.
|
5
|
+
class ConnectionError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Raised when the client is configured with an api key that the CopyTuner
|
9
|
+
# server does not recognize. Polling is aborted when this error is raised.
|
10
|
+
class InvalidApiKey < StandardError
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module CopyTunerClient
|
4
|
+
# I18n implementation designed to synchronize with CopyTuner.
|
5
|
+
#
|
6
|
+
# Expects an object that acts like a Hash, responding to +[]+, +[]=+, and +keys+.
|
7
|
+
#
|
8
|
+
# This backend will be used as the default I18n backend when the client is
|
9
|
+
# configured, so you will not need to instantiate this class from the
|
10
|
+
# application. Instead, just use methods on the I18n class.
|
11
|
+
#
|
12
|
+
# This implementation will also load translations from locale files.
|
13
|
+
class I18nBackend
|
14
|
+
include I18n::Backend::Simple::Implementation
|
15
|
+
|
16
|
+
# Usually instantiated when {Configuration#apply} is invoked.
|
17
|
+
# @param cache [Cache] must act like a hash, returning and accept blurbs by key.
|
18
|
+
def initialize(cache)
|
19
|
+
@cache = cache
|
20
|
+
end
|
21
|
+
|
22
|
+
# Translates the given local and key. See the I18n API documentation for details.
|
23
|
+
#
|
24
|
+
# @return [Object] the translated key (usually a String)
|
25
|
+
def translate(locale, key, options = {})
|
26
|
+
content = super(locale, key, options.merge(:fallback => true))
|
27
|
+
if content.respond_to?(:html_safe)
|
28
|
+
content.html_safe
|
29
|
+
else
|
30
|
+
content
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns locales availabile for this CopyTuner project.
|
35
|
+
# @return [Array<String>] available locales
|
36
|
+
def available_locales
|
37
|
+
cached_locales = cache.keys.map { |key| key.split('.').first }
|
38
|
+
(cached_locales + super).uniq.map { |locale| locale.to_sym }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Stores the given translations.
|
42
|
+
#
|
43
|
+
# Updates will be visible in the current process immediately, and will
|
44
|
+
# propagate to CopyTuner during the next flush.
|
45
|
+
#
|
46
|
+
# @param [String] locale the locale (ie "en") to store translations for
|
47
|
+
# @param [Hash] data nested key-value pairs to be added as blurbs
|
48
|
+
# @param [Hash] options unused part of the I18n API
|
49
|
+
def store_translations(locale, data, options = {})
|
50
|
+
super
|
51
|
+
store_item(locale, data)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def lookup(locale, key, scope = [], options = {})
|
57
|
+
parts = I18n.normalize_keys(locale, key, scope, options[:separator])
|
58
|
+
key_with_locale = parts.join('.')
|
59
|
+
content = cache[key_with_locale] || super
|
60
|
+
cache[key_with_locale] = "" if content.nil?
|
61
|
+
content
|
62
|
+
end
|
63
|
+
|
64
|
+
def store_item(locale, data, scope = [])
|
65
|
+
if data.respond_to?(:to_hash)
|
66
|
+
data.to_hash.each do |key, value|
|
67
|
+
store_item(locale, value, scope + [key])
|
68
|
+
end
|
69
|
+
elsif data.respond_to?(:to_str)
|
70
|
+
key = ([locale] + scope).join('.')
|
71
|
+
cache[key] = data.to_str
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def load_translations(*filenames)
|
76
|
+
super
|
77
|
+
cache.wait_for_download
|
78
|
+
end
|
79
|
+
|
80
|
+
def default(locale, object, subject, options = {})
|
81
|
+
content = super(locale, object, subject, options)
|
82
|
+
if content.respond_to?(:to_str)
|
83
|
+
parts = I18n.normalize_keys(locale, object, options[:scope], options[:separator])
|
84
|
+
key = parts.join('.')
|
85
|
+
cache[key] = content.to_str
|
86
|
+
end
|
87
|
+
content
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :cache
|
91
|
+
end
|
92
|
+
end
|