copy_tuner_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|