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.
@@ -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