active_record-translated 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +249 -0
- data/lib/active_record/translated/engine.rb +9 -0
- data/lib/active_record/translated/model.rb +65 -0
- data/lib/active_record/translated/version.rb +8 -0
- data/lib/active_record/translated.rb +67 -0
- data/lib/tasks/active_record/translated_tasks.rake +5 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d359bbfb6e7b356977ec10452c42131aa680559399023ffcffd0980b625135ae
|
4
|
+
data.tar.gz: 947a046dcfedab49d567160b1cc9240e1c7a49b11fca0b3bc2bb06ccf78e000b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a58f249814f5390e80e2a6e1637134515767c2f8c635374e183051d6c6f5ef580f758b1f78be9c51e40c771d93129df844542b01537ce20b69dd1caea243cbe1
|
7
|
+
data.tar.gz: 176e32b5c73a13ee06df807d10d96acecd4bc9df7ee5f329133833d9e9f80d0377bb5277af6a472d672a3bcb05b2305e8c43a20ddba1c846a6538135f07916d7
|
data/README.md
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
active_record-translated
|
2
|
+
========
|
3
|
+
|
4
|
+
[![RuboCop](https://github.com/kjellberg/active_record-translated/actions/workflows/rubocop.yml/badge.svg)](https://github.com/kjellberg/active_record-translated/actions/workflows/rubocop.yml)
|
5
|
+
[![RSpec](https://github.com/kjellberg/active_record-translated/actions/workflows/rspec.yml/badge.svg)](https://github.com/kjellberg/active_record-translated/actions/workflows/rspec.yml)
|
6
|
+
[![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
|
7
|
+
|
8
|
+
This gem tackles localization of ActiveRecord models in Ruby on Rails by keeping records in different languages within the same database table. Translations are separated by a `locale`-column and linked together with an indexed `record_id`-column.
|
9
|
+
|
10
|
+
Since all database rows now are language-specific, this approach works best for models where **all or most of the attributes should be translatable**. Or else you may end up with a lot of duplicated data. For a visual example, check out examples further down this readme.
|
11
|
+
|
12
|
+
### Guidelines
|
13
|
+
|
14
|
+
These are the guidlines we should follow when developing this gem:
|
15
|
+
|
16
|
+
- Should work without any modifications to current application code.
|
17
|
+
- Should not decrease performance with multiple SQL queries.
|
18
|
+
- Should act like a normal record if no locale is set.
|
19
|
+
- Should accept an unlimited number of languages without decreased performance caused by this gem.
|
20
|
+
- Should not brake current specs/tests.
|
21
|
+
- Should not brake model validations
|
22
|
+
- All translations will have it's own record.
|
23
|
+
- Translations of the same object should have the same `record_id`
|
24
|
+
- A translation should know all other available translations with the same `record_id`
|
25
|
+
- Should work independently on what type (int/uuid..) of primary key the database table is using.
|
26
|
+
|
27
|
+
Installation
|
28
|
+
------------
|
29
|
+
|
30
|
+
Add the following line to Gemfile:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
gem "active_record-translated", github: 'kjellberg/active_record-translated'
|
34
|
+
```
|
35
|
+
|
36
|
+
and run `bundle install` from your terminal to install it.
|
37
|
+
|
38
|
+
Getting started
|
39
|
+
---------------
|
40
|
+
|
41
|
+
To generate the initializer and default configuration, run:
|
42
|
+
|
43
|
+
```console
|
44
|
+
rails generate translated:install
|
45
|
+
```
|
46
|
+
|
47
|
+
*This will create an initializer at `config/initializers/active_record-translated.rb`*
|
48
|
+
|
49
|
+
### Make a model translatable
|
50
|
+
|
51
|
+
To enable translation for a model, run:
|
52
|
+
|
53
|
+
```console
|
54
|
+
rails generate translated MODEL
|
55
|
+
```
|
56
|
+
|
57
|
+
*This will generate a database migration for the `locale` and `record_id` attributes and setup your model for translation.*
|
58
|
+
|
59
|
+
Examples
|
60
|
+
--------
|
61
|
+
|
62
|
+
The `posts` table below represents a translated `Post` model with the attributes `title` and `slug`. Note that `resource_id` and `locale` was created by this gem when you generated the migrations.
|
63
|
+
|
64
|
+
<table>
|
65
|
+
<thead>
|
66
|
+
<th>id</th>
|
67
|
+
<th>resource_id</th>
|
68
|
+
<th>locale</th>
|
69
|
+
<th>title</th>
|
70
|
+
<th>slug</th>
|
71
|
+
</thead>
|
72
|
+
<tbody>
|
73
|
+
<tr>
|
74
|
+
<td>1</td>
|
75
|
+
<td>42fb060d-2000-4a73-bb74-cfb5ca799e0d</td>
|
76
|
+
<td>en</td>
|
77
|
+
<td>Good morning!</td>
|
78
|
+
<td>a-blog-post</td>
|
79
|
+
</tr>
|
80
|
+
<tr>
|
81
|
+
<td>2</td>
|
82
|
+
<td>42fb060d-2000-4a73-bb74-cfb5ca799e0d</td>
|
83
|
+
<td>es</td>
|
84
|
+
<td>Buenos días!</td>
|
85
|
+
<td>una-publicacion-de-blog</td>
|
86
|
+
</tr>
|
87
|
+
<tr>
|
88
|
+
<td>3</td>
|
89
|
+
<td>160f6aee-ba2d-4c7e-a273-96b99468c8f9</td>
|
90
|
+
<td>en</td>
|
91
|
+
<td>Good night!</td>
|
92
|
+
<td>another-blog-post</td>
|
93
|
+
</tr>
|
94
|
+
<tr>
|
95
|
+
<td>4</td>
|
96
|
+
<td>160f6aee-ba2d-4c7e-a273-96b99468c8f9</td>
|
97
|
+
<td>es</td>
|
98
|
+
<td>Buenas noches!</td>
|
99
|
+
<td>otra-entrada</td>
|
100
|
+
</tr>
|
101
|
+
</tbody>
|
102
|
+
</table>
|
103
|
+
|
104
|
+
Your model may look something like this:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
# app/models/post.rb
|
108
|
+
|
109
|
+
class Post < ApplicationRecord
|
110
|
+
# Enable translations for this model
|
111
|
+
include ActiveRecord::Translated::Model
|
112
|
+
|
113
|
+
# You can keep using validations the same way you did before installing this gem. The only
|
114
|
+
# difference is that validations will be run on all translations separately.
|
115
|
+
#
|
116
|
+
# Examples:
|
117
|
+
# - Title must exist on each translation.
|
118
|
+
# - Every translation should have it's unique slug.
|
119
|
+
validates :title, presence: true
|
120
|
+
validates :slug, presence: true, uniqueness: true
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
### Create a new record
|
125
|
+
|
126
|
+
The creation of a new record will autmatically generate a unique `record_id` and set the `locale` attribute to the current locale. The current locale is determined globally by `I18n.locale` and/or within the gem via `ActiveRecord::Translated.locale`, the latter with greater priority. The default locale if nothing is set is `:en`.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
before_action do
|
130
|
+
I18n.locale = :en_US # fallback if "ActiveRecord::Translated.locale" is not set
|
131
|
+
ActiveRecord::Translated.locale = :en_UK # has priority over "I18n.locale"
|
132
|
+
end
|
133
|
+
|
134
|
+
# POST /posts/create
|
135
|
+
def create
|
136
|
+
@post = Post.create(title: "A post!", ...)
|
137
|
+
|
138
|
+
@post.id # => 5
|
139
|
+
@post.record_id # => 448ecc54-cc82-4c24-aed4-89fae5d38ec4 (auto-generated)
|
140
|
+
@post.locale # => :en_UK
|
141
|
+
...
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
### Create a new record in a specific language
|
146
|
+
|
147
|
+
To create a post in a language other than the current locale, just pass a language code to the `locale` attribute.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
post = Post.create(title: "Una entrada de blog", ..., locale: :es)
|
151
|
+
post.locale # => :es
|
152
|
+
```
|
153
|
+
|
154
|
+
#### Using #with_locale
|
155
|
+
|
156
|
+
You can also wrap your code inside `ActiveRecord::Translated#with_locale`. This will temporary override the current locale within the block.
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
post = ActiveRecord::Translated.with_locale(:es) do
|
160
|
+
Post.create(title: "Una entrada de blog", ...)
|
161
|
+
end
|
162
|
+
|
163
|
+
post.locale # => :es
|
164
|
+
```
|
165
|
+
|
166
|
+
### Create a new translation of an existing record
|
167
|
+
|
168
|
+
To translate an already existing post, create a second post with a different `locale` and add it to the first post via `#translations`. This will make sure that our two translations are linked together with a shared `record_id`.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# Create an english post
|
172
|
+
post = Post.create(title: "A post!", ...)
|
173
|
+
|
174
|
+
# Create a spanish translation
|
175
|
+
post_es = Post.create(title: "Una entrada de blog", ..., locale: :es)
|
176
|
+
|
177
|
+
# Associate the Spanish translation with the english post
|
178
|
+
post.translations << post_es
|
179
|
+
|
180
|
+
# Record ID should match:
|
181
|
+
post.record_id # => 448ecc54-cc82-4c24-aed4-89fae5d38ec4
|
182
|
+
post_es.record_id # => 448ecc54-cc82-4c24-aed4-89fae5d38ec4
|
183
|
+
|
184
|
+
# Locale should differ:
|
185
|
+
post.locale # => :en
|
186
|
+
post_es.locale # => :es
|
187
|
+
```
|
188
|
+
|
189
|
+
### Fetch a translated record
|
190
|
+
|
191
|
+
Your translated model will automatically scope query results by the current locale. For example, with `I18n.locale` set to `:sv`, your model will only return Swedish results. There's some different ways to fetch translations from the database using ActiveRecord's #find:
|
192
|
+
|
193
|
+
#### Find by primary key
|
194
|
+
|
195
|
+
The first and most obvious way is to just query a translation by its primary key. If you already now the ID, just do a normal #find:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
I18n.locale = :en # Ignored by #find when fetching a record by its unique primary key.
|
199
|
+
|
200
|
+
post = Post.find(4) # English translation
|
201
|
+
post_es = Post.find(5) # Spanish translation
|
202
|
+
|
203
|
+
post.locale # => :en
|
204
|
+
post_es.locale #=> :es
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Find by record_id
|
208
|
+
|
209
|
+
You can also find a translated record by using a record_id as the argument. This will look for a row that matches both the record_id and the current locale:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
I18n.locale = :es
|
213
|
+
|
214
|
+
post = Post.find("0e048f11-0ad9-48f1-b493-36e1f01d7994")
|
215
|
+
post_es.locale #=> :es
|
216
|
+
```
|
217
|
+
|
218
|
+
License
|
219
|
+
-------
|
220
|
+
|
221
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
222
|
+
|
223
|
+
Contributing
|
224
|
+
------------
|
225
|
+
|
226
|
+
New contributors are very welcome and needed. This gem is an open-source, community project that anyone can contribute to. Reviewing and testing is highly valued and the most effective way you can contribute as a new contributor. It also will teach you much more about the code and process than opening pull requests.
|
227
|
+
|
228
|
+
Except for testing, there are several ways you can contribute to the betterment of the project:
|
229
|
+
|
230
|
+
- **Report an issue?** - If the issue isn’t reported, we can’t fix it. Please report any bugs, feature, and/or improvement requests on the [GitHub Issues tracker](https://github.com/kjellberg/active_record-translated/issues).
|
231
|
+
- **Submit patches** - Do you have a new feature or a fix you'd like to share? [Submit a pull request](https://github.com/kjellberg/active_record-translated/pulls)!
|
232
|
+
- **Write blog articles** - Are you using this gem? We'd love to hear how you're using it in your projects. Write a tutorial and post it on your blog!
|
233
|
+
|
234
|
+
### Development process
|
235
|
+
|
236
|
+
The `main` branch is regularly built and tested, but it is not guaranteed to be completely stable. Tags are created regularly from release branches to indicate new official, stable release versions of the libraries.
|
237
|
+
|
238
|
+
### Commit message guidelines
|
239
|
+
|
240
|
+
A good commit message should describe what changed and why. This project uses [semantic commit messages](https://www.conventionalcommits.org/en/v1.0.0/) to streamline the release process. Before a pull request can be merged, it must have a pull request title with a semantic prefix.
|
241
|
+
|
242
|
+
### Versioning
|
243
|
+
|
244
|
+
This application aims to adhere to [Semantic Versioning](http://semver.org/). Violations
|
245
|
+
of this scheme should be reported as bugs. Specifically, if a minor or patch
|
246
|
+
version is released that breaks backward compatibility, that version should be
|
247
|
+
immediately yanked and/or a new version should be immediately released that
|
248
|
+
restores compatibility. Breaking changes to the public API will only be
|
249
|
+
introduced with new major versions.
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Translated
|
5
|
+
# Makes Translated available to Rails as an Engine.
|
6
|
+
module Model
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/BlockLength
|
10
|
+
included do
|
11
|
+
before_save :set_record_id
|
12
|
+
before_save :set_locale
|
13
|
+
|
14
|
+
has_many :translations, -> { global }, class_name: "Post", foreign_key: :record_id, primary_key: :record_id
|
15
|
+
default_scope { where(locale: ActiveRecord::Translated.locale) }
|
16
|
+
scope :global, -> { unscope(where: :locale) }
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def find(*args)
|
20
|
+
id = args.first
|
21
|
+
|
22
|
+
return find_translated(id) if ActiveRecord::Translated.record_id?(id)
|
23
|
+
|
24
|
+
super(*args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def exists?(id)
|
28
|
+
return super unless ActiveRecord::Translated.record_id?(id)
|
29
|
+
return true if exists_translated?(id)
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def find_translated(record_id, allow_nil: false)
|
37
|
+
result = find_by(record_id: record_id)
|
38
|
+
return result if result
|
39
|
+
|
40
|
+
raise ActiveRecord::RecordNotFound unless allow_nil
|
41
|
+
end
|
42
|
+
|
43
|
+
# rubocop:disable Rails/WhereExists
|
44
|
+
def exists_translated?(record_id)
|
45
|
+
where(record_id: record_id, locale: ActiveRecord::Translated.locale).exists?
|
46
|
+
end
|
47
|
+
# rubocop:enable Rails/WhereExists
|
48
|
+
end
|
49
|
+
end
|
50
|
+
# rubocop:enable Metrics/BlockLength
|
51
|
+
|
52
|
+
def set_locale
|
53
|
+
return if locale.present?
|
54
|
+
|
55
|
+
self.locale = ActiveRecord::Translated.locale
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_record_id
|
59
|
+
return if record_id.present?
|
60
|
+
|
61
|
+
self.record_id = ActiveRecord::Translated.generate_record_id
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/translated/version"
|
4
|
+
require "active_record/translated/engine"
|
5
|
+
|
6
|
+
require "dry-configurable"
|
7
|
+
require "request_store"
|
8
|
+
require "securerandom"
|
9
|
+
|
10
|
+
module ActiveRecord
|
11
|
+
module Translated
|
12
|
+
extend Dry::Configurable
|
13
|
+
|
14
|
+
# Defaults to nil if no default value is given
|
15
|
+
setting :default_locale, default: nil
|
16
|
+
|
17
|
+
autoload :Model, "active_record/translated/model"
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def generate_record_id
|
21
|
+
SecureRandom.uuid.split("-")[1..3].join("-")
|
22
|
+
end
|
23
|
+
|
24
|
+
def record_id?(string)
|
25
|
+
return false unless string.is_a?(String)
|
26
|
+
|
27
|
+
string.match(/^[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}$\z/)
|
28
|
+
end
|
29
|
+
|
30
|
+
def locale
|
31
|
+
read_locale || config.default_locale || I18n.locale || :en
|
32
|
+
end
|
33
|
+
|
34
|
+
def locale=(locale)
|
35
|
+
set_locale(locale)
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_locale(locale)
|
39
|
+
previous_locale = read_locale
|
40
|
+
begin
|
41
|
+
set_locale(locale)
|
42
|
+
yield(locale)
|
43
|
+
ensure
|
44
|
+
set_locale(previous_locale)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# @!endgroup
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def read_locale
|
52
|
+
storage[:art_locale]
|
53
|
+
end
|
54
|
+
|
55
|
+
# rubocop:disable Naming/AccessorMethodName
|
56
|
+
def set_locale(locale)
|
57
|
+
locale = locale.to_sym if locale.is_a?(String)
|
58
|
+
storage[:art_locale] = locale
|
59
|
+
end
|
60
|
+
# rubocop:enable Naming/AccessorMethodName
|
61
|
+
|
62
|
+
def storage
|
63
|
+
RequestStore.store
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_record-translated
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rasmus Kjellberg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-configurable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-smtp
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.3.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.3.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: request_store
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.2.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.2.0
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- README.md
|
62
|
+
- lib/active_record/translated.rb
|
63
|
+
- lib/active_record/translated/engine.rb
|
64
|
+
- lib/active_record/translated/model.rb
|
65
|
+
- lib/active_record/translated/version.rb
|
66
|
+
- lib/tasks/active_record/translated_tasks.rake
|
67
|
+
homepage: https://github.com/kjellberg/active_record-translated
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata:
|
71
|
+
bug_tracker_uri: https://github.com/kjellberg/active_record-translated/issues
|
72
|
+
documentation_uri: https://github.com/kjellberg/active_record-translated/issues
|
73
|
+
source_code_uri: https://github.com/kjellberg/active_record-translated
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.7'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubygems_version: 3.3.7
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: Separate database records for each language, grouped together with an ID
|
93
|
+
test_files: []
|