i18n-active_record 0.2.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6b1b7eba888729976d370e18c88c1710d818e8d4
4
- data.tar.gz: cc0b86a33d8742fc2cf6a2db0bf3891681bc7b1c
2
+ SHA256:
3
+ metadata.gz: 934e01acf036e3a1c8eb23f5f8f96c55e84d462a31a10183c12389e8466e233d
4
+ data.tar.gz: a0c8301e9e0c5eaac6badf2dfdeca19b581296eaa0e0a15464a13fbc6796087f
5
5
  SHA512:
6
- metadata.gz: 532cadaf5a7b6a349a4d84f5658be0c914521ec931e1c03089bd02d4e217279f080924568c8461c140ba3dd3d265eb5b3a017f817ec1723b91b6c6c4ca20aabd
7
- data.tar.gz: c74ac31d2b20876ba61042b46de889f49f678f9fa23cf5d4173db7fc0d8ec4a878f779f03e53c76f563a051f99b2774fbcd7d28f3a7d9fe644cbf37b3880c184
6
+ metadata.gz: 0702e26d0b3a600ff2d28db7473e9a34311f18a099a0009ec6bccec5382d5c4bcc9042b33653a214c377c332db0d902adb64103e05a98c1371d6d19ccb3c0297
7
+ data.tar.gz: f2dd449433f4b88763c5952831feff25956c727aa4ff086c2ae130709bfd968ef36f61bf7b2af9065681ac7132e7e7c3a05c13b268e5c1c3e282285cd1787c6b
data/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # I18n::Backend::ActiveRecord [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop) [![Tests Status](https://github.com/svenfuchs/i18n-active_record/actions/workflows/test.yml/badge.svg)](https://github.com/svenfuchs/i18n-active_record/actions) [![Linter Status](https://github.com/svenfuchs/i18n-active_record/actions/workflows/linter.yml/badge.svg)](https://github.com/svenfuchs/i18n-active_record/actions)
2
+
3
+ This repository contains the I18n ActiveRecord backend and support code that has been extracted from the `I18n` gem: http://github.com/svenfuchs/i18n.
4
+ It is fully compatible with Rails 4, 5 and 6.
5
+
6
+ ## Installation
7
+
8
+ For Bundler put the following in your Gemfile:
9
+
10
+ ```ruby
11
+ gem 'i18n-active_record', require: 'i18n/active_record'
12
+ ```
13
+
14
+ After updating your bundle, run the installer
15
+
16
+ $ rails g i18n:active_record:install
17
+
18
+ It creates a migration:
19
+
20
+ ```ruby
21
+ class CreateTranslations < ActiveRecord::Migration
22
+ def change
23
+ create_table :translations do |t|
24
+ t.string :locale
25
+ t.string :key
26
+ t.text :value
27
+ t.text :interpolations
28
+ t.boolean :is_proc, default: false
29
+
30
+ t.timestamps
31
+ end
32
+ end
33
+ end
34
+ ```
35
+
36
+ To specify table name use:
37
+
38
+ $ rails g i18n:active_record:install MyTranslation
39
+
40
+ With the translation model you will be able to manage your translation, and add new translations or languages through
41
+ it.
42
+
43
+ By default the installer creates a new file in `config/initializers` named `i18n_active_record.rb` with the following content.
44
+
45
+ ```ruby
46
+ require 'i18n/backend/active_record'
47
+
48
+ Translation = I18n::Backend::ActiveRecord::Translation
49
+
50
+ if Translation.table_exists?
51
+ I18n.backend = I18n::Backend::ActiveRecord.new
52
+
53
+ I18n::Backend::ActiveRecord.send(:include, I18n::Backend::Memoize)
54
+ I18n::Backend::Simple.send(:include, I18n::Backend::Memoize)
55
+ I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
56
+
57
+ I18n.backend = I18n::Backend::Chain.new(I18n::Backend::Simple.new, I18n.backend)
58
+ end
59
+ ```
60
+
61
+ To perform a simpler installation use:
62
+
63
+ $ rails g i18n:active_record:install --simple
64
+
65
+ It generates:
66
+
67
+ ```ruby
68
+ require 'i18n/backend/active_record'
69
+ I18n.backend = I18n::Backend::ActiveRecord.new
70
+ ```
71
+
72
+ You may also configure whether the ActiveRecord backend should use `destroy` or `delete` when cleaning up internally.
73
+
74
+ ```ruby
75
+ I18n::Backend::ActiveRecord.configure do |config|
76
+ config.cleanup_with_destroy = true # defaults to false
77
+ end
78
+ ```
79
+
80
+ To configure the ActiveRecord backend to cache translations(might be useful in production) use:
81
+
82
+ ```ruby
83
+ I18n::Backend::ActiveRecord.configure do |config|
84
+ config.cache_translations = true # defaults to false
85
+ end
86
+ ```
87
+
88
+ ## Usage
89
+
90
+ You can now use `I18n.t('Your String')` to lookup translations in the database.
91
+
92
+ ## Missing Translations
93
+
94
+ ### Usage
95
+
96
+ In order to make the `I18n::Backend::ActiveRecord::Missing` module working correctly pluralization rules should be configured properly.
97
+ The `i18n.plural.keys` translation key should be present in any of the backends.
98
+ See https://github.com/svenfuchs/i18n-active_record/blob/master/lib/i18n/backend/active_record/missing.rb for more information.
99
+
100
+ ```yaml
101
+ en:
102
+ i18n:
103
+ plural:
104
+ keys:
105
+ - :zero
106
+ - :one
107
+ - :other
108
+ ```
109
+
110
+ ### Interpolations
111
+
112
+ The `interpolations` field in the `translations` table is used by `I18n::Backend::ActiveRecord::Missing` to store the interpolations seen the first time this Translation was requested. This will help translators understand what interpolations to expect, and thus to include when providing the translations.
113
+
114
+ The `interpolations` field is otherwise unused since the "value" in `Translation#value` is actually used for interpolation during actual translations.
115
+
116
+ ## Examples
117
+
118
+ * http://collectiveidea.com/blog/archives/2016/05/31/beyond-yml-files-dynamic-translations/
119
+
120
+ ## Contributing
121
+
122
+ ### Test suite
123
+
124
+ The test suite can be run with:
125
+
126
+ bundle exec rake
127
+
128
+ By default it runs the tests for SQLite database, to specify a database the `DB` env variable can be used:
129
+
130
+ DB=postgres bundle exec rake
131
+ DB=mysql bundle exec rake
132
+
133
+ To run tests for a specific rails version see [Appraisal](https://github.com/thoughtbot/appraisal):
134
+
135
+ bundle exec appraisal rails-4 rake test
136
+
137
+ ## Maintainers
138
+
139
+ * Sven Fuchs
140
+ * Tim Masliuchenko
data/Rakefile CHANGED
@@ -1,61 +1,12 @@
1
- require 'rake'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'rake/testtask'
3
4
  require 'bundler/gem_tasks'
4
5
 
5
- def execute(command)
6
- puts command
7
- system command
8
- end
9
-
10
- def bundle_options
11
- opt = ''
12
- opt += "--gemfile #{ENV['BUNDLE_GEMFILE']}" if ENV['BUNDLE_GEMFILE']
13
- end
14
-
15
- def each_database(&block)
16
- ['sqlite', 'postgres', 'mysql'].each &block
17
- end
18
-
19
- namespace :bundle do
20
- task :env do
21
- ar = ENV['AR'].to_s
22
-
23
- next if ar.empty?
24
-
25
- gemfile = "gemfiles/Gemfile.rails_#{ar}"
26
- raise "Cannot find gemfile at #{gemfile}" unless File.exist?(gemfile)
27
-
28
- ENV['BUNDLE_GEMFILE'] = gemfile
29
- puts "Using gemfile: #{gemfile}"
30
- end
31
-
32
- task install: :env do
33
- execute "bundle install #{bundle_options}"
34
- end
35
-
36
- task :install_all do
37
- [nil, '3', '4', '5', 'master'].each do |ar|
38
- opt = ar && "AR=#{ar}"
39
- execute "rake bundle:install #{opt}"
40
- end
41
- end
42
- end
43
-
44
- task :test do
45
- each_database { |db| execute "rake #{db}:test" }
46
- end
47
-
48
- Rake::TestTask.new :_test do |t|
6
+ Rake::TestTask.new :test do |t|
49
7
  t.libs << 'test'
50
8
  t.pattern = 'test/**/*_test.rb'
51
9
  t.verbose = false
52
10
  end
53
11
 
54
- each_database do |db|
55
- namespace db do
56
- task(:env) { ENV['DB'] = db }
57
- task test: ['env', 'bundle:env', '_test']
58
- end
59
- end
60
-
61
12
  task default: :test
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record'
4
+
5
+ module I18n
6
+ module ActiveRecord
7
+ module Generators
8
+ class InstallGenerator < ::ActiveRecord::Generators::Base
9
+ desc 'Installs i18n-active_record and generates the necessary migrations'
10
+
11
+ argument :name, type: :string, default: 'Translation'
12
+
13
+ class_option :simple, type: :boolean, default: false, desc: 'Perform the simple setup'
14
+
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ def copy_initializer
18
+ tpl = "#{options[:simple] ? 'simple' : 'advanced'}_initializer.rb.erb"
19
+
20
+ template tpl, 'config/initializers/i18n_active_record.rb'
21
+ end
22
+
23
+ def create_migrations
24
+ migration_template 'migration.rb.erb', "db/migrate/create_#{table_name}.rb"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ require 'i18n/backend/active_record'
2
+
3
+ Translation = I18n::Backend::ActiveRecord::Translation
4
+
5
+ if Translation.table_exists?
6
+ I18n.backend = I18n::Backend::ActiveRecord.new
7
+
8
+ I18n::Backend::ActiveRecord.send(:include, I18n::Backend::Memoize)
9
+ I18n::Backend::Simple.send(:include, I18n::Backend::Memoize)
10
+ I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
11
+
12
+ I18n.backend = I18n::Backend::Chain.new(I18n::Backend::Simple.new, I18n.backend)
13
+ end
14
+
15
+ I18n::Backend::ActiveRecord.configure do |config|
16
+ # config.cache_translations = true # defaults to false
17
+ # config.cleanup_with_destroy = true # defaults to false
18
+ end
@@ -0,0 +1,13 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
+ t.string :locale
5
+ t.string :key
6
+ t.text :value
7
+ t.text :interpolations
8
+ t.boolean :is_proc, default: false
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ require 'i18n/backend/active_record'
2
+ I18n.backend = I18n::Backend::ActiveRecord.new
3
+
4
+ I18n::Backend::ActiveRecord.configure do |config|
5
+ # config.cache_translations = true # defaults to false
6
+ # config.cleanup_with_destroy = true # defaults to false
7
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module I18n
2
4
  module ActiveRecord
3
- VERSION = '0.2.2'
5
+ VERSION = '1.1.0'
4
6
  end
5
7
  end
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'i18n'
@@ -1,11 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module I18n
2
4
  module Backend
3
5
  class ActiveRecord
4
6
  class Configuration
5
- attr_accessor :cleanup_with_destroy
7
+ attr_accessor :cleanup_with_destroy, :cache_translations
6
8
 
7
9
  def initialize
8
10
  @cleanup_with_destroy = false
11
+ @cache_translations = false
9
12
  end
10
13
  end
11
14
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This extension stores translation stub records for missing translations to
2
4
  # the database.
3
5
  #
@@ -36,27 +38,28 @@ module I18n
36
38
  include Flatten
37
39
 
38
40
  def store_default_translations(locale, key, options = {})
39
- count, scope, default, separator = options.values_at(:count, :scope, :default, :separator)
41
+ count, scope, _, separator = options.values_at(:count, :scope, :default, :separator)
40
42
  separator ||= I18n.default_separator
41
43
  key = normalize_flat_keys(locale, key, scope, separator)
42
44
 
43
- unless ActiveRecord::Translation.locale(locale).lookup(key).exists?
44
- interpolations = options.keys - I18n::RESERVED_KEYS
45
- keys = count ? I18n.t('i18n.plural.keys', :locale => locale).map { |k| [key, k].join(FLATTEN_SEPARATOR) } : [key]
46
- keys.each { |key| store_default_translation(locale, key, interpolations) }
47
- end
45
+ return if ActiveRecord::Translation.locale(locale).lookup(key).exists?
46
+
47
+ interpolations = options.keys - I18n::RESERVED_KEYS
48
+ keys = count ? I18n.t('i18n.plural.keys', locale: locale).map { |k| [key, k].join(FLATTEN_SEPARATOR) } : [key]
49
+ keys.each { |k| store_default_translation(locale, k, interpolations) }
48
50
  end
49
51
 
50
52
  def store_default_translation(locale, key, interpolations)
51
- translation = ActiveRecord::Translation.new :locale => locale.to_s, :key => key
53
+ translation = ActiveRecord::Translation.new locale: locale.to_s, key: key
52
54
  translation.interpolations = interpolations
53
55
  translation.save
54
56
  end
55
57
 
56
58
  def translate(locale, key, options = {})
57
59
  result = catch(:exception) { super }
60
+
58
61
  if result.is_a?(I18n::MissingTranslation)
59
- self.store_default_translations(locale, key, options)
62
+ store_default_translations(locale, key, options)
60
63
  throw(:exception, result)
61
64
  else
62
65
  result
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This module is intended to be mixed into the ActiveRecord backend to allow
2
4
  # storing Ruby Procs as translation values in the database.
3
5
  #
@@ -21,13 +23,13 @@ module I18n
21
23
  module Backend
22
24
  class ActiveRecord
23
25
  module StoreProcs
24
- def value=(v)
25
- case v
26
+ def value=(val)
27
+ case val
26
28
  when Proc
27
- write_attribute(:value, v.to_ruby)
29
+ write_attribute(:value, val.to_ruby)
28
30
  write_attribute(:is_proc, true)
29
31
  else
30
- write_attribute(:value, v)
32
+ write_attribute(:value, val)
31
33
  end
32
34
  end
33
35
 
@@ -36,4 +38,3 @@ module I18n
36
38
  end
37
39
  end
38
40
  end
39
-
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
5
  module I18n
@@ -53,19 +55,20 @@ module I18n
53
55
 
54
56
  serialize :value
55
57
  serialize :interpolations, Array
58
+ after_commit :invalidate_translations_cache
56
59
 
57
60
  class << self
58
61
  def locale(locale)
59
- where(:locale => locale.to_s)
62
+ where(locale: locale.to_s)
60
63
  end
61
64
 
62
65
  def lookup(keys, *separator)
63
66
  column_name = connection.quote_column_name('key')
64
- keys = Array(keys).map! { |key| key.to_s }
67
+ keys = Array(keys).map!(&:to_s)
65
68
 
66
69
  unless separator.empty?
67
- warn "[DEPRECATION] Giving a separator to Translation.lookup is deprecated. " <<
68
- "You can change the internal separator by overwriting FLATTEN_SEPARATOR."
70
+ warn '[DEPRECATION] Giving a separator to Translation.lookup is deprecated. ' \
71
+ 'You can change the internal separator by overwriting FLATTEN_SEPARATOR.'
69
72
  end
70
73
 
71
74
  namespace = "#{keys.last}#{I18n::Backend::Flatten::FLATTEN_SEPARATOR}%"
@@ -75,10 +78,22 @@ module I18n
75
78
  def available_locales
76
79
  Translation.select('DISTINCT locale').to_a.map { |t| t.locale.to_sym }
77
80
  end
81
+
82
+ def to_hash
83
+ Translation.all.each.with_object({}) do |t, memo|
84
+ locale_hash = (memo[t.locale.to_sym] ||= {})
85
+ keys = t.key.split('.')
86
+ keys.each.with_index.inject(locale_hash) do |iterator, (key_part, index)|
87
+ key = key_part.to_sym
88
+ iterator[key] = keys[index + 1] ? (iterator[key] || {}) : t.value
89
+ iterator[key] # rubocop:disable Lint/UnmodifiedReduceAccumulator
90
+ end
91
+ end
92
+ end
78
93
  end
79
94
 
80
95
  def interpolates?(key)
81
- self.interpolations.include?(key) if self.interpolations
96
+ interpolations&.include?(key)
82
97
  end
83
98
 
84
99
  def value
@@ -95,14 +110,19 @@ module I18n
95
110
  end
96
111
 
97
112
  def value=(value)
98
- if value === false
113
+ case value
114
+ when false
99
115
  value = FALSY_CHAR
100
- elsif value === true
116
+ when true
101
117
  value = TRUTHY_CHAR
102
118
  end
103
119
 
104
120
  write_attribute(:value, value)
105
121
  end
122
+
123
+ def invalidate_translations_cache
124
+ I18n.backend.reload! if I18n::Backend::ActiveRecord.config.cache_translations
125
+ end
106
126
  end
107
127
  end
108
128
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'i18n/backend/base'
2
4
  require 'i18n/backend/active_record/translation'
3
5
 
@@ -19,41 +21,68 @@ module I18n
19
21
  end
20
22
  end
21
23
 
24
+ def initialize
25
+ reload!
26
+ end
27
+
22
28
  module Implementation
23
- include Base, Flatten
29
+ include Base
30
+ include Flatten
24
31
 
25
32
  def available_locales
26
- begin
27
- Translation.available_locales
28
- rescue ::ActiveRecord::StatementInvalid
29
- []
30
- end
33
+ Translation.available_locales
34
+ rescue ::ActiveRecord::StatementInvalid
35
+ []
31
36
  end
32
37
 
33
38
  def store_translations(locale, data, options = {})
34
39
  escape = options.fetch(:escape, true)
40
+
35
41
  flatten_translations(locale, data, escape, false).each do |key, value|
36
42
  translation = Translation.locale(locale).lookup(expand_keys(key))
37
43
 
38
- if ActiveRecord.config.cleanup_with_destroy
44
+ if self.class.config.cleanup_with_destroy
39
45
  translation.destroy_all
40
46
  else
41
47
  translation.delete_all
42
48
  end
43
49
 
44
- Translation.create(:locale => locale.to_s, :key => key.to_s, :value => value)
50
+ Translation.create(locale: locale.to_s, key: key.to_s, value: value)
45
51
  end
52
+
53
+ reload! if self.class.config.cache_translations
54
+ end
55
+
56
+ def reload!
57
+ @translations = nil
58
+
59
+ self
60
+ end
61
+
62
+ def initialized?
63
+ !@translations.nil?
64
+ end
65
+
66
+ def init_translations
67
+ @translations = Translation.to_hash
68
+ end
69
+
70
+ def translations(do_init: false)
71
+ init_translations if do_init || !initialized?
72
+ @translations ||= {}
46
73
  end
47
74
 
48
- protected
75
+ protected
49
76
 
50
77
  def lookup(locale, key, scope = [], options = {})
51
78
  key = normalize_flat_keys(locale, key, scope, options[:separator])
52
- if key.first == '.'
53
- key = key[1..-1]
54
- end
55
- if key.last == '.'
56
- key = key[0..-2]
79
+ key = key[1..-1] if key.first == '.'
80
+ key = key[0..-2] if key.last == '.'
81
+
82
+ if self.class.config.cache_translations
83
+ keys = ([locale] + key.split(I18n::Backend::Flatten::FLATTEN_SEPARATOR)).map(&:to_sym)
84
+
85
+ return translations.dig(*keys)
57
86
  end
58
87
 
59
88
  result = if key == ''
@@ -76,23 +105,25 @@ module I18n
76
105
 
77
106
  def build_translation_hash_by_key(lookup_key, translation)
78
107
  hash = {}
79
- if lookup_key == ''
80
- chop_range = 0..-1
108
+
109
+ chop_range = if lookup_key == ''
110
+ 0..-1
81
111
  else
82
- chop_range = (lookup_key.size + FLATTEN_SEPARATOR.size)..-1
112
+ (lookup_key.size + FLATTEN_SEPARATOR.size)..-1
83
113
  end
84
114
  translation_nested_keys = translation.key.slice(chop_range).split(FLATTEN_SEPARATOR)
85
115
  translation_nested_keys.each.with_index.inject(hash) do |iterator, (key, index)|
86
116
  iterator[key] = translation_nested_keys[index + 1] ? {} : translation.value
87
117
  iterator[key]
88
118
  end
119
+
89
120
  hash
90
121
  end
91
122
 
92
123
  # For a key :'foo.bar.baz' return ['foo', 'foo.bar', 'foo.bar.baz']
93
124
  def expand_keys(key)
94
- key.to_s.split(FLATTEN_SEPARATOR).inject([]) do |keys, key|
95
- keys << [keys.last, key].compact.join(FLATTEN_SEPARATOR)
125
+ key.to_s.split(FLATTEN_SEPARATOR).inject([]) do |keys, k|
126
+ keys << [keys.last, k].compact.join(FLATTEN_SEPARATOR)
96
127
  end
97
128
  end
98
129
  end