i18n-active_record 0.3.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 437efadf4380a5cd99eccb3c468bf25f0aa0343808e89e0dfad9ac676b31743f
4
- data.tar.gz: 5080254a77103719b02c55127da824ad505e6d76d0ef7b57a8773a7b08db84ca
3
+ metadata.gz: 402118f9fb5b5b88dec6745249ded8e131b509c3bb4b8b35e5857182c85e6ca8
4
+ data.tar.gz: f120411a8ffc00c065ef50eda91d32515e23f51d6e4125184513c5a34496edb5
5
5
  SHA512:
6
- metadata.gz: 4f5823aa96600903b72bb9a6203a6f6b065482b8cc78bcf97df884758893d69a4d1b9e55fe973830027ebc9e19a72c431410bed38ec0e05c4854a59cca1d17f1
7
- data.tar.gz: ea61b155163a0eecd7e2ea10317d761b9add3b3c0ad4ef7ce713a87cfa7abc250ae25d21753c66a12f4db2921c4749d77b20f2fa1e2003ad5e49906b3cd71e9f
6
+ metadata.gz: f36d522e0792d4c5460fe9d7b3af69d2fa27817f2f2ecd58eb18ecdcea13012e542bfe4022d2e27c18f454107e63c1406450c3d709830caeb1c33f1a0b2997b3
7
+ data.tar.gz: 7ee1bec18a25efbad1fbec55e89b061b050cfdf2fc51c2453f6dee559ad71bf60ab8c5d8397900c4d87be82dff59f830e494c427c42214d60469c047d06c08fc
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
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails/generators/active_record'
2
4
 
3
5
  module I18n
4
6
  module ActiveRecord
5
7
  module Generators
6
8
  class InstallGenerator < ::ActiveRecord::Generators::Base
7
- desc "Installs i18n-active_record and generates the necessary migrations"
9
+ desc 'Installs i18n-active_record and generates the necessary migrations'
8
10
 
9
11
  argument :name, type: :string, default: 'Translation'
10
12
 
@@ -19,7 +21,7 @@ module I18n
19
21
  end
20
22
 
21
23
  def create_migrations
22
- migration_template 'migration.rb.erb', 'db/migrate/create_translations.rb'
24
+ migration_template 'migration.rb.erb', "db/migrate/create_#{table_name}.rb"
23
25
  end
24
26
  end
25
27
  end
@@ -13,5 +13,6 @@ if Translation.table_exists?
13
13
  end
14
14
 
15
15
  I18n::Backend::ActiveRecord.configure do |config|
16
+ # config.cache_translations = true # defaults to false
16
17
  # config.cleanup_with_destroy = true # defaults to false
17
18
  end
@@ -1,17 +1,13 @@
1
- class Create<%= name.classify %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
2
- def self.up
3
- create_table :<%= name.tableize %> do |t|
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
4
  t.string :locale
5
5
  t.string :key
6
- t.text :value
7
- t.text :interpolations
8
- t.boolean :is_proc, :default => false
6
+ t.text :value
7
+ t.text :interpolations
8
+ t.boolean :is_proc, default: false
9
9
 
10
10
  t.timestamps
11
11
  end
12
12
  end
13
-
14
- def self.down
15
- drop_table :<%= name.tableize %>
16
- end
17
13
  end
@@ -2,5 +2,6 @@ require 'i18n/backend/active_record'
2
2
  I18n.backend = I18n::Backend::ActiveRecord.new
3
3
 
4
4
  I18n::Backend::ActiveRecord.configure do |config|
5
+ # config.cache_translations = true # defaults to false
5
6
  # config.cleanup_with_destroy = true # defaults to false
6
7
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module I18n
2
4
  module ActiveRecord
3
- VERSION = '0.3.0'
5
+ VERSION = '1.0.1'
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
 
@@ -9,6 +11,9 @@ module I18n
9
11
  autoload :Translation, 'i18n/backend/active_record/translation'
10
12
  autoload :Configuration, 'i18n/backend/active_record/configuration'
11
13
 
14
+ include Base
15
+ include Flatten
16
+
12
17
  class << self
13
18
  def configure
14
19
  yield(config) if block_given?
@@ -19,85 +24,109 @@ module I18n
19
24
  end
20
25
  end
21
26
 
22
- module Implementation
23
- include Base, Flatten
27
+ def initialize(*args)
28
+ super
24
29
 
25
- def available_locales
26
- begin
27
- Translation.available_locales
28
- rescue ::ActiveRecord::StatementInvalid
29
- []
30
- end
31
- end
30
+ reload!
31
+ end
32
32
 
33
- def store_translations(locale, data, options = {})
34
- escape = options.fetch(:escape, true)
35
- flatten_translations(locale, data, escape, false).each do |key, value|
36
- translation = Translation.locale(locale).lookup(expand_keys(key))
33
+ def available_locales
34
+ Translation.available_locales
35
+ rescue ::ActiveRecord::StatementInvalid
36
+ []
37
+ end
38
+
39
+ def store_translations(locale, data, options = {})
40
+ escape = options.fetch(:escape, true)
37
41
 
38
- if ActiveRecord.config.cleanup_with_destroy
39
- translation.destroy_all
40
- else
41
- translation.delete_all
42
- end
42
+ flatten_translations(locale, data, escape, false).each do |key, value|
43
+ translation = Translation.locale(locale).lookup(expand_keys(key))
43
44
 
44
- Translation.create(:locale => locale.to_s, :key => key.to_s, :value => value)
45
+ if self.class.config.cleanup_with_destroy
46
+ translation.destroy_all
47
+ else
48
+ translation.delete_all
45
49
  end
50
+
51
+ Translation.create(locale: locale.to_s, key: key.to_s, value: value)
46
52
  end
47
53
 
54
+ reload! if self.class.config.cache_translations
55
+ end
56
+
57
+ def reload!
58
+ @translations = nil
59
+
60
+ self
61
+ end
62
+
63
+ def initialized?
64
+ !@translations.nil?
65
+ end
66
+
67
+ def init_translations
68
+ @translations = Translation.to_hash
69
+ end
70
+
71
+ def translations(do_init: false)
72
+ init_translations if do_init || !initialized?
73
+ @translations ||= {}
74
+ end
75
+
48
76
  protected
49
77
 
50
- def lookup(locale, key, scope = [], options = {})
51
- 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]
57
- end
78
+ def lookup(locale, key, scope = [], options = {})
79
+ key = normalize_flat_keys(locale, key, scope, options[:separator])
80
+ key = key[1..-1] if key.first == '.'
81
+ key = key[0..-2] if key.last == '.'
58
82
 
59
- result = if key == ''
60
- Translation.locale(locale).all
61
- else
62
- Translation.locale(locale).lookup(key)
63
- end
83
+ if self.class.config.cache_translations
84
+ keys = ([locale] + key.split(I18n::Backend::Flatten::FLATTEN_SEPARATOR)).map(&:to_sym)
64
85
 
65
- if result.empty?
66
- nil
67
- elsif result.first.key == key
68
- result.first.value
69
- else
70
- result = result.inject({}) do |hash, translation|
71
- hash.deep_merge build_translation_hash_by_key(key, translation)
72
- end
73
- result.deep_symbolize_keys
74
- end
86
+ return translations.dig(*keys)
75
87
  end
76
88
 
77
- def build_translation_hash_by_key(lookup_key, translation)
78
- hash = {}
79
- if lookup_key == ''
80
- chop_range = 0..-1
81
- else
82
- chop_range = (lookup_key.size + FLATTEN_SEPARATOR.size)..-1
83
- end
84
- translation_nested_keys = translation.key.slice(chop_range).split(FLATTEN_SEPARATOR)
85
- translation_nested_keys.each.with_index.inject(hash) do |iterator, (key, index)|
86
- iterator[key] = translation_nested_keys[index + 1] ? {} : translation.value
87
- iterator[key]
88
- end
89
- hash
89
+ result = if key == ''
90
+ Translation.locale(locale).all
91
+ else
92
+ Translation.locale(locale).lookup(key)
90
93
  end
91
94
 
92
- # For a key :'foo.bar.baz' return ['foo', 'foo.bar', 'foo.bar.baz']
93
- def expand_keys(key)
94
- key.to_s.split(FLATTEN_SEPARATOR).inject([]) do |keys, key|
95
- keys << [keys.last, key].compact.join(FLATTEN_SEPARATOR)
95
+ if result.empty?
96
+ nil
97
+ elsif result.first.key == key
98
+ result.first.value
99
+ else
100
+ result = result.inject({}) do |hash, translation|
101
+ hash.deep_merge build_translation_hash_by_key(key, translation)
96
102
  end
103
+ result.deep_symbolize_keys
97
104
  end
98
105
  end
99
106
 
100
- include Implementation
107
+ def build_translation_hash_by_key(lookup_key, translation)
108
+ hash = {}
109
+
110
+ chop_range = if lookup_key == ''
111
+ 0..-1
112
+ else
113
+ (lookup_key.size + FLATTEN_SEPARATOR.size)..-1
114
+ end
115
+ translation_nested_keys = translation.key.slice(chop_range).split(FLATTEN_SEPARATOR)
116
+ translation_nested_keys.each.with_index.inject(hash) do |iterator, (key, index)|
117
+ iterator[key] = translation_nested_keys[index + 1] ? {} : translation.value
118
+ iterator[key]
119
+ end
120
+
121
+ hash
122
+ end
123
+
124
+ # For a key :'foo.bar.baz' return ['foo', 'foo.bar', 'foo.bar.baz']
125
+ def expand_keys(key)
126
+ key.to_s.split(FLATTEN_SEPARATOR).inject([]) do |keys, k|
127
+ keys << [keys.last, k].compact.join(FLATTEN_SEPARATOR)
128
+ end
129
+ end
101
130
  end
102
131
  end
103
132
  end