internationalize 0.3.0 → 0.5.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: '0808602c10b51ee1884f04c5b9fd66d05381d07c0f86d284517dce7f02ebb6cd'
4
- data.tar.gz: fe0c214e9c386b191d4e8f06e1ddee136d9e7a15b25a59d4dd0c9a4860a6589a
3
+ metadata.gz: e35fc3d32a34b9dde3097192bd791ffb50b8d62a68b1520347da4ed2ac72224f
4
+ data.tar.gz: decad482b4b5f99fedf070f2f64fb655f088ab6397f709a5362623f59570342d
5
5
  SHA512:
6
- metadata.gz: 18cf19554d3187476b26505e555e7f181fb6c31edc100e054a2a96f8eafbdb09fa1ad424544973abc349a8f68c62ec0d5634216c6d4fd4cfd029221331d620ed
7
- data.tar.gz: d998cbcd3ede8f8a1574e3adcec37ae3afb6216772de801ed0693199bbe70befe398f89a0ff001a66f149cc10f226ed5fd93721c44067e3ebd0ba62ad6e6643d
6
+ metadata.gz: 2a652e6a3725561e8b5455b6676a490d214aff84f78163b5ab02385a4708381e80cdb8125782b1cb36eab9ae386384768e0231a7e988cf637ddbf464a8ff62d8
7
+ data.tar.gz: d4e980bb9d34168a802660b918bf3d092cf6b9be3b62d1b81696aa3d9ee5d026dcb335e4993fa6ca55a9b4964d58a82eaefd7d6fbedd8eae64ede2a99391519b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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
+ ## [0.4.0] - 2025-12-02
9
+
10
+ ### Added
11
+
12
+ - `i18n_where` query method as a short, convenient alias for querying translated attributes
13
+ - `Article.i18n_where(title: "Hello")` - exact match
14
+ - `Article.i18n_where(title: "hello", match: :partial)` - LIKE match
15
+
16
+ ### Deprecated
17
+
18
+ - Using `international` for querying is now deprecated; use `i18n_where` or `international_where` instead
19
+ - A deprecation warning is now emitted when using `international` for queries
20
+
8
21
  ## [0.3.0] - 2024-12-01
9
22
 
10
23
  ### Added
@@ -13,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
26
  - `uniqueness: true` - validates uniqueness per-locale (requires JSON column querying)
14
27
  - `presence: { locales: [:en, :de] }` - requires translations for specific locales (useful for admin interfaces)
15
28
  - Standard Rails validations (`validates :title, presence: true`) now work with virtual accessors
29
+ - `international_where` query method as a clearer alternative to `international` for queries
16
30
 
17
31
  ## [0.2.4] - 2024-11-29
18
32
 
data/README.md CHANGED
@@ -119,13 +119,12 @@ All query methods default to the current `I18n.locale` and return ActiveRecord r
119
119
 
120
120
  ```ruby
121
121
  # Exact match on translation (uses current locale by default)
122
- Article.international(title: "Hello World")
123
- Article.international(title: "Hallo Welt", locale: :de)
122
+ Article.i18n_where(title: "Hello World")
123
+ Article.i18n_where(title: "Hallo Welt", locale: :de)
124
124
 
125
125
  # Partial match / search (case-insensitive LIKE)
126
- Article.international(title: "hello", match: :partial)
127
- Article.international(title: "Hello", match: :partial, case_sensitive: true)
128
- Article.international(title: "hallo", match: :partial, locale: :de)
126
+ Article.i18n_where(title: "hello", match: :partial)
127
+ Article.i18n_where(title: "Hello", match: :partial, case_sensitive: true)
129
128
 
130
129
  # Exclude matches
131
130
  Article.international_not(title: "Draft")
@@ -142,19 +141,15 @@ Article.translated(:title, locale: :de)
142
141
  Article.untranslated(:title, locale: :de)
143
142
 
144
143
  # Chain with ActiveRecord methods
145
- Article.international(title: "Hello World")
144
+ Article.i18n_where(title: "Hello World")
146
145
  .where(published: true)
147
146
  .includes(:author)
148
147
  .limit(10)
149
148
 
150
149
  # Combine queries
151
- Article.international(title: "hello", match: :partial)
150
+ Article.i18n_where(title: "hello", match: :partial)
152
151
  .where(status: "published")
153
152
  .merge(Article.international_order(:title, :desc))
154
-
155
- # Query across multiple locales
156
- Article.international(title: "Hello World", locale: :en)
157
- .merge(Article.international(title: "Hallo Welt", locale: :de))
158
153
  ```
159
154
 
160
155
  ### Helper Methods
@@ -210,12 +205,50 @@ class Article < ApplicationRecord
210
205
  end
211
206
  ```
212
207
 
213
- Errors from `validates_international` are added to locale-specific keys:
208
+ Errors from `validates_international` are added to the base attribute name for clean user-facing messages:
214
209
 
215
210
  ```ruby
216
- article.errors[:title_en] # => ["has already been taken"]
211
+ article.errors[:title] # => ["has already been taken"]
212
+ # Displays as: "Title has already been taken" (not "Title en has already been taken")
213
+ ```
214
+
215
+ #### Locale-Specific Error Messages
216
+
217
+ For admin interfaces where you need to indicate which specific locale failed validation, use custom validations that add errors to locale-suffixed attributes:
218
+
219
+ ```ruby
220
+ class Article < ApplicationRecord
221
+ include Internationalize::Model
222
+ international :title
223
+
224
+ validate :validate_required_translations
225
+
226
+ private
227
+
228
+ def validate_required_translations
229
+ [:en, :de].each do |locale|
230
+ if send("title_#{locale}").blank?
231
+ errors.add("title_#{locale}", :blank)
232
+ end
233
+ end
234
+ end
235
+ end
217
236
  ```
218
237
 
238
+ When using locale-suffixed error keys, configure Rails I18n to provide user-friendly attribute names:
239
+
240
+ ```yaml
241
+ # config/locales/en.yml
242
+ en:
243
+ activerecord:
244
+ attributes:
245
+ article:
246
+ title_en: "Title (English)"
247
+ title_de: "Title (German)"
248
+ ```
249
+
250
+ This displays as "Title (German) can't be blank" instead of "Title de can't be blank".
251
+
219
252
  ### ActionText Support
220
253
 
221
254
  For rich text with attachments (requires ActionText):
@@ -71,10 +71,10 @@ article.title_translations # => {"en" => "Hello World", "de" => "Hallo Welt"}
71
71
 
72
72
  ```ruby
73
73
  # Exact match
74
- Article.international(title: "Hello World")
74
+ Article.i18n_where(title: "Hello World")
75
75
 
76
76
  # Partial match (LIKE)
77
- Article.international(title: "Hello", match: :partial)
77
+ Article.i18n_where(title: "Hello", match: :partial)
78
78
 
79
79
  # Order by translation
80
80
  Article.international_order(:title, :desc)
data/context/model-api.md CHANGED
@@ -49,17 +49,17 @@ All query methods default to current `I18n.locale` and return `ActiveRecord::Rel
49
49
 
50
50
  | Method | Description |
51
51
  |--------|-------------|
52
- | `international(**conditions, locale: nil)` | Exact match query |
52
+ | `i18n_where(**conditions, locale: nil, match: :exact)` | Query by translation (short alias) |
53
+ | `international_where(**conditions, locale: nil, match: :exact)` | Query by translation |
53
54
  | `international_not(**conditions, locale: nil)` | Exclude matching records |
54
- | `international_search(**conditions, locale: nil, case_sensitive: false)` | Substring search |
55
55
  | `international_order(attr, dir = :asc, locale: nil)` | Order by translation |
56
56
  | `translated(*attrs, locale: nil)` | Find records with translation |
57
57
  | `untranslated(*attrs, locale: nil)` | Find records missing translation |
58
58
 
59
59
  ```ruby
60
60
  # Examples
61
- Article.international(title: "Hello World")
62
- Article.international_search(title: "hello", locale: :de)
61
+ Article.i18n_where(title: "Hello World")
62
+ Article.i18n_where(title: "hello", match: :partial) # case-insensitive LIKE
63
63
  Article.international_order(:title, :desc)
64
64
  Article.translated(:title, locale: :de)
65
65
  ```
@@ -120,18 +120,45 @@ end
120
120
 
121
121
  ### Error Keys
122
122
 
123
- Standard Rails validations add errors to the virtual attribute:
123
+ Both standard Rails validations and `validates_international` add errors to the base attribute for clean user-facing messages:
124
124
 
125
125
  ```ruby
126
- article.errors[:title] # => ["can't be blank"]
126
+ article.errors[:title] # => ["has already been taken"]
127
+ # Displays as: "Title has already been taken" (not "Title en has already been taken")
127
128
  ```
128
129
 
129
- `validates_international` adds errors to locale-specific keys:
130
+ #### Locale-Specific Error Messages
131
+
132
+ For admin interfaces where you need to indicate which specific locale failed, use custom validations that add errors to locale-suffixed attributes:
130
133
 
131
134
  ```ruby
132
- article.errors[:title_en] # => ["has already been taken"]
135
+ validate :validate_required_translations
136
+
137
+ private
138
+
139
+ def validate_required_translations
140
+ [:en, :de].each do |locale|
141
+ if send("title_#{locale}").blank?
142
+ errors.add("title_#{locale}", :blank)
143
+ end
144
+ end
145
+ end
133
146
  ```
134
147
 
148
+ When using locale-suffixed error keys, configure Rails I18n for user-friendly display:
149
+
150
+ ```yaml
151
+ # config/locales/en.yml
152
+ en:
153
+ activerecord:
154
+ attributes:
155
+ article:
156
+ title_en: "Title (English)"
157
+ title_de: "Title (German)"
158
+ ```
159
+
160
+ This displays as "Title (German) can't be blank" instead of "Title de can't be blank".
161
+
135
162
  ## ActionText Support
136
163
 
137
164
  For rich text with attachments, include `Internationalize::RichText` and use `international_rich_text`:
data/context/query-api.md CHANGED
@@ -8,47 +8,40 @@ All query methods default to the current `I18n.locale`. Use the `locale:` option
8
8
 
9
9
  ```ruby
10
10
  # Uses current I18n.locale
11
- Article.international(title: "Hello World")
11
+ Article.i18n_where(title: "Hello World")
12
12
 
13
13
  # Explicit locale
14
- Article.international(title: "Hallo Welt", locale: :de)
14
+ Article.i18n_where(title: "Hallo Welt", locale: :de)
15
15
  ```
16
16
 
17
17
  ## Query Methods
18
18
 
19
- ### international(**conditions, locale: nil)
19
+ ### i18n_where(**conditions, locale: nil, match: :exact, case_sensitive: false)
20
20
 
21
- Exact match on translated attributes:
21
+ Query by translated attributes. Also available as `international_where`.
22
+
23
+ Exact match (default):
22
24
 
23
25
  ```ruby
24
- Article.international(title: "Hello World")
25
- Article.international(title: "Hello", status: "published")
26
- Article.international(title: "Hallo Welt", locale: :de)
26
+ Article.i18n_where(title: "Hello World")
27
+ Article.i18n_where(title: "Hello", status: "published")
28
+ Article.i18n_where(title: "Hallo Welt", locale: :de)
27
29
  ```
28
30
 
29
- ### international_not(**conditions, locale: nil)
30
-
31
- Exclude records matching conditions:
31
+ Partial match (case-insensitive LIKE):
32
32
 
33
33
  ```ruby
34
- Article.international_not(title: "Draft")
35
- Article.international_not(title: "Entwurf", locale: :de)
34
+ Article.i18n_where(title: "hello", match: :partial)
35
+ Article.i18n_where(title: "Hello", match: :partial, case_sensitive: true)
36
36
  ```
37
37
 
38
- ### international_search(**conditions, locale: nil, case_sensitive: false)
38
+ ### international_not(**conditions, locale: nil)
39
39
 
40
- Substring search (LIKE/ILIKE):
40
+ Exclude records matching conditions:
41
41
 
42
42
  ```ruby
43
- # Case-insensitive (default)
44
- Article.international_search(title: "hello")
45
- Article.international_search(title: "hello", description: "world")
46
-
47
- # Case-sensitive
48
- Article.international_search(title: "Hello", case_sensitive: true)
49
-
50
- # With explicit locale
51
- Article.international_search(title: "welt", locale: :de)
43
+ Article.international_not(title: "Draft")
44
+ Article.international_not(title: "Entwurf", locale: :de)
52
45
  ```
53
46
 
54
47
  ### international_order(attribute, direction = :asc, locale: nil)
@@ -85,7 +78,7 @@ Article.untranslated(:title, locale: :de)
85
78
  All methods return `ActiveRecord::Relation`, so they chain naturally with AR methods:
86
79
 
87
80
  ```ruby
88
- Article.international(title: "Hello World")
81
+ Article.i18n_where(title: "Hello World")
89
82
  .where(published: true)
90
83
  .order(created_at: :desc)
91
84
  .limit(10)
@@ -98,20 +91,20 @@ Use `merge` to combine multiple international queries:
98
91
 
99
92
  ```ruby
100
93
  # Search + filter + order
101
- Article.international_search(title: "hello")
102
- .merge(Article.international(status: "published"))
94
+ Article.i18n_where(title: "hello", match: :partial)
95
+ .merge(Article.i18n_where(status: "published"))
103
96
  .merge(Article.international_order(:title, :desc))
104
97
 
105
98
  # Query across multiple locales
106
- Article.international(title: "Hello World", locale: :en)
107
- .merge(Article.international(title: "Hallo Welt", locale: :de))
99
+ Article.i18n_where(title: "Hello World", locale: :en)
100
+ .merge(Article.i18n_where(title: "Hallo Welt", locale: :de))
108
101
  ```
109
102
 
110
103
  ## Examples
111
104
 
112
105
  ```ruby
113
106
  # Find published articles with German title containing "Welt"
114
- Article.international_search(title: "Welt", locale: :de)
107
+ Article.i18n_where(title: "Welt", match: :partial, locale: :de)
115
108
  .where(published: true)
116
109
  .merge(Article.international_order(:title, locale: :de))
117
110
  .limit(10)
@@ -12,8 +12,8 @@ module Internationalize
12
12
  # end
13
13
  #
14
14
  # # Querying
15
- # Article.international(title: "Hello")
16
- # Article.international(title: "hello", match: :partial)
15
+ # Article.i18n_where(title: "Hello")
16
+ # Article.i18n_where(title: "hello", match: :partial)
17
17
  # Article.international_order(:title, :desc)
18
18
  #
19
19
  module Model
@@ -27,33 +27,57 @@ module Internationalize
27
27
  end
28
28
 
29
29
  class_methods do
30
- # Declares attributes as internationalized OR queries by translated attributes
30
+ # Declares attributes as internationalized
31
31
  #
32
- # When called with Symbol arguments, declares attributes as internationalized:
32
+ # @example
33
33
  # international :title, :description
34
34
  #
35
- # When called with keyword arguments, queries translated attributes:
36
- # Article.international(title: "Hello") # exact match
37
- # Article.international(title: "Hello", locale: :de) # exact match in German
38
- # Article.international(title: "hello", match: :partial) # LIKE match (case-insensitive)
39
- # Article.international(title: "Hello", match: :partial, case_sensitive: true)
40
- #
41
35
  # @param attributes [Array<Symbol>] attributes to declare as internationalized
42
- # @param locale [Symbol] locale to query (default: current locale)
43
- # @param match [Symbol] :exact or :partial (default: :exact)
44
- # @param case_sensitive [Boolean] for partial matching only (default: false)
45
- # @param conditions [Hash] attribute => value pairs to query
36
+ #
37
+ # @deprecated Using this method for querying is deprecated.
38
+ # Use {#i18n_where} or {#international_where} instead.
46
39
  #
47
40
  def international(*attributes, locale: nil, match: :exact, case_sensitive: false, **conditions)
48
41
  if attributes.any? && attributes.first.is_a?(Symbol) && conditions.empty?
49
42
  # Declaration mode: international :title, :description
50
43
  declare_international_attributes(attributes)
51
44
  else
52
- # Query mode: Article.international(title: "Hello")
45
+ # Query mode: Article.international(title: "Hello") - DEPRECATED
46
+ warn("[Internationalize] DEPRECATION WARNING: Using `international` for querying is deprecated. " \
47
+ "Use `i18n_where` or `international_where` instead. " \
48
+ "(called from #{caller(1..1).first})")
53
49
  international_query(conditions, locale: locale, match: match, case_sensitive: case_sensitive)
54
50
  end
55
51
  end
56
52
 
53
+ # Query translated attributes
54
+ #
55
+ # @param conditions [Hash] attribute => value pairs to query
56
+ # @param locale [Symbol] locale to query (default: current locale)
57
+ # @param match [Symbol] :exact or :partial (default: :exact)
58
+ # @param case_sensitive [Boolean] for partial matching only (default: false)
59
+ # @return [ActiveRecord::Relation]
60
+ #
61
+ # @example
62
+ # Article.international_where(title: "Hello")
63
+ # Article.international_where(title: "hello", match: :partial)
64
+ #
65
+ def international_where(locale: nil, match: :exact, case_sensitive: false, **conditions)
66
+ international_query(conditions, locale: locale, match: match, case_sensitive: case_sensitive)
67
+ end
68
+
69
+ # Short alias for international_where
70
+ #
71
+ # @see #international_where
72
+ #
73
+ # @example
74
+ # Article.i18n_where(title: "Hello")
75
+ # Article.i18n_where(title: "hello", match: :partial)
76
+ #
77
+ def i18n_where(locale: nil, match: :exact, case_sensitive: false, **conditions)
78
+ international_query(conditions, locale: locale, match: match, case_sensitive: case_sensitive)
79
+ end
80
+
57
81
  # Order by translated attribute
58
82
  #
59
83
  # @param attribute [Symbol] attribute to order by
@@ -51,7 +51,7 @@ module Internationalize
51
51
  def validate_international_presence(attr, options)
52
52
  unless options.is_a?(Hash) && options[:locales]
53
53
  raise ArgumentError, "validates_international presence requires locales: option. " \
54
- "For current locale, use: validates :#{attr}, presence: true"
54
+ "For current locale, use: validates :#{attr}, presence: true"
55
55
  end
56
56
 
57
57
  locales = options[:locales].map(&:to_s)
@@ -60,7 +60,7 @@ module Internationalize
60
60
  locales.each do |locale|
61
61
  value = record.send("#{attr}_#{locale}")
62
62
  if value.blank?
63
- record.errors.add("#{attr}_#{locale}", :blank)
63
+ record.errors.add(attr, :blank)
64
64
  end
65
65
  end
66
66
  end
@@ -76,7 +76,7 @@ module Internationalize
76
76
  scope = scope.where.not(id: record.id) if record.persisted?
77
77
 
78
78
  if scope.exists?
79
- record.errors.add("#{attr}_#{locale}", :taken)
79
+ record.errors.add(attr, :taken)
80
80
  end
81
81
  end
82
82
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Internationalize
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampo Kuokkanen