internationalize 0.2.4 → 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: bfc56411ee42ac5d8377fe409418e1692acc4cb0c5cd60899acd70003f0d4dd7
4
- data.tar.gz: 9a437ba5771695b1d84b7d95e61d4df4e342725501df6dc947809245750054a9
3
+ metadata.gz: '0808602c10b51ee1884f04c5b9fd66d05381d07c0f86d284517dce7f02ebb6cd'
4
+ data.tar.gz: fe0c214e9c386b191d4e8f06e1ddee136d9e7a15b25a59d4dd0c9a4860a6589a
5
5
  SHA512:
6
- metadata.gz: 4354ea3355e467ebfc465feacdb707189b5981f6df04e8620e8b72dbf4c84f4df8e2d454e3a405f0ffd7c923ec4c8c946f119241b99ddf978d700fbeebb1ecbf
7
- data.tar.gz: 82e139593ed3296be30e601376b147cfaf2690255f25c2be69b7add169568b388ad721bd14a0f6244489987e4bbf9fc4a89f303a8c87a1f94eca99f5e524b8b0
6
+ metadata.gz: 18cf19554d3187476b26505e555e7f181fb6c31edc100e054a2a96f8eafbdb09fa1ad424544973abc349a8f68c62ec0d5634216c6d4fd4cfd029221331d620ed
7
+ data.tar.gz: d998cbcd3ede8f8a1574e3adcec37ae3afb6216772de801ed0693199bbe70befe398f89a0ff001a66f149cc10f226ed5fd93721c44067e3ebd0ba62ad6e6643d
data/CHANGELOG.md CHANGED
@@ -5,7 +5,14 @@ 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
9
16
 
10
17
  ## [0.2.4] - 2024-11-29
11
18
 
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
@@ -211,15 +254,20 @@ hello_world:
211
254
 
212
255
  ## Configuration
213
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
+
214
264
  ```ruby
215
265
  # config/initializers/internationalize.rb
216
266
  Internationalize.configure do |config|
217
- config.available_locales = [:en, :de, :fr] # Defaults to I18n.available_locales
267
+ config.available_locales = [:en, :de, :fr]
218
268
  end
219
269
  ```
220
270
 
221
- Fallback uses `I18n.default_locale` automatically.
222
-
223
271
  ## Performance Comparison
224
272
 
225
273
  Benchmark with 1000 records, 2 translated attributes (title + body), 3 locales:
@@ -250,13 +298,13 @@ Benchmark with 1000 records, 2 translated attributes (title + body), 3 locales:
250
298
 
251
299
  ### When to consider Mobility
252
300
 
253
- For table-based translation storage, consider [Mobility](https://github.com/shioyama/mobility) instead:
301
+ Consider [Mobility](https://github.com/shioyama/mobility) if:
254
302
 
255
- - Existing projects with large tables where adding columns is expensive
256
- - When you need to add translations without modifying existing table schemas
257
- - 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
258
306
 
259
- 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.
260
308
 
261
309
  ## License
262
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
@@ -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.4"
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.4
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: