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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b22ab7e48ce183d9094dada5300dde85ee7985f3703db4db569f91556507454
4
- data.tar.gz: b306eb40ab2635fcf416ce80cd7932b7868dc51a324bfc31b70ae928bc1a72e3
3
+ metadata.gz: '0808602c10b51ee1884f04c5b9fd66d05381d07c0f86d284517dce7f02ebb6cd'
4
+ data.tar.gz: fe0c214e9c386b191d4e8f06e1ddee136d9e7a15b25a59d4dd0c9a4860a6589a
5
5
  SHA512:
6
- metadata.gz: 9e09d4c92ae4d85e65d69d9089146184da9679f82ed3880b5f35c925aeb929853fcadc703771de60e5dccc867160683c71e46ac9db83ca4bd926bb1f936fe5fe
7
- data.tar.gz: 81026001e7c16d09a11c9815d37e35757c46c5dd86039ed44bf409a5144ead87e5cb724166d63475323f145b1dcf04df5e27d26e9f01b3cef781b3a38fa788a8
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
- ## [Unreleased]
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
+ [![Gem Version](https://badge.fury.io/rb/internationalize.svg)](https://rubygems.org/gems/internationalize)
4
+ [![Build](https://github.com/sampokuokkanen/internationalize/workflows/CI/badge.svg)](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 or Mobility which use separate translation tables requiring JOINs, Internationalize stores translations inline using JSON columns. This means:
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
- - **Multi-database support** - works with SQLite, PostgreSQL, and MySQL
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, use `international_rich_text` (auto-loaded when ActionText is available):
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] # Defaults to I18n.available_locales
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
- For table-based translation storage, consider [Mobility](https://github.com/shioyama/mobility) instead:
301
+ Consider [Mobility](https://github.com/shioyama/mobility) if:
241
302
 
242
- - Existing projects with large tables where adding columns is expensive
243
- - When you need to add translations without modifying existing table schemas
244
- - Applications that rarely query by translated content
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
 
@@ -1,17 +1,21 @@
1
1
  # Internationalize: Configuration
2
2
 
3
- ## Global Configuration
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, use `international_rich_text` (auto-loaded when ActionText is available):
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Internationalize
4
- VERSION = "0.2.3"
4
+ VERSION = "0.3.0"
5
5
  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.2.3
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: