mobility 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +19 -0
  3. data/Gemfile.lock +153 -0
  4. data/Guardfile +70 -0
  5. data/README.md +603 -13
  6. data/Rakefile +42 -0
  7. data/lib/generators/mobility/install_generator.rb +45 -0
  8. data/lib/generators/mobility/templates/create_string_translations.rb +15 -0
  9. data/lib/generators/mobility/templates/create_text_translations.rb +15 -0
  10. data/lib/mobility.rb +203 -2
  11. data/lib/mobility/active_model.rb +6 -0
  12. data/lib/mobility/active_model/attribute_methods.rb +27 -0
  13. data/lib/mobility/active_model/backend_resetter.rb +26 -0
  14. data/lib/mobility/active_record.rb +39 -0
  15. data/lib/mobility/active_record/backend_resetter.rb +26 -0
  16. data/lib/mobility/active_record/model_translation.rb +14 -0
  17. data/lib/mobility/active_record/string_translation.rb +7 -0
  18. data/lib/mobility/active_record/text_translation.rb +7 -0
  19. data/lib/mobility/active_record/translation.rb +14 -0
  20. data/lib/mobility/attributes.rb +210 -0
  21. data/lib/mobility/backend.rb +152 -0
  22. data/lib/mobility/backend/active_model.rb +7 -0
  23. data/lib/mobility/backend/active_model/dirty.rb +84 -0
  24. data/lib/mobility/backend/active_record.rb +13 -0
  25. data/lib/mobility/backend/active_record/column.rb +52 -0
  26. data/lib/mobility/backend/active_record/column/query_methods.rb +40 -0
  27. data/lib/mobility/backend/active_record/hash_valued.rb +58 -0
  28. data/lib/mobility/backend/active_record/hstore.rb +36 -0
  29. data/lib/mobility/backend/active_record/hstore/query_methods.rb +53 -0
  30. data/lib/mobility/backend/active_record/jsonb.rb +43 -0
  31. data/lib/mobility/backend/active_record/jsonb/query_methods.rb +53 -0
  32. data/lib/mobility/backend/active_record/key_value.rb +126 -0
  33. data/lib/mobility/backend/active_record/key_value/query_methods.rb +63 -0
  34. data/lib/mobility/backend/active_record/query_methods.rb +36 -0
  35. data/lib/mobility/backend/active_record/serialized.rb +93 -0
  36. data/lib/mobility/backend/active_record/serialized/query_methods.rb +32 -0
  37. data/lib/mobility/backend/active_record/table.rb +197 -0
  38. data/lib/mobility/backend/active_record/table/query_methods.rb +91 -0
  39. data/lib/mobility/backend/cache.rb +110 -0
  40. data/lib/mobility/backend/column.rb +52 -0
  41. data/lib/mobility/backend/dirty.rb +28 -0
  42. data/lib/mobility/backend/fallbacks.rb +89 -0
  43. data/lib/mobility/backend/hstore.rb +21 -0
  44. data/lib/mobility/backend/jsonb.rb +21 -0
  45. data/lib/mobility/backend/key_value.rb +71 -0
  46. data/lib/mobility/backend/null.rb +24 -0
  47. data/lib/mobility/backend/orm_delegator.rb +33 -0
  48. data/lib/mobility/backend/sequel.rb +14 -0
  49. data/lib/mobility/backend/sequel/column.rb +40 -0
  50. data/lib/mobility/backend/sequel/column/query_methods.rb +24 -0
  51. data/lib/mobility/backend/sequel/dirty.rb +54 -0
  52. data/lib/mobility/backend/sequel/hash_valued.rb +51 -0
  53. data/lib/mobility/backend/sequel/hstore.rb +36 -0
  54. data/lib/mobility/backend/sequel/hstore/query_methods.rb +42 -0
  55. data/lib/mobility/backend/sequel/jsonb.rb +43 -0
  56. data/lib/mobility/backend/sequel/jsonb/query_methods.rb +42 -0
  57. data/lib/mobility/backend/sequel/key_value.rb +139 -0
  58. data/lib/mobility/backend/sequel/key_value/query_methods.rb +48 -0
  59. data/lib/mobility/backend/sequel/query_methods.rb +22 -0
  60. data/lib/mobility/backend/sequel/serialized.rb +133 -0
  61. data/lib/mobility/backend/sequel/serialized/query_methods.rb +20 -0
  62. data/lib/mobility/backend/sequel/table.rb +149 -0
  63. data/lib/mobility/backend/sequel/table/query_methods.rb +48 -0
  64. data/lib/mobility/backend/serialized.rb +53 -0
  65. data/lib/mobility/backend/table.rb +93 -0
  66. data/lib/mobility/backend_resetter.rb +44 -0
  67. data/lib/mobility/configuration.rb +31 -0
  68. data/lib/mobility/core_ext/nil.rb +10 -0
  69. data/lib/mobility/core_ext/object.rb +19 -0
  70. data/lib/mobility/core_ext/string.rb +16 -0
  71. data/lib/mobility/instance_methods.rb +34 -0
  72. data/lib/mobility/orm.rb +4 -0
  73. data/lib/mobility/sequel.rb +26 -0
  74. data/lib/mobility/sequel/backend_resetter.rb +26 -0
  75. data/lib/mobility/sequel/column_changes.rb +29 -0
  76. data/lib/mobility/sequel/model_translation.rb +20 -0
  77. data/lib/mobility/sequel/string_translation.rb +7 -0
  78. data/lib/mobility/sequel/text_translation.rb +7 -0
  79. data/lib/mobility/sequel/translation.rb +53 -0
  80. data/lib/mobility/translates.rb +75 -0
  81. data/lib/mobility/wrapper.rb +31 -0
  82. metadata +152 -12
  83. data/.gitignore +0 -9
  84. data/.rspec +0 -2
  85. data/.travis.yml +0 -5
  86. data/bin/console +0 -14
  87. data/bin/setup +0 -8
  88. data/mobility.gemspec +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 129d5dea382b25f9a274cb64d68314fc6ca4b86e
4
- data.tar.gz: 65fbb23cd0ba0151c572e81e22d53ebe2da3ebe2
3
+ metadata.gz: 9b8a61fafb34ad1bf6bbb68752bc86783129c501
4
+ data.tar.gz: abd3cc3d8c50b1ea7eac43cfe4ede3783701daf8
5
5
  SHA512:
6
- metadata.gz: cd9e46684846e986a28832a222dc1d09f13954aa51403a5bf82d6924b869e92e09543dc7976951b66bab58747bdead8709e36611ee2a3da48d66225c8a532643
7
- data.tar.gz: 60d37a1bb54ce98629f0858cb64d8fd53b48240a81a84feb2773c24f322d72c4ef8a8513ffe9bccab0be2bcdc6306f7bc690b36652e15bc55781a44f186edff6
6
+ metadata.gz: 0e83ad48432de117487fae60d0c5053d0cd72eb9ae82914d1d983d1adc1fc206ed326b200da730261b9fe76dd81dac5eb31bcb782d6094cb19efe3082fc037e1
7
+ data.tar.gz: c974558f2b6627b9e0845852000b49a5803561b9caec7370e0567998a9d85e3250d13f8640c28f4b5061060bdc2cb13b36e51b8d33479c23d48a51439a0dabbe
data/Gemfile CHANGED
@@ -2,3 +2,22 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in mobility.gemspec
4
4
  gemspec
5
+
6
+ group :development, :test do
7
+ if ENV['ORM'] == 'active_record'
8
+ gem 'activerecord', '>= 5.0', '< 5.1'
9
+ gem "generator_spec", '~> 0.9.3'
10
+ end
11
+
12
+ if ENV['ORM'] == 'sequel'
13
+ gem 'sequel', '>= 4.0.0', '< 5.0'
14
+ end
15
+
16
+ platforms :ruby do
17
+ gem 'guard-rspec'
18
+ gem 'pry-byebug'
19
+ gem 'sqlite3'
20
+ gem 'mysql2', '~> 0.3.10'
21
+ gem 'pg'
22
+ end
23
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,153 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mobility (0.0.1)
5
+ i18n
6
+ request_store (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionpack (5.0.1)
12
+ actionview (= 5.0.1)
13
+ activesupport (= 5.0.1)
14
+ rack (~> 2.0)
15
+ rack-test (~> 0.6.3)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
+ actionview (5.0.1)
19
+ activesupport (= 5.0.1)
20
+ builder (~> 3.1)
21
+ erubis (~> 2.7.0)
22
+ rails-dom-testing (~> 2.0)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
24
+ activemodel (5.0.1)
25
+ activesupport (= 5.0.1)
26
+ activerecord (5.0.1)
27
+ activemodel (= 5.0.1)
28
+ activesupport (= 5.0.1)
29
+ arel (~> 7.0)
30
+ activesupport (5.0.1)
31
+ concurrent-ruby (~> 1.0, >= 1.0.2)
32
+ i18n (~> 0.7)
33
+ minitest (~> 5.1)
34
+ tzinfo (~> 1.1)
35
+ arel (7.1.4)
36
+ builder (3.2.3)
37
+ byebug (9.0.6)
38
+ coderay (1.1.1)
39
+ concurrent-ruby (1.0.4)
40
+ database_cleaner (1.5.3)
41
+ diff-lcs (1.3)
42
+ erubis (2.7.0)
43
+ ffi (1.9.17)
44
+ formatador (0.2.5)
45
+ generator_spec (0.9.3)
46
+ activesupport (>= 3.0.0)
47
+ railties (>= 3.0.0)
48
+ guard (2.14.0)
49
+ formatador (>= 0.2.4)
50
+ listen (>= 2.7, < 4.0)
51
+ lumberjack (~> 1.0)
52
+ nenv (~> 0.1)
53
+ notiffany (~> 0.0)
54
+ pry (>= 0.9.12)
55
+ shellany (~> 0.0)
56
+ thor (>= 0.18.1)
57
+ guard-compat (1.2.1)
58
+ guard-rspec (4.7.3)
59
+ guard (~> 2.1)
60
+ guard-compat (~> 1.1)
61
+ rspec (>= 2.99.0, < 4.0)
62
+ i18n (0.7.0)
63
+ listen (3.1.5)
64
+ rb-fsevent (~> 0.9, >= 0.9.4)
65
+ rb-inotify (~> 0.9, >= 0.9.7)
66
+ ruby_dep (~> 1.2)
67
+ loofah (2.0.3)
68
+ nokogiri (>= 1.5.9)
69
+ lumberjack (1.0.11)
70
+ method_source (0.8.2)
71
+ mini_portile2 (2.1.0)
72
+ minitest (5.10.1)
73
+ mysql2 (0.3.21)
74
+ nenv (0.3.0)
75
+ nokogiri (1.7.0.1)
76
+ mini_portile2 (~> 2.1.0)
77
+ notiffany (0.1.1)
78
+ nenv (~> 0.1)
79
+ shellany (~> 0.0)
80
+ pg (0.19.0)
81
+ pry (0.10.4)
82
+ coderay (~> 1.1.0)
83
+ method_source (~> 0.8.1)
84
+ slop (~> 3.4)
85
+ pry-byebug (3.4.2)
86
+ byebug (~> 9.0)
87
+ pry (~> 0.10)
88
+ rack (2.0.1)
89
+ rack-test (0.6.3)
90
+ rack (>= 1.0)
91
+ rails-dom-testing (2.0.2)
92
+ activesupport (>= 4.2.0, < 6.0)
93
+ nokogiri (~> 1.6)
94
+ rails-html-sanitizer (1.0.3)
95
+ loofah (~> 2.0)
96
+ railties (5.0.1)
97
+ actionpack (= 5.0.1)
98
+ activesupport (= 5.0.1)
99
+ method_source
100
+ rake (>= 0.8.7)
101
+ thor (>= 0.18.1, < 2.0)
102
+ rake (10.5.0)
103
+ rb-fsevent (0.9.8)
104
+ rb-inotify (0.9.7)
105
+ ffi (>= 0.5.0)
106
+ request_store (1.3.2)
107
+ rspec (3.5.0)
108
+ rspec-core (~> 3.5.0)
109
+ rspec-expectations (~> 3.5.0)
110
+ rspec-mocks (~> 3.5.0)
111
+ rspec-core (3.5.4)
112
+ rspec-support (~> 3.5.0)
113
+ rspec-expectations (3.5.0)
114
+ diff-lcs (>= 1.2.0, < 2.0)
115
+ rspec-support (~> 3.5.0)
116
+ rspec-its (1.2.0)
117
+ rspec-core (>= 3.0.0)
118
+ rspec-expectations (>= 3.0.0)
119
+ rspec-mocks (3.5.0)
120
+ diff-lcs (>= 1.2.0, < 2.0)
121
+ rspec-support (~> 3.5.0)
122
+ rspec-support (3.5.0)
123
+ ruby_dep (1.5.0)
124
+ shellany (0.0.1)
125
+ slop (3.6.0)
126
+ sqlite3 (1.3.13)
127
+ thor (0.19.4)
128
+ thread_safe (0.3.5)
129
+ tzinfo (1.2.2)
130
+ thread_safe (~> 0.1)
131
+ yard (0.9.8)
132
+
133
+ PLATFORMS
134
+ ruby
135
+
136
+ DEPENDENCIES
137
+ activerecord (>= 5.0, < 5.1)
138
+ bundler (~> 1.12)
139
+ database_cleaner (~> 1.5.3)
140
+ generator_spec (~> 0.9.3)
141
+ guard-rspec
142
+ mobility!
143
+ mysql2 (~> 0.3.10)
144
+ pg
145
+ pry-byebug
146
+ rake (~> 10.0)
147
+ rspec (~> 3.0)
148
+ rspec-its (~> 1.2.0)
149
+ sqlite3
150
+ yard
151
+
152
+ BUNDLED WITH
153
+ 1.12.5
data/Guardfile ADDED
@@ -0,0 +1,70 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features) \
6
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
+
8
+ ## Note: if you are using the `directories` clause above and you are not
9
+ ## watching the project directory ('.'), then you will want to move
10
+ ## the Guardfile to a watched dir and symlink it back, e.g.
11
+ #
12
+ # $ mkdir config
13
+ # $ mv Guardfile config/
14
+ # $ ln -s config/Guardfile .
15
+ #
16
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
+
18
+ # Note: The cmd option is now required due to the increasing number of ways
19
+ # rspec may be run, below are examples of the most common uses.
20
+ # * bundler: 'bundle exec rspec'
21
+ # * bundler binstubs: 'bin/rspec'
22
+ # * spring: 'bin/rspec' (This will use spring if running and you have
23
+ # installed the spring binstubs per the docs)
24
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
25
+ # * 'just' rspec: 'rspec'
26
+
27
+ guard :rspec, cmd: "bundle exec rspec" do
28
+ require "guard/rspec/dsl"
29
+ dsl = Guard::RSpec::Dsl.new(self)
30
+
31
+ # Feel free to open issues for suggestions and improvements
32
+
33
+ # RSpec files
34
+ rspec = dsl.rspec
35
+ watch(rspec.spec_helper) { rspec.spec_dir }
36
+ watch(rspec.spec_support) { rspec.spec_dir }
37
+ watch(rspec.spec_files)
38
+
39
+ # Ruby files
40
+ ruby = dsl.ruby
41
+ dsl.watch_spec_files_for(ruby.lib_files)
42
+
43
+ # Rails files
44
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
45
+ dsl.watch_spec_files_for(rails.app_files)
46
+ dsl.watch_spec_files_for(rails.views)
47
+
48
+ watch(rails.controllers) do |m|
49
+ [
50
+ rspec.spec.call("routing/#{m[1]}_routing"),
51
+ rspec.spec.call("controllers/#{m[1]}_controller"),
52
+ rspec.spec.call("acceptance/#{m[1]}")
53
+ ]
54
+ end
55
+
56
+ # Rails config changes
57
+ watch(rails.spec_helper) { rspec.spec_dir }
58
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
59
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
60
+
61
+ # Capybara features specs
62
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
63
+ watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
64
+
65
+ # Turnip features and steps
66
+ watch(%r{^spec/acceptance/(.+)\.feature$})
67
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
68
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
69
+ end
70
+ end
data/README.md CHANGED
@@ -1,41 +1,631 @@
1
1
  # Mobility
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/mobility`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Mobility is a gem for storing and retrieving localized data through attributes
4
+ on a class. A variety of different storage strategies are supported through
5
+ pluggable, customizable "backends" implemented via a common interface.
4
6
 
5
- TODO: Delete this and the text above, and describe your gem
7
+ Out of the box, Mobility supports:
8
+
9
+ - translations as localized columns on the model table (like [Traco](https://github.com/barsoom/traco))
10
+ - translations on a model-specific table (like [Globalize](https://github.com/globalize/globalize))
11
+ - translations as values on globally shared key-value tables (the default, see [below](#backend))
12
+ - translations as values of a hash serialized on a text column of the model table (like [Multilang](https://github.com/artworklv/multilang))
13
+ - translations as values of a hash stored as an hstore column on a Postgres model table (like [Trasto](https://github.com/yabawock/trasto), [Multilang-hstore](https://github.com/bithavoc/multilang-hstore), [hstore_translate](https://github.com/Leadformance/hstore_translate), etc.)
14
+ - translations as values of a hash stored as a jsonb column on a Postgres model table (like [json_translate](https://github.com/cfabianski/json_translate))
15
+
16
+ Each backend is implemented for both
17
+ [ActiveRecord](http://api.rubyonrails.org/classes/ActiveRecord/Base.html) and
18
+ [Sequel](http://sequel.jeremyevans.net/) ORM, including a common interface for
19
+ [querying](#querying) the database on translated attributes using extended
20
+ scopes/datasets. Mobility is however flexible enough to support any storage
21
+ strategy, including ones not backed by a database.
22
+
23
+ All backends can optionally enable any of a set of common, ORM-independent
24
+ features, including:
25
+
26
+ - a [cache](#cache) to improve read/write performance (included by default)
27
+ - translation [fallbacks](#fallbacks), in case a translation is missing in a
28
+ given locale
29
+ - (for classes that support it) [dirty](#dirty) tracking of changed attributes
30
+ (`ActiveModel::Dirty` in Rails)
31
+ - [locale-specific accessors](#locale-accessors) for translated attributes, of
32
+ the form `<attribute>_<locale>` (similar to
33
+ [globalize-accessors](https://github.com/globalize/globalize-accessors))
6
34
 
7
35
  ## Installation
8
36
 
9
37
  Add this line to your application's Gemfile:
10
38
 
11
39
  ```ruby
12
- gem 'mobility'
40
+ gem 'mobility', git: "https://github.com/shioyama/mobility.git"
41
+ ```
42
+
43
+ To translate attributes on a model, you must include (or extend) `Mobility`,
44
+ then call `translates` specifying the backend to use and any backend-specific
45
+ options.
46
+
47
+ ### ActiveRecord (Rails)
48
+
49
+ Requirements:
50
+ - ActiveRecord >= 5.0
51
+
52
+ If using Mobility in a Rails project, you can run the generator to create an
53
+ initializer and (optionally) a migration to create shared tables for the
54
+ default key-value backend:
55
+
56
+ ```
57
+ rails generate mobility:install
58
+ ```
59
+
60
+ To skip the migration (if you do not plan to use the default `KeyValue`
61
+ backend), use the `--without_tables` option:
62
+
63
+ ```
64
+ rails generate mobility:install --without_tables
65
+ ```
66
+
67
+ The generator will create an initializer file `config/initializers/mobility.rb`
68
+ with the line:
69
+
70
+ ```
71
+ Mobility.config.default_backend = :key_value
72
+ ```
73
+
74
+ To set a different default backend, set `default_backend` to another value (see
75
+ possibilities below). Other configuration options can be set using the
76
+ `configure` method, see: {Mobility::Configuration} for details.
77
+
78
+ The default key-value backend, which stores attributes and their translations
79
+ as key/value pairs on shared tables, can be included in a model with the
80
+ following two lines:
81
+
82
+ ```ruby
83
+ class Post < ActiveRecord::Base
84
+ include Mobility
85
+ translates :title, :author, backend: :key_value, type: :string
86
+ translates :content, backend: :key_value, type: :text
87
+ end
13
88
  ```
14
89
 
15
- And then execute:
90
+ You can now store translations of `title`, `author` and `content` on shared
91
+ translation tables (a string-valued translation table for the first two, and a
92
+ text-valued translation table for the last one). For more information on
93
+ backends, see [Choosing a Backend](#backend).
94
+
95
+ ### Sequel
16
96
 
17
- $ bundle
97
+ Requirements:
98
+ - Sequel >= 4.0
18
99
 
19
- Or install it yourself as:
100
+ Essentially identical to ActiveRecord, with the exception that there is no
101
+ equivalent to a Rails generator (so you will need to create the migration for
102
+ the translation table(s) yourself, see the API docs for details).
20
103
 
21
- $ gem install mobility
104
+ To include translations on a model, simply call `translates`:
105
+
106
+ ```ruby
107
+ class Post < Sequel::Model
108
+ include Mobility
109
+ translates :title, :author, backend: :key_value, type: :string
110
+ translates :content, backend: :key_value, type: :text
111
+ end
112
+ ```
113
+
114
+ Note that Mobility will detect the parent class and use an ORM-specific
115
+ backend, in this case the {Mobility::Backend::Sequel::KeyValue} backend.
22
116
 
23
117
  ## Usage
24
118
 
25
- TODO: Write usage instructions here
119
+ ### Setting the Locale
120
+
121
+ Similar to [Globalize](https://github.com/globalize/globalize), Mobility has
122
+ its own `locale` which defaults to the value of `I18n.locale` but can also be
123
+ set independently with a setter:
124
+
125
+ ```ruby
126
+ I18n.locale = :en
127
+ Mobility.locale #=> :en
128
+ Mobility.locale = :fr
129
+ Mobility.locale #=> :fr
130
+ I18n.locale #=> :en
131
+ ```
132
+
133
+ To set the Mobility locale in a block, use {Mobility.with_locale}:
134
+
135
+ ```ruby
136
+ Mobility.locale = :en
137
+ Mobility.with_locale(:ja) do
138
+ Mobility.locale #=> :ja
139
+ end
140
+ Mobility.locale #=> :en
141
+ ```
142
+
143
+ ### Getting and Setting Translations
144
+
145
+ Mobility defines getter, setter, and presence methods for translated attributes
146
+ on the model class. Regardless of which backend you use to store translations,
147
+ the basic interface for accessing them is the same.
148
+
149
+ Assuming we have a model `Post` as above, we can first set the locale, then
150
+ create a post with a translated attribute:
151
+
152
+ ```ruby
153
+ Mobility.locale = :en
154
+ post = Post.create(title: "Mobility")
155
+ post.title
156
+ #=> "Mobility"
157
+ post.title?
158
+ #=> true
159
+ ```
160
+
161
+ Attributes can similarly be written just like a normal attribute:
162
+
163
+ ```ruby
164
+ post.title = "Mobility (noun): quality of being changeable, adaptable or versatile"
165
+ post.title
166
+ #=> "Mobility (noun): quality of being changeable, adaptable or versatile"
167
+ ```
168
+
169
+ If you change locale, you will read/write the attribute in that locale:
170
+
171
+ ```ruby
172
+ Mobility.locale = :ja
173
+ post.title
174
+ #=> nil
175
+ post.title?
176
+ #=> false
177
+ post.title = "Mobility(名詞):動きやすさ、可動性"
178
+ post.title
179
+ #=> "Mobility(名詞):動きやすさ、可動性"
180
+ post.title?
181
+ #=> true
182
+ ```
183
+
184
+ Internally, Mobility maps the `title` accessor method to a backend, which then
185
+ handles reading and writing of data. You can access the backend instance for a
186
+ given attribute with `<attribute>_backend`, in this case `post.title_backend`,
187
+ and read and write locale values directly to/from the backend (although this
188
+ should not generally be necessary):
189
+
190
+ ```ruby
191
+ post.title_backend.read(:ja)
192
+ #=> "Mobility(名詞):動きやすさ、可動性"
193
+ post.title_backend.read(:en)
194
+ #=> "Mobility (noun): quality of being changeable, adaptable or versatile"
195
+ ```
196
+
197
+ You can also access different locales by passing the locale into the getter
198
+ method in the options hash:
199
+
200
+ ```ruby
201
+ post.title(locale: :ja)
202
+ #=> "Mobility(名詞):動きやすさ、可動性"
203
+ post.title(locale: :en)
204
+ #=> "Mobility (noun): quality of being changeable, adaptable or versatile"
205
+ ```
206
+
207
+ The translated value can be written using the backend's `write` method:
208
+
209
+ ```ruby
210
+ post.title_backend.write(:en, "new title")
211
+ post.save
212
+ post.title
213
+ #=> "new title"
214
+ post.title_backend.write(:en, "Mobility (noun): quality of being changeable, adaptable or versatile")
215
+ post.save
216
+ post.title
217
+ #=> "Mobility (noun): quality of being changeable, adaptable or versatile"
218
+ ```
219
+
220
+ Backends vary in how they implement reading and writing of translated
221
+ attributes. The default {Mobility::Backend::KeyValue} backend stores these translations on two
222
+ shared tables, `mobility_string_translations` and `mobility_text_translations`,
223
+ depending on the `type` of the attribute (corresponding to the type of column
224
+ used).
225
+
226
+ For more details on backend-specific options, see the documentation for each
227
+ backend ([below](#backend)).
228
+
229
+ ### <a name="backend"></a>Choosing a Backend
230
+
231
+ Mobility supports six different (database) backends:
232
+
233
+ - **{Mobility::Backend::Column}**<br>
234
+ Store translations as columns on a table with locale as a postfix, of the
235
+ form `title_en`, `title_fr`, etc. for an attribute `title`.
236
+ - **{Mobility::Backend::Table}**<br>
237
+ Store translations on a model-specific table, e.g. for a model `Post` with
238
+ table `posts`, store translations on a table `post_translations`, and join
239
+ the translation table when fetching translated values.
240
+ - **{Mobility::Backend::KeyValue}**<br>
241
+ Store translations on a shared table of locale/attribute translation pairs,
242
+ associated through a polymorphic relation with multiple models.
243
+ - **{Mobility::Backend::Serialized}**<br>
244
+ Store translations as serialized YAML or JSON on a text column.
245
+ - **{Mobility::Backend::Hstore}**<br>
246
+ Store translations as values of a hash stored as a PostgreSQL hstore column.
247
+ - **{Mobility::Backend::Jsonb}**<br>
248
+ Store translations as values of a hash stored as a PostgreSQL jsonb column.
249
+
250
+ Each backend has strengths and weaknesses. If you're unsure of which backend to
251
+ use, a rule of thumb would be:
252
+
253
+ - If you're using PostgreSQL as your database, use {Mobility::Backend::Jsonb}.
254
+ - If you have a fixed, small set of locales that are not likely to increase,
255
+ and have a small number of models to translate, consider
256
+ {Mobility::Backend::Column}.
257
+ - If you have a small set of models to be translated but translation to
258
+ potentially many different languages, consider {Mobility::Backend::Table}.
259
+ - For all other cases (many locales, many translated models), or if you're just
260
+ not sure, the recommended solution is {Mobility::Backend::KeyValue} for
261
+ maximum flexibility and minimum database migrations.
262
+
263
+
264
+ ### <a name="locale-accessors"></a>Locale Accessors
265
+
266
+ It can sometimes be more convenient to access translations through dedicated
267
+ locale-specific methods (for example to update multiple locales at once in a
268
+ form). For this purpose, Mobility has a `locale_accessors` option that can be
269
+ used to define such methods on a given class:
270
+
271
+ ```ruby
272
+ class Post < ActiveRecord::Base
273
+ include Mobility
274
+ translates :title, locale_accessors: [:en, :ja]
275
+ end
276
+ ```
277
+
278
+ (Note: The backend defaults to `key_value`, and `type` defaults to `text`, but
279
+ options described here are independent of backend so we will omit both for what
280
+ follows.)
281
+
282
+ Since we have enabled locale accessors for English and Japanese, we can access
283
+ translations for these locales with:
284
+
285
+ ```ruby
286
+ post.title_en
287
+ #=> "Mobility (noun): quality of being changeable, adaptable or versatile"
288
+ post.title_ja
289
+ #=> "Mobility(名詞):動きやすさ、可動性"
290
+ post.title_en = "foo"
291
+ post.title
292
+ #=> "foo"
293
+ ```
294
+
295
+ Alternatively, just using `locale_accessors: true` will enable all locales in
296
+ `I18n.available_locales`.
297
+
298
+ For more details, see: {Mobility::Attributes} (specifically, the private method
299
+ `define_locale_accessors`).
300
+
301
+ ### <a name="cache"></a>Cache
302
+
303
+ The Mobility cache caches localized values that have been fetched once so they
304
+ can be quickly retrieved again, and also speeds up writes for some backends.
305
+ The cache is enabled by default and should generally only be disabled when
306
+ debugging; this can be done by passing `cache: false` to any backend.
307
+
308
+ In general, you should not need to actually see the cache, but for debugging
309
+ purposes you can access it by calling the private `cache` method on the
310
+ backend:
311
+
312
+ ```ruby
313
+ post.title_backend.send :cache
314
+ #=> #<Mobility::Backend::KeyValue::TranslationsCache:0x0056139b391b38 @cache={}>
315
+ ```
316
+
317
+ For more details, see: {Mobility::Backend::Cache}.
318
+
319
+ ### <a name="fallbacks"></a>Fallbacks
320
+
321
+ Mobility offers basic support for translation fallbacks (similar to gems such
322
+ as [Globalize](https://github.com/globalize/globalize) and
323
+ [Traco](https://github.com/barsoom/traco)). To enable fallbacks, pass a hash
324
+ with fallbacks for each locale as an option to the backend:
325
+
326
+ ```ruby
327
+ class Post < ActiveRecord::Base
328
+ include Mobility
329
+ translates :title, locale_accessors: [:en, :ja, :fr], fallbacks: { en: :ja, fr: :ja }
330
+ end
331
+ ```
332
+
333
+ By setting fallbacks for English and French to Japanese, values will fall
334
+ through to the Japanese value if none is present for either of these locales:
335
+
336
+ ```ruby
337
+ Mobility.locale = :en
338
+ post = Post.first
339
+ post.title = nil
340
+ post.save
341
+ post.title_en
342
+ #=> "Mobility(名詞):動きやすさ、可動性"
343
+ post.title_ja
344
+ #=> "Mobility(名詞):動きやすさ、可動性"
345
+ post.title_fr
346
+ #=> "Mobility(名詞):動きやすさ、可動性"
347
+ ```
348
+
349
+ You can optionally disable fallbacks to get the real value for a given locale
350
+ (for example, to check if a value in a particular locale is set or not) by
351
+ passing `fallbacks: false` to the getter method:
352
+
353
+ ```ruby
354
+ post.title(fallbacks: false)
355
+ #=> nil
356
+ post.title_fr(fallbacks: false)
357
+ #=> nil
358
+ ```
359
+
360
+ (Mobility assigns the fallbacks hash to an instance of
361
+ `I18n::Locale::Fallbacks.new`.)
362
+
363
+ For more details, see: {Mobility::Backend::Fallbacks}.
364
+
365
+ ### <a name="dirty"></a>Dirty Tracking
366
+
367
+ Dirty tracking (tracking of changed attributes) can be enabled for models which support it. Currently this includes models including `ActiveModel::Dirty` or Sequel models with the `dirty` plugin enabled.
368
+
369
+ Enabling dirty tracking is as simple as sending the `dirty: true` option to any
370
+ backend. The way dirty tracking works is somewhat dependent on the model class
371
+ (ActiveModel or Sequel); we will describe the ActiveModel implementation here.
372
+
373
+ First, enable dirty tracking (note that this is a persisted AR model, although
374
+ dirty tracking is not specific to AR and works for non-persisted models as well):
375
+
376
+ ```ruby
377
+ class Post < ActiveRecord::Base
378
+ include Mobility
379
+ translates :title, locale_accessors: [:en, :ja], dirty: true
380
+ end
381
+ ```
382
+
383
+ Now set the attribute in both locales:
384
+
385
+ ```ruby
386
+ post.title
387
+ #=> "Mobility (noun): quality of being changeable, adaptable or versatile"
388
+ post.title = "a new title"
389
+ post.title_ja
390
+ #=> "Mobility(名詞):動きやすさ、可動性"
391
+ post.title = "新しいタイトル"
392
+ ```
393
+
394
+ Now you can use dirty methods as you would any other (untranslated) attribute:
395
+
396
+ ```ruby
397
+ post.title_was
398
+ #=> "Mobility (noun): quality of being changeable, adaptable or versatile"
399
+ Mobility.locale = :ja
400
+ post.title_was
401
+ #=> "Mobility(名詞):動きやすさ、可動性"
402
+ post.changed
403
+ ["title_en", "title_ja"]
404
+ post.save
405
+ ```
406
+
407
+ You can also access `previous_changes`:
408
+
409
+ ```ruby
410
+ post.previous_changes
411
+ #=>
412
+ {
413
+ "title_en" =>
414
+ [
415
+ "Mobility (noun): quality of being changeable, adaptable or versatile",
416
+ "a new title"
417
+ ],
418
+ "title_ja" =>
419
+ [
420
+ "Mobility(名詞):動きやすさ、可動性",
421
+ "新しいタイトル"
422
+ ]
423
+ }
424
+ ```
425
+
426
+ You will notice that Mobility uses locale accessors to indicate which locale
427
+ has changed; dirty tracking is implemented this way to ensure that it is clear
428
+ what has changed in which locale, avoiding any possible ambiguity.
429
+
430
+ For more details, see: {Mobility::Backend::Dirty}.
431
+
432
+ ### <a name="querying"></a>Querying
433
+
434
+ Database-backed Mobility backends also optionally support querying through
435
+ `where` and other query methods (`not` and `find_by` for ActiveRecord models,
436
+ `except` for Sequel models, etc). To query on these attributes, use the `i18n`
437
+ class method, which will return a model relation extended with
438
+ Mobility-specific query method overrides.
439
+
440
+ So assuming a model:
441
+
442
+ ```ruby
443
+ class Post < ActiveRecord::Base
444
+ include Mobility
445
+ translates :title, backend: :key_value, type: :string
446
+ translates :content, backend: :key_value, type: :text
447
+ end
448
+ ```
449
+
450
+ we can query for posts with title "foo" and content "bar" just as we would
451
+ query on untranslated attributes, and Mobility will convert the queries to
452
+ whatever the backend requires to actually return the correct results:
453
+
454
+ ```ruby
455
+ Post.i18n.find_by(title: "foo", content: "bar")
456
+ ```
457
+
458
+ results in the SQL:
459
+
460
+ ```sql
461
+ SELECT "posts".* FROM "posts"
462
+ INNER JOIN "mobility_string_translations" "title_mobility_string_translations"
463
+ ON "title_mobility_string_translations"."key" = 'title'
464
+ AND "title_mobility_string_translations"."locale" = 'en'
465
+ AND "title_mobility_string_translations"."translatable_type" = 'Post'
466
+ AND "title_mobility_string_translations"."translatable_id" = "posts"."id"
467
+ INNER JOIN "mobility_text_translations" "content_mobility_text_translations"
468
+ ON "content_mobility_text_translations"."key" = 'content'
469
+ AND "content_mobility_text_translations"."locale" = 'en'
470
+ AND "content_mobility_text_translations"."translatable_type" = 'Post'
471
+ AND "content_mobility_text_translations"."translatable_id" = "posts"."id"
472
+ WHERE "content_mobility_text_translations"."value" = 'bar' AND
473
+ "title_mobility_string_translations"."value" = 'foo'
474
+ ```
475
+
476
+ As can be seen in the query above, behind the scenes Mobility joins two tables,
477
+ one with string translations and one with text translations, and aliases the
478
+ joins for each attribute so as to match the particular values passed in to the
479
+ query. Details of how this is done can be found in
480
+ {Mobility::Backend::ActiveRecord::QueryMethods}.
481
+
482
+ Note that this feature is available for all backends *except* the `serialized`
483
+ backend, since serialized database values are not query-able (an
484
+ `ArgumentError` error will be raised if you try to query on attributes of this
485
+ backend).
486
+
487
+ For more details, see subclasses of
488
+ {Mobility::Backend::ActiveRecord::QueryMethods} or
489
+ {Mobility::Backend::Sequel::QueryMethods}.
490
+
491
+ ## Philosophy
492
+
493
+ As its name implies, Mobility was created with a very specific design goal: to
494
+ separate the problem of translating model attributes from the constraints of
495
+ any particular translation solution, so that application designers are free to
496
+ mix, match and customize strategies to suit their needs.
497
+
498
+ To this end, Mobility backends strictly enforce the rule that *no backend
499
+ should modify a parent class in any way which would interfere with other
500
+ backends operating on the same class*. This is done using a heavy dose of
501
+ metaprogramming, details of which can be found in the [API
502
+ documentation](http://www.rubydoc.info/gems/mobility) and in the actual code.
503
+
504
+ In practice, this means that you can use different backends for different
505
+ attributes *on the same class* without any conflict, e.g. (assuming we
506
+ are using Postgres as our database):
507
+
508
+ ```ruby
509
+ class Post < ActiveRecord::Base
510
+ include Mobility
511
+ translates :title, backend: :key_value, type: :string
512
+ translates :content, backend: :column, cache: false
513
+ translates :author_name, backend: :jsonb
514
+ end
515
+ ```
516
+
517
+ Attributes can be set and fetched and Mobility will transparently handle
518
+ reading and writing through the respective backend: a shared
519
+ `mobility_string_translations` table for `title`, the `content_en` and
520
+ `content_ja` columns on the `posts` table for `content`, and JSON keys and
521
+ values on the jsonb `author_name` column for `author_name`.
522
+
523
+ Similarly, we can query for a particular post using the `i18n` scope without worrying about how attributes are actually stored. So this query:
524
+
525
+ ```ruby
526
+ Post.i18n.where(title: "foo",
527
+ content: "bar",
528
+ author_name: "baz")
529
+ ```
530
+
531
+ will result in the following SQL:
532
+
533
+ ```sql
534
+ SELECT "posts".* FROM "posts"
535
+ INNER JOIN "mobility_string_translations" "title_mobility_string_translations"
536
+ ON "title_mobility_string_translations"."key" = 'title'
537
+ AND "title_mobility_string_translations"."locale" = 'en'
538
+ AND "title_mobility_string_translations"."translatable_type" = 'Post'
539
+ AND "title_mobility_string_translations"."translatable_id" = "posts"."id"
540
+ WHERE (posts.author_name @> ('{"en":"baz"}')::jsonb)
541
+ AND "posts"."content_en" = 'bar'
542
+ AND "title_mobility_string_translations"."value" = 'foo'
543
+ ```
544
+
545
+ The query combines conditions specific to each backend, together fetching the
546
+ record which satisfies all of them.
547
+
548
+ Beyond the goal of making it easy to combine backends in a single class (which
549
+ admittedly is a rather specialized use-case), the flexibility Mobility enforces
550
+ makes it possible to build more complex translation-based applications without
551
+ worrying about the details of the translation storage strategy used. It also
552
+ saves effort in integrating translation storage with various other gems, since
553
+ only one integration is required rather than one for each translation gem.
26
554
 
27
555
  ## Development
28
556
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
557
+ ### Custom Backends
558
+
559
+ Although Mobility is primarily oriented toward storing ActiveRecord model
560
+ translations, it can potentially be used to handle storing translations in
561
+ other formats, for example in the cloud through an API, or in files. In
562
+ particular, the features mentioned above (locale accessors, caching, fallbacks,
563
+ dirty tracking to some degree) are not specific to database storage.
564
+
565
+ To use a custom backend, simply pass the name of a class which includes
566
+ `Mobility::Backend` to `translates`:
567
+
568
+ ```ruby
569
+ class MyBackend
570
+ include Mobility::Backend
571
+ # ...
572
+ end
573
+
574
+ class MyClass
575
+ include Mobility
576
+ translates :foo, backend: MyBackend
577
+ end
578
+ ```
579
+
580
+ For details on how to define a backend class, see the {Mobility::Backend}
581
+ module and other classes defined in the [API
582
+ documentation](http://www.rubydoc.info/gems/mobility).
583
+
584
+ ### Testing Backends
585
+
586
+ All included backends are tested against a suite of shared specs which ensure
587
+ they conform to the same expected behaviour. These examples can be found in:
30
588
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
589
+ - `spec/support/shared_examples/accessor_examples.rb` (minimal specs testing
590
+ translation setting/getting)
591
+ - `spec/support/shared_examples/querying_examples.rb` (specs for
592
+ [querying](#querying))
593
+ - `spec/support/shared_examples/serialization_examples.rb` (specialized specs
594
+ for backends which store translations as a Hash: `serialized`, `hstore` and
595
+ `jsonb` backends)
32
596
 
33
- ## Contributing
597
+ A minimal test can simply define a model class and use helpers defined in
598
+ `spec/support/helpers.rb` to run these examples, by extending either
599
+ `Helpers::ActiveRecord` or `Helpers::Sequel`:
34
600
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/mobility.
601
+ ```ruby
602
+ describe MyBackend do
603
+ extend Helpers::ActiveRecord
604
+
605
+ before do
606
+ stub_const 'MyPost', Class.new(ActiveRecord::Base)
607
+ MyPost.include Mobility
608
+ MyPost.translates :title, :content, backend: MyBackend
609
+ end
36
610
 
611
+ include_accessor_examples 'MyPost'
612
+ include_querying_examples 'MyPost'
613
+ # ...
614
+ end
615
+ ```
616
+
617
+ Shared examples expect the model class to have translated attributes `title`
618
+ and `content`, and an untranslated boolean column `published`. These defaults
619
+ can be changed, see the shared examples for details.
620
+
621
+ Backends are also each tested against specialized specs targeted at their
622
+ particular implementations.
623
+
624
+ ## More Information
625
+
626
+ - [Github repository](https://www.github.com/shioyama/mobility)
627
+ - [API documentation](http://www.rubydoc.info/gems/mobility)
37
628
 
38
629
  ## License
39
630
 
40
631
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
-