mobility 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2476bba0d2ac7649a1fa88b936c4cb51935d4b84
4
- data.tar.gz: a15090756064bce65bc9c7554f331fa63378b5ce
3
+ metadata.gz: c134f78117865e131baf9ceecb0c902cb63abb08
4
+ data.tar.gz: 2076ecc0e3164b962d1f5147e0af81d2ba9baac3
5
5
  SHA512:
6
- metadata.gz: 7d93ae6f961fe488dc877dee008f8152b0390e8b485a4e34a558b49c9b9642bee79c7df60fd456732fdbc9c19aa9506e97295caba2d4014e1f834ea2e046faae
7
- data.tar.gz: 8d4f4e0404691953173798542fb6d2021b206b42a1e2fea5d4ca7350414281261c86eb2610ef4cb8f6d2efd2def2630dfb40c92c0d1b7f071d66eeae9f252da4
6
+ metadata.gz: f6ec9f5003e16ace7ea7a77136266af20f5c86beb1ebea6ae4e83598d20bdf4d14971d6f029c2b1bd74763c599287e362b7b3204c51660606ab2b371a4620842
7
+ data.tar.gz: 8eae38b70cb15cb0eeec7dfb51f72e4076268312bdd73d63a9fda7611414d116e5bd95771a3042ca080ea4495c335e50a66f5c92a1beb6994e2d1d8d4c9d3bb9
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Mobility Changelog
2
2
 
3
+ ## 0.3
4
+
5
+ ### 0.3.0 (November 30, 2017)
6
+ * `dup` support for table backend ([#84](https://github.com/shioyama/mobility/pull/84)). Thanks [@pwim](https://github.com/pwim)!
7
+ * Disable fallbacks when using locale/fallthrough accessors
8
+ ([#86](https://github.com/shioyama/mobility/pull/86), [#87](https://github.com/shioyama/mobility/pull/87),
9
+ [#88](https://github.com/shioyama/mobility/pull/88), [#89](https://github.com/shioyama/mobility/pull/89))
10
+ * Convert AttributeMethods to plugin
11
+ ([#102](https://github.com/shioyama/mobility/pull/102))
12
+ * Ensure `cache_key` is invalidated when updating translations
13
+ ([#104](https://github.com/shioyama/mobility/pull/102)) Thanks
14
+ [@pwim](https://github.com/pwim)!
15
+ * Update dependency versions ([#107](https://github.com/shioyama/mobility/pull/107))
16
+ * Support new AR::Dirty methods ([#111](https://github.com/shioyama/mobility/pull/111))
17
+ * Use `public_send` in LocaleAccessors plugin ([#117](https://github.com/shioyama/mobility/pull/117))
18
+
3
19
  ## 0.2
4
20
 
5
21
  ### 0.2.3 (September 14, 2017)
data/Gemfile CHANGED
@@ -9,6 +9,9 @@ group :development, :test do
9
9
  gem 'activerecord', '>= 5.0', '< 5.1'
10
10
  elsif ENV['RAILS_VERSION'] == '4.2'
11
11
  gem 'activerecord', '>= 4.2.6', '< 5.0'
12
+ elsif ENV['RAILS_VERSION'] == '5.2'
13
+ gem 'activerecord', '>= 5.2.0.beta1'
14
+ gem 'railties', '>= 5.2.0.beta1'
12
15
  else
13
16
  gem 'activerecord', '>= 5.1', '< 5.2'
14
17
  end
@@ -16,6 +19,8 @@ group :development, :test do
16
19
  elsif ENV['ORM'] == 'sequel'
17
20
  if ENV['SEQUEL_VERSION'] == '4.41'
18
21
  gem 'sequel', '>= 4.41.0', '< 4.46.0'
22
+ elsif ENV['SEQUEL_VERSION'] == 'latest'
23
+ gem 'sequel', '>= 5.0.0'
19
24
  else
20
25
  gem 'sequel', '>= 4.46.0', '< 5.0'
21
26
  end
@@ -27,7 +32,7 @@ group :development, :test do
27
32
  gem 'guard-rspec'
28
33
  gem 'pry-byebug'
29
34
  gem 'sqlite3'
30
- gem 'mysql2', '~> 0.3.10'
35
+ gem 'mysql2', '~> 0.4.9'
31
36
  gem 'pg'
32
37
  end
33
38
  end
data/Gemfile.lock CHANGED
@@ -1,45 +1,46 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mobility (0.2.2)
5
- i18n (>= 0.6.10, < 0.9)
4
+ mobility (0.3.0.pre.alpha)
5
+ i18n (>= 0.6.10, < 0.10)
6
6
  request_store (~> 1.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- actionpack (5.1.3)
12
- actionview (= 5.1.3)
13
- activesupport (= 5.1.3)
11
+ actionpack (5.2.0.beta2)
12
+ actionview (= 5.2.0.beta2)
13
+ activesupport (= 5.2.0.beta2)
14
14
  rack (~> 2.0)
15
- rack-test (~> 0.6.3)
15
+ rack-test (>= 0.6.3)
16
16
  rails-dom-testing (~> 2.0)
17
17
  rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
- actionview (5.1.3)
19
- activesupport (= 5.1.3)
18
+ actionview (5.2.0.beta2)
19
+ activesupport (= 5.2.0.beta2)
20
20
  builder (~> 3.1)
21
21
  erubi (~> 1.4)
22
22
  rails-dom-testing (~> 2.0)
23
23
  rails-html-sanitizer (~> 1.0, >= 1.0.3)
24
- activemodel (5.1.3)
25
- activesupport (= 5.1.3)
26
- activerecord (5.1.3)
27
- activemodel (= 5.1.3)
28
- activesupport (= 5.1.3)
29
- arel (~> 8.0)
30
- activesupport (5.1.3)
24
+ activemodel (5.2.0.beta2)
25
+ activesupport (= 5.2.0.beta2)
26
+ activerecord (5.2.0.beta2)
27
+ activemodel (= 5.2.0.beta2)
28
+ activesupport (= 5.2.0.beta2)
29
+ arel (>= 9.0)
30
+ activesupport (5.2.0.beta2)
31
31
  concurrent-ruby (~> 1.0, >= 1.0.2)
32
32
  i18n (~> 0.7)
33
33
  minitest (~> 5.1)
34
34
  tzinfo (~> 1.1)
35
- arel (8.0.0)
35
+ arel (9.0.0)
36
36
  builder (3.2.3)
37
- byebug (9.0.6)
38
- coderay (1.1.1)
37
+ byebug (9.1.0)
38
+ coderay (1.1.2)
39
39
  concurrent-ruby (1.0.5)
40
- database_cleaner (1.6.1)
40
+ crass (1.0.3)
41
+ database_cleaner (1.6.2)
41
42
  diff-lcs (1.3)
42
- erubi (1.6.1)
43
+ erubi (1.7.0)
43
44
  ffi (1.9.18)
44
45
  formatador (0.2.5)
45
46
  generator_spec (0.9.4)
@@ -59,91 +60,92 @@ GEM
59
60
  guard (~> 2.1)
60
61
  guard-compat (~> 1.1)
61
62
  rspec (>= 2.99.0, < 4.0)
62
- i18n (0.8.6)
63
+ i18n (0.9.1)
64
+ concurrent-ruby (~> 1.0)
63
65
  listen (3.1.5)
64
66
  rb-fsevent (~> 0.9, >= 0.9.4)
65
67
  rb-inotify (~> 0.9, >= 0.9.7)
66
68
  ruby_dep (~> 1.2)
67
- loofah (2.0.3)
69
+ loofah (2.1.1)
70
+ crass (~> 1.0.2)
68
71
  nokogiri (>= 1.5.9)
69
72
  lumberjack (1.0.12)
70
- method_source (0.8.2)
71
- mini_portile2 (2.2.0)
73
+ method_source (0.9.0)
74
+ mini_portile2 (2.3.0)
72
75
  minitest (5.10.3)
73
- mysql2 (0.3.21)
76
+ mysql2 (0.4.10)
74
77
  nenv (0.3.0)
75
- nokogiri (1.8.0)
76
- mini_portile2 (~> 2.2.0)
78
+ nokogiri (1.8.1)
79
+ mini_portile2 (~> 2.3.0)
77
80
  notiffany (0.1.1)
78
81
  nenv (~> 0.1)
79
82
  shellany (~> 0.0)
80
83
  pg (0.21.0)
81
- pry (0.10.4)
84
+ pry (0.11.3)
82
85
  coderay (~> 1.1.0)
83
- method_source (~> 0.8.1)
84
- slop (~> 3.4)
85
- pry-byebug (3.4.2)
86
- byebug (~> 9.0)
86
+ method_source (~> 0.9.0)
87
+ pry-byebug (3.5.1)
88
+ byebug (~> 9.1)
87
89
  pry (~> 0.10)
88
90
  rack (2.0.3)
89
- rack-test (0.6.3)
90
- rack (>= 1.0)
91
+ rack-test (0.8.2)
92
+ rack (>= 1.0, < 3)
91
93
  rails-dom-testing (2.0.3)
92
94
  activesupport (>= 4.2.0)
93
95
  nokogiri (>= 1.6)
94
96
  rails-html-sanitizer (1.0.3)
95
97
  loofah (~> 2.0)
96
- railties (5.1.3)
97
- actionpack (= 5.1.3)
98
- activesupport (= 5.1.3)
98
+ railties (5.2.0.beta2)
99
+ actionpack (= 5.2.0.beta2)
100
+ activesupport (= 5.2.0.beta2)
99
101
  method_source
100
102
  rake (>= 0.8.7)
101
103
  thor (>= 0.18.1, < 2.0)
102
- rake (10.5.0)
104
+ rake (12.3.0)
103
105
  rb-fsevent (0.10.2)
104
106
  rb-inotify (0.9.10)
105
107
  ffi (>= 0.5.0, < 2)
106
108
  request_store (1.3.2)
107
- rspec (3.6.0)
108
- rspec-core (~> 3.6.0)
109
- rspec-expectations (~> 3.6.0)
110
- rspec-mocks (~> 3.6.0)
111
- rspec-core (3.6.0)
112
- rspec-support (~> 3.6.0)
113
- rspec-expectations (3.6.0)
109
+ rspec (3.7.0)
110
+ rspec-core (~> 3.7.0)
111
+ rspec-expectations (~> 3.7.0)
112
+ rspec-mocks (~> 3.7.0)
113
+ rspec-core (3.7.0)
114
+ rspec-support (~> 3.7.0)
115
+ rspec-expectations (3.7.0)
114
116
  diff-lcs (>= 1.2.0, < 2.0)
115
- rspec-support (~> 3.6.0)
116
- rspec-mocks (3.6.0)
117
+ rspec-support (~> 3.7.0)
118
+ rspec-mocks (3.7.0)
117
119
  diff-lcs (>= 1.2.0, < 2.0)
118
- rspec-support (~> 3.6.0)
119
- rspec-support (3.6.0)
120
+ rspec-support (~> 3.7.0)
121
+ rspec-support (3.7.0)
120
122
  ruby_dep (1.5.0)
121
123
  shellany (0.0.1)
122
- slop (3.6.0)
123
124
  sqlite3 (1.3.13)
124
- thor (0.19.4)
125
+ thor (0.20.0)
125
126
  thread_safe (0.3.6)
126
- tzinfo (1.2.3)
127
+ tzinfo (1.2.4)
127
128
  thread_safe (~> 0.1)
128
- yard (0.9.9)
129
+ yard (0.9.12)
129
130
 
130
131
  PLATFORMS
131
132
  ruby
132
133
 
133
134
  DEPENDENCIES
134
- activerecord (>= 5.1, < 5.2)
135
+ activerecord (>= 5.2.0.beta1)
135
136
  bundler (~> 1.12)
136
137
  database_cleaner (~> 1.5, >= 1.5.3)
137
138
  generator_spec (~> 0.9.4)
138
139
  guard-rspec
139
140
  mobility!
140
- mysql2 (~> 0.3.10)
141
+ mysql2 (~> 0.4.9)
141
142
  pg
142
143
  pry-byebug
143
- rake (~> 10.0)
144
+ railties (>= 5.2.0.beta1)
145
+ rake (~> 12, >= 12.2.1)
144
146
  rspec (~> 3.0)
145
147
  sqlite3
146
148
  yard (~> 0.9.0)
147
149
 
148
150
  BUNDLED WITH
149
- 1.15.3
151
+ 1.16.0.pre.2
data/README.md CHANGED
@@ -16,7 +16,8 @@ Mobility
16
16
  Mobility is a gem for storing and retrieving translations as attributes on a
17
17
  class. These translations could be the content of blog posts, captions on
18
18
  images, tags on bookmarks, or anything else you might want to store in
19
- different languages.
19
+ different languages. For examples of what Mobility can do, see the
20
+ <a href="#companies-using-mobility">Companies using Mobility</a> section below.
20
21
 
21
22
  Storage of translations is handled by customizable "backends" which encapsulate
22
23
  different storage strategies. The default, preferred way to store translations
@@ -41,13 +42,17 @@ Mobility](http://dejimata.com/2017/3/3/translating-with-mobility). See also the
41
42
  works for future releases, and other pages of the [wiki][wiki] for more detail
42
43
  on usage.
43
44
 
45
+ If you're coming from Globalize, be sure to also read the [Migrating from
46
+ Globalize](https://github.com/shioyama/mobility/wiki/Migrating-from-Globalize)
47
+ section of the wiki.
48
+
44
49
  Installation
45
50
  ------------
46
51
 
47
52
  Add this line to your application's Gemfile:
48
53
 
49
54
  ```ruby
50
- gem 'mobility', '~> 0.2.3'
55
+ gem 'mobility', '~> 0.3.0'
51
56
  ```
52
57
 
53
58
  Mobility is cryptographically signed. To be sure the gem you install hasn't
@@ -104,7 +109,7 @@ See [Getting Started](#quickstart) to get started translating your models.
104
109
  ### Sequel
105
110
 
106
111
  Requirements:
107
- - Sequel >= 4.0
112
+ - Sequel >= 4.0, < 5.0 (5.0 support is in the works)
108
113
 
109
114
  You can extend `Mobility` just like in ActiveRecord, or you can use the
110
115
  `mobility` plugin, which does the same thing:
@@ -222,7 +227,7 @@ locations, usually database columns. By default these values are stored as keys
222
227
  tables, one for strings and one for text columns, but this can be easily
223
228
  changed and/or customized (see the [Backends](#backends) section below).
224
229
 
225
- ### Getting and Setting Translations
230
+ ### <a name="getset"></a> Getting and Setting Translations
226
231
 
227
232
  The easiest way to get or set a translation is to use the getter and setter
228
233
  methods described above (`word.name` and `word.name=`), but you may want to
@@ -309,6 +314,11 @@ word.name(locale: :fr)
309
314
  #=> "mobilité"
310
315
  ```
311
316
 
317
+ Note that setting the locale this way will pass an option `locale: true` to the
318
+ backend and all plugins. Plugins may use this option to change their behavior
319
+ (passing the locale explicitly this way, for example, disables
320
+ [fallbacks](#fallbacks), see below for details).
321
+
312
322
  You can also *set* the value of an attribute this way; however, since the
313
323
  `word.name = <value>` syntax does not accept any options, the only way to do this is to
314
324
  use `send` (this is included mostly for consistency):
@@ -387,8 +397,8 @@ translated attributes on a class:
387
397
  ```ruby
388
398
  class Word < ApplicationRecord
389
399
  extend Mobility
390
- translates :name, type: :string, fallbacks: { de: :ja, fr: :ja }
391
- translates :meaning, type: :text, fallbacks: { de: :ja, fr: :ja }
400
+ translates :name, type: :string, fallbacks: { de: :ja, fr: :ja }, locale_accessors: true
401
+ translates :meaning, type: :text, fallbacks: { de: :ja, fr: :ja }, locale_accessors: true
392
402
  end
393
403
  ```
394
404
 
@@ -404,17 +414,20 @@ but not for other locales:
404
414
  ```ruby
405
415
  Mobility.locale = :ja
406
416
  word = Word.create(name: "モビリティ", meaning: "(名詞):動きやすさ、可動性")
407
- word.name(locale: :de)
417
+ Mobility.locale = :de
418
+ word.name
408
419
  #=> "モビリティ"
409
- word.meaning(locale: :de)
420
+ word.meaning
410
421
  #=> "(名詞):動きやすさ、可動性"
411
- word.name(locale: :fr)
422
+ Mobility.locale = :fr
423
+ word.name
412
424
  #=> "モビリティ"
413
- word.meaning(locale: :fr)
425
+ word.meaning
414
426
  #=> "(名詞):動きやすさ、可動性"
415
- word.name(locale: :ru)
427
+ Mobility.locale = :ru
428
+ word.name
416
429
  #=> nil
417
- word.meaning(locale: :ru)
430
+ word.meaning
418
431
  #=> nil
419
432
  ```
420
433
 
@@ -423,11 +436,14 @@ You can optionally disable fallbacks to get the real value for a given locale
423
436
  passing `fallback: false` (*singular*, not plural) to the getter method:
424
437
 
425
438
  ```ruby
426
- word.meaning(locale: :de, fallback: false)
439
+ Mobility.locale = :de
440
+ word.meaning(fallback: false)
427
441
  #=> nil
428
- word.meaning(locale: :fr, fallback: false)
442
+ Mobility.locale = :fr
443
+ word.meaning(fallback: false)
429
444
  #=> nil
430
- word.meaning(locale: :ja, fallback: false)
445
+ Mobility.locale = :ja
446
+ word.meaning(fallback: false)
431
447
  #=> "(名詞):動きやすさ、可動性"
432
448
  ```
433
449
 
@@ -439,11 +455,28 @@ Mobility.with_locale(:fr) do
439
455
  word.meaning = "(nf): aptitude à bouger, à se déplacer, à changer, à évoluer"
440
456
  end
441
457
  word.save
442
- word.meaning(locale: :de, fallback: false)
458
+ Mobility.locale = :de
459
+ word.meaning(fallback: false)
443
460
  #=> nil
444
- word.meaning(locale: :de, fallback: :fr)
461
+ word.meaning(fallback: :fr)
445
462
  #=> "(nf): aptitude à bouger, à se déplacer, à changer, à évoluer"
446
- word.meaning(locale: :de, fallback: [:ja, :fr])
463
+ word.meaning(fallback: [:ja, :fr])
464
+ #=> "(名詞):動きやすさ、可動性"
465
+ ```
466
+
467
+ Also note that passing a `locale` option into an attribute reader or writer, or
468
+ using [locale accessors or fallthrough accessors](#getset) to get or set
469
+ any attribute value, will disable fallbacks (just like `fallback: false`).
470
+ (This will take precedence over any value of the `fallback` option.)
471
+
472
+ Continuing from the last example:
473
+
474
+ ```ruby
475
+ word.meaning(locale: :de)
476
+ #=> nil
477
+ word.meaning_de
478
+ #=> nil
479
+ Mobility.with_locale(:de) { word.meaning }
447
480
  #=> "(名詞):動きやすさ、可動性"
448
481
  ```
449
482
 
@@ -830,6 +863,18 @@ More Information
830
863
  - [API documentation][docs]
831
864
  - [Wiki][wiki]
832
865
 
866
+ <a name="#companies-using-mobility"></a>Companies using Mobility
867
+ ------------------------
868
+
869
+ <img alt="Logos of companies using Mobility" src="./img/companies-using-mobility.png" style="width: 100%" />
870
+
871
+ - [Doorkeeper](https://www.doorkeeper.jp/)
872
+ - [Oreegano](https://www.oreegano.com/)
873
+ - [Venuu](https://venuu.fi)
874
+ - ... <sup>&#10033;</sup>
875
+
876
+ <sup>&#10033;</sup> <small>Post an issue or email me to add your company's name to this list.</small>
877
+
833
878
  License
834
879
  -------
835
880
 
data/lib/mobility.rb CHANGED
@@ -89,7 +89,6 @@ module Mobility
89
89
 
90
90
  if Loaded::ActiveRecord
91
91
  model_class.include(ActiveRecord) if model_class < ::ActiveRecord::Base
92
- model_class.include(ActiveRecord::AttributeMethods) if model_class.ancestors.include?(::ActiveRecord::AttributeMethods)
93
92
  end
94
93
 
95
94
  if Loaded::Sequel
@@ -5,7 +5,6 @@ Module loading ActiveRecord-specific classes for Mobility models.
5
5
 
6
6
  =end
7
7
  module ActiveRecord
8
- require "mobility/active_record/attribute_methods"
9
8
  require "mobility/active_record/uniqueness_validator"
10
9
 
11
10
  def self.included(model_class)
@@ -4,7 +4,7 @@ module Mobility
4
4
  class Translation < ::ActiveRecord::Base
5
5
  self.abstract_class = true
6
6
 
7
- belongs_to :translatable, polymorphic: true
7
+ belongs_to :translatable, polymorphic: true, touch: true
8
8
 
9
9
  validates :key, presence: true, uniqueness: { scope: [:translatable_id, :translatable_type, :locale] }
10
10
  validates :translatable, presence: true
@@ -19,7 +19,7 @@ like including a module. Creating an instance like this:
19
19
 
20
20
  Attributes.new("title", backend: :my_backend, locale_accessors: [:en, :ja], cache: true, fallbacks: true)
21
21
 
22
- will generate an anonymous module that behaves like this:
22
+ will generate an anonymous module that behaves (approximately) like this:
23
23
 
24
24
  Module.new do
25
25
  def title_backend
@@ -131,7 +131,7 @@ with other backends.
131
131
  def initialize(*attribute_names, method: :accessor, backend: Mobility.default_backend, **backend_options)
132
132
  raise ArgumentError, "method must be one of: reader, writer, accessor" unless %i[reader writer accessor].include?(method)
133
133
  @method = method
134
- @options = Mobility.default_options.merge(backend_options)
134
+ @options = Mobility.default_options.to_h.merge(backend_options)
135
135
  @names = attribute_names.map(&:to_s)
136
136
  raise Mobility::BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
137
137
  @backend_name = backend
@@ -168,6 +168,17 @@ with other backends.
168
168
  names.each(&block)
169
169
  end
170
170
 
171
+ # Process options passed into accessor method before calling backend, and
172
+ # return locale
173
+ # @param [Hash] options Options hash passed to accessor method
174
+ # @return [Symbol] locale
175
+ def self.process_options!(options)
176
+ (options[:locale] || Mobility.locale).tap { |locale|
177
+ Mobility.enforce_available_locales!(locale)
178
+ options[:locale] &&= !!locale
179
+ }.to_sym
180
+ end
181
+
171
182
  private
172
183
 
173
184
  def define_backend(attribute)
@@ -179,24 +190,24 @@ with other backends.
179
190
  end
180
191
 
181
192
  def define_reader(attribute)
182
- define_method attribute do |locale: Mobility.locale, **options|
193
+ define_method attribute do |**options|
183
194
  return super() if options.delete(:super)
184
- Mobility.enforce_available_locales!(locale)
185
- mobility_backend_for(attribute).read(locale.to_sym, options)
195
+ locale = Mobility::Attributes.process_options!(options)
196
+ mobility_backend_for(attribute).read(locale, options)
186
197
  end
187
198
 
188
- define_method "#{attribute}?" do |locale: Mobility.locale, **options|
199
+ define_method "#{attribute}?" do |**options|
189
200
  return super() if options.delete(:super)
190
- Mobility.enforce_available_locales!(locale)
191
- mobility_backend_for(attribute).present?(locale.to_sym, options)
201
+ locale = Mobility::Attributes.process_options!(options)
202
+ mobility_backend_for(attribute).present?(locale, options)
192
203
  end
193
204
  end
194
205
 
195
206
  def define_writer(attribute)
196
- define_method "#{attribute}=" do |value, locale: Mobility.locale, **options|
207
+ define_method "#{attribute}=" do |value, **options|
197
208
  return super(value) if options.delete(:super)
198
- Mobility.enforce_available_locales!(locale)
199
- mobility_backend_for(attribute).write(locale.to_sym, value, options)
209
+ locale = Mobility::Attributes.process_options!(options)
210
+ mobility_backend_for(attribute).write(locale, value, options)
200
211
  end
201
212
  end
202
213
 
@@ -136,7 +136,19 @@ columns to that table.
136
136
  translation_class.belongs_to :translated_model,
137
137
  class_name: name,
138
138
  foreign_key: options[:foreign_key],
139
- inverse_of: association_name
139
+ inverse_of: association_name,
140
+ touch: true
141
+
142
+ module_name = "MobilityArTable#{association_name.to_s.camelcase}"
143
+ unless const_defined?(module_name)
144
+ callback_methods = Module.new do
145
+ define_method :initialize_dup do |source|
146
+ super(source)
147
+ self.send("#{association_name}=", source.send(association_name).map(&:dup))
148
+ end
149
+ end
150
+ include const_set(module_name, callback_methods)
151
+ end
140
152
  end
141
153
 
142
154
  setup_query_methods(QueryMethods)
@@ -20,7 +20,17 @@ Stores shared Mobility configuration referenced by all backends.
20
20
  # may not include the keys 'backend' or 'model_class'.
21
21
  # @return [Hash]
22
22
  attr_reader :default_options
23
+
24
+ # @deprecated The default_options= setter has been deprecated. Set each
25
+ # option on the default_options hash instead.
23
26
  def default_options=(options)
27
+ warn %{
28
+ WARNING: The default_options= setter has been deprecated.
29
+ Set each option on the default_options hash instead, like this:
30
+
31
+ config.default_options[:fallbacks] = { ... }
32
+ config.default_options[:dirty] = true
33
+ }
24
34
  if (keys = options.keys & RESERVED_OPTION_KEYS).present?
25
35
  raise ReservedOptionKey,
26
36
  "Default options may not contain the following reserved keys: #{keys.join(', ')}"
@@ -63,24 +73,35 @@ Stores shared Mobility configuration referenced by all backends.
63
73
  @query_method = :i18n
64
74
  @default_fallbacks = lambda { |fallbacks| I18n::Locale::Fallbacks.new(fallbacks) }
65
75
  @default_accessor_locales = lambda { I18n.available_locales }
66
- @default_options = {
76
+ @default_options = Options[{
67
77
  cache: true,
68
78
  dirty: false,
69
79
  fallbacks: nil,
70
80
  presence: true,
71
- default: nil
72
- }
81
+ default: nil,
82
+ attribute_methods: false
83
+ }]
73
84
  @plugins = %i[
74
85
  cache
75
86
  dirty
76
87
  fallbacks
77
88
  presence
78
89
  default
90
+ attribute_methods
79
91
  fallthrough_accessors
80
92
  locale_accessors
81
93
  ]
82
94
  end
83
95
 
84
96
  class ReservedOptionKey < Exception; end
97
+
98
+ class Options < Hash
99
+ def []=(key, _)
100
+ if RESERVED_OPTION_KEYS.include?(key)
101
+ raise Configuration::ReservedOptionKey, "Default options may not contain the following reserved key: #{key}"
102
+ end
103
+ super
104
+ end
105
+ end
85
106
  end
86
107
  end
@@ -30,8 +30,9 @@ value of the translated attribute if passed to it.
30
30
  def write(locale, value, options = {})
31
31
  locale_accessor = Mobility.normalize_locale_accessor(attribute, locale)
32
32
  if model.changed_attributes.has_key?(locale_accessor) && model.changed_attributes[locale_accessor] == value
33
- model.attributes_changed_by_setter.except!(locale_accessor)
33
+ model.send(:attributes_changed_by_setter).except!(locale_accessor)
34
34
  elsif read(locale, options.merge(fallback: false)) != value
35
+ model.send(:mobility_changed_attributes) << locale_accessor
35
36
  model.send(:attribute_will_change!, locale_accessor)
36
37
  end
37
38
  super
@@ -63,6 +64,10 @@ value of the translated attribute if passed to it.
63
64
  private :restore_attribute!
64
65
  end
65
66
 
67
+ def included(model_class)
68
+ model_class.include ChangedAttributes
69
+ end
70
+
66
71
  private
67
72
 
68
73
  # Get method suffixes. Creating an object just to get the list of
@@ -74,6 +79,19 @@ value of the translated attribute if passed to it.
74
79
  include ::ActiveModel::Dirty
75
80
  end.attribute_method_matchers.map(&:suffix).select { |m| m =~ /\A_/ }
76
81
  end
82
+
83
+ # Tracks which translated attributes have been changed, separate from
84
+ # the default tracking of changes in ActiveModel/ActiveRecord Dirty.
85
+ # This is required in order for the Mobility ActiveRecord Dirty
86
+ # plugin to correctly read the value of locale accessors like
87
+ # +title_en+ in dirty tracking.
88
+ module ChangedAttributes
89
+ private
90
+
91
+ def mobility_changed_attributes
92
+ @mobility_changed_attributes ||= Set.new
93
+ end
94
+ end
77
95
  end
78
96
  end
79
97
  end
@@ -0,0 +1,39 @@
1
+ module Mobility
2
+ module Plugins
3
+ =begin
4
+
5
+ Module builder adding translated attributes to #attributes hash on model
6
+ instance. See {Mobility::Plugins::AttributeMethods} for further details.
7
+
8
+ =end
9
+ module ActiveRecord
10
+ module TranslatedAttributes
11
+ def translated_attributes
12
+ {}
13
+ end
14
+
15
+ def attributes
16
+ super.merge(translated_attributes)
17
+ end
18
+ end
19
+
20
+ class AttributeMethods < Module
21
+ def initialize(*attribute_names)
22
+ include TranslatedAttributes
23
+ define_method :translated_attributes do
24
+ super().merge(attribute_names.inject({}) do |attributes, name|
25
+ attributes.merge(name.to_s => send(name))
26
+ end)
27
+ end
28
+ delegate :translated_attribute_names, to: :class
29
+ end
30
+
31
+ def included(model_class)
32
+ model_class.class_eval do
33
+ define_method :untranslated_attributes, ::ActiveRecord::Base.instance_method(:attributes)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -8,6 +8,17 @@ module Mobility
8
8
  Dirty tracking for AR models. See {Mobility::Plugins::ActiveModel::Dirty} for
9
9
  details on usage.
10
10
 
11
+ In addition to methods added by {Mobility::Plugins::ActiveModel::Diryt}, the
12
+ AR::Dirty plugin adds support for the following persistence-specific methods
13
+ (for a model with a translated attribute +title+):
14
+ - +saved_changes+
15
+ - +saved_change_to_title?+
16
+ - +saved_change_to_title+
17
+ - +title_before_last_save+
18
+ - +will_save_change_to_title?+
19
+ - +title_change_to_be_saved+
20
+ - +title_in_database+
21
+
11
22
  =end
12
23
  module ActiveRecord
13
24
  module Dirty
@@ -19,7 +30,33 @@ details on usage.
19
30
  def initialize(*attribute_names)
20
31
  super
21
32
  @attribute_names = attribute_names
33
+ define_method_overrides
34
+ define_attribute_methods if ::ActiveRecord::VERSION::STRING >= '5.1'
35
+ end
22
36
 
37
+ # Overrides +ActiveRecord::AttributeMethods::ClassMethods#has_attribute+ and
38
+ # +ActiveModel::AttributeMethods#_read_attribute+ to treat
39
+ # fallthrough attribute methods just like "real" attribute methods.
40
+ #
41
+ # @note Patching +has_attribute?+ is necessary as of AR 5.1 due to this commit[https://github.com/rails/rails/commit/4fed08fa787a316fa51f14baca9eae11913f5050].
42
+ # (I have voiced my opposition to this change here[https://github.com/rails/rails/pull/27963#issuecomment-310092787]).
43
+ # @param [Attributes] attributes
44
+ def included(model_class)
45
+ super
46
+ names = @attribute_names
47
+ method_name_regex = /\A(#{names.join('|'.freeze)})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
48
+ has_attribute = Module.new do
49
+ define_method :has_attribute? do |attr_name|
50
+ super(attr_name) || !!method_name_regex.match(attr_name)
51
+ end
52
+ end
53
+ model_class.extend has_attribute
54
+ model_class.include ReadAttribute if ::ActiveRecord::VERSION::STRING >= '5.2'
55
+ end
56
+
57
+ private
58
+
59
+ def define_method_overrides
23
60
  changes_applied_method = ::ActiveRecord::VERSION::STRING < '5.1' ? :changes_applied : :changes_internally_applied
24
61
  define_method changes_applied_method do
25
62
  @previously_changed = changes
@@ -36,21 +73,49 @@ details on usage.
36
73
  end
37
74
  end
38
75
 
39
- # Overrides +ActiveRecord::AttributeMethods::ClassMethods#has_attribute+ to treat fallthrough attribute methods
40
- # just like "real" attribute methods.
76
+ # For AR >= 5.1 only
77
+ def define_attribute_methods
78
+ define_method :saved_changes do
79
+ (@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new).merge(super())
80
+ end
81
+
82
+ @attribute_names.each do |name|
83
+ define_method :"saved_change_to_#{name}?" do
84
+ previous_changes.include?(Mobility.normalize_locale_accessor(name))
85
+ end
86
+
87
+ define_method :"saved_change_to_#{name}" do
88
+ previous_changes[Mobility.normalize_locale_accessor(name)]
89
+ end
90
+
91
+ define_method :"#{name}_before_last_save" do
92
+ previous_changes[Mobility.normalize_locale_accessor(name)].first
93
+ end
94
+
95
+ alias_method :"will_save_change_to_#{name}?", :"#{name}_changed?"
96
+ alias_method :"#{name}_change_to_be_saved", :"#{name}_change"
97
+ alias_method :"#{name}_in_database", :"#{name}_was"
98
+ end
99
+ end
100
+
101
+ # Overrides _read_attribute to correctly dispatch reads on translated
102
+ # attributes to their respective setters, rather than to
103
+ # +@attributes+, which would otherwise return +nil+.
41
104
  #
42
- # @note Patching +has_attribute?+ is necessary as of AR 5.1 due to this commit[https://github.com/rails/rails/commit/4fed08fa787a316fa51f14baca9eae11913f5050].
43
- # (I have voiced my opposition to this change here[https://github.com/rails/rails/pull/27963#issuecomment-310092787]).
44
- # @param [Attributes] attributes
45
- def included(model_class)
46
- names = @attribute_names
47
- method_name_regex = /\A(#{names.join('|'.freeze)})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
48
- has_attribute = Module.new do
49
- define_method :has_attribute? do |attr_name|
50
- super(attr_name) || !!method_name_regex.match(attr_name)
105
+ # For background on why this is necessary, see:
106
+ # https://github.com/shioyama/mobility/issues/115
107
+ module ReadAttribute
108
+ # @note We first check if attributes has the key +attr+ to avoid
109
+ # doing any extra work in case this is a "normal"
110
+ # (non-translated) attribute.
111
+ def _read_attribute(attr, *args)
112
+ if @attributes.key?(attr)
113
+ super
114
+ else
115
+ mobility_changed_attributes.include?(attr) ? __send__(attr) : super
51
116
  end
52
117
  end
53
- model_class.extend has_attribute
118
+ private :_read_attribute
54
119
  end
55
120
  end
56
121
  end
@@ -0,0 +1,41 @@
1
+ module Mobility
2
+ module Plugins
3
+ =begin
4
+
5
+ Adds translated attribute names and values to the hash returned by #attributes.
6
+ Also adds a method #translated_attributes with names and values of translated
7
+ attributes only.
8
+
9
+ @note Adding translated attributes to +attributes+ can have unexpected
10
+ consequences, since these attributes do not have corresponding columns in the
11
+ model table. Using this plugin may lead to conflicts with other gems.
12
+
13
+ =end
14
+ module AttributeMethods
15
+ class << self
16
+ # Applies attribute_methods plugin for a given option value.
17
+ # @param [Attributes] attributes
18
+ # @param [Boolean] option Value of option
19
+ # @raise [ArgumentError] if model class does not support dirty tracking
20
+ def apply(attributes, option)
21
+ if option
22
+ include_attribute_methods_module(attributes.model_class, *attributes.names)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def include_attribute_methods_module(model_class, *attribute_names)
29
+ module_builder =
30
+ if Loaded::ActiveRecord && model_class.ancestors.include?(::ActiveRecord::AttributeMethods)
31
+ require "mobility/plugins/active_record/attribute_methods"
32
+ Plugins::ActiveRecord::AttributeMethods
33
+ else
34
+ raise ArgumentError, "#{model_class} does not support AttributeMethods plugin."
35
+ end
36
+ model_class.include module_builder.new(*attribute_names)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -14,11 +14,7 @@ details.
14
14
  @note Dirty tracking can have unexpected results when combined with fallbacks.
15
15
  A change in the fallback locale value will not mark an attribute falling
16
16
  through to that locale as changed, even though it may look like it has
17
- changed. However, when the value for the current locale is changed from nil
18
- or blank to a new value, the change will be recorded as a change from that
19
- fallback value, rather than from the nil or blank value. The specs are the
20
- most reliable source of information on the interaction between dirty tracking
21
- and fallbacks.
17
+ changed. See the specs for details on expected behavior.
22
18
 
23
19
  =end
24
20
  module Dirty
@@ -15,14 +15,20 @@ defaults to an instance of +I18n::Locale::Fallbacks+, but can be configured
15
15
  If a hash is passed to the +fallbacks+ option, a new fallbacks instance will be
16
16
  created for the model with the hash defining additional fallbacks.
17
17
 
18
- In addition, fallbacks can be disabled when reading by passing <tt>fallback:
19
- false</tt> to the reader method. This can be useful to determine the actual
20
- value of the translated attribute, including a possible +nil+ value. You can
21
- also pass a locale or array of locales to the +fallback+ option to use that
22
- locale or locales that read, e.g. <tt>fallback: :fr</tt> would fetch the French
23
- translation if the value in the current locale was +nil+, whereas <tt>fallback:
24
- [:fr, :es]</tt> would try French, then Spanish if the value in the current
25
- locale was +nil+.
18
+ In addition, fallbacks are disabled in certain situation. To explicitly disable
19
+ fallbacks when reading and writing, you can pass the <tt>fallback: false</tt>
20
+ option to the reader method. This can be useful to determine the actual
21
+ value of the translated attribute, including a possible +nil+ value.
22
+
23
+ The other situation where fallbacks are disabled is when the locale is
24
+ specified explicitly, either by passing a `locale` option to the accessor or by
25
+ using locale or fallthrough accessors. (See example below.)
26
+
27
+ You can also pass a locale or array of locales to the +fallback+ option to use
28
+ that locale or locales that read, e.g. <tt>fallback: :fr</tt> would fetch the
29
+ French translation if the value in the current locale was +nil+, whereas
30
+ <tt>fallback: [:fr, :es]</tt> would try French, then Spanish if the value in
31
+ the current locale was +nil+.
26
32
 
27
33
  @see https://github.com/svenfuchs/i18n/wiki/Fallbacks I18n Fallbacks
28
34
 
@@ -76,6 +82,26 @@ locale was +nil+.
76
82
  #=> nil
77
83
  post.title(fallback: :fr)
78
84
  #=> "Mobilité"
85
+
86
+ @example Fallbacks disabled
87
+ class Post
88
+ translates :title, fallbacks: { :'fr' => 'en' }, locale_accessors: true
89
+ end
90
+
91
+ I18n.default_locale = :en
92
+ Mobility.locale = :en
93
+ post = Post.new(title: "Mobility")
94
+
95
+ Mobility.locale = :fr
96
+ post.title
97
+ #=> "Mobility"
98
+ post.title(fallback: false)
99
+ #=> nil
100
+ post.title(locale: :fr)
101
+ #=> nil
102
+ post.title_fr
103
+ #=> nil
104
+
79
105
  =end
80
106
  class Fallbacks < Module
81
107
  # Applies fallbacks plugin to attributes.
@@ -92,17 +118,16 @@ locale was +nil+.
92
118
  private
93
119
 
94
120
  def define_read(fallbacks)
95
- define_method :read do |locale, **options|
96
- fallback = options.delete(:fallback)
97
-
98
- if fallback == false || (fallback.nil? && fallbacks.nil?)
99
- super(locale, options)
100
- else
101
- (fallback ? [locale, *fallback] : fallbacks[locale]).detect do |fallback_locale|
102
- value = super(fallback_locale, options)
103
- break value if Util.present?(value)
104
- end
121
+ define_method :read do |locale, fallback: true, **options|
122
+ return super(locale, options) if !fallback || options[:locale]
123
+
124
+ locales = fallback == true ? fallbacks[locale] : [locale, *fallback]
125
+ locales.each do |fallback_locale|
126
+ value = super(fallback_locale, options)
127
+ return value if Util.present?(value)
105
128
  end
129
+
130
+ super(locale, options)
106
131
  end
107
132
  end
108
133
 
@@ -112,7 +137,7 @@ locale was +nil+.
112
137
  elsif option == true
113
138
  Mobility.default_fallbacks
114
139
  else
115
- option
140
+ Hash.new { [] }
116
141
  end
117
142
  end
118
143
  end
@@ -46,12 +46,12 @@ model class is generated.
46
46
  def initialize(*attributes)
47
47
  method_name_regex = /\A(#{attributes.join('|'.freeze)})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
48
48
 
49
- define_method :method_missing do |method_name, *arguments, &block|
49
+ define_method :method_missing do |method_name, *arguments, **options, &block|
50
50
  if method_name =~ method_name_regex
51
51
  attribute = $1.to_sym
52
52
  locale, suffix = $2.split('_'.freeze)
53
53
  locale = "#{locale}-#{suffix.upcase}".freeze if suffix
54
- Mobility.with_locale(locale) { public_send("#{attribute}#{$4}".freeze, *arguments) }
54
+ public_send("#{attribute}#{$4}".freeze, *arguments, **options, locale: locale.to_sym)
55
55
  else
56
56
  super(method_name, *arguments, &block)
57
57
  end
@@ -58,14 +58,14 @@ If no locales are passed as an option to the initializer,
58
58
 
59
59
  define_method "#{name}_#{normalized_locale}" do |**options|
60
60
  return super() if options.delete(:super)
61
- warn warning_message if options.delete(:locale)
62
- Mobility.with_locale(locale) { send(name, options) }
61
+ warn warning_message if options[:locale]
62
+ public_send(name, **options, locale: locale)
63
63
  end
64
64
 
65
65
  define_method "#{name}_#{normalized_locale}?" do |**options|
66
66
  return super() if options.delete(:super)
67
- warn warning_message if options.delete(:locale)
68
- Mobility.with_locale(locale) { send("#{name}?", options) }
67
+ warn warning_message if options[:locale]
68
+ public_send("#{name}?", **options, locale: locale)
69
69
  end
70
70
  end
71
71
 
@@ -75,8 +75,8 @@ If no locales are passed as an option to the initializer,
75
75
 
76
76
  define_method "#{name}_#{normalized_locale}=" do |value, **options|
77
77
  return super(value) if options.delete(:super)
78
- warn warning_message if options.delete(:locale)
79
- Mobility.with_locale(locale) { send("#{name}=", value, options) }
78
+ warn warning_message if options[:locale]
79
+ public_send("#{name}=", value, **options, locale: locale)
80
80
  end
81
81
  end
82
82
  end
@@ -1,3 +1,3 @@
1
1
  module Mobility
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mobility
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Salzberg
@@ -30,7 +30,7 @@ cert_chain:
30
30
  eGDROPZoL5RXwiOnRbexxa7dcAxMrDfGB/hpiunIPWPsi4n5P7K/6OO/sGVMl9xv
31
31
  SZBPXjzrHdyOFLBYXB+PG7s3F/4=
32
32
  -----END CERTIFICATE-----
33
- date: 2017-09-14 00:00:00.000000000 Z
33
+ date: 2017-11-30 00:00:00.000000000 Z
34
34
  dependencies:
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: request_store
@@ -55,7 +55,7 @@ dependencies:
55
55
  version: 0.6.10
56
56
  - - "<"
57
57
  - !ruby/object:Gem::Version
58
- version: '0.9'
58
+ version: '0.10'
59
59
  type: :runtime
60
60
  prerelease: false
61
61
  version_requirements: !ruby/object:Gem::Requirement
@@ -65,7 +65,7 @@ dependencies:
65
65
  version: 0.6.10
66
66
  - - "<"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.9'
68
+ version: '0.10'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: bundler
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -106,14 +106,20 @@ dependencies:
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '10.0'
109
+ version: '12'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 12.2.1
110
113
  type: :development
111
114
  prerelease: false
112
115
  version_requirements: !ruby/object:Gem::Requirement
113
116
  requirements:
114
117
  - - "~>"
115
118
  - !ruby/object:Gem::Version
116
- version: '10.0'
119
+ version: '12'
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 12.2.1
117
123
  - !ruby/object:Gem::Dependency
118
124
  name: rspec
119
125
  requirement: !ruby/object:Gem::Requirement
@@ -163,7 +169,6 @@ files:
163
169
  - lib/mobility/active_model.rb
164
170
  - lib/mobility/active_model/backend_resetter.rb
165
171
  - lib/mobility/active_record.rb
166
- - lib/mobility/active_record/attribute_methods.rb
167
172
  - lib/mobility/active_record/backend_resetter.rb
168
173
  - lib/mobility/active_record/model_translation.rb
169
174
  - lib/mobility/active_record/string_translation.rb
@@ -221,7 +226,9 @@ files:
221
226
  - lib/mobility/plugins/active_model.rb
222
227
  - lib/mobility/plugins/active_model/dirty.rb
223
228
  - lib/mobility/plugins/active_record.rb
229
+ - lib/mobility/plugins/active_record/attribute_methods.rb
224
230
  - lib/mobility/plugins/active_record/dirty.rb
231
+ - lib/mobility/plugins/attribute_methods.rb
225
232
  - lib/mobility/plugins/cache.rb
226
233
  - lib/mobility/plugins/cache/translation_cacher.rb
227
234
  - lib/mobility/plugins/default.rb
metadata.gz.sig CHANGED
Binary file
@@ -1,36 +0,0 @@
1
- module Mobility
2
- module ActiveRecord
3
- =begin
4
-
5
- Included into model if model has +ActiveRecord::AttributeMethods+ among its
6
- ancestors.
7
-
8
- =end
9
- module AttributeMethods
10
- delegate :translated_attribute_names, to: :class
11
-
12
- # Adds translated attributes to +attributes+.
13
- # @return [Array<String>] Model attributes
14
- # @!method attributes
15
- def self.included(model)
16
- attributes_method = Module.new do
17
- def attributes
18
- super.merge(translated_attributes)
19
- end
20
- end
21
- model.class_eval do
22
- alias_method :untranslated_attributes, :attributes
23
- include attributes_method
24
- end
25
- end
26
-
27
- # Translated attributes defined on model.
28
- # @return [Array<String>] Translated attributes
29
- def translated_attributes
30
- translated_attribute_names.inject({}) do |attributes, name|
31
- attributes.merge(name.to_s => send(name))
32
- end
33
- end
34
- end
35
- end
36
- end