copycopter_client 1.0.0.beta6 → 1.0.0.beta7
Sign up to get free protection for your applications and to get access to all the features.
- data/features/rails.feature +25 -1
- data/lib/copycopter_client/configuration.rb +2 -5
- data/lib/copycopter_client/i18n_backend.rb +41 -49
- data/lib/copycopter_client/rails.rb +0 -1
- data/lib/copycopter_client/version.rb +1 -1
- data/spec/copycopter_client/configuration_spec.rb +2 -3
- data/spec/copycopter_client/i18n_backend_spec.rb +29 -23
- metadata +3 -3
data/features/rails.feature
CHANGED
@@ -264,7 +264,6 @@ Feature: Using copycopter in a rails app
|
|
264
264
|
| key | draft content |
|
265
265
|
| user.attributes.name.blank | can't be blank |
|
266
266
|
|
267
|
-
|
268
267
|
Scenario: ensure keys are synced with short lived processes
|
269
268
|
When I configure the copycopter client to have a polling delay of 86400 seconds
|
270
269
|
And I start the application
|
@@ -273,3 +272,28 @@ Feature: Using copycopter in a rails app
|
|
273
272
|
| key | draft content |
|
274
273
|
| en.threaded.key | all your base |
|
275
274
|
|
275
|
+
Scenario: support pluralization
|
276
|
+
When I write to "app/controllers/users_controller.rb" with:
|
277
|
+
"""
|
278
|
+
class UsersController < ActionController::Base
|
279
|
+
def index
|
280
|
+
render
|
281
|
+
end
|
282
|
+
end
|
283
|
+
"""
|
284
|
+
When I route the "users" resource
|
285
|
+
And I write to "app/views/users/index.html.erb" with:
|
286
|
+
"""
|
287
|
+
<%= time_ago_in_words(1.hour.ago) %> ago
|
288
|
+
<%= time_ago_in_words(2.hours.ago) %> ago
|
289
|
+
"""
|
290
|
+
When I start the application
|
291
|
+
And I visit /users/
|
292
|
+
Then the response should contain "1 hour ago"
|
293
|
+
And the response should contain "2 hours ago"
|
294
|
+
When I wait for changes to be synchronized
|
295
|
+
Then the "abc123" project should have the following blurbs:
|
296
|
+
| key | draft content |
|
297
|
+
| en.datetime.distance_in_words.about_x_hours.one | about 1 hour |
|
298
|
+
| en.datetime.distance_in_words.about_x_hours.other | about %{count} hours |
|
299
|
+
|
@@ -13,7 +13,7 @@ module CopycopterClient
|
|
13
13
|
:http_open_timeout, :http_read_timeout, :client_name, :client_url,
|
14
14
|
:client_version, :port, :protocol, :proxy_host, :proxy_pass,
|
15
15
|
:proxy_port, :proxy_user, :secure, :polling_delay, :logger,
|
16
|
-
:framework
|
16
|
+
:framework].freeze
|
17
17
|
|
18
18
|
# @return [String] The API key for your project, found on the project edit form.
|
19
19
|
attr_accessor :api_key
|
@@ -72,9 +72,6 @@ module CopycopterClient
|
|
72
72
|
# @return [Logger] Where to log messages. Must respond to same interface as Logger.
|
73
73
|
attr_reader :logger
|
74
74
|
|
75
|
-
# @return [I18n::Backend::Base] where to look for translations missing on the Copycopter server
|
76
|
-
attr_accessor :fallback_backend
|
77
|
-
|
78
75
|
alias_method :secure?, :secure
|
79
76
|
|
80
77
|
# Instantiated from {CopycopterClient.configure}. Sets defaults.
|
@@ -147,7 +144,7 @@ module CopycopterClient
|
|
147
144
|
def apply
|
148
145
|
client = Client.new(to_hash)
|
149
146
|
sync = Sync.new(client, to_hash)
|
150
|
-
I18n.backend = I18nBackend.new(sync
|
147
|
+
I18n.backend = I18nBackend.new(sync)
|
151
148
|
CopycopterClient.client = client
|
152
149
|
CopycopterClient.sync = sync
|
153
150
|
@applied = true
|
@@ -1,54 +1,29 @@
|
|
1
1
|
require 'i18n'
|
2
2
|
|
3
3
|
module CopycopterClient
|
4
|
-
#
|
4
|
+
# I18n implementation designed to synchronize with Copycopter.
|
5
5
|
#
|
6
6
|
# Expects an object that acts like a Hash, responding to +[]+, +[]=+, and +keys+.
|
7
7
|
#
|
8
|
-
# This backend will be used as the default
|
8
|
+
# This backend will be used as the default I18n backend when the client is
|
9
9
|
# configured, so you will not need to instantiate this class from the
|
10
|
-
# application. Instead, just use methods on the
|
10
|
+
# application. Instead, just use methods on the I18n class.
|
11
11
|
#
|
12
|
-
#
|
13
|
-
# will be used as defaults when those keys aren't available on the Copycopter
|
14
|
-
# server.
|
12
|
+
# This implementation will also load translations from locale files.
|
15
13
|
class I18nBackend
|
16
|
-
include I18n::Backend::
|
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
|
14
|
+
include I18n::Backend::Simple::Implementation
|
21
15
|
|
22
16
|
# Usually instantiated when {Configuration#apply} is invoked.
|
23
17
|
# @param sync [Sync] must act like a hash, returning and accept blurbs by key.
|
24
|
-
|
25
|
-
|
26
|
-
def initialize(sync, options)
|
27
|
-
@sync = sync
|
28
|
-
@base_url = URI.parse("#{options[:protocol]}://#{options[:host]}:#{options[:port]}")
|
29
|
-
@fallback = options[:fallback_backend]
|
18
|
+
def initialize(sync)
|
19
|
+
@sync = sync
|
30
20
|
end
|
31
21
|
|
32
|
-
#
|
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.
|
22
|
+
# Translates the given local and key. See the I18n API documentation for details.
|
44
23
|
#
|
45
24
|
# @return [Object] the translated key (usually a String)
|
46
25
|
def translate(locale, key, options = {})
|
47
|
-
|
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, :fallback => true))
|
26
|
+
content = super(locale, key, options.merge(:fallback => true))
|
52
27
|
if content.respond_to?(:html_safe)
|
53
28
|
content.html_safe
|
54
29
|
else
|
@@ -59,32 +34,47 @@ module CopycopterClient
|
|
59
34
|
# Returns locales availabile for this Copycopter project.
|
60
35
|
# @return [Array<String>] available locales
|
61
36
|
def available_locales
|
62
|
-
sync.keys.map { |key| key.split('.').first }
|
37
|
+
sync_locales = sync.keys.map { |key| key.split('.').first }
|
38
|
+
(sync_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 Copycopter during the next sync.
|
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)
|
63
52
|
end
|
64
53
|
|
65
54
|
private
|
66
55
|
|
67
56
|
def lookup(locale, key, scope = [], options = {})
|
68
57
|
parts = I18n.normalize_keys(locale, key, scope, options[:separator])
|
69
|
-
|
70
|
-
content = sync[
|
71
|
-
sync[
|
58
|
+
key_with_locale = parts.join('.')
|
59
|
+
content = sync[key_with_locale] || super
|
60
|
+
sync[key_with_locale] = "" if content.nil?
|
72
61
|
content
|
73
62
|
end
|
74
63
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
fallback_options = options.dup
|
80
|
-
(fallback_options.keys - RESERVED_KEYS).each do |interpolated_key|
|
81
|
-
fallback_options[interpolated_key] = "%{#{interpolated_key}}"
|
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])
|
82
68
|
end
|
83
|
-
|
84
|
-
|
69
|
+
elsif data.respond_to?(:to_str)
|
70
|
+
key = ([locale] + scope).join('.')
|
71
|
+
sync[key] = data.to_str
|
85
72
|
end
|
86
|
-
|
87
|
-
|
73
|
+
end
|
74
|
+
|
75
|
+
def load_translations(*filenames)
|
76
|
+
super
|
77
|
+
sync.wait_for_download
|
88
78
|
end
|
89
79
|
|
90
80
|
def default(locale, object, subject, options = {})
|
@@ -96,5 +86,7 @@ module CopycopterClient
|
|
96
86
|
end
|
97
87
|
content
|
98
88
|
end
|
89
|
+
|
90
|
+
attr_reader :sync
|
99
91
|
end
|
100
92
|
end
|
@@ -42,7 +42,6 @@ describe CopycopterClient::Configuration do
|
|
42
42
|
it { should have_config_option(:api_key). overridable }
|
43
43
|
it { should have_config_option(:polling_delay). overridable.default(300) }
|
44
44
|
it { should have_config_option(:framework). overridable }
|
45
|
-
it { should have_config_option(:fallback_backend). overridable }
|
46
45
|
|
47
46
|
it "should provide default values for secure connections" do
|
48
47
|
config = CopycopterClient::Configuration.new
|
@@ -72,7 +71,7 @@ describe CopycopterClient::Configuration do
|
|
72
71
|
[:api_key, :environment_name, :host, :http_open_timeout,
|
73
72
|
:http_read_timeout, :client_name, :client_url, :client_version, :port,
|
74
73
|
:protocol, :proxy_host, :proxy_pass, :proxy_port, :proxy_user, :secure,
|
75
|
-
:development_environments, :logger, :framework
|
74
|
+
:development_environments, :logger, :framework].each do |option|
|
76
75
|
hash[option].should == config[option]
|
77
76
|
end
|
78
77
|
hash[:public].should == config.public?
|
@@ -203,7 +202,7 @@ share_examples_for "applied configuration" do
|
|
203
202
|
it "builds and assigns an I18n backend" do
|
204
203
|
CopycopterClient::Client.should have_received(:new).with(subject.to_hash)
|
205
204
|
CopycopterClient::Sync.should have_received(:new).with(client, subject.to_hash)
|
206
|
-
CopycopterClient::I18nBackend.should have_received(:new).with(sync
|
205
|
+
CopycopterClient::I18nBackend.should have_received(:new).with(sync)
|
207
206
|
I18n.backend.should == backend
|
208
207
|
end
|
209
208
|
|
@@ -3,24 +3,28 @@ require 'spec_helper'
|
|
3
3
|
describe CopycopterClient::I18nBackend do
|
4
4
|
let(:sync) { {} }
|
5
5
|
|
6
|
-
def build_backend
|
7
|
-
|
8
|
-
backend = CopycopterClient::I18nBackend.new(sync, default_config.update(config))
|
6
|
+
def build_backend
|
7
|
+
backend = CopycopterClient::I18nBackend.new(sync)
|
9
8
|
I18n.backend = backend
|
10
9
|
backend
|
11
10
|
end
|
12
11
|
|
13
|
-
before
|
12
|
+
before do
|
13
|
+
@default_backend = I18n.backend
|
14
|
+
sync.stubs(:wait_for_download)
|
15
|
+
end
|
16
|
+
|
14
17
|
after { I18n.backend = @default_backend }
|
15
18
|
|
16
19
|
subject { build_backend }
|
17
20
|
|
18
|
-
it "waits
|
19
|
-
|
20
|
-
|
21
|
+
it "reloads locale files and waits for the sync to complete" do
|
22
|
+
I18n.stubs(:load_path => [])
|
21
23
|
subject.reload!
|
24
|
+
subject.translate('en', 'test.key', :default => 'something')
|
22
25
|
|
23
26
|
sync.should have_received(:wait_for_download)
|
27
|
+
I18n.should have_received(:load_path)
|
24
28
|
end
|
25
29
|
|
26
30
|
it "includes the base i18n backend" do
|
@@ -36,11 +40,14 @@ describe CopycopterClient::I18nBackend do
|
|
36
40
|
backend.translate('en', 'test.key', :scope => 'prefix').should == value
|
37
41
|
end
|
38
42
|
|
39
|
-
it "finds available locales" do
|
43
|
+
it "finds available locales from locale files and sync" do
|
44
|
+
YAML.stubs(:load_file => { 'es' => { 'key' => 'value' } })
|
45
|
+
I18n.stubs(:load_path => ["test.yml"])
|
46
|
+
|
40
47
|
sync['en.key'] = ''
|
41
48
|
sync['fr.key'] = ''
|
42
49
|
|
43
|
-
subject.available_locales.should =~
|
50
|
+
subject.available_locales.should =~ [:en, :es, :fr]
|
44
51
|
end
|
45
52
|
|
46
53
|
it "queues missing keys with default" do
|
@@ -80,52 +87,51 @@ describe CopycopterClient::I18nBackend do
|
|
80
87
|
should == 'Expected'
|
81
88
|
end
|
82
89
|
|
83
|
-
describe "with
|
84
|
-
|
85
|
-
subject { build_backend(:fallback_backend => fallback) }
|
90
|
+
describe "with stored translations" do
|
91
|
+
subject { build_backend }
|
86
92
|
|
87
|
-
it "uses
|
88
|
-
|
93
|
+
it "uses stored translations as a default" do
|
94
|
+
subject.store_translations('en', 'test' => { 'key' => 'Expected' })
|
89
95
|
subject.translate('en', 'test.key', :default => 'Unexpected').
|
90
96
|
should include('Expected')
|
91
97
|
sync['en.test.key'].should == 'Expected'
|
92
98
|
end
|
93
99
|
|
94
|
-
it "preserves interpolation markers in the
|
95
|
-
|
100
|
+
it "preserves interpolation markers in the stored translation" do
|
101
|
+
subject.store_translations('en', 'test' => { 'key' => '%{interpolate}' })
|
96
102
|
subject.translate('en', 'test.key', :interpolate => 'interpolated').
|
97
103
|
should include('interpolated')
|
98
104
|
sync['en.test.key'].should == '%{interpolate}'
|
99
105
|
end
|
100
106
|
|
101
|
-
it "uses the default if the
|
107
|
+
it "uses the default if the stored translations don't have the key" do
|
102
108
|
subject.translate('en', 'test.key', :default => 'Expected').
|
103
109
|
should include('Expected')
|
104
110
|
end
|
105
111
|
|
106
112
|
it "uses the syncd key when present" do
|
107
|
-
|
113
|
+
subject.store_translations('en', 'test' => { 'key' => 'Unexpected' })
|
108
114
|
sync['en.test.key'] = 'Expected'
|
109
115
|
subject.translate('en', 'test.key', :default => 'default').
|
110
116
|
should include('Expected')
|
111
117
|
end
|
112
118
|
|
113
|
-
it "
|
119
|
+
it "stores a nested hash" do
|
114
120
|
nested = { :nested => 'value' }
|
115
|
-
|
121
|
+
subject.store_translations('en', 'key' => nested)
|
116
122
|
subject.translate('en', 'key', :default => 'Unexpected').should == nested
|
117
|
-
sync['en.key'].should
|
123
|
+
sync['en.key.nested'].should == 'value'
|
118
124
|
end
|
119
125
|
|
120
126
|
it "returns an array directly without storing" do
|
121
127
|
array = ['value']
|
122
|
-
|
128
|
+
subject.store_translations('en', 'key' => array)
|
123
129
|
subject.translate('en', 'key', :default => 'Unexpected').should == array
|
124
130
|
sync['en.key'].should be_nil
|
125
131
|
end
|
126
132
|
|
127
133
|
it "looks up an array of defaults" do
|
128
|
-
|
134
|
+
subject.store_translations('en', 'key' => { 'one' => 'Expected' })
|
129
135
|
subject.translate('en', 'key.three', :default => [:"key.two", :"key.one"]).
|
130
136
|
should include('Expected')
|
131
137
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: copycopter_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 299253591
|
5
5
|
prerelease: true
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 0
|
9
9
|
- 0
|
10
|
-
-
|
11
|
-
version: 1.0.0.
|
10
|
+
- beta7
|
11
|
+
version: 1.0.0.beta7
|
12
12
|
platform: ruby
|
13
13
|
authors:
|
14
14
|
- thoughtbot
|