i18n-active_record 0.3.0 → 1.0.1

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
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