Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope config option to isolate translations #140

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading