internationalize 0.1.0 → 0.2.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 +23 -1
- data/README.md +28 -8
- data/context/configuration.md +66 -0
- data/context/getting-started.md +98 -0
- data/context/index.yaml +14 -0
- data/context/model-api.md +81 -0
- data/context/query-api.md +129 -0
- data/lib/internationalize/model.rb +16 -32
- data/lib/internationalize/rich_text.rb +73 -0
- data/lib/internationalize/version.rb +1 -1
- data/lib/internationalize.rb +1 -11
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 520c17d4993a0145c00081a95a30f21c9a3b56b0879589fc87f4731564be7f1e
|
|
4
|
+
data.tar.gz: 24478d271a6500c4104c8e26db4238ed1857b533b25e5c351e30cfc45fe7c13a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a15ca8db8848c85feec7d912c47905a0cbd665aed43721642b97e039f607699edbeb207666d3af98e92e412d6c6ac0c89298b4ac4aba5c9643bf2554c2f59a08
|
|
7
|
+
data.tar.gz: b380cf31e89bd26994935e733c89df8b23771e83a73606f39dd6092f833aec11f0a4e471ec8c8c8ff914cd3079a15b0d05a4d016962936adc707302acaec5683
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ 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
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- `fallback: false` option - translations now always fallback to default locale
|
|
21
|
+
- `set_translation(attr, locale, value)` - use `title_de = "value"` instead
|
|
22
|
+
- `translation_for(attr, locale)` - use `title_de` instead
|
|
23
|
+
|
|
24
|
+
## [0.1.1] - 2024-11-29
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Include `context/` directory in gem for `bake agent:context:install` support
|
|
29
|
+
|
|
8
30
|
## [0.1.0] - 2024-11-29
|
|
9
31
|
|
|
10
32
|
### Added
|
|
@@ -28,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
28
50
|
- `translation_for(attr, locale)`
|
|
29
51
|
- `translated?(attr, locale)`
|
|
30
52
|
- `translated_locales(attr)`
|
|
31
|
-
- Fallback to default locale when translation missing
|
|
53
|
+
- Fallback to default locale when translation missing
|
|
32
54
|
- SQLite adapter using `json_extract()`
|
|
33
55
|
- PostgreSQL adapter using `->>` operator
|
|
34
56
|
- 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
|
-
|
|
176
|
+
### ActionText Support
|
|
177
|
+
|
|
178
|
+
For rich text with attachments, use `international_rich_text` (requires ActionText):
|
|
177
179
|
|
|
178
180
|
```ruby
|
|
179
|
-
|
|
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.
|
|
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:
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Internationalize: Configuration
|
|
2
|
+
|
|
3
|
+
## Global Configuration
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
# config/initializers/internationalize.rb
|
|
7
|
+
Internationalize.configure do |config|
|
|
8
|
+
# Override available locales (defaults to I18n.available_locales)
|
|
9
|
+
config.available_locales = [:en, :de, :fr, :es]
|
|
10
|
+
end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Fallback locale is always `I18n.default_locale`.
|
|
14
|
+
|
|
15
|
+
## Database Setup
|
|
16
|
+
|
|
17
|
+
Use the generator to create migrations:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
rails generate internationalize:translation Article title description
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or create migrations manually:
|
|
24
|
+
|
|
25
|
+
### SQLite / MySQL
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class AddTranslationsToArticles < ActiveRecord::Migration[7.1]
|
|
29
|
+
def change
|
|
30
|
+
add_column :articles, :title_translations, :json, default: {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### PostgreSQL
|
|
36
|
+
|
|
37
|
+
For better query performance, use `jsonb`:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
class AddTranslationsToArticles < ActiveRecord::Migration[7.1]
|
|
41
|
+
def change
|
|
42
|
+
add_column :articles, :title_translations, :jsonb, default: {}
|
|
43
|
+
|
|
44
|
+
# Optional: Add GIN index for faster queries
|
|
45
|
+
add_index :articles, :title_translations, using: :gin
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Supported Databases
|
|
51
|
+
|
|
52
|
+
| Database | Minimum Version | JSON Type | Query Syntax |
|
|
53
|
+
|----------|-----------------|-----------|--------------|
|
|
54
|
+
| SQLite | 3.38+ | `json` | `json_extract()` |
|
|
55
|
+
| PostgreSQL | 9.4+ | `json`/`jsonb` | `->>` operator |
|
|
56
|
+
| MySQL | 8.0+ | `json` | `->>` operator |
|
|
57
|
+
|
|
58
|
+
## I18n Integration
|
|
59
|
+
|
|
60
|
+
Internationalize uses Rails I18n settings directly:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
I18n.locale # Used for current locale
|
|
64
|
+
I18n.default_locale # Used for fallbacks
|
|
65
|
+
I18n.available_locales # Used for locale-specific accessors (can be overridden)
|
|
66
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Internationalize: Getting Started
|
|
2
|
+
|
|
3
|
+
Internationalize is a lightweight internationalization gem for Rails that stores translations in JSON columns instead of separate tables.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Gemfile
|
|
9
|
+
gem "internationalize"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Setup
|
|
13
|
+
|
|
14
|
+
### 1. Generate a migration for translation columns
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
rails generate internationalize:translation Article title description
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This creates a migration adding `title_translations` and `description_translations` JSON columns.
|
|
21
|
+
|
|
22
|
+
Or write the migration manually:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class AddTranslationsToArticles < ActiveRecord::Migration[7.1]
|
|
26
|
+
def change
|
|
27
|
+
add_column :articles, :title_translations, :json, default: {}
|
|
28
|
+
add_column :articles, :description_translations, :json, default: {}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Include the mixin and declare translatable attributes
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
class Article < ApplicationRecord
|
|
37
|
+
include Internationalize::Model
|
|
38
|
+
international :title, :description
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Use translations
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# Create with translations (hash for multiple locales)
|
|
46
|
+
Article.international_create!(
|
|
47
|
+
title: { en: "Hello World", de: "Hallo Welt" },
|
|
48
|
+
status: "published"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Or direct string for current locale
|
|
52
|
+
I18n.locale = :de
|
|
53
|
+
Article.international_create!(title: "Achtung!") # Sets title_de
|
|
54
|
+
|
|
55
|
+
# Set translation for current locale
|
|
56
|
+
article.title = "Hello World"
|
|
57
|
+
|
|
58
|
+
# Set for specific locale
|
|
59
|
+
article.title_en = "Hello World"
|
|
60
|
+
article.title_de = "Hallo Welt"
|
|
61
|
+
|
|
62
|
+
# Read translation
|
|
63
|
+
article.title # => uses I18n.locale
|
|
64
|
+
article.title_de # => "Hallo Welt"
|
|
65
|
+
|
|
66
|
+
# Access raw hash
|
|
67
|
+
article.title_translations # => {"en" => "Hello World", "de" => "Hallo Welt"}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Query translations
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# Exact match
|
|
74
|
+
Article.international(title: "Hello World")
|
|
75
|
+
|
|
76
|
+
# Partial match (LIKE)
|
|
77
|
+
Article.international(title: "Hello", match: :partial)
|
|
78
|
+
|
|
79
|
+
# Order by translation
|
|
80
|
+
Article.international_order(:title, :desc)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Key Concepts
|
|
84
|
+
|
|
85
|
+
- Translations stored in `*_translations` JSON columns
|
|
86
|
+
- No JOINs - data lives in the same table
|
|
87
|
+
- Automatic fallback to default locale
|
|
88
|
+
- Works with SQLite, PostgreSQL, and MySQL
|
|
89
|
+
|
|
90
|
+
## Important: Column Defaults
|
|
91
|
+
|
|
92
|
+
JSON columns **must** have `default: {}` set in the migration. The generator does this automatically, but if writing migrations manually, ensure you include it:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
add_column :articles, :title_translations, :json, default: {}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This is required for optimal performance and correct behavior.
|
data/context/index.yaml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
description: "Lightweight, performant internationalization for Rails using JSON columns"
|
|
2
|
+
files:
|
|
3
|
+
- path: getting-started.md
|
|
4
|
+
title: "Getting Started"
|
|
5
|
+
description: "Quick setup guide for Internationalize"
|
|
6
|
+
- path: model-api.md
|
|
7
|
+
title: "Model API"
|
|
8
|
+
description: "Model mixin methods and generated accessors"
|
|
9
|
+
- path: query-api.md
|
|
10
|
+
title: "Query API"
|
|
11
|
+
description: "Query methods for searching and filtering translations"
|
|
12
|
+
- path: configuration.md
|
|
13
|
+
title: "Configuration"
|
|
14
|
+
description: "Global and per-attribute configuration options"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Internationalize: Model API
|
|
2
|
+
|
|
3
|
+
## Including the Mixin
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Article < ApplicationRecord
|
|
7
|
+
include Internationalize::Model
|
|
8
|
+
international :title, :description
|
|
9
|
+
end
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Generated Instance Methods
|
|
13
|
+
|
|
14
|
+
For each `international :title` declaration:
|
|
15
|
+
|
|
16
|
+
### Accessors
|
|
17
|
+
|
|
18
|
+
| Method | Description |
|
|
19
|
+
|--------|-------------|
|
|
20
|
+
| `title` | Get translation for current `I18n.locale` |
|
|
21
|
+
| `title=` | Set translation for current `I18n.locale` |
|
|
22
|
+
| `title?` | Check if translation exists |
|
|
23
|
+
| `title_translations` | Get raw hash of all translations |
|
|
24
|
+
| `title_translations=` | Set all translations at once |
|
|
25
|
+
|
|
26
|
+
### Locale-Specific Accessors
|
|
27
|
+
|
|
28
|
+
For each locale in `I18n.available_locales`:
|
|
29
|
+
|
|
30
|
+
| Method | Description |
|
|
31
|
+
|--------|-------------|
|
|
32
|
+
| `title_en` | Get English translation directly |
|
|
33
|
+
| `title_en=` | Set English translation |
|
|
34
|
+
| `title_en?` | Check if English translation exists |
|
|
35
|
+
| `title_de` | Get German translation |
|
|
36
|
+
| `title_de=` | Set German translation |
|
|
37
|
+
| ... | etc. for all locales |
|
|
38
|
+
|
|
39
|
+
### Instance Helper Methods
|
|
40
|
+
|
|
41
|
+
| Method | Description |
|
|
42
|
+
|--------|-------------|
|
|
43
|
+
| `translated?(:title, :de)` | Check if translation exists |
|
|
44
|
+
| `translated_locales(:title)` | Array of locales with translations |
|
|
45
|
+
|
|
46
|
+
## Class Methods (Querying)
|
|
47
|
+
|
|
48
|
+
All query methods default to current `I18n.locale` and return `ActiveRecord::Relation`:
|
|
49
|
+
|
|
50
|
+
| Method | Description |
|
|
51
|
+
|--------|-------------|
|
|
52
|
+
| `international(**conditions, locale: nil)` | Exact match query |
|
|
53
|
+
| `international_not(**conditions, locale: nil)` | Exclude matching records |
|
|
54
|
+
| `international_search(**conditions, locale: nil, case_sensitive: false)` | Substring search |
|
|
55
|
+
| `international_order(attr, dir = :asc, locale: nil)` | Order by translation |
|
|
56
|
+
| `translated(*attrs, locale: nil)` | Find records with translation |
|
|
57
|
+
| `untranslated(*attrs, locale: nil)` | Find records missing translation |
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Examples
|
|
61
|
+
Article.international(title: "Hello World")
|
|
62
|
+
Article.international_search(title: "hello", locale: :de)
|
|
63
|
+
Article.international_order(:title, :desc)
|
|
64
|
+
Article.translated(:title, locale: :de)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Class Attributes
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
Article.international_attributes # => [:title, :description]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Fallback Behavior
|
|
74
|
+
|
|
75
|
+
Translations automatically fall back to the default locale when missing:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
article.title_en = "Hello"
|
|
79
|
+
I18n.locale = :de
|
|
80
|
+
article.title # => "Hello" (falls back to default locale)
|
|
81
|
+
```
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Internationalize: Query API
|
|
2
|
+
|
|
3
|
+
All query methods are class methods on models that include `Internationalize::Model`. They return `ActiveRecord::Relation` objects that can be chained with standard AR methods.
|
|
4
|
+
|
|
5
|
+
## Default Locale Behavior
|
|
6
|
+
|
|
7
|
+
All query methods default to the current `I18n.locale`. Use the `locale:` option to override:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Uses current I18n.locale
|
|
11
|
+
Article.international(title: "Hello World")
|
|
12
|
+
|
|
13
|
+
# Explicit locale
|
|
14
|
+
Article.international(title: "Hallo Welt", locale: :de)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Query Methods
|
|
18
|
+
|
|
19
|
+
### international(**conditions, locale: nil)
|
|
20
|
+
|
|
21
|
+
Exact match on translated attributes:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
Article.international(title: "Hello World")
|
|
25
|
+
Article.international(title: "Hello", status: "published")
|
|
26
|
+
Article.international(title: "Hallo Welt", locale: :de)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### international_not(**conditions, locale: nil)
|
|
30
|
+
|
|
31
|
+
Exclude records matching conditions:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Article.international_not(title: "Draft")
|
|
35
|
+
Article.international_not(title: "Entwurf", locale: :de)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### international_search(**conditions, locale: nil, case_sensitive: false)
|
|
39
|
+
|
|
40
|
+
Substring search (LIKE/ILIKE):
|
|
41
|
+
|
|
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)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### international_order(attribute, direction = :asc, locale: nil)
|
|
55
|
+
|
|
56
|
+
Order by translated attribute:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Article.international_order(:title)
|
|
60
|
+
Article.international_order(:title, :desc)
|
|
61
|
+
Article.international_order(:title, :asc, locale: :de)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### translated(*attributes, locale: nil)
|
|
65
|
+
|
|
66
|
+
Find records with translations in specified locale:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Article.translated(:title)
|
|
70
|
+
Article.translated(:title, locale: :de)
|
|
71
|
+
Article.translated(:title, :description, locale: :de)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### untranslated(*attributes, locale: nil)
|
|
75
|
+
|
|
76
|
+
Find records missing translations:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
Article.untranslated(:title)
|
|
80
|
+
Article.untranslated(:title, locale: :de)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Chaining with ActiveRecord
|
|
84
|
+
|
|
85
|
+
All methods return `ActiveRecord::Relation`, so they chain naturally with AR methods:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
Article.international(title: "Hello World")
|
|
89
|
+
.where(published: true)
|
|
90
|
+
.order(created_at: :desc)
|
|
91
|
+
.limit(10)
|
|
92
|
+
.includes(:author)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Combining Multiple Queries
|
|
96
|
+
|
|
97
|
+
Use `merge` to combine multiple international queries:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Search + filter + order
|
|
101
|
+
Article.international_search(title: "hello")
|
|
102
|
+
.merge(Article.international(status: "published"))
|
|
103
|
+
.merge(Article.international_order(:title, :desc))
|
|
104
|
+
|
|
105
|
+
# Query across multiple locales
|
|
106
|
+
Article.international(title: "Hello World", locale: :en)
|
|
107
|
+
.merge(Article.international(title: "Hallo Welt", locale: :de))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Examples
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# Find published articles with German title containing "Welt"
|
|
114
|
+
Article.international_search(title: "Welt", locale: :de)
|
|
115
|
+
.where(published: true)
|
|
116
|
+
.merge(Article.international_order(:title, locale: :de))
|
|
117
|
+
.limit(10)
|
|
118
|
+
|
|
119
|
+
# Find articles missing German translation
|
|
120
|
+
Article.untranslated(:title, locale: :de).count
|
|
121
|
+
|
|
122
|
+
# Complex multi-locale query - find articles with both EN and DE titles
|
|
123
|
+
Article.translated(:title, locale: :en)
|
|
124
|
+
.merge(Article.translated(:title, locale: :de))
|
|
125
|
+
|
|
126
|
+
# Exclude drafts and order by title
|
|
127
|
+
Article.international_not(status: "draft")
|
|
128
|
+
.merge(Article.international_order(:title))
|
|
129
|
+
```
|
|
@@ -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,
|
|
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
|
|
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 ||=
|
|
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 ||=
|
|
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 ||=
|
|
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 ||=
|
|
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
|
|
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
|
|
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 ||=
|
|
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
|
|
289
|
+
def define_translation_accessors(attr)
|
|
292
290
|
translations_column = "#{attr}_translations"
|
|
293
291
|
|
|
294
|
-
# Main getter - returns translation for current locale
|
|
295
|
-
|
|
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
|
|
304
|
-
return value if !
|
|
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
|
|
@@ -362,23 +359,10 @@ module Internationalize
|
|
|
362
359
|
|
|
363
360
|
# Instance methods
|
|
364
361
|
|
|
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
362
|
# Check if a translation exists for a specific locale
|
|
380
363
|
def translated?(attr, locale)
|
|
381
|
-
|
|
364
|
+
translations = read_attribute("#{attr}_translations")
|
|
365
|
+
translations[locale.to_s].present?
|
|
382
366
|
end
|
|
383
367
|
|
|
384
368
|
# 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
|
data/lib/internationalize.rb
CHANGED
|
@@ -10,22 +10,12 @@ require_relative "internationalize/model"
|
|
|
10
10
|
module Internationalize
|
|
11
11
|
class << self
|
|
12
12
|
# Configuration
|
|
13
|
-
attr_accessor :
|
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sampo Kuokkanen
|
|
@@ -49,6 +49,11 @@ files:
|
|
|
49
49
|
- CHANGELOG.md
|
|
50
50
|
- LICENSE.txt
|
|
51
51
|
- README.md
|
|
52
|
+
- context/configuration.md
|
|
53
|
+
- context/getting-started.md
|
|
54
|
+
- context/index.yaml
|
|
55
|
+
- context/model-api.md
|
|
56
|
+
- context/query-api.md
|
|
52
57
|
- lib/generators/internationalize/translation/templates/add_translations_migration.rb.erb
|
|
53
58
|
- lib/generators/internationalize/translation_generator.rb
|
|
54
59
|
- lib/internationalize.rb
|
|
@@ -58,6 +63,7 @@ files:
|
|
|
58
63
|
- lib/internationalize/adapters/postgresql.rb
|
|
59
64
|
- lib/internationalize/adapters/sqlite.rb
|
|
60
65
|
- lib/internationalize/model.rb
|
|
66
|
+
- lib/internationalize/rich_text.rb
|
|
61
67
|
- lib/internationalize/version.rb
|
|
62
68
|
homepage: https://github.com/sampokuokkanen/internationalize
|
|
63
69
|
licenses:
|