Skip to content

Commit

Permalink
Scope config option to isolate translations (#140)
Browse files Browse the repository at this point in the history
* Improve CI

- Add Rails 8 to test matrix
- Add excludes for incompatable Ruby/Rails combinations
- Fix sqlite3 dependencies for older Rails versions

* Add scope config option

Allow the entire backend to use translations only from a subset of
the translation records.
  • Loading branch information
vipera authored Dec 16, 2024
1 parent fce95f2 commit b22bd9f
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 34 deletions.
62 changes: 34 additions & 28 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,41 @@ jobs:
strategy:
fail-fast: true
matrix:
ruby: [2.5, 2.6, 2.7, '3.0', 3.1, 3.2, 'head']
rails: [4, 5, 6, 7, 'head']
ruby: [2.5, 2.6, 2.7, '3.0', 3.1, 3.2, 3.3, 'head']
rails: [4, 5, 6, 7, 8, 'head']
exclude:
- ruby: 2.5
rails: 7
- ruby: 2.5
rails: head
- ruby: 2.6
rails: 7
- ruby: 2.6
rails: head
- ruby: 2.7
rails: 4
- ruby: '3.0'
rails: 4
- ruby: '3.0'
rails: 5
- ruby: 3.1
rails: 4
- ruby: 3.1
rails: 5
- ruby: 3.2
rails: 4
- ruby: 3.2
rails: 5
- ruby: head
rails: 4
- ruby: head
rails: 5
- { 'ruby': '2.5', 'rails': '7' }
- { 'ruby': '2.5', 'rails': '8' }
- { 'ruby': '2.5', 'rails': 'head' }

- { 'ruby': '2.6', 'rails': '7' }
- { 'ruby': '2.6', 'rails': '8' }
- { 'ruby': '2.6', 'rails': 'head' }

- { 'ruby': '2.7', 'rails': '4' }
- { 'ruby': '2.7', 'rails': '8' }
- { 'ruby': '2.7', 'rails': 'head' }

- { 'ruby': '3.0', 'rails': '4' }
- { 'ruby': '3.0', 'rails': '5' }
- { 'ruby': '3.0', 'rails': '8' }
- { 'ruby': '3.0', 'rails': 'head' }

- { 'ruby': '3.1', 'rails': '4' }
- { 'ruby': '3.1', 'rails': '5' }
- { 'ruby': '3.1', 'rails': '8' }
- { 'ruby': '3.1', 'rails': 'head' }

- { 'ruby': '3.2', 'rails': '4' }
- { 'ruby': '3.2', 'rails': '5' }

- { 'ruby': '3.3', 'rails': '4' }
- { 'ruby': '3.3', 'rails': '5' }

- { 'ruby': 'head', 'rails': '4' }
- { 'ruby': 'head', 'rails': '5' }
- { 'ruby': 'head', 'rails': '6' }
- { 'ruby': 'head', 'rails': '7' }
name: 'Ruby: ${{ matrix.ruby }}, Rails: ${{ matrix.rails }}'
runs-on: ubuntu-latest
env:
Expand Down
9 changes: 8 additions & 1 deletion Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,28 @@
appraise 'rails-4' do
gem 'activerecord', '~> 4.2.0'
gem 'mysql2', '~> 0.4.10'
gem 'sqlite3', '~> 1.3.13'
gem 'pg', '~> 0.18.0'
gem 'sqlite3', '~> 1.3.13'
end

appraise 'rails-5' do
gem 'activerecord', '~> 5.2.0'
gem 'sqlite3', '~> 1.3.13'
gem 'psych', '~> 3.1'
end

appraise 'rails-6' do
gem 'activerecord', '~> 6.1.0'
gem 'sqlite3', '~> 1.4.4'
end

appraise 'rails-7' do
gem 'activerecord', '~> 7.0.0'
gem 'sqlite3', '~> 1.4.4'
end

appraise 'rails-8' do
gem 'activerecord', '~> 8.0.0'
end

appraise 'rails-head' do
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ I18n::Backend::ActiveRecord.configure do |config|
end
```

The ActiveRecord backend can be configured to use a `scope` to isolate sets of translations. That way, two applications
using the backend with the same database table can use translation data independently of one another.
If configured with a scope, all data used will be limited to records with that particular scope identifier:

```ruby
I18n::Backend::ActiveRecord.configure do |config|
config.scope = 'app1' # defaults to nil, disabling scope
end
```

## Usage

You can now use `I18n.t('Your String')` to lookup translations in the database.
Expand Down
2 changes: 1 addition & 1 deletion gemfiles/rails_5.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ source "https://rubygems.org"
gem "activerecord", "~> 5.2.0"
gem "mysql2"
gem "pg"
gem "sqlite3"
gem "sqlite3", "~> 1.3.13"
gem "psych", "~> 3.1"

gemspec path: "../"
2 changes: 1 addition & 1 deletion gemfiles/rails_6.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ source "https://rubygems.org"
gem "activerecord", "~> 6.1.0"
gem "mysql2"
gem "pg"
gem "sqlite3"
gem "sqlite3", "~> 1.4.4"

gemspec path: "../"
2 changes: 1 addition & 1 deletion gemfiles/rails_7.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ source "https://rubygems.org"
gem "activerecord", "~> 7.0.0"
gem "mysql2"
gem "pg"
gem "sqlite3"
gem "sqlite3", "~> 1.4.4"

gemspec path: "../"
10 changes: 10 additions & 0 deletions gemfiles/rails_8.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file was generated by Appraisal

source "https://rubygems.org"

gem "activerecord", "~> 8.0.0"
gem "mysql2"
gem "pg"
gem "sqlite3"

gemspec path: "../"
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ end
I18n::Backend::ActiveRecord.configure do |config|
# config.cache_translations = true # defaults to false
# config.cleanup_with_destroy = true # defaults to false
# config.scope = 'app_scope' # defaults to nil, won't be used
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
def change
create_table :<%= table_name %> do |t|
t.string :scope
t.string :locale
t.string :key
t.text :value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ I18n.backend = I18n::Backend::ActiveRecord.new
I18n::Backend::ActiveRecord.configure do |config|
# config.cache_translations = true # defaults to false
# config.cleanup_with_destroy = true # defaults to false
# config.scope = 'app_scope' # defaults to nil, won't be used
end
3 changes: 2 additions & 1 deletion lib/i18n/backend/active_record/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ module I18n
module Backend
class ActiveRecord
class Configuration
attr_accessor :cleanup_with_destroy, :cache_translations, :translation_model
attr_accessor :cleanup_with_destroy, :cache_translations, :translation_model, :scope

def initialize
@cleanup_with_destroy = false
@cache_translations = false
@translation_model = I18n::Backend::ActiveRecord::Translation
@scope = nil
end
end
end
Expand Down
17 changes: 17 additions & 0 deletions lib/i18n/backend/active_record/translation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,24 @@ class Translation < ::ActiveRecord::Base
serialize :interpolations, Array
end

before_validation :set_scope
after_commit :invalidate_translations_cache

default_scope { scoped }

class << self
def locale(locale)
where(locale: locale.to_s)
end

def scoped
if (record_scope = ActiveRecord.config.scope)
where(scope: record_scope)
else
all
end
end

def lookup(keys, *separator)
column_name = connection.quote_column_name('key')
keys = Array(keys).map!(&:to_s)
Expand Down Expand Up @@ -129,6 +140,12 @@ def value=(value)
def invalidate_translations_cache
I18n.backend.reload! if I18n::Backend::ActiveRecord.config.cache_translations
end

private

def set_scope
self.scope ||= ActiveRecord.config.scope if ActiveRecord.config.scope
end
end
end
end
Expand Down
31 changes: 31 additions & 0 deletions test/active_record_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,35 @@ def setup
assert_equal I18n.t(:foo), 'custom foo'
end
end

class ScopeTest < I18nBackendActiveRecordTest
def setup
super

I18n::Backend::ActiveRecord.config.scope = 'scope1'
end

test 'scope config option divides translations into isolated sets' do
store_translations(:en, foo: 'foo1')
assert_equal('foo1', I18n.t(:foo))

I18n::Backend::ActiveRecord.config.scope = 'scope2'
store_translations(:en, foo: 'foo2')
assert_equal('foo2', I18n.t(:foo))

I18n::Backend::ActiveRecord.config.scope = 'scope1'
assert_equal('foo1', I18n.t(:foo))
end

test 'scope config of nil disables scope' do
store_translations(:en, bar1: 'bar1')

I18n::Backend::ActiveRecord.config.scope = 'scope2'
store_translations(:en, bar2: 'bar2')

I18n::Backend::ActiveRecord.config.scope = nil
assert_equal('bar1', I18n.t(:bar1))
assert_equal('bar2', I18n.t(:bar2))
end
end
end
3 changes: 2 additions & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define(version: 1) do
create_table :translations, force: true do |t|
t.string :scope
t.string :locale
t.string :key
t.text :value
t.text :interpolations
t.boolean :is_proc, default: false
end
add_index :translations, %i[locale key], unique: true
add_index :translations, %i[scope locale key], unique: true
end

if ActiveRecord::Base.respond_to?(:yaml_column_permitted_classes=)
Expand Down

0 comments on commit b22bd9f

Please sign in to comment.