mobility 0.1.10 → 0.1.11
Sign up to get free protection for your applications and to get access to all the features.
- 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]
|