i18n-active_record 0.4.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7aeed8d25f893fefde754e0836bdc6338529b6313a5809d4312fb7fd30087f3a
4
- data.tar.gz: e924f6936a5f956f6c62c095374b76d1c84e504d65bdf07f4463d2659cb7c50d
3
+ metadata.gz: 934e01acf036e3a1c8eb23f5f8f96c55e84d462a31a10183c12389e8466e233d
4
+ data.tar.gz: a0c8301e9e0c5eaac6badf2dfdeca19b581296eaa0e0a15464a13fbc6796087f
5
5
  SHA512:
6
- metadata.gz: 100bd6c09aa1307292e2f751d45b601e47837e58d708bf881b9e0ba6d02e1ec1d8911d5fa4c3d4f0d6d9409e5997d90772e23e1fdbf86e0e4ed6b1af48101988
7
- data.tar.gz: 29bd66389ac95bf5afa7f97f89e9676b6bb83be6abaa02876e7577dff6c1a83b7f91fbe70f6166841e5924b403e9449c071338076ef8f3c5d1189bd8f3e9f29a
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', '6', '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
 
@@ -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
1
  class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
- def self.up
3
- create_table :<%= name.tableize %> do |t|
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.4.0'
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}%"
@@ -83,14 +86,14 @@ module I18n
83
86
  keys.each.with_index.inject(locale_hash) do |iterator, (key_part, index)|
84
87
  key = key_part.to_sym
85
88
  iterator[key] = keys[index + 1] ? (iterator[key] || {}) : t.value
86
- iterator[key]
89
+ iterator[key] # rubocop:disable Lint/UnmodifiedReduceAccumulator
87
90
  end
88
91
  end
89
92
  end
90
93
  end
91
94
 
92
95
  def interpolates?(key)
93
- self.interpolations.include?(key) if self.interpolations
96
+ interpolations&.include?(key)
94
97
  end
95
98
 
96
99
  def value
@@ -107,14 +110,19 @@ module I18n
107
110
  end
108
111
 
109
112
  def value=(value)
110
- if value === false
113
+ case value
114
+ when false
111
115
  value = FALSY_CHAR
112
- elsif value === true
116
+ when true
113
117
  value = TRUTHY_CHAR
114
118
  end
115
119
 
116
120
  write_attribute(:value, value)
117
121
  end
122
+
123
+ def invalidate_translations_cache
124
+ I18n.backend.reload! if I18n::Backend::ActiveRecord.config.cache_translations
125
+ end
118
126
  end
119
127
  end
120
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,34 +21,41 @@ 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
46
54
  end
47
55
 
48
56
  def reload!
49
57
  @translations = nil
58
+
50
59
  self
51
60
  end
52
61
 
@@ -63,15 +72,17 @@ module I18n
63
72
  @translations ||= {}
64
73
  end
65
74
 
66
- protected
75
+ protected
67
76
 
68
77
  def lookup(locale, key, scope = [], options = {})
69
78
  key = normalize_flat_keys(locale, key, scope, options[:separator])
70
- if key.first == '.'
71
- key = key[1..-1]
72
- end
73
- if key.last == '.'
74
- 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)
75
86
  end
76
87
 
77
88
  result = if key == ''
@@ -94,23 +105,25 @@ module I18n
94
105
 
95
106
  def build_translation_hash_by_key(lookup_key, translation)
96
107
  hash = {}
97
- if lookup_key == ''
98
- chop_range = 0..-1
108
+
109
+ chop_range = if lookup_key == ''
110
+ 0..-1
99
111
  else
100
- chop_range = (lookup_key.size + FLATTEN_SEPARATOR.size)..-1
112
+ (lookup_key.size + FLATTEN_SEPARATOR.size)..-1
101
113
  end
102
114
  translation_nested_keys = translation.key.slice(chop_range).split(FLATTEN_SEPARATOR)
103
115
  translation_nested_keys.each.with_index.inject(hash) do |iterator, (key, index)|
104
116
  iterator[key] = translation_nested_keys[index + 1] ? {} : translation.value
105
117
  iterator[key]
106
118
  end
119
+
107
120
  hash
108
121
  end
109
122
 
110
123
  # For a key :'foo.bar.baz' return ['foo', 'foo.bar', 'foo.bar.baz']
111
124
  def expand_keys(key)
112
- key.to_s.split(FLATTEN_SEPARATOR).inject([]) do |keys, key|
113
- 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)
114
127
  end
115
128
  end
116
129
  end