copycopter_client 1.0.0.beta6 → 1.0.0.beta7
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/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
|