mobility 0.1.10 → 0.1.11
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -1
- data/Gemfile.lock +3 -1
- data/README.md +428 -411
- data/lib/generators/rails/mobility/backend_generators/base.rb +69 -0
- data/lib/generators/rails/mobility/backend_generators/column_backend.rb +15 -0
- data/lib/generators/rails/mobility/backend_generators/table_backend.rb +42 -0
- data/lib/generators/rails/mobility/generators.rb +5 -0
- data/lib/generators/rails/mobility/templates/column_translations.rb +17 -0
- data/lib/generators/rails/mobility/templates/table_migration.rb +17 -0
- data/lib/generators/rails/mobility/templates/table_translations.rb +28 -0
- data/lib/generators/rails/mobility/translations_generator.rb +75 -0
- data/lib/mobility.rb +10 -3
- data/lib/mobility/attributes.rb +4 -4
- data/lib/mobility/backend/active_model/dirty.rb +0 -2
- data/lib/mobility/backend/active_record/column.rb +15 -2
- data/lib/mobility/backend/active_record/table.rb +9 -0
- data/lib/mobility/backend/column.rb +11 -16
- data/lib/mobility/backend/sequel/column.rb +8 -2
- data/lib/mobility/backend/sequel/dirty.rb +0 -2
- data/lib/mobility/backend/table.rb +5 -0
- data/lib/mobility/fallthrough_accessors.rb +2 -3
- data/lib/mobility/instance_methods.rb +5 -4
- data/lib/mobility/rails.rb +1 -2
- data/lib/mobility/version.rb +1 -1
- metadata +10 -2
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
require "rails/generators/active_record/migration/migration_generator"
|
3
|
+
|
4
|
+
module Mobility
|
5
|
+
module BackendGenerators
|
6
|
+
class Base < ::Rails::Generators::NamedBase
|
7
|
+
argument :attributes, type: :array, default: []
|
8
|
+
include ::ActiveRecord::Generators::Migration
|
9
|
+
|
10
|
+
def create_migration_file
|
11
|
+
if self.class.migration_exists?(migration_dir, migration_file)
|
12
|
+
::Kernel.warn "Migration already exists: #{migration_file}"
|
13
|
+
else
|
14
|
+
migration_template "#{template}.rb", "db/migrate/#{migration_file}.rb"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.next_migration_number(dirname)
|
19
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
20
|
+
end
|
21
|
+
|
22
|
+
def backend
|
23
|
+
self.class.name.split('::').last.gsub(/Backend$/,'').underscore
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def attributes_with_index
|
29
|
+
attributes.select { |a| !a.reference? && a.has_index? }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def check_data_source!
|
35
|
+
unless data_source_exists?
|
36
|
+
raise NoTableDefined, "The table #{table_name} does not exist. Create it first before generating translated columns."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def data_source_exists?
|
41
|
+
connection.data_source_exists?(table_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
delegate :connection, to: ::ActiveRecord::Base
|
45
|
+
|
46
|
+
def truncate_index_name(index_name)
|
47
|
+
if index_name.size < connection.index_name_length
|
48
|
+
index_name
|
49
|
+
else
|
50
|
+
"index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length].freeze
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def template
|
55
|
+
"#{backend}_translations".freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
def migration_dir
|
59
|
+
File.expand_path("db/migrate".freeze)
|
60
|
+
end
|
61
|
+
|
62
|
+
def migration_file
|
63
|
+
"create_#{file_name}_#{attributes.map(&:name).join('_and_')}_translations_for_mobility_#{backend}_backend".freeze
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class NoTableDefined < StandardError; end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
require "rails/generators"
|
3
|
+
|
4
|
+
module Mobility
|
5
|
+
module BackendGenerators
|
6
|
+
class ColumnBackend < Mobility::BackendGenerators::Base
|
7
|
+
source_root File.expand_path("../../templates", __FILE__)
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
super
|
11
|
+
check_data_source!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
require "rails/generators"
|
3
|
+
|
4
|
+
module Mobility
|
5
|
+
module BackendGenerators
|
6
|
+
class TableBackend < Mobility::BackendGenerators::Base
|
7
|
+
source_root File.expand_path("../../templates", __FILE__)
|
8
|
+
|
9
|
+
def create_migration_file
|
10
|
+
if data_source_exists? && !self.class.migration_exists?(migration_dir, migration_file)
|
11
|
+
migration_template "#{backend}_migration.rb", "db/migrate/#{migration_file}.rb"
|
12
|
+
else
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
alias_method :model_table_name, :table_name
|
20
|
+
def table_name
|
21
|
+
model_table_name = super
|
22
|
+
"#{model_table_name.singularize}_translations"
|
23
|
+
end
|
24
|
+
|
25
|
+
def foreign_key
|
26
|
+
"#{model_table_name.singularize}_id"
|
27
|
+
end
|
28
|
+
|
29
|
+
def translation_index_name
|
30
|
+
truncate_index_name("index_#{table_name}_on_#{foreign_key}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def translation_locale_index_name
|
34
|
+
truncate_index_name("index_#{table_name}_on_locale")
|
35
|
+
end
|
36
|
+
|
37
|
+
def translation_unique_index_name
|
38
|
+
truncate_index_name("index_#{table_name}_on_#{foreign_key}_and_locale")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
<% attributes.each do |attribute| -%>
|
4
|
+
<% I18n.available_locales.each do |locale| -%>
|
5
|
+
<% column_name = Mobility.normalize_locale_accessor(attribute.name, locale) -%>
|
6
|
+
<% if connection.column_exists?(table_name, column_name) -%>
|
7
|
+
<% warn "#{column_name} already exists, skipping." %>
|
8
|
+
<% else -%>
|
9
|
+
add_column :<%= table_name %>, :<%= column_name %>, :<%= attribute.type %><%= attribute.inject_options %>
|
10
|
+
<%- if attribute.has_index? -%>
|
11
|
+
add_index :<%= table_name %>, :<%= column_name %><%= attribute.inject_index_options %>
|
12
|
+
<%- end -%>
|
13
|
+
<% end -%>
|
14
|
+
<% end -%>
|
15
|
+
<% end -%>
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
<% attributes.each do |attribute| -%>
|
4
|
+
<%- if attribute.reference? -%>
|
5
|
+
add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
|
6
|
+
<%- elsif attribute.token? -%>
|
7
|
+
add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
|
8
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
|
9
|
+
<%- else -%>
|
10
|
+
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
|
11
|
+
<%- if attribute.has_index? -%>
|
12
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
13
|
+
<%- end -%>
|
14
|
+
<%- end -%>
|
15
|
+
<% end -%>
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
create_table :<%= table_name %><%= primary_key_type %> do |t|
|
4
|
+
|
5
|
+
# Translated attribute(s)
|
6
|
+
<% attributes.each do |attribute| -%>
|
7
|
+
<% if attribute.token? -%>
|
8
|
+
t.string :<%= attribute.name %><%= attribute.inject_options %>
|
9
|
+
<% else -%>
|
10
|
+
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
|
11
|
+
<% end -%>
|
12
|
+
<% end -%>
|
13
|
+
|
14
|
+
t.string :locale, null: false
|
15
|
+
t.integer :<%= foreign_key %>, null: false
|
16
|
+
|
17
|
+
t.timestamps null: false
|
18
|
+
end
|
19
|
+
|
20
|
+
add_index :<%= table_name %>, :<%= foreign_key %>, name: :<%= translation_index_name %>
|
21
|
+
add_index :<%= table_name %>, :locale, name: :<%= translation_locale_index_name %>
|
22
|
+
add_index :<%= table_name %>, [:<%= foreign_key %>, :locale], name: :<%= translation_unique_index_name %>, unique: true
|
23
|
+
|
24
|
+
<%- attributes_with_index.each do |attribute| -%>
|
25
|
+
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
|
26
|
+
<%- end -%>
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Mobility
|
4
|
+
=begin
|
5
|
+
|
6
|
+
Generator to create translation tables or add translation columns to a model
|
7
|
+
table, for either Table or Column backends.
|
8
|
+
|
9
|
+
==Usage
|
10
|
+
|
11
|
+
To add translations for a string attribute +title+ to a model +Post+, call the
|
12
|
+
generator with:
|
13
|
+
|
14
|
+
rails generate mobility:translations post title:string
|
15
|
+
|
16
|
+
Here, the backend is implicit in the value of +Mobility.default_backend+, but
|
17
|
+
it can be explicitly set using the +backend+ option:
|
18
|
+
|
19
|
+
rails generate mobility:translations post title:string --backend=table
|
20
|
+
|
21
|
+
For the +table+ backend, the generator will either create a translation table
|
22
|
+
(in this case, +post_translations+) or add columns to the table if it already
|
23
|
+
exists.
|
24
|
+
|
25
|
+
For the +column+ backend, the generator will add columns for all locales in
|
26
|
+
+I18n.available_locales+. If some columns already exist, they will simply be
|
27
|
+
skipped.
|
28
|
+
|
29
|
+
Other backends are not supported, for obvious reasons:
|
30
|
+
* the +key_value+ backend does not need any model-specific migrations, simply
|
31
|
+
run the install generator.
|
32
|
+
* +jsonb+, +hstore+ and +serialized+ backends simply require a single column on
|
33
|
+
a model table, which can be added with the normal Rails migration generator.
|
34
|
+
|
35
|
+
=end
|
36
|
+
class TranslationsGenerator < ::Rails::Generators::NamedBase
|
37
|
+
SUPPORTED_BACKENDS = %w[column table]
|
38
|
+
BACKEND_OPTIONS = { type: :string, desc: "Backend to use for translations (defaults to Mobility.default_backend)".freeze }
|
39
|
+
argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
|
40
|
+
|
41
|
+
class_option(:backend, BACKEND_OPTIONS)
|
42
|
+
invoke_from_option :backend
|
43
|
+
|
44
|
+
def self.class_options(options = nil)
|
45
|
+
super
|
46
|
+
@class_options[:backend] = Thor::Option.new(:backend, BACKEND_OPTIONS.merge(default: Mobility.default_backend.to_s.freeze))
|
47
|
+
@class_options
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.prepare_for_invocation(name, value)
|
51
|
+
if name == :backend
|
52
|
+
if SUPPORTED_BACKENDS.include?(value)
|
53
|
+
require_relative "./backend_generators/#{value}_backend".freeze
|
54
|
+
Mobility::BackendGenerators.const_get("#{value}_backend".camelcase.freeze)
|
55
|
+
elsif Mobility::Backend.const_get(value.to_s.camelize.gsub(/\s+/, ''.freeze))
|
56
|
+
raise Thor::Error, "The #{value} backend does not have a translations generator."
|
57
|
+
else
|
58
|
+
raise Thor::Error, "#{value} is not a Mobility backend."
|
59
|
+
end
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def say_status(status, message, *args)
|
68
|
+
if status == :invoke && SUPPORTED_BACKENDS.include?(message)
|
69
|
+
super(status, "#{message}_backend".freeze, *args)
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/mobility.rb
CHANGED
@@ -208,6 +208,15 @@ module Mobility
|
|
208
208
|
"#{attribute}_#{normalize_locale(locale)}".freeze
|
209
209
|
end
|
210
210
|
|
211
|
+
# Raises InvalidLocale exception if the locale passed in is present but not available.
|
212
|
+
# @param [String,Symbol] locale
|
213
|
+
# @raise [InvalidLocale] if locale is present but not available
|
214
|
+
def enforce_available_locales!(locale)
|
215
|
+
if I18n.enforce_available_locales
|
216
|
+
raise Mobility::InvalidLocale.new(locale) unless (I18n.locale_available?(locale) || locale.nil?)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
211
220
|
protected
|
212
221
|
|
213
222
|
def read_locale
|
@@ -216,9 +225,7 @@ module Mobility
|
|
216
225
|
|
217
226
|
def set_locale(locale)
|
218
227
|
locale = locale.to_sym if locale
|
219
|
-
|
220
|
-
raise Mobility::InvalidLocale.new(locale) unless (I18n.available_locales.include?(locale) || locale.nil?)
|
221
|
-
end
|
228
|
+
enforce_available_locales!(locale)
|
222
229
|
storage[:mobility_locale] = locale
|
223
230
|
end
|
224
231
|
end
|
data/lib/mobility/attributes.rb
CHANGED
@@ -137,6 +137,7 @@ with other backends.
|
|
137
137
|
if (options[:dirty] && options[:fallthrough_accessors] != false)
|
138
138
|
options[:fallthrough_accessors] = true
|
139
139
|
end
|
140
|
+
include FallthroughAccessors.new(attributes) if options[:fallthrough_accessors]
|
140
141
|
|
141
142
|
@backend_class.configure!(options) if @backend_class.respond_to?(:configure!)
|
142
143
|
|
@@ -158,8 +159,8 @@ with other backends.
|
|
158
159
|
end
|
159
160
|
end
|
160
161
|
|
161
|
-
define_method "#{attribute}=" do |value|
|
162
|
-
mobility_set(attribute, value)
|
162
|
+
define_method "#{attribute}=" do |value, **options|
|
163
|
+
mobility_set(attribute, value, **options)
|
163
164
|
end if %i[accessor writer].include?(method)
|
164
165
|
|
165
166
|
define_locale_accessors(attribute, @accessor_locales) if @accessor_locales
|
@@ -187,7 +188,6 @@ with other backends.
|
|
187
188
|
backend_class.include(Backend::Cache) unless options[:cache] == false
|
188
189
|
backend_class.include(Backend::Dirty.for(options[:model_class])) if options[:dirty]
|
189
190
|
backend_class.include(Backend::Fallbacks) unless options[:fallbacks] == false
|
190
|
-
backend_class.include(FallthroughAccessors.new(attributes)) if options[:fallthrough_accessors]
|
191
191
|
end
|
192
192
|
|
193
193
|
def define_backend(attribute)
|
@@ -215,7 +215,7 @@ with other backends.
|
|
215
215
|
|
216
216
|
def get_backend_class(backend: nil, model_class: nil)
|
217
217
|
raise Mobility::BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
|
218
|
-
klass = Module === backend ? backend : Mobility::Backend.const_get(backend.to_s.camelize.gsub(/\s+/, ''))
|
218
|
+
klass = Module === backend ? backend : Mobility::Backend.const_get(backend.to_s.camelize.gsub(/\s+/, ''.freeze).freeze)
|
219
219
|
model_class.nil? ? klass : klass.for(model_class)
|
220
220
|
end
|
221
221
|
end
|
@@ -4,6 +4,15 @@ module Mobility
|
|
4
4
|
|
5
5
|
Implements the {Mobility::Backend::Column} backend for ActiveRecord models.
|
6
6
|
|
7
|
+
You can use the +mobility:translations+ generator to create a migration adding
|
8
|
+
translatable columns to the model table with:
|
9
|
+
|
10
|
+
rails generate mobility:translations post title:string
|
11
|
+
|
12
|
+
The generated migration will add columns +title_<locale>+ for every locale in
|
13
|
+
+I18n.available_locales+. (The generator can be run again to add new attributes
|
14
|
+
or locales.)
|
15
|
+
|
7
16
|
@note This backend disables the +locale_accessors+ option, which would
|
8
17
|
otherwise interfere with column methods.
|
9
18
|
|
@@ -27,11 +36,15 @@ Implements the {Mobility::Backend::Column} backend for ActiveRecord models.
|
|
27
36
|
|
28
37
|
# @!group Backend Accessors
|
29
38
|
# @!macro backend_reader
|
30
|
-
|
39
|
+
def read(locale, **_)
|
40
|
+
model.read_attribute(column(locale))
|
41
|
+
end
|
31
42
|
|
32
43
|
# @!group Backend Accessors
|
33
44
|
# @!macro backend_writer
|
34
|
-
|
45
|
+
def write(locale, value, **_)
|
46
|
+
model.write_attribute(column(locale), value)
|
47
|
+
end
|
35
48
|
|
36
49
|
# @!group Backend Configuration
|
37
50
|
def self.configure!(options)
|
@@ -6,6 +6,15 @@ module Mobility
|
|
6
6
|
|
7
7
|
Implements the {Mobility::Backend::Table} backend for ActiveRecord models.
|
8
8
|
|
9
|
+
To generate a translation table for a model +Post+, you can use the included
|
10
|
+
+mobility:translations+ generator:
|
11
|
+
|
12
|
+
rails generate mobility:translations post title:string content:text
|
13
|
+
|
14
|
+
This will create a migration which can be run to create the translation table.
|
15
|
+
If the translation table already exists, it will create a migration adding
|
16
|
+
columns to that table.
|
17
|
+
|
9
18
|
@example Model with table backend
|
10
19
|
class Post < ActiveRecord::Base
|
11
20
|
translates :title, backend: :table, association_name: :translations
|
@@ -2,10 +2,18 @@ module Mobility
|
|
2
2
|
module Backend
|
3
3
|
=begin
|
4
4
|
|
5
|
-
Stores translated attribute as a column on the model table.
|
5
|
+
Stores translated attribute as a column on the model table. To use this
|
6
|
+
backend, ensure that the model table has columns named +<attribute>_<locale>+
|
7
|
+
for every locale in +I18n.available_locales+.
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
+
If you are using Rails, you can use the +mobility:translations+ generator to
|
10
|
+
create a migration adding these columns to the model table with:
|
11
|
+
|
12
|
+
rails generate mobility:translations post title:string
|
13
|
+
|
14
|
+
The generated migration will add columns +title_<locale>+ for every locale in
|
15
|
+
+I18n.available_locales+. (The generator can be run again to add new attributes
|
16
|
+
or locales.)
|
9
17
|
|
10
18
|
==Backend Options
|
11
19
|
|
@@ -19,19 +27,6 @@ be ignored if set, since it would cause a conflict with column accessors.
|
|
19
27
|
module Column
|
20
28
|
include OrmDelegator
|
21
29
|
|
22
|
-
# @!group Backend Accessors
|
23
|
-
#
|
24
|
-
# @!macro backend_reader
|
25
|
-
def read(locale, **_)
|
26
|
-
model.send(column(locale))
|
27
|
-
end
|
28
|
-
|
29
|
-
# @!macro backend_writer
|
30
|
-
def write(locale, value, **_)
|
31
|
-
model.send("#{column(locale)}=", value)
|
32
|
-
end
|
33
|
-
# @!endgroup
|
34
|
-
|
35
30
|
# Returns name of column where translated attribute is stored
|
36
31
|
# @param [Symbol] locale
|
37
32
|
# @return [String]
|