json_translatable 1.0.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.
- checksums.yaml +7 -0
- data/lib/translatable/concern.rb +215 -0
- data/lib/translatable/configuration.rb +10 -0
- data/lib/translatable/database_strategies/base.rb +30 -0
- data/lib/translatable/database_strategies/mysql.rb +41 -0
- data/lib/translatable/database_strategies/postgresql.rb +41 -0
- data/lib/translatable/database_strategies/sqlite.rb +40 -0
- data/lib/translatable/translation_class_generator.rb +83 -0
- data/lib/translatable/version.rb +3 -0
- data/lib/translatable.rb +26 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1016e8df0b9e0934d8a9534ca2a9a5e03539fe14ba240b2c25b499a3b8e9edcf
|
4
|
+
data.tar.gz: 180b1f534a2c3895a4e722523aa1cdcb1727edaacf53b7581db19ec1abb6c00e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1297496e70aef539c0e9ee423ec203c368cc5da9bce49dd135388cc2201894144b95938b3f8638eeef3a7e00c2400e29fcbcd0f65861ac3868d835463b882c7b
|
7
|
+
data.tar.gz: 8e776d74beef3b31a213922e161cdaa6e8c38203499f835ef14cd31e879ac8976532b0cbb43c7bb2c577ded263c87852786a1638fd146635290949540f18a476
|
@@ -0,0 +1,215 @@
|
|
1
|
+
module Translatable
|
2
|
+
module Concern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class Error < StandardError; end
|
6
|
+
class MissingTranslationsColumnError < Error; end
|
7
|
+
class UndefinedTranslatableFieldsError < Error; end
|
8
|
+
class DatabaseColumnConflictError < Error; end
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :translatable_fields, instance_writer: false, default: []
|
12
|
+
class_attribute :translatable_locales, instance_writer: false, default: []
|
13
|
+
class_attribute :translation_validations, instance_writer: false, default: []
|
14
|
+
class_attribute :translations_column_name, instance_writer: false, default: :translations
|
15
|
+
|
16
|
+
validate :validate_translations_structure
|
17
|
+
validate :validate_translation_fields
|
18
|
+
|
19
|
+
after_initialize :initialize_translations
|
20
|
+
|
21
|
+
def translate(locale = I18n.locale)
|
22
|
+
translation_class = self.class.translation_class
|
23
|
+
column_name = self.class.translations_column_name.to_s
|
24
|
+
translations_data = send(column_name) || {}
|
25
|
+
|
26
|
+
translated_data = self.class.translatable_fields.index_with do |field|
|
27
|
+
translations_data.dig(locale.to_s, field.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
translation_class.new(translated_data.merge(locale: locale.to_sym))
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validate_translations_structure
|
36
|
+
column_name = self.class.translations_column_name.to_s
|
37
|
+
translations_data = send(column_name)
|
38
|
+
return unless translations_data.present?
|
39
|
+
|
40
|
+
translations_data.each do |locale, fields|
|
41
|
+
unless translatable_locales.map(&:to_s).include?(locale)
|
42
|
+
errors.add(column_name.to_sym, "contains invalid locale: #{locale}")
|
43
|
+
end
|
44
|
+
|
45
|
+
next unless fields.is_a?(Hash)
|
46
|
+
|
47
|
+
fields.each_key do |field|
|
48
|
+
unless translatable_fields.map(&:to_s).include?(field)
|
49
|
+
errors.add(column_name.to_sym, "contains invalid field: #{field} for locale #{locale}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_translation_fields
|
56
|
+
column_name = self.class.translations_column_name.to_s
|
57
|
+
translations_data = send(column_name) || {}
|
58
|
+
|
59
|
+
self.class.translation_validations.each do |validation|
|
60
|
+
field = validation[:field]
|
61
|
+
options = validation[:options]
|
62
|
+
locales = validation[:locales] || self.class.translatable_locales
|
63
|
+
|
64
|
+
locales.each do |locale|
|
65
|
+
field_value = translations_data.dig(locale.to_s, field.to_s)
|
66
|
+
error_key = "#{column_name}.#{locale}.#{field}"
|
67
|
+
|
68
|
+
if options[:custom]
|
69
|
+
options[:custom].call(self, locale, field, field_value)
|
70
|
+
end
|
71
|
+
|
72
|
+
if options[:presence] && field_value.blank?
|
73
|
+
errors.add(column_name.to_sym, :blank, translation_key: error_key)
|
74
|
+
end
|
75
|
+
|
76
|
+
if options[:length] && field_value.present?
|
77
|
+
length_options = options[:length]
|
78
|
+
if length_options[:minimum] && field_value.length < length_options[:minimum]
|
79
|
+
errors.add(column_name.to_sym, :too_short, count: length_options[:minimum], translation_key: error_key)
|
80
|
+
end
|
81
|
+
if length_options[:maximum] && field_value.length > length_options[:maximum]
|
82
|
+
errors.add(column_name.to_sym, :too_long, count: length_options[:maximum], translation_key: error_key)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if options[:format] && field_value.present?
|
87
|
+
format_options = options[:format]
|
88
|
+
if format_options[:with] && !field_value.match?(format_options[:with])
|
89
|
+
errors.add(column_name.to_sym, :invalid, translation_key: error_key)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize_translations
|
98
|
+
self.class.validate_translatable
|
99
|
+
|
100
|
+
column_name = self.class.translations_column_name.to_s
|
101
|
+
current_translations = send(column_name) || {}
|
102
|
+
|
103
|
+
self.class.translatable_locales.map(&:to_s).uniq.each do |locale|
|
104
|
+
current_translations[locale] ||= {}
|
105
|
+
self.class.translatable_fields.map(&:to_s).uniq.each do |field|
|
106
|
+
unless current_translations[locale].key?(field)
|
107
|
+
current_translations[locale][field] = nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
send("#{column_name}=", current_translations)
|
113
|
+
end
|
114
|
+
|
115
|
+
class_methods do
|
116
|
+
def translatable(*fields, locales: nil, column: nil)
|
117
|
+
resolved_locales = locales || Translatable.configuration.default_locales.call
|
118
|
+
resolved_column = column || Translatable.configuration.default_column_name
|
119
|
+
|
120
|
+
self.translatable_fields = fields.map(&:to_sym)
|
121
|
+
self.translatable_locales = resolved_locales.map(&:to_sym)
|
122
|
+
self.translations_column_name = resolved_column.to_sym
|
123
|
+
end
|
124
|
+
|
125
|
+
def where_translations(attributes, locales: [], case_sensitive: false)
|
126
|
+
return none if attributes.blank?
|
127
|
+
|
128
|
+
strategy = database_strategy
|
129
|
+
strategy.where_translations_scope(self, attributes, locales: locales, case_sensitive: case_sensitive)
|
130
|
+
end
|
131
|
+
|
132
|
+
def validates_translation(*fields, **options, &block)
|
133
|
+
fields.flatten.each do |field|
|
134
|
+
if !translatable_fields.include?(field.to_sym)
|
135
|
+
raise ArgumentError, "Field :#{field} is not defined as translatable. " \
|
136
|
+
"Available translatable fields: #{translatable_fields.map(&:inspect).join(', ')}"
|
137
|
+
end
|
138
|
+
|
139
|
+
valid_options = [:presence, :length, :format, :locales]
|
140
|
+
invalid_options = options.keys - valid_options
|
141
|
+
if invalid_options.any?
|
142
|
+
raise ArgumentError, "Invalid validation option(s): #{invalid_options.map(&:inspect).join(', ')}. " \
|
143
|
+
"Valid options are: #{valid_options.map(&:inspect).join(', ')}"
|
144
|
+
end
|
145
|
+
|
146
|
+
if options[:locales]
|
147
|
+
invalid_locales = options[:locales].map(&:to_sym) - translatable_locales
|
148
|
+
if invalid_locales.any?
|
149
|
+
raise ArgumentError, "Invalid locale(s): #{invalid_locales.map(&:inspect).join(', ')}. " \
|
150
|
+
"Available locales: #{translatable_locales.map(&:inspect).join(', ')}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
if block_given?
|
155
|
+
validation_options = options.merge(custom: block)
|
156
|
+
else
|
157
|
+
validation_options = options
|
158
|
+
end
|
159
|
+
|
160
|
+
locales = validation_options.delete(:locales)
|
161
|
+
|
162
|
+
self.translation_validations = self.translation_validations + [{
|
163
|
+
field: field.to_sym,
|
164
|
+
options: validation_options,
|
165
|
+
locales: locales&.map(&:to_sym)
|
166
|
+
}]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def translations_permit_list
|
171
|
+
self.translatable_locales.map do |locale|
|
172
|
+
[locale.to_s, self.translatable_fields.map(&:to_s)]
|
173
|
+
end.to_h
|
174
|
+
end
|
175
|
+
|
176
|
+
def database_strategy
|
177
|
+
@database_strategy ||= DatabaseStrategies::Base.for_adapter(connection.adapter_name)
|
178
|
+
end
|
179
|
+
|
180
|
+
def translation_class
|
181
|
+
@translation_class ||= TranslationClassGenerator.generate_for(self.name)
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate_translatable
|
185
|
+
column_name = translations_column_name.to_s
|
186
|
+
strategy = database_strategy
|
187
|
+
|
188
|
+
unless self.column_names.include?(column_name) &&
|
189
|
+
self.columns_hash[column_name]&.type == strategy.column_type
|
190
|
+
raise MissingTranslationsColumnError,
|
191
|
+
"Model #{self.name} is missing a '#{column_name}' #{strategy.column_type} column. " \
|
192
|
+
"Please add it via a migration:\n#{strategy.migration_example(self.table_name, column_name)}"
|
193
|
+
end
|
194
|
+
|
195
|
+
unless self.respond_to?(:translatable_fields) && self.translatable_fields.present?
|
196
|
+
raise UndefinedTranslatableFieldsError,
|
197
|
+
"Model #{self.name} must call the class method `translatable` with field names. " \
|
198
|
+
"Example: \n" \
|
199
|
+
"class #{self.name} < ApplicationRecord\n" \
|
200
|
+
" include Translatable\n" \
|
201
|
+
" translatable :title, :content\n" \
|
202
|
+
"end"
|
203
|
+
end
|
204
|
+
|
205
|
+
conflicting_columns = self.column_names.select { |column| self.translatable_fields.include?(column.to_sym) }
|
206
|
+
if conflicting_columns.any?
|
207
|
+
raise DatabaseColumnConflictError,
|
208
|
+
"Model #{self.name} has database columns that conflict with translatable fields. " \
|
209
|
+
"Translatable fields should not exist as actual database columns." \
|
210
|
+
"\n\nConflicting columns: #{conflicting_columns.join(', ')}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Translatable
|
2
|
+
module DatabaseStrategies
|
3
|
+
class Base
|
4
|
+
def self.for_adapter(adapter_name)
|
5
|
+
case adapter_name.downcase
|
6
|
+
when /postgresql/
|
7
|
+
PostgreSQL.new
|
8
|
+
when /mysql/
|
9
|
+
MySQL.new
|
10
|
+
when /sqlite/
|
11
|
+
SQLite.new
|
12
|
+
else
|
13
|
+
raise ArgumentError, "Unsupported database adapter: #{adapter_name}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def column_type
|
18
|
+
raise NotImplementedError, "Subclasses must implement column_type"
|
19
|
+
end
|
20
|
+
|
21
|
+
def migration_example(table_name, column_name)
|
22
|
+
raise NotImplementedError, "Subclasses must implement migration_example"
|
23
|
+
end
|
24
|
+
|
25
|
+
def where_translations_scope(model_class, attributes, locales: [], case_sensitive: false)
|
26
|
+
raise NotImplementedError, "#{self.class.name} does not support where_translations yet"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Translatable
|
2
|
+
module DatabaseStrategies
|
3
|
+
class MySQL < Base
|
4
|
+
def column_type
|
5
|
+
:json
|
6
|
+
end
|
7
|
+
|
8
|
+
def migration_example(table_name, column_name)
|
9
|
+
"add_column :#{table_name}, :#{column_name}, :json, null: false"
|
10
|
+
end
|
11
|
+
|
12
|
+
def where_translations_scope(model_class, attributes, locales: [], case_sensitive: false)
|
13
|
+
column_name = model_class.translations_column_name
|
14
|
+
search_locales = locales.present? ? locales.map(&:to_s) : model_class.translatable_locales.map(&:to_s)
|
15
|
+
|
16
|
+
return model_class.none if attributes.blank?
|
17
|
+
|
18
|
+
conditions = []
|
19
|
+
bind_values = []
|
20
|
+
|
21
|
+
if case_sensitive
|
22
|
+
search_locales.each do |locale|
|
23
|
+
locale_attributes = { locale => attributes }
|
24
|
+
conditions << "JSON_CONTAINS(#{column_name}, ?)"
|
25
|
+
bind_values << locale_attributes.to_json
|
26
|
+
end
|
27
|
+
else
|
28
|
+
search_locales.each do |locale|
|
29
|
+
attributes.each do |field, value|
|
30
|
+
conditions << "UPPER(JSON_UNQUOTE(JSON_EXTRACT(#{column_name}, ?))) LIKE UPPER(?)"
|
31
|
+
bind_values += ["$.#{locale}.#{field}", value.to_s]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
where_clause = conditions.join(' OR ')
|
37
|
+
model_class.where(where_clause, *bind_values)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Translatable
|
2
|
+
module DatabaseStrategies
|
3
|
+
class PostgreSQL < Base
|
4
|
+
def column_type
|
5
|
+
:jsonb
|
6
|
+
end
|
7
|
+
|
8
|
+
def migration_example(table_name, column_name)
|
9
|
+
"add_column :#{table_name}, :#{column_name}, :jsonb, default: {}, null: false"
|
10
|
+
end
|
11
|
+
|
12
|
+
def where_translations_scope(model_class, attributes, locales: [], case_sensitive: false)
|
13
|
+
column_name = model_class.translations_column_name
|
14
|
+
search_locales = locales.present? ? locales.map(&:to_s) : model_class.translatable_locales.map(&:to_s)
|
15
|
+
|
16
|
+
return model_class.none if attributes.blank?
|
17
|
+
|
18
|
+
conditions = []
|
19
|
+
bind_values = []
|
20
|
+
|
21
|
+
if case_sensitive
|
22
|
+
search_locales.each do |locale|
|
23
|
+
locale_attributes = { locale => attributes }
|
24
|
+
conditions << "#{column_name} @> ?"
|
25
|
+
bind_values << locale_attributes.to_json
|
26
|
+
end
|
27
|
+
else
|
28
|
+
search_locales.each do |locale|
|
29
|
+
attributes.each do |field, value|
|
30
|
+
conditions << "(#{column_name} -> ? ->> ?) ILIKE ?"
|
31
|
+
bind_values += [locale, field.to_s, value.to_s]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
where_clause = conditions.join(' OR ')
|
37
|
+
model_class.where(where_clause, *bind_values)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Translatable
|
2
|
+
module DatabaseStrategies
|
3
|
+
class SQLite < Base
|
4
|
+
def column_type
|
5
|
+
:json
|
6
|
+
end
|
7
|
+
|
8
|
+
def migration_example(table_name, column_name)
|
9
|
+
"add_column :#{table_name}, :#{column_name}, :json, default: '{}', null: false"
|
10
|
+
end
|
11
|
+
|
12
|
+
def where_translations_scope(model_class, attributes, locales: [], case_sensitive: false)
|
13
|
+
column_name = model_class.translations_column_name
|
14
|
+
search_locales = locales.present? ? locales.map(&:to_s) : model_class.translatable_locales.map(&:to_s)
|
15
|
+
|
16
|
+
return model_class.none if attributes.blank?
|
17
|
+
|
18
|
+
conditions = []
|
19
|
+
bind_values = []
|
20
|
+
|
21
|
+
search_locales.each do |locale|
|
22
|
+
attributes.each do |field, value|
|
23
|
+
json_path = "$.#{locale}.#{field}"
|
24
|
+
|
25
|
+
if case_sensitive
|
26
|
+
conditions << "json_extract(#{column_name}, ?) = ?"
|
27
|
+
bind_values += [json_path, value.to_s]
|
28
|
+
else
|
29
|
+
conditions << "json_extract(#{column_name}, ?) LIKE ? COLLATE NOCASE"
|
30
|
+
bind_values += [json_path, value.to_s]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
where_clause = conditions.join(' OR ')
|
36
|
+
model_class.where(where_clause, *bind_values)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Translatable
|
2
|
+
class TranslationClassGenerator
|
3
|
+
def self.generate_for(model_class_name)
|
4
|
+
class_name = "#{model_class_name}Translation"
|
5
|
+
|
6
|
+
return Translatable.const_get(class_name) if Translatable.const_defined?(class_name)
|
7
|
+
|
8
|
+
model_class = Object.const_get(model_class_name)
|
9
|
+
translatable_fields = model_class.translatable_fields
|
10
|
+
|
11
|
+
translation_class = Class.new do
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
translatable_fields.each do |field|
|
15
|
+
define_method(field) do
|
16
|
+
@attributes[field]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(attributes = {})
|
21
|
+
@attributes = attributes.dup
|
22
|
+
@locale = attributes[:locale]
|
23
|
+
end
|
24
|
+
|
25
|
+
def locale
|
26
|
+
@locale
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
@attributes[key.to_sym]
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h
|
34
|
+
@attributes.dup
|
35
|
+
end
|
36
|
+
|
37
|
+
def each(&block)
|
38
|
+
return enum_for(:each) unless block_given?
|
39
|
+
|
40
|
+
@attributes.each do |key, value|
|
41
|
+
next if key == :locale
|
42
|
+
yield(key, value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def keys
|
47
|
+
@attributes.keys.reject { |k| k == :locale }
|
48
|
+
end
|
49
|
+
|
50
|
+
def values
|
51
|
+
keys.map { |key| @attributes[key] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def empty?
|
55
|
+
keys.empty? || values.all?(&:blank?)
|
56
|
+
end
|
57
|
+
|
58
|
+
def size
|
59
|
+
keys.size
|
60
|
+
end
|
61
|
+
|
62
|
+
alias_method :length, :size
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
fields = @attributes.reject { |k, _| k == :locale }
|
66
|
+
.map { |field, value| "#{field}: #{value.inspect.to_s[0, 30]}" }
|
67
|
+
.append("locale: #{@locale.inspect}")
|
68
|
+
.join(', ')
|
69
|
+
"#<#{self.class} #{fields}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
inspect
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
translation_class.define_singleton_method(:name) { class_name }
|
78
|
+
|
79
|
+
Translatable.const_set(class_name, translation_class)
|
80
|
+
translation_class
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/translatable.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require_relative 'translatable/version'
|
3
|
+
require_relative 'translatable/configuration'
|
4
|
+
require_relative 'translatable/database_strategies/base'
|
5
|
+
require_relative 'translatable/database_strategies/postgresql'
|
6
|
+
require_relative 'translatable/database_strategies/mysql'
|
7
|
+
require_relative 'translatable/database_strategies/sqlite'
|
8
|
+
require_relative 'translatable/translation_class_generator'
|
9
|
+
require_relative 'translatable/concern'
|
10
|
+
|
11
|
+
module Translatable
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
include Translatable::Concern
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.configure
|
24
|
+
yield(configuration)
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json_translatable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luka Bak
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-05-31 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activerecord
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '7.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '7.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: i18n
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.8'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.8'
|
40
|
+
description: I18n Rails gem that allows storing and querying translations for ActiveRecord
|
41
|
+
models in a single JSON/JSONB column.
|
42
|
+
email: bakluka@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/translatable.rb
|
48
|
+
- lib/translatable/concern.rb
|
49
|
+
- lib/translatable/configuration.rb
|
50
|
+
- lib/translatable/database_strategies/base.rb
|
51
|
+
- lib/translatable/database_strategies/mysql.rb
|
52
|
+
- lib/translatable/database_strategies/postgresql.rb
|
53
|
+
- lib/translatable/database_strategies/sqlite.rb
|
54
|
+
- lib/translatable/translation_class_generator.rb
|
55
|
+
- lib/translatable/version.rb
|
56
|
+
homepage: https://github.com/bakluka/json_translatable
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata: {}
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 2.7.0
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.6.2
|
75
|
+
specification_version: 4
|
76
|
+
summary: I18n Rails gem that allows storing and querying translations for ActiveRecord
|
77
|
+
models in a single JSON/JSONB column.
|
78
|
+
test_files: []
|