inertia_i18n 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +418 -0
- data/exe/inertia_i18n +16 -0
- data/lib/generators/inertia_i18n/install_generator.rb +102 -0
- data/lib/generators/inertia_i18n/templates/common.en.yml +6 -0
- data/lib/generators/inertia_i18n/templates/i18n_spec.rb +33 -0
- data/lib/generators/inertia_i18n/templates/i18n_test.rb +31 -0
- data/lib/generators/inertia_i18n/templates/inertia_i18n.rb.tt +41 -0
- data/lib/generators/inertia_i18n/test_generator.rb +23 -0
- data/lib/inertia_i18n/cli.rb +223 -0
- data/lib/inertia_i18n/configuration.rb +79 -0
- data/lib/inertia_i18n/converter.rb +79 -0
- data/lib/inertia_i18n/file_converter.rb +69 -0
- data/lib/inertia_i18n/health_checker.rb +132 -0
- data/lib/inertia_i18n/locale_loader.rb +42 -0
- data/lib/inertia_i18n/parsers/base_parser.rb +20 -0
- data/lib/inertia_i18n/parsers/javascript_parser.rb +16 -0
- data/lib/inertia_i18n/parsers/javascript_parsing.rb +66 -0
- data/lib/inertia_i18n/parsers/react_parser.rb +17 -0
- data/lib/inertia_i18n/parsers/svelte_parser.rb +16 -0
- data/lib/inertia_i18n/parsers/vue_parser.rb +43 -0
- data/lib/inertia_i18n/scan_results.rb +31 -0
- data/lib/inertia_i18n/scanner.rb +44 -0
- data/lib/inertia_i18n/version.rb +5 -0
- data/lib/inertia_i18n.rb +19 -0
- metadata +157 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c5d21515efb4733bc47d4a1f7b4964768893478366e101bfd4209c4cd45f3fee
|
|
4
|
+
data.tar.gz: fd44f198a5d66b793459eac5ab9b0b43487e3c4d4abc9b5f1e2145a995ceacd3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a54935ae8c875ba3b9baf3fadab5bc7033150da97eb72ed7c453b7dd14e315809df3cfc7ac58cf5f34eb0a426b1343a9cfe206da45e897a93e4ac20edc957b12
|
|
7
|
+
data.tar.gz: 02f8b5b053cf02be28186c79a137935d83fd55c82c7e27f1c7a9531c46803e16a5aab8d8748f4c46576800f7322ce755c99154d26ae2c350360d7efbf95f6011
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexey Poimtsev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# InertiaI18n
|
|
2
|
+
|
|
3
|
+
Translation management for Inertia.js applications with Rails backend
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/inertia_i18n)
|
|
6
|
+
[](https://github.com/alec-c4/inertia_i18n/actions)
|
|
7
|
+
|
|
8
|
+
## The Problem
|
|
9
|
+
|
|
10
|
+
Inertia.js applications have a split architecture:
|
|
11
|
+
|
|
12
|
+
- **Backend (Rails):** Uses YAML locale files (`config/locales/*.yml`)
|
|
13
|
+
- **Frontend (React/Svelte/Vue):** Uses i18next JSON files
|
|
14
|
+
|
|
15
|
+
This creates several challenges:
|
|
16
|
+
|
|
17
|
+
1. **Duplicate management:** Maintaining translations in two formats
|
|
18
|
+
2. **Sync issues:** Keys in YAML but missing in JSON (or vice versa)
|
|
19
|
+
3. **No usage tracking:** Unused translation keys accumulate
|
|
20
|
+
4. **Manual process:** Converting YAML → JSON by hand is error-prone
|
|
21
|
+
|
|
22
|
+
Existing tools like [i18n-tasks](https://github.com/glebm/i18n-tasks) only handle Rails/backend translations.
|
|
23
|
+
|
|
24
|
+
## The Solution
|
|
25
|
+
|
|
26
|
+
InertiaI18n provides:
|
|
27
|
+
|
|
28
|
+
- **YAML → JSON conversion** with interpolation mapping (`%{var}` → `{{var}}`)
|
|
29
|
+
- **AST-based scanning** to find translation usage in `.svelte`, `.tsx`, `.vue` files
|
|
30
|
+
- **Health checks** to detect missing, unused, and unsynchronized keys
|
|
31
|
+
- **Watch mode** for automatic regeneration during development
|
|
32
|
+
- **Rails integration** via initializers and rake tasks
|
|
33
|
+
|
|
34
|
+
**One source of truth:** Rails YAML files, with JSON auto-generated.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Add to your Gemfile:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem 'inertia_i18n'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run the installer:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
rails generate inertia_i18n:install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This generator will:
|
|
53
|
+
|
|
54
|
+
1. Create the locale directory structure (`config/locales/frontend`, `config/locales/backend`).
|
|
55
|
+
2. Generate the configuration file (`config/initializers/inertia_i18n.rb`).
|
|
56
|
+
3. Create a sample locale file.
|
|
57
|
+
4. Detect your frontend framework (React, Vue, or Svelte) and add necessary dependencies (e.g., `react-i18next`) to your `package.json`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Recommended Directory Structure
|
|
62
|
+
|
|
63
|
+
To avoid conflicts between backend and frontend translation keys, it is recommended to separate your locale files into subdirectories:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
config/
|
|
67
|
+
└── locales/
|
|
68
|
+
├── backend/ # Rails-specific translations
|
|
69
|
+
│ ├── en.yml
|
|
70
|
+
│ └── ru.yml
|
|
71
|
+
├── frontend/ # Frontend-specific translations
|
|
72
|
+
│ ├── common.en.yml
|
|
73
|
+
│ ├── pages.en.yml
|
|
74
|
+
│ └── pages.ru.yml
|
|
75
|
+
└── en.yml # Optional: shared or legacy keys
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
By default, InertiaI18n will look for YAML files in `config/locales/frontend`. You can customize this using the `source_paths` configuration.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Quick Start
|
|
83
|
+
|
|
84
|
+
### 1. Configure
|
|
85
|
+
|
|
86
|
+
The installer creates a default configuration file. You can customize it in `config/initializers/inertia_i18n.rb`.
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# config/initializers/inertia_i18n.rb
|
|
90
|
+
InertiaI18n.configure do |config|
|
|
91
|
+
# Recommended: point to a dedicated frontend folder
|
|
92
|
+
config.source_paths = [Rails.root.join('config', 'locales', 'frontend')]
|
|
93
|
+
|
|
94
|
+
config.target_path = Rails.root.join('app', 'frontend', 'locales')
|
|
95
|
+
config.locales = [:en, :ru]
|
|
96
|
+
|
|
97
|
+
# Scan paths are automatically set based on your detected framework
|
|
98
|
+
config.scan_paths = [
|
|
99
|
+
Rails.root.join('app', 'frontend', '**', '*.{svelte,tsx,vue}')
|
|
100
|
+
]
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Convert YAML to JSON
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# One-time conversion
|
|
108
|
+
bundle exec rake inertia_i18n:convert
|
|
109
|
+
|
|
110
|
+
# Watch mode (auto-convert on YAML changes)
|
|
111
|
+
bundle exec rake inertia_i18n:watch
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. Check Translation Health
|
|
115
|
+
|
|
116
|
+
The recommended way to check translation health is by running the generated test as part of your test suite. See the [CI Integration](#ci-integration) section for details.
|
|
117
|
+
|
|
118
|
+
You can also run a manual check from the command line:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Find missing, unused, and out-of-sync keys
|
|
122
|
+
bundle exec rake inertia_i18n:health
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## CLI Usage
|
|
128
|
+
|
|
129
|
+
All CLI commands load the Rails environment, so they have access to your application's configuration and behave identically to the `rake` tasks.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Generate a new configuration file
|
|
133
|
+
inertia_i18n init
|
|
134
|
+
|
|
135
|
+
# Convert YAML to JSON
|
|
136
|
+
inertia_i18n convert
|
|
137
|
+
|
|
138
|
+
# Convert specific locale
|
|
139
|
+
inertia_i18n convert --locale=ru
|
|
140
|
+
|
|
141
|
+
# Scan frontend code for translation usage
|
|
142
|
+
inertia_i18n scan
|
|
143
|
+
|
|
144
|
+
# Check translation health
|
|
145
|
+
inertia_i18n health
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Features
|
|
151
|
+
|
|
152
|
+
### YAML → JSON Conversion
|
|
153
|
+
|
|
154
|
+
**Input (Rails YAML):**
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
# config/locales/en.yml
|
|
158
|
+
en:
|
|
159
|
+
user:
|
|
160
|
+
greeting: "Hello, %{name}!"
|
|
161
|
+
items:
|
|
162
|
+
one: "1 item"
|
|
163
|
+
other: "%{count} items"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Output (i18next JSON):**
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"user": {
|
|
171
|
+
"greeting": "Hello, {{name}}!",
|
|
172
|
+
"items_one": "1 item",
|
|
173
|
+
"items_other": "{{count}} items"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Smart Scanning
|
|
179
|
+
|
|
180
|
+
Detects translation usage in:
|
|
181
|
+
|
|
182
|
+
- Svelte: `{t('key')}` and `t('key')` in `<script>`
|
|
183
|
+
- React: `{t('key')}` in JSX
|
|
184
|
+
- Vue: `{{ t('key') }}` and `t('key')` in script
|
|
185
|
+
|
|
186
|
+
Handles:
|
|
187
|
+
|
|
188
|
+
- Static keys: `t('user.greeting')`
|
|
189
|
+
- Template literals: `t(\`user.\${type}.title\`)` (flagged for review)
|
|
190
|
+
- Dynamic patterns: `t(keyVariable)` (flagged for review)
|
|
191
|
+
|
|
192
|
+
### Health Checks
|
|
193
|
+
|
|
194
|
+
| Check | Description |
|
|
195
|
+
| ---------------- | ------------------------------------------------ |
|
|
196
|
+
| **Missing Keys** | Used in code but not in JSON (breaks app) |
|
|
197
|
+
| **Unused Keys** | In JSON but never used (bloat) |
|
|
198
|
+
| **Locale Sync** | Key exists in `en.json` but missing in `ru.json` |
|
|
199
|
+
|
|
200
|
+
### Watch Mode
|
|
201
|
+
|
|
202
|
+
Auto-regenerates JSON when YAML files change:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
bundle exec rake inertia_i18n:watch
|
|
206
|
+
|
|
207
|
+
# Output:
|
|
208
|
+
👀 Watching config/locales for YAML changes...
|
|
209
|
+
📝 Detected locale file changes...
|
|
210
|
+
Changed: config/locales/hr.en.yml
|
|
211
|
+
🔄 Regenerating JSON files...
|
|
212
|
+
✅ Done!
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## CI Integration
|
|
218
|
+
|
|
219
|
+
The best way to ensure your translations stay healthy is to check them in your Continuous Integration (CI) pipeline.
|
|
220
|
+
|
|
221
|
+
### Test-based Health Check (Recommended)
|
|
222
|
+
|
|
223
|
+
Generate a dedicated test file that runs the health check as part of your test suite:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# For RSpec
|
|
227
|
+
rails g inertia_i18n:test
|
|
228
|
+
# Creates spec/inertia_i18n_health_spec.rb
|
|
229
|
+
|
|
230
|
+
# For Minitest
|
|
231
|
+
# Creates test/inertia_i18n_health_test.rb
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Now, your existing CI command will automatically catch translation issues:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Run your full test suite
|
|
238
|
+
bundle exec rspec
|
|
239
|
+
# or
|
|
240
|
+
bundle exec rails test
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When issues are found, the test will fail with a detailed report:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
Failure/Error: fail message.join("\n")
|
|
247
|
+
|
|
248
|
+
RuntimeError:
|
|
249
|
+
|
|
250
|
+
Translation health check failed!
|
|
251
|
+
|
|
252
|
+
Missing Keys (1):
|
|
253
|
+
- home.title
|
|
254
|
+
|
|
255
|
+
Unused Keys (1):
|
|
256
|
+
- unused.key
|
|
257
|
+
|
|
258
|
+
Locale Synchronization Issues (2):
|
|
259
|
+
- unused.key (in ru)
|
|
260
|
+
- home.title (in ru)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### GitHub Actions Example
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
# .github/workflows/ci.yml
|
|
267
|
+
name: CI
|
|
268
|
+
|
|
269
|
+
on: [push, pull_request]
|
|
270
|
+
|
|
271
|
+
jobs:
|
|
272
|
+
test:
|
|
273
|
+
runs-on: ubuntu-latest
|
|
274
|
+
steps:
|
|
275
|
+
- uses: actions/checkout@v3
|
|
276
|
+
- uses: ruby/setup-ruby@v1
|
|
277
|
+
with:
|
|
278
|
+
bundler-cache: true
|
|
279
|
+
|
|
280
|
+
# Run the full test suite, which now includes the translation health check
|
|
281
|
+
- name: Run tests
|
|
282
|
+
run: bundle exec rspec
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Compatibility with i18n-tasks
|
|
288
|
+
|
|
289
|
+
If you use [i18n-tasks](https://github.com/glebm/i18n-tasks) for your backend translations, it might flag your frontend keys as "unused" or "missing". To prevent this, configure `i18n-tasks` to ignore the frontend locale directory.
|
|
290
|
+
|
|
291
|
+
Add this to your `config/i18n-tasks.yml`:
|
|
292
|
+
|
|
293
|
+
```yaml
|
|
294
|
+
# config/i18n-tasks.yml
|
|
295
|
+
data:
|
|
296
|
+
read:
|
|
297
|
+
- "config/locales/backend/**/*.yml" # Read only backend locales
|
|
298
|
+
- "config/locales/*.yml" # Optional: shared keys
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Alternatively, you can exclude the frontend directory:
|
|
302
|
+
|
|
303
|
+
```yaml
|
|
304
|
+
# config/i18n-tasks.yml
|
|
305
|
+
ignore:
|
|
306
|
+
- "frontend.*" # Ignore all keys starting with "frontend." (if namespaced)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Configuration Reference
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
InertiaI18n.configure do |config|
|
|
315
|
+
# Source directories for your frontend YAML files.
|
|
316
|
+
# Default: ['config/locales/frontend']
|
|
317
|
+
config.source_paths = [
|
|
318
|
+
'config/locales/frontend',
|
|
319
|
+
'config/locales/common'
|
|
320
|
+
]
|
|
321
|
+
config.source_pattern = '**/*.{yml,yaml}'
|
|
322
|
+
|
|
323
|
+
# Target: i18next JSON files
|
|
324
|
+
config.target_path = 'app/frontend/locales'
|
|
325
|
+
|
|
326
|
+
# Locales to process
|
|
327
|
+
config.locales = [:en, :ru, :de]
|
|
328
|
+
|
|
329
|
+
# Frontend paths to scan
|
|
330
|
+
config.scan_paths = [
|
|
331
|
+
'app/frontend/**/*.{js,ts,jsx,tsx,svelte,vue}'
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
# Interpolation conversion
|
|
335
|
+
config.interpolation = { from: '%{', to: '{{' }
|
|
336
|
+
|
|
337
|
+
# Flatten nested keys (default: false)
|
|
338
|
+
config.flatten_keys = false
|
|
339
|
+
|
|
340
|
+
# Ignore patterns (don't scan these files)
|
|
341
|
+
config.ignore_patterns = [
|
|
342
|
+
'**/node_modules/**',
|
|
343
|
+
'**/vendor/**',
|
|
344
|
+
'**/*.test.{js,ts}'
|
|
345
|
+
]
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Comparison with Alternatives
|
|
352
|
+
|
|
353
|
+
| Feature | InertiaI18n | i18n-tasks | i18next-parser |
|
|
354
|
+
| ----------------------- | ----------- | ----------------- | -------------------- |
|
|
355
|
+
| Rails YAML support | ✅ | ✅ | ❌ |
|
|
356
|
+
| i18next JSON support | ✅ | ❌ | ✅ |
|
|
357
|
+
| YAML → JSON conversion | ✅ | ❌ | ❌ |
|
|
358
|
+
| Frontend usage scanning | ✅ | ❌ | ✅ (extraction only) |
|
|
359
|
+
| Missing keys detection | ✅ | ✅ (backend only) | ✅ (frontend only) |
|
|
360
|
+
| Unused keys detection | ✅ | ✅ (backend only) | ❌ |
|
|
361
|
+
| Locale sync check | ✅ | ✅ | ✅ |
|
|
362
|
+
| Watch mode | ✅ | ❌ | ✅ |
|
|
363
|
+
| Rails integration | ✅ | ✅ | ❌ |
|
|
364
|
+
| Inertia.js specific | ✅ | ❌ | ❌ |
|
|
365
|
+
|
|
366
|
+
**InertiaI18n = i18n-tasks + i18next-parser + YAML↔JSON bridge**
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Development
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# Clone repository
|
|
374
|
+
git clone https://github.com/alec-c4/inertia_i18n.git
|
|
375
|
+
cd inertia_i18n
|
|
376
|
+
|
|
377
|
+
# Install dependencies
|
|
378
|
+
bundle install
|
|
379
|
+
|
|
380
|
+
# Run tests
|
|
381
|
+
bundle exec rspec
|
|
382
|
+
|
|
383
|
+
# Run locally in your Rails app
|
|
384
|
+
# Add to Gemfile:
|
|
385
|
+
gem 'inertia_i18n', path: '../inertia_i18n'
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Contributing
|
|
391
|
+
|
|
392
|
+
1. Fork the repository
|
|
393
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
394
|
+
3. Write tests (TDD approach)
|
|
395
|
+
4. Implement the feature
|
|
396
|
+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
397
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
398
|
+
7. Open a Pull Request
|
|
399
|
+
|
|
400
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## License
|
|
405
|
+
|
|
406
|
+
MIT License - see [LICENSE.txt](LICENSE.txt)
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Credits
|
|
411
|
+
|
|
412
|
+
Created by [Alexey Poimtsev](https://alec-c4.com)
|
|
413
|
+
|
|
414
|
+
Inspired by:
|
|
415
|
+
|
|
416
|
+
- [i18n-tasks](https://github.com/glebm/i18n-tasks) - Rails i18n management
|
|
417
|
+
- [i18next-parser](https://github.com/i18next/i18next-parser) - Frontend key extraction
|
|
418
|
+
- Real-world pain from managing translations in Inertia.js apps
|
data/exe/inertia_i18n
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
loader = Zeitwerk::Loader.new
|
|
5
|
+
loader.push_dir(File.expand_path("../lib", __dir__))
|
|
6
|
+
loader.setup
|
|
7
|
+
|
|
8
|
+
# Load Rails environment if available
|
|
9
|
+
if File.exist?("config/environment.rb")
|
|
10
|
+
require "rails"
|
|
11
|
+
require File.expand_path("config/environment.rb")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require "inertia_i18n/cli"
|
|
15
|
+
|
|
16
|
+
InertiaI18n::Cli.start(ARGV)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module InertiaI18n
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
desc "Installs InertiaI18n, creates directory structure, and adds frontend dependencies."
|
|
10
|
+
|
|
11
|
+
def check_dependencies
|
|
12
|
+
@framework = detect_framework
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_directory_structure
|
|
16
|
+
say "Creating locale directory structure...", :green
|
|
17
|
+
empty_directory "config/locales/frontend"
|
|
18
|
+
empty_directory "config/locales/backend"
|
|
19
|
+
empty_directory "app/frontend/locales"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def move_existing_locales
|
|
23
|
+
# Move standard Rails locales to backend if they exist in the root
|
|
24
|
+
%w[en.yml ru.yml].each do |file|
|
|
25
|
+
source = "config/locales/#{file}"
|
|
26
|
+
if File.exist?(source)
|
|
27
|
+
say "Moving #{file} to config/locales/backend/", :yellow
|
|
28
|
+
rename_file source, "config/locales/backend/#{file}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_sample_locales
|
|
34
|
+
copy_file "common.en.yml", "config/locales/frontend/common.en.yml"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_initializer
|
|
38
|
+
template "inertia_i18n.rb.tt", "config/initializers/inertia_i18n.rb"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add_frontend_dependencies
|
|
42
|
+
packages_to_add = ["i18next"]
|
|
43
|
+
|
|
44
|
+
case @framework
|
|
45
|
+
when :react
|
|
46
|
+
say "Detected React. Adding react-i18next...", :green
|
|
47
|
+
packages_to_add << "react-i18next"
|
|
48
|
+
when :vue
|
|
49
|
+
say "Detected Vue. Adding i18next-vue...", :green
|
|
50
|
+
packages_to_add << "i18next-vue"
|
|
51
|
+
when :svelte
|
|
52
|
+
say "Detected Svelte.", :green
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return unless File.exist?("package.json")
|
|
56
|
+
|
|
57
|
+
# Detect package manager
|
|
58
|
+
cmd = if File.exist?("yarn.lock")
|
|
59
|
+
"yarn add"
|
|
60
|
+
elsif File.exist?("bun.lockb")
|
|
61
|
+
"bun add"
|
|
62
|
+
else
|
|
63
|
+
"npm install"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
run "#{cmd} #{packages_to_add.join(" ")}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def show_post_install_info
|
|
70
|
+
say "\n✅ InertiaI18n installed successfully!", :green
|
|
71
|
+
say "\nNext steps:", :bold
|
|
72
|
+
say "1. Import and initialize i18next in your frontend application (e.g., app/frontend/i18n.js)."
|
|
73
|
+
say "2. Use the generated locales in config/locales/frontend/."
|
|
74
|
+
say "3. Run `bundle exec inertia_i18n convert` to generate the initial JSON files."
|
|
75
|
+
say "4. Start your server and happy translating!\n\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def detect_framework
|
|
81
|
+
return unless File.exist?("package.json")
|
|
82
|
+
|
|
83
|
+
package_json = JSON.parse(File.read("package.json"))
|
|
84
|
+
dependencies = package_json["dependencies"] || {}
|
|
85
|
+
dev_dependencies = package_json["devDependencies"] || {}
|
|
86
|
+
all_deps = dependencies.merge(dev_dependencies)
|
|
87
|
+
|
|
88
|
+
if all_deps.key?("react")
|
|
89
|
+
:react
|
|
90
|
+
elsif all_deps.key?("vue")
|
|
91
|
+
:vue
|
|
92
|
+
elsif all_deps.key?("svelte")
|
|
93
|
+
:svelte
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def rename_file(old_name, new_name)
|
|
98
|
+
FileUtils.mv(old_name, new_name)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
require "inertia_i18n/health_checker"
|
|
3
|
+
require "inertia_i18n/file_converter"
|
|
4
|
+
|
|
5
|
+
RSpec.describe InertiaI18n::HealthChecker do
|
|
6
|
+
it "has healthy translations" do
|
|
7
|
+
# Ensure JSON files are up-to-date before checking
|
|
8
|
+
InertiaI18n::FileConverter.convert_all
|
|
9
|
+
|
|
10
|
+
checker = described_class.new.check!
|
|
11
|
+
|
|
12
|
+
return if checker.healthy?
|
|
13
|
+
|
|
14
|
+
message = ["\nTranslation health check failed!"]
|
|
15
|
+
|
|
16
|
+
if checker.issues[:missing].any?
|
|
17
|
+
message << "\nMissing Keys (#{checker.issues[:missing].count}):"
|
|
18
|
+
checker.issues[:missing].each { |i| message << " - #{i[:key]}" }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if checker.issues[:unused].any?
|
|
22
|
+
message << "\nUnused Keys (#{checker.issues[:unused].count}):"
|
|
23
|
+
checker.issues[:unused].each { |i| message << " - #{i[:key]}" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if checker.issues[:unsync].any?
|
|
27
|
+
message << "\nLocale Synchronization Issues (#{checker.issues[:unsync].count}):"
|
|
28
|
+
checker.issues[:unsync].each { |i| message << " - #{i[:key]} (in #{i[:locale]})" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
expect(checker.healthy?).to be(true), message.join("\n")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "inertia_i18n/health_checker"
|
|
3
|
+
require "inertia_i18n/file_converter"
|
|
4
|
+
|
|
5
|
+
class I18nTest < ActiveSupport::TestCase
|
|
6
|
+
test "translations are healthy" do
|
|
7
|
+
# Ensure JSON files are up-to-date before checking
|
|
8
|
+
InertiaI18n::FileConverter.convert_all
|
|
9
|
+
|
|
10
|
+
checker = InertiaI18n::HealthChecker.new.check!
|
|
11
|
+
|
|
12
|
+
message = ["\nTranslation health check failed!"]
|
|
13
|
+
|
|
14
|
+
if checker.issues[:missing].any?
|
|
15
|
+
message << "\nMissing Keys (#{checker.issues[:missing].count}):"
|
|
16
|
+
checker.issues[:missing].each { |i| message << " - #{i[:key]}" }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if checker.issues[:unused].any?
|
|
20
|
+
message << "\nUnused Keys (#{checker.issues[:unused].count}):"
|
|
21
|
+
checker.issues[:unused].each { |i| message << " - #{i[:key]}" }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if checker.issues[:unsync].any?
|
|
25
|
+
message << "\nLocale Synchronization Issues (#{checker.issues[:unsync].count}):"
|
|
26
|
+
checker.issues[:unsync].each { |i| message << " - #{i[:key]} (in #{i[:locale]})" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
assert checker.healthy?, message.join("\n")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
InertiaI18n.configure do |config|
|
|
4
|
+
# Source directories for your frontend YAML files.
|
|
5
|
+
config.source_paths = [Rails.root.join("config", "locales", "frontend")]
|
|
6
|
+
|
|
7
|
+
# Target path for generated i18next JSON files
|
|
8
|
+
config.target_path = Rails.root.join("app", "frontend", "locales")
|
|
9
|
+
|
|
10
|
+
# Locales to process (first is primary)
|
|
11
|
+
config.locales = %i[en]
|
|
12
|
+
|
|
13
|
+
# YAML file pattern to match
|
|
14
|
+
config.source_pattern = "**/*.{yml,yaml}"
|
|
15
|
+
|
|
16
|
+
# Paths to scan for translation usage
|
|
17
|
+
<% extensions = case @framework
|
|
18
|
+
when :react then "js,ts,jsx,tsx"
|
|
19
|
+
when :vue then "js,ts,vue"
|
|
20
|
+
when :svelte then "js,ts,svelte"
|
|
21
|
+
else "js,ts,jsx,tsx,svelte,vue"
|
|
22
|
+
end %>
|
|
23
|
+
config.scan_paths = [
|
|
24
|
+
"app/frontend/**/*.{<%= extensions %>}"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Interpolation conversion (Rails %{var} -> i18next {{var}})
|
|
28
|
+
config.interpolation = { from: "%{", to: "{{" }
|
|
29
|
+
|
|
30
|
+
# Dynamic key patterns (prefix => description)
|
|
31
|
+
# Keys matching these prefixes won't be marked as unused
|
|
32
|
+
config.dynamic_patterns = {
|
|
33
|
+
# "status." => "Dynamic status keys"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Keys to ignore during unused check
|
|
37
|
+
config.ignore_unused = []
|
|
38
|
+
|
|
39
|
+
# Keys to ignore during missing check
|
|
40
|
+
config.ignore_missing = []
|
|
41
|
+
end
|