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.
@@ -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, :fallback_backend].freeze
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, to_hash)
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
- # I81n implementation designed to synchronize with Copycopter.
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 I81n backend when the client is
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 I81n class.
10
+ # application. Instead, just use methods on the I18n class.
11
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.
12
+ # This implementation will also load translations from locale files.
15
13
  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
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
- # @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]
18
+ def initialize(sync)
19
+ @sync = sync
30
20
  end
31
21
 
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.
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
- 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, :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 }.uniq
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
- key = parts.join('.')
70
- content = sync[key]
71
- sync[key] = "" if content.nil?
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
- 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}}"
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
- @fallback.translate(locale, key, fallback_options)
69
+ elsif data.respond_to?(:to_str)
70
+ key = ([locale] + scope).join('.')
71
+ sync[key] = data.to_str
85
72
  end
86
- rescue I18n::MissingTranslationData
87
- nil
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
@@ -17,7 +17,6 @@ module CopycopterClient
17
17
  config.environment_name = ::Rails.env
18
18
  config.logger = ::Rails.logger
19
19
  config.framework = "Rails: #{::Rails::VERSION::STRING}"
20
- config.fallback_backend = I18n.backend
21
20
  end
22
21
  end
23
22
  end
@@ -1,6 +1,6 @@
1
1
  module CopycopterClient
2
2
  # Client version
3
- VERSION = "1.0.0.beta6"
3
+ VERSION = "1.0.0.beta7"
4
4
 
5
5
  # API version being used to communicate with the server
6
6
  API_VERSION = "2.0"
@@ -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, :fallback_backend].each do |option|
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, subject.to_hash)
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(config = {})
7
- default_config = CopycopterClient::Configuration.new.to_hash
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 { @default_backend = I18n.backend }
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 until the first download when reloaded" do
19
- sync.stubs(:wait_for_download)
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 =~ %w(en fr)
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 a fallback" do
84
- let(:fallback) { I18n::Backend::Simple.new }
85
- subject { build_backend(:fallback_backend => fallback) }
90
+ describe "with stored translations" do
91
+ subject { build_backend }
86
92
 
87
- it "uses the fallback as a default" do
88
- fallback.store_translations('en', 'test' => { 'key' => 'Expected' })
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 fallback" do
95
- fallback.store_translations('en', 'test' => { 'key' => '%{interpolate}' })
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 fallback doesn't have the key" do
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
- fallback.store_translations('en', 'test' => { 'key' => 'Unexpected' })
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 "returns a hash directly without storing" do
119
+ it "stores a nested hash" do
114
120
  nested = { :nested => 'value' }
115
- fallback.store_translations('en', 'key' => nested)
121
+ subject.store_translations('en', 'key' => nested)
116
122
  subject.translate('en', 'key', :default => 'Unexpected').should == nested
117
- sync['en.key'].should be_nil
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
- fallback.store_translations('en', 'key' => array)
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
- fallback.store_translations('en', 'key' => { 'one' => 'Expected' })
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: 299253590
4
+ hash: 299253591
5
5
  prerelease: true
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
9
  - 0
10
- - beta6
11
- version: 1.0.0.beta6
10
+ - beta7
11
+ version: 1.0.0.beta7
12
12
  platform: ruby
13
13
  authors:
14
14
  - thoughtbot