internationalize 0.1.1 → 0.2.1

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: 3f85c4969fc4c969bf8ad61c4cbdb76a5364c90b4c7853a446aaeb6a4ac629ff
4
- data.tar.gz: 83c19c5597f26d23634cc37f3aaede5d7a58c62460c6a2cd5295a1d3e43c3017
3
+ metadata.gz: 25678f8238179260f1496807a1a68a20d86e09e72e7d7c4097c4b14f41b060ec
4
+ data.tar.gz: b713e70a91f521041975b86e6e22be2239ebfddbd3c9867691c55ecf8fbfb380
5
5
  SHA512:
6
- metadata.gz: 02037bac922bbd2e7a7ccf82399e5892436a32f02970c87365935745ebc2858b16b4befbf11b26a9e70e54833ae0347a3d45a8450c839a95d93111dc92109035
7
- data.tar.gz: 7dced1dca63327c7e66c527be390ac39bff028615f10696094b545514573000a52962fdbd807a77e7bb823b4c6151c56284b075e18be37bbea36d4140e10450d
6
+ metadata.gz: 433cc8be47c2df3d5e48abf8eaa6cd0550cbe4ceaaf3fde9700ad08c82be24488eb5b7a52d8ec4b6555880119d8c961a451841ddeed9a52d2a455792ae5eb69d
7
+ data.tar.gz: e6656a5c811920e9e4b4b2184ee5b981496296f3588182d3ce2f53e6bcb9328b0051c210830c01d2eb23a9fea5fe3151181a5adb7d08c5222eac08a67c2661ac
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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]
9
+
10
+ ## [0.2.0] - 2024-11-29
11
+
12
+ ### Added
13
+
14
+ - ActionText support via `international_rich_text` (optional, requires ActionText)
15
+ - Generates `has_rich_text` for each locale with unified accessor
16
+ - Full attachment support per locale
17
+ - Validation for `title_translations=` setter - rejects non-Hash values and invalid locales
18
+
19
+ ### Removed
20
+
21
+ - `fallback: false` option - translations now always fallback to default locale
22
+ - `set_translation(attr, locale, value)` - use `title_de = "value"` instead
23
+ - `translation_for(attr, locale)` - use `title_de` instead
24
+
25
+ ## [0.1.1] - 2024-11-29
26
+
27
+ ### Fixed
28
+
29
+ - Include `context/` directory in gem for `bake agent:context:install` support
30
+
8
31
  ## [0.1.0] - 2024-11-29
9
32
 
10
33
  ### Added
@@ -28,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
28
51
  - `translation_for(attr, locale)`
29
52
  - `translated?(attr, locale)`
30
53
  - `translated_locales(attr)`
31
- - Fallback to default locale when translation missing (configurable)
54
+ - Fallback to default locale when translation missing
32
55
  - SQLite adapter using `json_extract()`
33
56
  - PostgreSQL adapter using `->>` operator
34
57
  - MySQL 8+ adapter using `->>` operator (supports mysql2 and trilogy gems)
data/README.md CHANGED
@@ -4,10 +4,14 @@ Lightweight, performant internationalization for Rails with JSON column storage.
4
4
 
5
5
  ## Why Internationalize?
6
6
 
7
+ 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
+
7
9
  Unlike Globalize or Mobility which use separate translation tables requiring JOINs, Internationalize stores translations inline using JSON columns. This means:
8
10
 
9
11
  - **No JOINs** - translations live in the same table
10
12
  - **No N+1 queries** - data is always loaded with the record
13
+ - **No backend overhead** - direct JSON column access, no abstraction layers
14
+ - **~50% less memory** - no per-instance backend objects or plugin chains
11
15
  - **Direct method dispatch** - no `method_missing` overhead
12
16
  - **Multi-database support** - works with SQLite, PostgreSQL, and MySQL
13
17
  - **Visible in schema.rb** - translated fields appear directly in your model's schema
@@ -155,10 +159,6 @@ article.translated?(:title, :de) # => true/false
155
159
 
156
160
  # Get all translated locales for an attribute
157
161
  article.translated_locales(:title) # => [:en, :de]
158
-
159
- # Set/get translation for specific locale
160
- article.set_translation(:title, :de, "Hallo Welt")
161
- article.translation_for(:title, :de) # => "Hallo Welt"
162
162
  ```
163
163
 
164
164
  ### Fallbacks
@@ -173,10 +173,29 @@ I18n.locale = :de
173
173
  article.title # => "Hello" (falls back to :en)
174
174
  ```
175
175
 
176
- Disable fallbacks:
176
+ ### ActionText Support
177
+
178
+ For rich text with attachments, use `international_rich_text` (requires ActionText):
177
179
 
178
180
  ```ruby
179
- international :title, fallback: false
181
+ require "internationalize/rich_text"
182
+
183
+ class Article < ApplicationRecord
184
+ include Internationalize::Model
185
+ include Internationalize::RichText
186
+
187
+ international_rich_text :content
188
+ end
189
+ ```
190
+
191
+ This generates `has_rich_text :content_en`, `has_rich_text :content_de`, etc. for each locale, with a unified accessor:
192
+
193
+ ```ruby
194
+ article.content = "<p>Hello</p>" # Sets for current locale
195
+ article.content # Gets for current locale (with fallback)
196
+ article.content_en # Direct access to English
197
+ article.content.body # ActionText::Content object
198
+ article.content.embeds # Attachments work per-locale
180
199
  ```
181
200
 
182
201
  ## Configuration
@@ -184,11 +203,12 @@ international :title, fallback: false
184
203
  ```ruby
185
204
  # config/initializers/internationalize.rb
186
205
  Internationalize.configure do |config|
187
- config.fallback_locale = :en
188
- config.available_locales = [:en, :de, :fr]
206
+ config.available_locales = [:en, :de, :fr] # Defaults to I18n.available_locales
189
207
  end
190
208
  ```
191
209
 
210
+ Fallback uses `I18n.default_locale` automatically.
211
+
192
212
  ## Performance Comparison
193
213
 
194
214
  Benchmark with 1000 records, 2 translated attributes (title + body), 3 locales:
@@ -5,27 +5,12 @@
5
5
  ```ruby
6
6
  # config/initializers/internationalize.rb
7
7
  Internationalize.configure do |config|
8
- # Override fallback locale (defaults to I18n.default_locale)
9
- config.fallback_locale = :en
10
-
11
8
  # Override available locales (defaults to I18n.available_locales)
12
9
  config.available_locales = [:en, :de, :fr, :es]
13
10
  end
14
11
  ```
15
12
 
16
- ## Per-Attribute Configuration
17
-
18
- ```ruby
19
- class Article < ApplicationRecord
20
- include Internationalize::Model
21
-
22
- # With fallback (default)
23
- international :title
24
-
25
- # Without fallback
26
- international :description, fallback: false
27
- end
28
- ```
13
+ Fallback locale is always `I18n.default_locale`.
29
14
 
30
15
  ## Database Setup
31
16
 
@@ -72,12 +57,10 @@ end
72
57
 
73
58
  ## I18n Integration
74
59
 
75
- Internationalize automatically uses Rails I18n settings:
60
+ Internationalize uses Rails I18n settings directly:
76
61
 
77
62
  ```ruby
78
63
  I18n.locale # Used for current locale
79
64
  I18n.default_locale # Used for fallbacks
80
- I18n.available_locales # Used for locale-specific accessors
65
+ I18n.available_locales # Used for locale-specific accessors (can be overridden)
81
66
  ```
82
-
83
- Override these with the configuration options above if needed.
@@ -86,6 +86,7 @@ Article.international_order(:title, :desc)
86
86
  - No JOINs - data lives in the same table
87
87
  - Automatic fallback to default locale
88
88
  - Works with SQLite, PostgreSQL, and MySQL
89
+ - ActionText support via `international_rich_text` (see Model API)
89
90
 
90
91
  ## Important: Column Defaults
91
92
 
data/context/model-api.md CHANGED
@@ -9,16 +9,6 @@ class Article < ApplicationRecord
9
9
  end
10
10
  ```
11
11
 
12
- ## Options
13
-
14
- ```ruby
15
- # With fallback (default: true)
16
- international :title, fallback: true
17
-
18
- # Without fallback - returns nil if translation missing
19
- international :title, fallback: false
20
- ```
21
-
22
12
  ## Generated Instance Methods
23
13
 
24
14
  For each `international :title` declaration:
@@ -31,7 +21,7 @@ For each `international :title` declaration:
31
21
  | `title=` | Set translation for current `I18n.locale` |
32
22
  | `title?` | Check if translation exists |
33
23
  | `title_translations` | Get raw hash of all translations |
34
- | `title_translations=` | Set all translations at once |
24
+ | `title_translations=` | Set all translations at once (validates locale keys) |
35
25
 
36
26
  ### Locale-Specific Accessors
37
27
 
@@ -50,8 +40,6 @@ For each locale in `I18n.available_locales`:
50
40
 
51
41
  | Method | Description |
52
42
  |--------|-------------|
53
- | `set_translation(:title, :de, "Hallo")` | Set translation for attribute/locale |
54
- | `translation_for(:title, :de)` | Get translation without fallback |
55
43
  | `translated?(:title, :de)` | Check if translation exists |
56
44
  | `translated_locales(:title)` | Array of locales with translations |
57
45
 
@@ -84,7 +72,7 @@ Article.international_attributes # => [:title, :description]
84
72
 
85
73
  ## Fallback Behavior
86
74
 
87
- When `fallback: true` (default):
75
+ Translations automatically fall back to the default locale when missing:
88
76
 
89
77
  ```ruby
90
78
  article.title_en = "Hello"
@@ -92,10 +80,38 @@ I18n.locale = :de
92
80
  article.title # => "Hello" (falls back to default locale)
93
81
  ```
94
82
 
95
- When `fallback: false`:
83
+ ## ActionText Support
84
+
85
+ For rich text with attachments, use `international_rich_text` (requires ActionText):
96
86
 
97
87
  ```ruby
98
- article.title_en = "Hello"
99
- I18n.locale = :de
100
- article.title # => nil
88
+ require "internationalize/rich_text"
89
+
90
+ class Article < ApplicationRecord
91
+ include Internationalize::Model
92
+ include Internationalize::RichText
93
+
94
+ international_rich_text :content
95
+ end
96
+ ```
97
+
98
+ This generates `has_rich_text :content_en`, `has_rich_text :content_de`, etc. for each locale.
99
+
100
+ ### Generated Methods
101
+
102
+ | Method | Description |
103
+ |--------|-------------|
104
+ | `content` | Get rich text for current locale (with fallback) |
105
+ | `content=` | Set rich text for current locale |
106
+ | `content?` | Check if rich text exists |
107
+ | `content_en` | Direct access to English rich text |
108
+ | `content_de` | Direct access to German rich text |
109
+ | `content_translated?(:de)` | Check if translation exists |
110
+ | `content_translated_locales` | Array of locales with content |
111
+
112
+ ```ruby
113
+ article.content = "<p>Hello</p>" # Sets for current locale
114
+ article.content # Gets for current locale (with fallback)
115
+ article.content.body # ActionText::Content object
116
+ article.content.embeds # Attachments work per-locale
101
117
  ```
@@ -28,7 +28,6 @@ module Internationalize
28
28
  #
29
29
  # When called with Symbol arguments, declares attributes as internationalized:
30
30
  # international :title, :description
31
- # international :title, fallback: false
32
31
  #
33
32
  # When called with keyword arguments, queries translated attributes:
34
33
  # Article.international(title: "Hello") # exact match
@@ -37,16 +36,15 @@ module Internationalize
37
36
  # Article.international(title: "Hello", match: :partial, case_sensitive: true)
38
37
  #
39
38
  # @param attributes [Array<Symbol>] attributes to declare as internationalized
40
- # @param fallback [Boolean] whether to fallback to default locale (default: true)
41
39
  # @param locale [Symbol] locale to query (default: current locale)
42
40
  # @param match [Symbol] :exact or :partial (default: :exact)
43
41
  # @param case_sensitive [Boolean] for partial matching only (default: false)
44
42
  # @param conditions [Hash] attribute => value pairs to query
45
43
  #
46
- def international(*attributes, fallback: true, locale: nil, match: :exact, case_sensitive: false, **conditions)
44
+ def international(*attributes, locale: nil, match: :exact, case_sensitive: false, **conditions)
47
45
  if attributes.any? && attributes.first.is_a?(Symbol) && conditions.empty?
48
46
  # Declaration mode: international :title, :description
49
- declare_international_attributes(attributes, fallback: fallback)
47
+ declare_international_attributes(attributes)
50
48
  else
51
49
  # Query mode: Article.international(title: "Hello")
52
50
  international_query(conditions, locale: locale, match: match, case_sensitive: case_sensitive)
@@ -71,7 +69,7 @@ module Internationalize
71
69
  "Use standard ActiveRecord methods for non-translated attributes."
72
70
  end
73
71
 
74
- locale ||= Internationalize.locale
72
+ locale ||= I18n.locale
75
73
  direction = direction.to_s.upcase
76
74
  direction = "ASC" unless VALID_DIRECTIONS.include?(direction)
77
75
 
@@ -91,7 +89,7 @@ module Internationalize
91
89
  # Article.translated(:title, :description, locale: :de)
92
90
  #
93
91
  def translated(*attributes, locale: nil)
94
- locale ||= Internationalize.locale
92
+ locale ||= I18n.locale
95
93
  adapter = Adapters.resolve(connection)
96
94
  scope = all
97
95
 
@@ -117,7 +115,7 @@ module Internationalize
117
115
  # Article.untranslated(:title, locale: :de)
118
116
  #
119
117
  def untranslated(*attributes, locale: nil)
120
- locale ||= Internationalize.locale
118
+ locale ||= I18n.locale
121
119
  adapter = Adapters.resolve(connection)
122
120
  scope = all
123
121
 
@@ -143,7 +141,7 @@ module Internationalize
143
141
  # Article.international_not(title: "Entwurf", locale: :de)
144
142
  #
145
143
  def international_not(locale: nil, **conditions)
146
- locale ||= Internationalize.locale
144
+ locale ||= I18n.locale
147
145
  adapter = Adapters.resolve(connection)
148
146
  scope = all
149
147
 
@@ -233,12 +231,12 @@ module Internationalize
233
231
  end
234
232
 
235
233
  # Declares attributes as internationalized
236
- def declare_international_attributes(attributes, fallback:)
234
+ def declare_international_attributes(attributes)
237
235
  self.international_attributes = international_attributes | attributes.map(&:to_sym)
238
236
 
239
237
  attributes.each do |attr|
240
238
  warn_if_missing_default(attr)
241
- define_translation_accessors(attr, fallback: fallback)
239
+ define_translation_accessors(attr)
242
240
  define_locale_accessors(attr)
243
241
  end
244
242
  end
@@ -258,7 +256,7 @@ module Internationalize
258
256
 
259
257
  # Query translated attributes with exact or partial matching
260
258
  def international_query(conditions, locale:, match:, case_sensitive:)
261
- locale ||= Internationalize.locale
259
+ locale ||= I18n.locale
262
260
  adapter = Adapters.resolve(connection)
263
261
  scope = all
264
262
 
@@ -288,20 +286,19 @@ module Internationalize
288
286
  end
289
287
 
290
288
  # Defines the main getter/setter for an attribute
291
- def define_translation_accessors(attr, fallback:)
289
+ def define_translation_accessors(attr)
292
290
  translations_column = "#{attr}_translations"
293
291
 
294
- # Main getter - returns translation for current locale
295
- # Cache default locale at definition time for faster fallback
296
- default_locale_str = fallback ? Internationalize.default_locale.to_s : nil
292
+ # Main getter - returns translation for current locale with fallback to default locale
293
+ default_locale_str = I18n.default_locale.to_s
297
294
 
298
295
  define_method(attr) do |locale = nil|
299
296
  locale_str = (locale || I18n.locale).to_s
300
297
  translations = read_attribute(translations_column)
301
298
  value = translations[locale_str]
302
299
 
303
- # Short-circuit: return early if value exists or no fallback needed
304
- return value if !fallback || !value.nil? || locale_str == default_locale_str
300
+ # Short-circuit: return early if value exists or already querying default locale
301
+ return value if !value.nil? || locale_str == default_locale_str
305
302
 
306
303
  translations[default_locale_str]
307
304
  end
@@ -327,7 +324,21 @@ module Internationalize
327
324
 
328
325
  # Set all translations at once
329
326
  define_method("#{attr}_translations=") do |hash|
330
- write_attribute(translations_column, hash&.stringify_keys || {})
327
+ return write_attribute(translations_column, {}) if hash.nil?
328
+
329
+ unless hash.is_a?(Hash)
330
+ raise ArgumentError, "#{attr}_translations must be a Hash, got #{hash.class}"
331
+ end
332
+
333
+ allowed_locales = Internationalize.locales.map(&:to_s)
334
+ hash.each_key do |key|
335
+ unless allowed_locales.include?(key.to_s)
336
+ raise ArgumentError, "Invalid locale '#{key}' for #{attr}_translations. " \
337
+ "Allowed locales: #{allowed_locales.join(', ')}"
338
+ end
339
+ end
340
+
341
+ write_attribute(translations_column, hash.stringify_keys)
331
342
  end
332
343
  end
333
344
 
@@ -362,23 +373,10 @@ module Internationalize
362
373
 
363
374
  # Instance methods
364
375
 
365
- # Set translation for a specific locale
366
- def set_translation(attr, locale, value)
367
- column = "#{attr}_translations"
368
- translations = read_attribute(column)
369
- translations[locale.to_s] = value
370
- write_attribute(column, translations)
371
- end
372
-
373
- # Get translation for a specific locale without fallback
374
- def translation_for(attr, locale)
375
- translations = read_attribute("#{attr}_translations")
376
- translations[locale.to_s]
377
- end
378
-
379
376
  # Check if a translation exists for a specific locale
380
377
  def translated?(attr, locale)
381
- translation_for(attr, locale).present?
378
+ translations = read_attribute("#{attr}_translations")
379
+ translations[locale.to_s].present?
382
380
  end
383
381
 
384
382
  # Returns all locales that have a translation for an attribute
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Internationalize
4
+ # Adds internationalization support for ActionText rich text attributes
5
+ #
6
+ # @example
7
+ # class Article < ApplicationRecord
8
+ # include Internationalize::Model
9
+ # include Internationalize::RichText
10
+ # international_rich_text :content
11
+ # end
12
+ #
13
+ # article.content = "<p>Hello</p>" # Sets for current locale
14
+ # article.content # Gets for current locale (with fallback)
15
+ # article.content_de # Direct access to German content
16
+ #
17
+ module RichText
18
+ extend ActiveSupport::Concern
19
+
20
+ class_methods do
21
+ # Declares a rich text attribute as internationalized
22
+ #
23
+ # Creates a has_rich_text association for each available locale and
24
+ # provides locale-aware accessors.
25
+ #
26
+ # @param name [Symbol] the attribute name
27
+ #
28
+ def international_rich_text(name)
29
+ # Generate has_rich_text for each locale
30
+ Internationalize.locales.each do |locale|
31
+ rich_text_name = :"#{name}_#{locale}"
32
+ has_rich_text(rich_text_name)
33
+ end
34
+
35
+ default_locale_str = I18n.default_locale.to_s
36
+
37
+ # Main getter - returns rich text for current locale with fallback to default locale
38
+ define_method(name) do
39
+ locale_str = I18n.locale.to_s
40
+ rich_text = send(:"#{name}_#{locale_str}")
41
+
42
+ if rich_text.blank? && locale_str != default_locale_str
43
+ send(:"#{name}_#{default_locale_str}")
44
+ else
45
+ rich_text
46
+ end
47
+ end
48
+
49
+ # Main setter - sets rich text for current locale
50
+ define_method(:"#{name}=") do |value|
51
+ send(:"#{name}_#{I18n.locale}=", value)
52
+ end
53
+
54
+ # Predicate method
55
+ define_method(:"#{name}?") do
56
+ send(name).present?
57
+ end
58
+
59
+ # Check if translation exists for a specific locale
60
+ define_method(:"#{name}_translated?") do |locale|
61
+ send(:"#{name}_#{locale}").present?
62
+ end
63
+
64
+ # Get all locales that have this rich text
65
+ define_method(:"#{name}_translated_locales") do
66
+ Internationalize.locales.select do |locale|
67
+ send(:"#{name}_#{locale}").present?
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Internationalize
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -10,22 +10,12 @@ require_relative "internationalize/model"
10
10
  module Internationalize
11
11
  class << self
12
12
  # Configuration
13
- attr_accessor :fallback_locale, :available_locales
13
+ attr_accessor :available_locales
14
14
 
15
15
  def configure
16
16
  yield self
17
17
  end
18
18
 
19
- # Returns the current locale, defaulting to I18n.locale
20
- def locale
21
- I18n.locale
22
- end
23
-
24
- # Returns the default locale for fallbacks
25
- def default_locale
26
- fallback_locale || I18n.default_locale
27
- end
28
-
29
19
  # Returns available locales, defaulting to I18n.available_locales
30
20
  def locales
31
21
  available_locales || I18n.available_locales
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.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampo Kuokkanen
@@ -63,6 +63,7 @@ files:
63
63
  - lib/internationalize/adapters/postgresql.rb
64
64
  - lib/internationalize/adapters/sqlite.rb
65
65
  - lib/internationalize/model.rb
66
+ - lib/internationalize/rich_text.rb
66
67
  - lib/internationalize/version.rb
67
68
  homepage: https://github.com/sampokuokkanen/internationalize
68
69
  licenses: