internationalize 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -1
- data/README.md +72 -11
- data/context/configuration.md +8 -4
- data/context/model-api.md +87 -2
- data/lib/internationalize/model.rb +5 -2
- data/lib/internationalize/rich_text.rb +1 -1
- data/lib/internationalize/validations.rb +85 -0
- data/lib/internationalize/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0808602c10b51ee1884f04c5b9fd66d05381d07c0f86d284517dce7f02ebb6cd'
|
|
4
|
+
data.tar.gz: fe0c214e9c386b191d4e8f06e1ddee136d9e7a15b25a59d4dd0c9a4860a6589a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18cf19554d3187476b26505e555e7f181fb6c31edc100e054a2a96f8eafbdb09fa1ad424544973abc349a8f68c62ec0d5634216c6d4fd4cfd029221331d620ed
|
|
7
|
+
data.tar.gz: d998cbcd3ede8f8a1574e3adcec37ae3afb6216772de801ed0693199bbe70befe398f89a0ff001a66f149cc10f226ed5fd93721c44067e3ebd0ba62ad6e6643d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.3.0] - 2024-12-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `validates_international` method for locale-specific validations:
|
|
13
|
+
- `uniqueness: true` - validates uniqueness per-locale (requires JSON column querying)
|
|
14
|
+
- `presence: { locales: [:en, :de] }` - requires translations for specific locales (useful for admin interfaces)
|
|
15
|
+
- Standard Rails validations (`validates :title, presence: true`) now work with virtual accessors
|
|
16
|
+
|
|
17
|
+
## [0.2.4] - 2024-11-29
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Fixtures documentation and tests demonstrating YAML format for translation columns
|
|
22
|
+
|
|
23
|
+
## [0.2.3] - 2024-11-29
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Auto-load RichText via Rails Railtie (fixes load order issue)
|
|
9
28
|
|
|
10
29
|
## [0.2.2] - 2024-11-29
|
|
11
30
|
|
data/README.md
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
# Internationalize
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/internationalize)
|
|
4
|
+
[](https://github.com/sampokuokkanen/internationalize/actions)
|
|
5
|
+
|
|
3
6
|
Lightweight, performant internationalization for Rails with JSON column storage.
|
|
4
7
|
|
|
5
8
|
## Why Internationalize?
|
|
6
9
|
|
|
7
10
|
Internationalize is a focused, lightweight gem that does one thing well: JSON column translations. No backend abstraction layers, no plugin systems, no extra memory overhead.
|
|
8
11
|
|
|
9
|
-
Unlike Globalize
|
|
12
|
+
Unlike Globalize (separate translation tables) or Mobility (JSON backend is PostgreSQL-only), Internationalize stores translations inline using JSON columns with **full SQLite, PostgreSQL, and MySQL support**:
|
|
10
13
|
|
|
11
14
|
- **No JOINs** - translations live in the same table
|
|
12
15
|
- **No N+1 queries** - data is always loaded with the record
|
|
13
16
|
- **No backend overhead** - direct JSON column access, no abstraction layers
|
|
14
17
|
- **~50% less memory** - no per-instance backend objects or plugin chains
|
|
15
18
|
- **Direct method dispatch** - no `method_missing` overhead
|
|
16
|
-
- **
|
|
19
|
+
- **True multi-database JSON support** - SQLite 3.38+, PostgreSQL 9.4+, MySQL 8.0+
|
|
20
|
+
- **ActionText support** - internationalized rich text with attachments
|
|
17
21
|
- **Visible in schema.rb** - translated fields appear directly in your model's schema
|
|
18
22
|
|
|
23
|
+
> **Note:** Mobility's JSON/JSONB backends only work with PostgreSQL. For SQLite or MySQL, Mobility requires separate translation tables with JOINs. Internationalize provides JSON column querying across all three databases.
|
|
24
|
+
|
|
19
25
|
## Supported Databases
|
|
20
26
|
|
|
21
27
|
| Database | JSON Column | Query Syntax |
|
|
@@ -173,9 +179,46 @@ I18n.locale = :de
|
|
|
173
179
|
article.title # => "Hello" (falls back to :en)
|
|
174
180
|
```
|
|
175
181
|
|
|
182
|
+
### Validations
|
|
183
|
+
|
|
184
|
+
For most validations, use standard Rails validators—they work with the virtual accessor for the current locale:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
class Article < ApplicationRecord
|
|
188
|
+
include Internationalize::Model
|
|
189
|
+
international :title
|
|
190
|
+
|
|
191
|
+
# Standard Rails validations (recommended)
|
|
192
|
+
validates :title, presence: true
|
|
193
|
+
validates :title, length: { minimum: 3, maximum: 100 }
|
|
194
|
+
validates :title, format: { with: /\A[a-z0-9-]+\z/ }
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use `validates_international` only when you need:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
class Article < ApplicationRecord
|
|
202
|
+
include Internationalize::Model
|
|
203
|
+
international :title
|
|
204
|
+
|
|
205
|
+
# Uniqueness per-locale (requires JSON column querying)
|
|
206
|
+
validates_international :title, uniqueness: true
|
|
207
|
+
|
|
208
|
+
# Multi-locale presence (for admin interfaces editing all translations at once)
|
|
209
|
+
validates_international :title, presence: { locales: [:en, :de] }
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Errors from `validates_international` are added to locale-specific keys:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
article.errors[:title_en] # => ["has already been taken"]
|
|
217
|
+
```
|
|
218
|
+
|
|
176
219
|
### ActionText Support
|
|
177
220
|
|
|
178
|
-
For rich text with attachments
|
|
221
|
+
For rich text with attachments (requires ActionText):
|
|
179
222
|
|
|
180
223
|
```ruby
|
|
181
224
|
class Article < ApplicationRecord
|
|
@@ -196,17 +239,35 @@ article.content.body # ActionText::Content object
|
|
|
196
239
|
article.content.embeds # Attachments work per-locale
|
|
197
240
|
```
|
|
198
241
|
|
|
242
|
+
### Fixtures
|
|
243
|
+
|
|
244
|
+
Use the `*_translations` column name with nested locale keys:
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
# test/fixtures/articles.yml
|
|
248
|
+
hello_world:
|
|
249
|
+
title_translations:
|
|
250
|
+
en: "Hello World"
|
|
251
|
+
de: "Hallo Welt"
|
|
252
|
+
status: published
|
|
253
|
+
```
|
|
254
|
+
|
|
199
255
|
## Configuration
|
|
200
256
|
|
|
257
|
+
No configuration required. Internationalize uses your existing Rails I18n settings:
|
|
258
|
+
|
|
259
|
+
- **Locales**: `I18n.available_locales`
|
|
260
|
+
- **Fallback**: `I18n.default_locale`
|
|
261
|
+
|
|
262
|
+
To override locales (rarely needed):
|
|
263
|
+
|
|
201
264
|
```ruby
|
|
202
265
|
# config/initializers/internationalize.rb
|
|
203
266
|
Internationalize.configure do |config|
|
|
204
|
-
config.available_locales = [:en, :de, :fr]
|
|
267
|
+
config.available_locales = [:en, :de, :fr]
|
|
205
268
|
end
|
|
206
269
|
```
|
|
207
270
|
|
|
208
|
-
Fallback uses `I18n.default_locale` automatically.
|
|
209
|
-
|
|
210
271
|
## Performance Comparison
|
|
211
272
|
|
|
212
273
|
Benchmark with 1000 records, 2 translated attributes (title + body), 3 locales:
|
|
@@ -237,13 +298,13 @@ Benchmark with 1000 records, 2 translated attributes (title + body), 3 locales:
|
|
|
237
298
|
|
|
238
299
|
### When to consider Mobility
|
|
239
300
|
|
|
240
|
-
|
|
301
|
+
Consider [Mobility](https://github.com/shioyama/mobility) if:
|
|
241
302
|
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
303
|
+
- You need to add translations without modifying existing table schemas
|
|
304
|
+
- You're on PostgreSQL and want their JSON backend (note: PostgreSQL-only)
|
|
305
|
+
- You need the flexibility of multiple backend strategies
|
|
245
306
|
|
|
246
|
-
Mobility's table backend stores translations in separate tables with JOINs, which trades query performance for schema flexibility.
|
|
307
|
+
Mobility's table backend stores translations in separate tables with JOINs, which trades query performance for schema flexibility. Their JSON backend is PostgreSQL-only.
|
|
247
308
|
|
|
248
309
|
## License
|
|
249
310
|
|
data/context/configuration.md
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# Internationalize: Configuration
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Zero Configuration
|
|
4
|
+
|
|
5
|
+
No configuration required. Internationalize uses your existing Rails I18n settings:
|
|
6
|
+
|
|
7
|
+
- `I18n.available_locales` - determines which locale accessors are generated
|
|
8
|
+
- `I18n.default_locale` - used for fallback when translation is missing
|
|
9
|
+
|
|
10
|
+
## Override Locales (Rarely Needed)
|
|
4
11
|
|
|
5
12
|
```ruby
|
|
6
13
|
# config/initializers/internationalize.rb
|
|
7
14
|
Internationalize.configure do |config|
|
|
8
|
-
# Override available locales (defaults to I18n.available_locales)
|
|
9
15
|
config.available_locales = [:en, :de, :fr, :es]
|
|
10
16
|
end
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
Fallback locale is always `I18n.default_locale`.
|
|
14
|
-
|
|
15
19
|
## Database Setup
|
|
16
20
|
|
|
17
21
|
Use the generator to create migrations:
|
data/context/model-api.md
CHANGED
|
@@ -80,14 +80,66 @@ I18n.locale = :de
|
|
|
80
80
|
article.title # => "Hello" (falls back to default locale)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
## Validations
|
|
84
|
+
|
|
85
|
+
For most validations, use standard Rails validators—they work with the virtual accessor:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
class Article < ApplicationRecord
|
|
89
|
+
include Internationalize::Model
|
|
90
|
+
international :title
|
|
91
|
+
|
|
92
|
+
# Standard Rails validations (recommended)
|
|
93
|
+
validates :title, presence: true
|
|
94
|
+
validates :title, length: { minimum: 3, maximum: 100 }
|
|
95
|
+
validates :title, format: { with: /\A[a-z]+\z/ }
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Use `validates_international` only for uniqueness or multi-locale presence:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class Article < ApplicationRecord
|
|
103
|
+
include Internationalize::Model
|
|
104
|
+
international :title
|
|
105
|
+
|
|
106
|
+
# Uniqueness per-locale (requires JSON column querying)
|
|
107
|
+
validates_international :title, uniqueness: true
|
|
108
|
+
|
|
109
|
+
# Multi-locale presence (for admin interfaces editing all translations at once)
|
|
110
|
+
validates_international :title, presence: { locales: [:en, :de] }
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Validation Options
|
|
115
|
+
|
|
116
|
+
| Option | Description |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| `uniqueness: true` | Validates uniqueness per-locale (current locale) |
|
|
119
|
+
| `presence: { locales: [:en, :de] }` | Requires translations for specific locales |
|
|
120
|
+
|
|
121
|
+
### Error Keys
|
|
122
|
+
|
|
123
|
+
Standard Rails validations add errors to the virtual attribute:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
article.errors[:title] # => ["can't be blank"]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`validates_international` adds errors to locale-specific keys:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
article.errors[:title_en] # => ["has already been taken"]
|
|
133
|
+
```
|
|
134
|
+
|
|
83
135
|
## ActionText Support
|
|
84
136
|
|
|
85
|
-
For rich text with attachments,
|
|
137
|
+
For rich text with attachments, include `Internationalize::RichText` and use `international_rich_text`:
|
|
86
138
|
|
|
87
139
|
```ruby
|
|
88
140
|
class Article < ApplicationRecord
|
|
89
141
|
include Internationalize::Model
|
|
90
|
-
include Internationalize::RichText
|
|
142
|
+
include Internationalize::RichText # Requires ActionText
|
|
91
143
|
|
|
92
144
|
international_rich_text :content
|
|
93
145
|
end
|
|
@@ -113,3 +165,36 @@ article.content # Gets for current locale (with fallback)
|
|
|
113
165
|
article.content.body # ActionText::Content object
|
|
114
166
|
article.content.embeds # Attachments work per-locale
|
|
115
167
|
```
|
|
168
|
+
|
|
169
|
+
## Fixtures
|
|
170
|
+
|
|
171
|
+
Use the actual column name (`*_translations`) in fixtures, not the virtual accessor:
|
|
172
|
+
|
|
173
|
+
### Nested Format
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
# test/fixtures/articles.yml
|
|
177
|
+
hello_world:
|
|
178
|
+
title_translations:
|
|
179
|
+
en: "Hello World"
|
|
180
|
+
de: "Hallo Welt"
|
|
181
|
+
description_translations:
|
|
182
|
+
en: "A greeting"
|
|
183
|
+
de: "Eine Begrüßung"
|
|
184
|
+
status: published
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Inline Hash Format
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
japanese_post:
|
|
191
|
+
title_translations: { en: "Hello", ja: "こんにちは" }
|
|
192
|
+
status: published
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Important Notes
|
|
196
|
+
|
|
197
|
+
- Use `title_translations:` (column name), NOT `title:`
|
|
198
|
+
- Keys can be symbols (`:en`) or strings (`"en"`) - YAML converts them
|
|
199
|
+
- Both nested and inline hash formats work identically
|
|
200
|
+
- Missing locales are simply not set (no error)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "validations"
|
|
4
|
+
|
|
3
5
|
module Internationalize
|
|
4
6
|
# Mixin for ActiveRecord models to enable internationalization
|
|
5
7
|
#
|
|
@@ -16,6 +18,7 @@ module Internationalize
|
|
|
16
18
|
#
|
|
17
19
|
module Model
|
|
18
20
|
extend ActiveSupport::Concern
|
|
21
|
+
include Internationalize::Validations
|
|
19
22
|
|
|
20
23
|
VALID_DIRECTIONS = ["ASC", "DESC"].freeze
|
|
21
24
|
|
|
@@ -334,7 +337,7 @@ module Internationalize
|
|
|
334
337
|
hash.each_key do |key|
|
|
335
338
|
unless allowed_locales.include?(key.to_s)
|
|
336
339
|
raise ArgumentError, "Invalid locale '#{key}' for #{attr}_translations. " \
|
|
337
|
-
"Allowed locales: #{allowed_locales.join(
|
|
340
|
+
"Allowed locales: #{allowed_locales.join(", ")}"
|
|
338
341
|
end
|
|
339
342
|
end
|
|
340
343
|
|
|
@@ -351,7 +354,7 @@ module Internationalize
|
|
|
351
354
|
|
|
352
355
|
if locale_str.include?("-")
|
|
353
356
|
raise ArgumentError, "Locale '#{locale}' contains a hyphen which is invalid for Ruby method names. " \
|
|
354
|
-
"Use underscore format instead: :#{locale_str.tr(
|
|
357
|
+
"Use underscore format instead: :#{locale_str.tr("-", "_")}"
|
|
355
358
|
end
|
|
356
359
|
|
|
357
360
|
getter_method = :"#{attr}_#{locale}"
|
|
@@ -31,7 +31,7 @@ module Internationalize
|
|
|
31
31
|
locale_str = locale.to_s
|
|
32
32
|
if locale_str.include?("-")
|
|
33
33
|
raise ArgumentError, "Locale '#{locale}' contains a hyphen which is invalid for Ruby method names. " \
|
|
34
|
-
"Use underscore format instead: :#{locale_str.tr(
|
|
34
|
+
"Use underscore format instead: :#{locale_str.tr("-", "_")}"
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Internationalize
|
|
4
|
+
# Validation support for internationalized attributes
|
|
5
|
+
#
|
|
6
|
+
# For simple validations (presence, length, format), use standard Rails validations
|
|
7
|
+
# which work with the virtual accessor for the current locale:
|
|
8
|
+
#
|
|
9
|
+
# validates :title, presence: true
|
|
10
|
+
# validates :title, length: { minimum: 3 }
|
|
11
|
+
#
|
|
12
|
+
# Use validates_international for:
|
|
13
|
+
# - Uniqueness (requires JSON column querying)
|
|
14
|
+
# - Multi-locale presence (admin interfaces editing all translations at once)
|
|
15
|
+
#
|
|
16
|
+
# @example Uniqueness per-locale
|
|
17
|
+
# validates_international :title, uniqueness: true
|
|
18
|
+
#
|
|
19
|
+
# @example Require specific locales (admin/manager interfaces)
|
|
20
|
+
# validates_international :title, presence: { locales: [:en, :de] }
|
|
21
|
+
#
|
|
22
|
+
module Validations
|
|
23
|
+
extend ActiveSupport::Concern
|
|
24
|
+
|
|
25
|
+
class_methods do
|
|
26
|
+
# Validates internationalized attributes
|
|
27
|
+
#
|
|
28
|
+
# @param attrs [Array<Symbol>] attribute names to validate
|
|
29
|
+
# @param options [Hash] validation options
|
|
30
|
+
# @option options [Boolean] :uniqueness validate uniqueness per-locale (current locale)
|
|
31
|
+
# @option options [Hash] :presence with :locales array for multi-locale presence
|
|
32
|
+
#
|
|
33
|
+
# @example Uniqueness validation (per-locale)
|
|
34
|
+
# validates_international :title, uniqueness: true
|
|
35
|
+
#
|
|
36
|
+
# @example Multi-locale presence (for admin interfaces)
|
|
37
|
+
# validates_international :title, presence: { locales: [:en, :de] }
|
|
38
|
+
#
|
|
39
|
+
def validates_international(*attrs, **options)
|
|
40
|
+
presence_opts = options.delete(:presence)
|
|
41
|
+
uniqueness_opts = options.delete(:uniqueness)
|
|
42
|
+
|
|
43
|
+
attrs.each do |attr|
|
|
44
|
+
validate_international_presence(attr, presence_opts) if presence_opts
|
|
45
|
+
validate_international_uniqueness(attr, uniqueness_opts) if uniqueness_opts
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate_international_presence(attr, options)
|
|
52
|
+
unless options.is_a?(Hash) && options[:locales]
|
|
53
|
+
raise ArgumentError, "validates_international presence requires locales: option. " \
|
|
54
|
+
"For current locale, use: validates :#{attr}, presence: true"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
locales = options[:locales].map(&:to_s)
|
|
58
|
+
|
|
59
|
+
validate do |record|
|
|
60
|
+
locales.each do |locale|
|
|
61
|
+
value = record.send("#{attr}_#{locale}")
|
|
62
|
+
if value.blank?
|
|
63
|
+
record.errors.add("#{attr}_#{locale}", :blank)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_international_uniqueness(attr, _options)
|
|
70
|
+
validate do |record|
|
|
71
|
+
locale = I18n.locale.to_s
|
|
72
|
+
value = record.send("#{attr}_#{locale}")
|
|
73
|
+
next if value.blank?
|
|
74
|
+
|
|
75
|
+
scope = record.class.international(attr => value, locale: locale)
|
|
76
|
+
scope = scope.where.not(id: record.id) if record.persisted?
|
|
77
|
+
|
|
78
|
+
if scope.exists?
|
|
79
|
+
record.errors.add("#{attr}_#{locale}", :taken)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: internationalize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sampo Kuokkanen
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- lib/internationalize/adapters/sqlite.rb
|
|
65
65
|
- lib/internationalize/model.rb
|
|
66
66
|
- lib/internationalize/rich_text.rb
|
|
67
|
+
- lib/internationalize/validations.rb
|
|
67
68
|
- lib/internationalize/version.rb
|
|
68
69
|
homepage: https://github.com/sampokuokkanen/internationalize
|
|
69
70
|
licenses:
|