copycopter_client 1.0.0.beta1
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/MIT-LICENSE +20 -0
- data/README.textile +71 -0
- data/Rakefile +38 -0
- data/features/rails.feature +267 -0
- data/features/step_definitions/copycopter_server_steps.rb +65 -0
- data/features/step_definitions/rails_steps.rb +134 -0
- data/features/support/env.rb +8 -0
- data/features/support/rails_server.rb +118 -0
- data/init.rb +2 -0
- data/lib/copycopter_client/client.rb +117 -0
- data/lib/copycopter_client/configuration.rb +197 -0
- data/lib/copycopter_client/errors.rb +13 -0
- data/lib/copycopter_client/helper.rb +40 -0
- data/lib/copycopter_client/i18n_backend.rb +100 -0
- data/lib/copycopter_client/prefixed_logger.rb +41 -0
- data/lib/copycopter_client/rails.rb +31 -0
- data/lib/copycopter_client/railtie.rb +13 -0
- data/lib/copycopter_client/sync.rb +145 -0
- data/lib/copycopter_client/version.rb +8 -0
- data/lib/copycopter_client.rb +58 -0
- data/lib/tasks/copycopter_client_tasks.rake +6 -0
- data/spec/copycopter_client/client_spec.rb +208 -0
- data/spec/copycopter_client/configuration_spec.rb +252 -0
- data/spec/copycopter_client/helper_spec.rb +86 -0
- data/spec/copycopter_client/i18n_backend_spec.rb +133 -0
- data/spec/copycopter_client/prefixed_logger_spec.rb +25 -0
- data/spec/copycopter_client/sync_spec.rb +295 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/client_spec_helpers.rb +9 -0
- data/spec/support/defines_constants.rb +38 -0
- data/spec/support/fake_client.rb +42 -0
- data/spec/support/fake_copycopter_app.rb +136 -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_unicorn.rb +14 -0
- metadata +121 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
# Starts a Rails application server in a fork and waits for it to be responsive
|
4
|
+
class RailsServer
|
5
|
+
HOST = 'localhost'.freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.start(port = nil, debug = nil)
|
12
|
+
self.instance = new(port, debug)
|
13
|
+
self.instance.start
|
14
|
+
self.instance
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.stop
|
18
|
+
self.instance.stop if instance
|
19
|
+
self.instance = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.get(path)
|
23
|
+
self.instance.get(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.post(path, data)
|
27
|
+
self.instance.post(path, data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.run(port, silent)
|
31
|
+
require 'config/environment'
|
32
|
+
require 'thin'
|
33
|
+
|
34
|
+
if Rails::VERSION::MAJOR == 3
|
35
|
+
rails = Rails.application
|
36
|
+
else
|
37
|
+
rails = ActionController::Dispatcher.new
|
38
|
+
end
|
39
|
+
app = Identify.new(rails)
|
40
|
+
|
41
|
+
Thin::Logging.silent = silent
|
42
|
+
Rack::Handler::Thin.run(app, :Port => port, :AccessLog => [])
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.app_host
|
46
|
+
self.instance.app_host
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(port, debug)
|
50
|
+
@port = (port || 3001).to_i
|
51
|
+
@debug = debug
|
52
|
+
end
|
53
|
+
|
54
|
+
def start
|
55
|
+
@pid = fork do
|
56
|
+
command = "ruby -r#{__FILE__} -e 'RailsServer.run(#{@port}, #{(!@debug).inspect})'"
|
57
|
+
puts command if @debug
|
58
|
+
exec(command)
|
59
|
+
end
|
60
|
+
wait_until_responsive
|
61
|
+
end
|
62
|
+
|
63
|
+
def stop
|
64
|
+
if @pid
|
65
|
+
Process.kill('INT', @pid)
|
66
|
+
Process.wait(@pid)
|
67
|
+
@pid = nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def get(path)
|
72
|
+
puts "GET #{path}" if @debug
|
73
|
+
Net::HTTP.start(HOST, @port) { |http| http.get(path) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def post(path, data)
|
77
|
+
puts "POST #{path}\n#{data}" if @debug
|
78
|
+
Net::HTTP.start(HOST, @port) { |http| http.post(path, data) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def wait_until_responsive
|
82
|
+
20.times do
|
83
|
+
if responsive?
|
84
|
+
return true
|
85
|
+
else
|
86
|
+
sleep(0.5)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
raise "Couldn't connect to Rails application server at #{HOST}:#{@port}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def responsive?
|
93
|
+
response = Net::HTTP.start(HOST, @port) { |http| http.get('/__identify__') }
|
94
|
+
response.is_a?(Net::HTTPSuccess)
|
95
|
+
rescue Errno::ECONNREFUSED, Errno::EBADF
|
96
|
+
return false
|
97
|
+
end
|
98
|
+
|
99
|
+
def app_host
|
100
|
+
"http://#{HOST}:#{@port}"
|
101
|
+
end
|
102
|
+
|
103
|
+
# From Capybara::Server
|
104
|
+
|
105
|
+
class Identify
|
106
|
+
def initialize(app)
|
107
|
+
@app = app
|
108
|
+
end
|
109
|
+
|
110
|
+
def call(env)
|
111
|
+
if env["PATH_INFO"] == "/__identify__"
|
112
|
+
[200, {}, 'OK']
|
113
|
+
else
|
114
|
+
@app.call(env)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'copycopter_client/errors'
|
4
|
+
|
5
|
+
module CopycopterClient
|
6
|
+
# Communicates with the Copycopter 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 Copycopter.
|
13
|
+
HTTP_ERRORS = [Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
14
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
15
|
+
Net::ProtocolError]
|
16
|
+
|
17
|
+
# Usually instantiated from {Configuration#apply}. Copies options.
|
18
|
+
# @param options [Hash]
|
19
|
+
# @option options [String] :api_key API key of the project to connect to
|
20
|
+
# @option options [Fixnum] :port the port to connect to
|
21
|
+
# @option options [Boolean] :public whether to download draft or published content
|
22
|
+
# @option options [Fixnum] :http_read_timeout how long to wait before timing out when reading data from the socket
|
23
|
+
# @option options [Fixnum] :http_open_timeout how long to wait before timing out when opening the socket
|
24
|
+
# @option options [Boolean] :secure whether to use SSL
|
25
|
+
# @option options [Logger] :logger where to log transactions
|
26
|
+
def initialize(options)
|
27
|
+
[:api_key, :host, :port, :public, :http_read_timeout,
|
28
|
+
:http_open_timeout, :secure, :logger].each do |option|
|
29
|
+
instance_variable_set("@#{option}", options[option])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Downloads all blurbs for the given api_key.
|
34
|
+
#
|
35
|
+
# If the +public+ option was set to +true+, this will use published blurbs.
|
36
|
+
# Otherwise, draft content is fetched.
|
37
|
+
#
|
38
|
+
# @return [Hash] blurbs
|
39
|
+
# @raise [ConnectionError] if the connection fails
|
40
|
+
def download
|
41
|
+
connect do |http|
|
42
|
+
response = http.get(uri(download_resource))
|
43
|
+
check(response)
|
44
|
+
log("Downloaded translations")
|
45
|
+
JSON.parse(response.body)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Uploads the given hash of blurbs as draft content.
|
50
|
+
# @param data [Hash] the blurbs to upload
|
51
|
+
# @raise [ConnectionError] if the connection fails
|
52
|
+
def upload(data)
|
53
|
+
connect do |http|
|
54
|
+
response = http.post(uri("draft_blurbs"), data.to_json)
|
55
|
+
check(response)
|
56
|
+
log("Uploaded missing translations")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Issues a deploy, marking all draft content as published for this project.
|
61
|
+
# @raise [ConnectionError] if the connection fails
|
62
|
+
def deploy
|
63
|
+
connect do |http|
|
64
|
+
response = http.post(uri("deploys"), "")
|
65
|
+
check(response)
|
66
|
+
log("Deployed")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
attr_reader :host, :port, :api_key, :http_read_timeout,
|
73
|
+
:http_open_timeout, :secure, :logger
|
74
|
+
|
75
|
+
def public?
|
76
|
+
@public
|
77
|
+
end
|
78
|
+
|
79
|
+
def uri(resource)
|
80
|
+
"/api/v2/projects/#{api_key}/#{resource}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def download_resource
|
84
|
+
if public?
|
85
|
+
"published_blurbs"
|
86
|
+
else
|
87
|
+
"draft_blurbs"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def connect
|
92
|
+
http = Net::HTTP.new(host, port)
|
93
|
+
http.open_timeout = http_open_timeout
|
94
|
+
http.read_timeout = http_read_timeout
|
95
|
+
http.use_ssl = secure
|
96
|
+
begin
|
97
|
+
yield(http)
|
98
|
+
rescue *HTTP_ERRORS => exception
|
99
|
+
raise ConnectionError, "#{exception.class.name}: #{exception.message}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def check(response)
|
104
|
+
if Net::HTTPNotFound === response
|
105
|
+
raise InvalidApiKey, "Invalid API key: #{api_key}"
|
106
|
+
end
|
107
|
+
|
108
|
+
unless Net::HTTPSuccess === response
|
109
|
+
raise ConnectionError, "#{response.code}: #{response.body}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def log(message)
|
114
|
+
logger.info(message)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'copycopter_client/i18n_backend'
|
3
|
+
require 'copycopter_client/client'
|
4
|
+
require 'copycopter_client/sync'
|
5
|
+
require 'copycopter_client/prefixed_logger'
|
6
|
+
|
7
|
+
module CopycopterClient
|
8
|
+
# Used to set up and modify settings for the client.
|
9
|
+
class Configuration
|
10
|
+
|
11
|
+
# These options will be present in the Hash returned by {#to_hash}.
|
12
|
+
OPTIONS = [:api_key, :development_environments, :environment_name, :host,
|
13
|
+
:http_open_timeout, :http_read_timeout, :client_name, :client_url,
|
14
|
+
:client_version, :port, :protocol, :proxy_host, :proxy_pass,
|
15
|
+
:proxy_port, :proxy_user, :secure, :polling_delay, :logger,
|
16
|
+
:framework, :fallback_backend].freeze
|
17
|
+
|
18
|
+
# @return [String] The API key for your project, found on the project edit form.
|
19
|
+
attr_accessor :api_key
|
20
|
+
|
21
|
+
# @return [String] The host to connect to (defaults to +copycopter.com+).
|
22
|
+
attr_accessor :host
|
23
|
+
|
24
|
+
# @return [Fixnum] The port on which your Copycopter server runs (defaults to +443+ for secure connections, +80+ for insecure connections).
|
25
|
+
attr_accessor :port
|
26
|
+
|
27
|
+
# @return [Boolean] +true+ for https connections, +false+ for http connections.
|
28
|
+
attr_accessor :secure
|
29
|
+
|
30
|
+
# @return [Fixnum] The HTTP open timeout in seconds (defaults to +2+).
|
31
|
+
attr_accessor :http_open_timeout
|
32
|
+
|
33
|
+
# @return [Fixnum] The HTTP read timeout in seconds (defaults to +5+).
|
34
|
+
attr_accessor :http_read_timeout
|
35
|
+
|
36
|
+
# @return [String, NilClass] The hostname of your proxy server (if using a proxy)
|
37
|
+
attr_accessor :proxy_host
|
38
|
+
|
39
|
+
# @return [String, Fixnum] The port of your proxy server (if using a proxy)
|
40
|
+
attr_accessor :proxy_port
|
41
|
+
|
42
|
+
# @return [String, NilClass] The username to use when logging into your proxy server (if using a proxy)
|
43
|
+
attr_accessor :proxy_user
|
44
|
+
|
45
|
+
# @return [String, NilClass] The password to use when logging into your proxy server (if using a proxy)
|
46
|
+
attr_accessor :proxy_pass
|
47
|
+
|
48
|
+
# @return [Array<String>] A list of environments in which content should be editable
|
49
|
+
attr_accessor :development_environments
|
50
|
+
|
51
|
+
# @return [Array<String>] A list of environments in which the server should not be contacted
|
52
|
+
attr_accessor :test_environments
|
53
|
+
|
54
|
+
# @return [String] The name of the environment the application is running in
|
55
|
+
attr_accessor :environment_name
|
56
|
+
|
57
|
+
# @return [String] The name of the client library being used to send notifications (defaults to +Copycopter Client+)
|
58
|
+
attr_accessor :client_name
|
59
|
+
|
60
|
+
# @return [String, NilClass] The framework notifications are being sent from, if any (such as +Rails 2.3.9+)
|
61
|
+
attr_accessor :framework
|
62
|
+
|
63
|
+
# @return [String] The version of the client library being used to send notifications (such as +1.0.2+)
|
64
|
+
attr_accessor :client_version
|
65
|
+
|
66
|
+
# @return [String] The url of the client library being used
|
67
|
+
attr_accessor :client_url
|
68
|
+
|
69
|
+
# @return [Integer] The time, in seconds, in between each sync to the server. Defaults to +300+.
|
70
|
+
attr_accessor :polling_delay
|
71
|
+
|
72
|
+
# @return [Logger] Where to log messages. Must respond to same interface as Logger.
|
73
|
+
attr_reader :logger
|
74
|
+
|
75
|
+
# @return [I18n::Backend::Base] where to look for translations missing on the Copycopter server
|
76
|
+
attr_accessor :fallback_backend
|
77
|
+
|
78
|
+
alias_method :secure?, :secure
|
79
|
+
|
80
|
+
# Instantiated from {CopycopterClient.configure}. Sets defaults.
|
81
|
+
def initialize
|
82
|
+
self.secure = false
|
83
|
+
self.host = 'copycopter.com'
|
84
|
+
self.http_open_timeout = 2
|
85
|
+
self.http_read_timeout = 5
|
86
|
+
self.development_environments = %w(development staging)
|
87
|
+
self.test_environments = %w(test cucumber)
|
88
|
+
self.client_name = 'Copycopter Client'
|
89
|
+
self.client_version = VERSION
|
90
|
+
self.client_url = 'http://copycopter.com'
|
91
|
+
self.polling_delay = 300
|
92
|
+
self.logger = Logger.new($stdout)
|
93
|
+
|
94
|
+
@applied = false
|
95
|
+
end
|
96
|
+
|
97
|
+
# Allows config options to be read like a hash
|
98
|
+
#
|
99
|
+
# @param [Symbol] option Key for a given attribute
|
100
|
+
# @return [Object] the given attribute
|
101
|
+
def [](option)
|
102
|
+
send(option)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns a hash of all configurable options
|
106
|
+
# @return [Hash] configuration attributes
|
107
|
+
def to_hash
|
108
|
+
base_options = { :public => public? }
|
109
|
+
OPTIONS.inject(base_options) do |hash, option|
|
110
|
+
hash.merge(option.to_sym => send(option))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns a hash of all configurable options merged with +hash+
|
115
|
+
#
|
116
|
+
# @param [Hash] hash A set of configuration options that will take precedence over the defaults
|
117
|
+
# @return [Hash] the merged configuration hash
|
118
|
+
def merge(hash)
|
119
|
+
to_hash.merge(hash)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Determines if the content will be editable
|
123
|
+
# @return [Boolean] Returns +false+ if in a development environment, +true+ otherwise.
|
124
|
+
def public?
|
125
|
+
!(development_environments + test_environments).include?(environment_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Determines if the content will fetched from the server
|
129
|
+
# @return [Boolean] Returns +true+ if in a test environment, +false+ otherwise.
|
130
|
+
def test?
|
131
|
+
test_environments.include?(environment_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Determines if the configuration has been applied (internal)
|
135
|
+
# @return [Boolean] Returns +true+ if applied, +false+ otherwise.
|
136
|
+
def applied?
|
137
|
+
@applied
|
138
|
+
end
|
139
|
+
|
140
|
+
# Applies the configuration (internal).
|
141
|
+
#
|
142
|
+
# Called automatically when {CopycopterClient.configure} is called in the application.
|
143
|
+
#
|
144
|
+
# This creates the {Client}, {Sync}, and {I18nBackend} and puts them together.
|
145
|
+
#
|
146
|
+
# When {#test?} returns +false+, the sync will be started.
|
147
|
+
def apply
|
148
|
+
client = Client.new(to_hash)
|
149
|
+
sync = Sync.new(client, to_hash)
|
150
|
+
I18n.backend = I18nBackend.new(sync, to_hash)
|
151
|
+
CopycopterClient.client = client
|
152
|
+
CopycopterClient.sync = sync
|
153
|
+
@applied = true
|
154
|
+
logger.info("Client #{VERSION} ready")
|
155
|
+
logger.info("Environment Info: #{environment_info}")
|
156
|
+
sync.start unless test?
|
157
|
+
end
|
158
|
+
|
159
|
+
def port
|
160
|
+
@port || default_port
|
161
|
+
end
|
162
|
+
|
163
|
+
# The protocol that should be used when generating URLs to Copycopter.
|
164
|
+
# @return [String] +https+ if {#secure?} returns +true+, +http+ otherwise.
|
165
|
+
def protocol
|
166
|
+
if secure?
|
167
|
+
'https'
|
168
|
+
else
|
169
|
+
'http'
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# For logging/debugging (internal).
|
174
|
+
# @return [String] a description of the environment in which this configuration was built.
|
175
|
+
def environment_info
|
176
|
+
parts = ["Ruby: #{RUBY_VERSION}", framework, "Env: #{environment_name}"]
|
177
|
+
parts.compact.map { |part| "[#{part}]" }.join(" ")
|
178
|
+
end
|
179
|
+
|
180
|
+
# Wraps the given logger in a PrefixedLogger. This way, CopycopterClient
|
181
|
+
# log messages are recognizable.
|
182
|
+
# @param original_logger [Logger] the upstream logger to use, which must respond to the standard +Logger+ severity methods.
|
183
|
+
def logger=(original_logger)
|
184
|
+
@logger = PrefixedLogger.new("** [Copycopter]", original_logger)
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def default_port
|
190
|
+
if secure?
|
191
|
+
443
|
192
|
+
else
|
193
|
+
80
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module CopycopterClient
|
2
|
+
# Raised when an error occurs while contacting the Copycopter server. This is
|
3
|
+
# raised by {Client} and generally rescued by {Sync}. 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 Copycopter
|
9
|
+
# server does not recognize. Polling is aborted when this error is raised.
|
10
|
+
class InvalidApiKey < StandardError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CopycopterClient
|
2
|
+
# Helper methods for Copycopter
|
3
|
+
# @deprecated use +I81n#translate+ instead.
|
4
|
+
module Helper
|
5
|
+
# Returns copy for the given key in the current locale.
|
6
|
+
# @param key [String] the key you want copy for
|
7
|
+
# @param default [String, Hash] an optional default value, used if this key is missing
|
8
|
+
# @option default [String] :default the default text
|
9
|
+
def copy_for(key, default=nil)
|
10
|
+
default = if default.respond_to?(:to_hash)
|
11
|
+
default[:default]
|
12
|
+
else
|
13
|
+
default
|
14
|
+
end
|
15
|
+
|
16
|
+
key = scope_copycopter_key_by_partial(key)
|
17
|
+
warn("WARNING: #s is deprecated; use t(#{key.inspect}, :default => #{default.inspect}) instead.")
|
18
|
+
I18n.translate(key, { :default => default })
|
19
|
+
end
|
20
|
+
alias_method :s, :copy_for
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def scope_copycopter_key_by_partial(key)
|
25
|
+
if respond_to?(:scope_key_by_partial, true)
|
26
|
+
scope_key_by_partial(key)
|
27
|
+
elsif key.to_s[0].chr == "."
|
28
|
+
if respond_to?(:template)
|
29
|
+
"#{template.path_without_format_and_extension.gsub(%r{/_?}, '.')}#{key}"
|
30
|
+
else
|
31
|
+
"#{controller_name}.#{action_name}#{key}"
|
32
|
+
end
|
33
|
+
else
|
34
|
+
key
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
extend Helper
|
40
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module CopycopterClient
|
4
|
+
# I81n implementation designed to synchronize with Copycopter.
|
5
|
+
#
|
6
|
+
# Expects an object that acts like a Hash, responding to +[]+, +[]=+, and +keys+.
|
7
|
+
#
|
8
|
+
# This backend will be used as the default I81n 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 I81n class.
|
11
|
+
#
|
12
|
+
# If a fallback backend is provided, keys available in the fallback backend
|
13
|
+
# will be used as defaults when those keys aren't available on the Copycopter
|
14
|
+
# server.
|
15
|
+
class I18nBackend
|
16
|
+
include I18n::Backend::Base
|
17
|
+
|
18
|
+
# These keys aren't used in interpolation
|
19
|
+
RESERVED_KEYS = [:scope, :default, :separator, :resolve, :object,
|
20
|
+
:fallback, :format, :cascade, :raise, :rescue_format].freeze
|
21
|
+
|
22
|
+
# Usually instantiated when {Configuration#apply} is invoked.
|
23
|
+
# @param sync [Sync] must act like a hash, returning and accept blurbs by key.
|
24
|
+
# @param options [Hash]
|
25
|
+
# @option options [I18n::Backend::Base] :fallback_backend I18n backend where missing translations can be found
|
26
|
+
def initialize(sync, options)
|
27
|
+
@sync = sync
|
28
|
+
@base_url = URI.parse("#{options[:protocol]}://#{options[:host]}:#{options[:port]}")
|
29
|
+
@fallback = options[:fallback_backend]
|
30
|
+
end
|
31
|
+
|
32
|
+
# This is invoked by frameworks when locales should be loaded. The
|
33
|
+
# Copycopter client loads content in the background, so this method waits
|
34
|
+
# until the first download is complete.
|
35
|
+
def reload!
|
36
|
+
sync.wait_for_download
|
37
|
+
end
|
38
|
+
|
39
|
+
# Translates the given local and key. See the I81n API documentation for details.
|
40
|
+
#
|
41
|
+
# Because the Copycopter API only supports copy text and doesn't support
|
42
|
+
# nested structures or arrays, the fallback value will be returned without
|
43
|
+
# using the Copycopter API if that value doesn't respond to to_str.
|
44
|
+
#
|
45
|
+
# @return [Object] the translated key (usually a String)
|
46
|
+
def translate(locale, key, options = {})
|
47
|
+
fallback_value = fallback(locale, key, options)
|
48
|
+
return fallback_value if fallback_value && !fallback_value.respond_to?(:to_str)
|
49
|
+
|
50
|
+
default = fallback_value || options.delete(:default)
|
51
|
+
content = super(locale, key, options.update(:default => default))
|
52
|
+
if content.respond_to?(:html_safe)
|
53
|
+
content.html_safe
|
54
|
+
else
|
55
|
+
content
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns locales availabile for this Copycopter project.
|
60
|
+
# @return [Array<String>] available locales
|
61
|
+
def available_locales
|
62
|
+
sync.keys.map { |key| key.split('.').first }.uniq
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def lookup(locale, key, scope = [], options = {})
|
68
|
+
parts = I18n.normalize_keys(locale, key, scope, options[:separator])
|
69
|
+
key = parts.join('.')
|
70
|
+
content = sync[key]
|
71
|
+
sync[key] = "" if content.nil?
|
72
|
+
content
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :sync
|
76
|
+
|
77
|
+
def fallback(locale, key, options)
|
78
|
+
if @fallback
|
79
|
+
fallback_options = options.dup
|
80
|
+
(fallback_options.keys - RESERVED_KEYS).each do |interpolated_key|
|
81
|
+
fallback_options[interpolated_key] = "%{#{interpolated_key}}"
|
82
|
+
end
|
83
|
+
|
84
|
+
@fallback.translate(locale, key, fallback_options)
|
85
|
+
end
|
86
|
+
rescue I18n::MissingTranslationData
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def default(locale, object, subject, options = {})
|
91
|
+
content = super(locale, object, subject, options)
|
92
|
+
if content.respond_to?(:to_str)
|
93
|
+
parts = I18n.normalize_keys(locale, object, options[:scope], options[:separator])
|
94
|
+
key = parts.join('.')
|
95
|
+
sync[key] = content.to_str
|
96
|
+
end
|
97
|
+
content
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CopycopterClient
|
2
|
+
class PrefixedLogger
|
3
|
+
attr_reader :prefix, :original_logger
|
4
|
+
|
5
|
+
def initialize(prefix, logger)
|
6
|
+
@prefix = prefix
|
7
|
+
@original_logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def info(message = nil, &block)
|
11
|
+
log(:info, message, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(message = nil, &block)
|
15
|
+
log(:debug, message, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def warn(message = nil, &block)
|
19
|
+
log(:warn, message, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(message = nil, &block)
|
23
|
+
log(:error, message, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def fatal(message = nil, &block)
|
27
|
+
log(:fatal, message, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def log(severity, message, &block)
|
33
|
+
prefixed_message = "#{prefix} #{thread_info} #{message}"
|
34
|
+
original_logger.send(severity, prefixed_message, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def thread_info
|
38
|
+
"[P:#{Process.pid}] [T:#{Thread.current.object_id}]"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'copycopter_client/helper'
|
2
|
+
|
3
|
+
if defined?(ActionController::Base)
|
4
|
+
ActionController::Base.send :include, CopycopterClient::Helper
|
5
|
+
end
|
6
|
+
if defined?(ActionView::Base)
|
7
|
+
ActionView::Base.send :include, CopycopterClient::Helper
|
8
|
+
end
|
9
|
+
|
10
|
+
module CopycopterClient
|
11
|
+
# Responsible for Rails initialization
|
12
|
+
module Rails
|
13
|
+
# Sets up the logger, environment, name, project root, and framework name
|
14
|
+
# for Rails applications. Must be called after framework initialization.
|
15
|
+
def self.initialize
|
16
|
+
CopycopterClient.configure(false) do |config|
|
17
|
+
config.environment_name = ::Rails.env
|
18
|
+
config.logger = ::Rails.logger
|
19
|
+
config.framework = "Rails: #{::Rails::VERSION::STRING}"
|
20
|
+
config.fallback_backend = I18n.backend
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined?(Rails::Railtie)
|
27
|
+
require 'copycopter_client/railtie'
|
28
|
+
else
|
29
|
+
CopycopterClient::Rails.initialize
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module CopycopterClient
|
2
|
+
# Connects to integration points for Rails 3 applications
|
3
|
+
class Railtie < ::Rails::Railtie
|
4
|
+
initializer :initialize_copycopter_rails, :after => :before_initialize do
|
5
|
+
CopycopterClient::Rails.initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
rake_tasks do
|
9
|
+
load "tasks/copycopter_client_tasks.rake"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|