copycopter_client 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|