mobility 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
-