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 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Translated
5
+ # Makes Translated available to Rails as an Engine.
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
9
+ end
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module ActiveRecord
5
+ module Translated
6
+ VERSION = "0.1.1"
7
+ end
8
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :active_record_translated do
4
+ # # Task goes here
5
+ # 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: []