i18nize 0.5 → 0.6
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 +98 -3
- data/README.md +89 -39
- data/lib/i18nize/cli.rb +62 -67
- data/lib/i18nize/comparer.rb +3 -2
- data/lib/i18nize/inserter.rb +67 -13
- data/lib/i18nize/loader.rb +1 -0
- data/lib/i18nize/missing_set.rb +1 -1
- data/lib/i18nize/translator.rb +5 -4
- data/lib/i18nize/version.rb +1 -1
- metadata +15 -9
- data/.idea/.gitignore +0 -8
- data/.idea/git_toolbox_prj.xml +0 -15
- data/.idea/i18nize.iml +0 -53
- data/.idea/material_theme_project_new.xml +0 -12
- data/.idea/vcs.xml +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed1bfd2cff604aa20337cfd2e87dd47985ab66d470ee13fa1241378e8c7613d2
|
|
4
|
+
data.tar.gz: 8e2a54ea7acb3ff57233944f81648f8243f732768278cd77478f7bf5fed08d35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7322c2b6b1bc505090789774f340404186be3dcaed272629b19ab914bd36af3f8c1d50cde289ce29e203c60fc3273d5a8981c11060fdbfd571d261bc96803a3
|
|
7
|
+
data.tar.gz: ff89ec4d79479de9c40855916fc0a5dfa3f87a6e300459625edcfb7928aafb54ac15faec21e3a9e68d6a8c47543d716ac53a41620496363e86c7abb5314d00e4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
# Changelog
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.6] - 2025-09-11
|
|
4
|
+
### Added
|
|
5
|
+
- CLI flag `--only_blank`: fills only missing or blank keys with a placeholder
|
|
6
|
+
(`<locale>.<full.key> missing`) instead of translating with DeepL.
|
|
7
|
+
- Environment variable `I18NIZE_ALLOW_EMPTY=1`: skips keys where the source value is empty
|
|
8
|
+
or whitespace-only, preventing them from being copied/translated.
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
### Changed
|
|
11
|
+
- CLI parsing improved: `--from` can now be used **before or after** the target locale.
|
|
12
|
+
Examples:
|
|
13
|
+
```bash
|
|
14
|
+
i18nize cs --from de
|
|
15
|
+
i18nize --from de cs
|
|
16
|
+
````
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [0.5.0] - 2025-09-08
|
|
21
|
+
### Added
|
|
22
|
+
- **Conflict policy**: source locale is now treated as the source of truth.
|
|
23
|
+
If the target locale has a scalar where a nested hash is required, the scalar is **overwritten** with the correct structure from the source.
|
|
24
|
+
- **Pluralization support**: automatic handling of `one`, `other`, and related plural keys. Scalars can be coerced into plural hashes.
|
|
25
|
+
- **Placeholder preservation**: `%{count}` and other I18n-style placeholders are protected from being altered by DeepL.
|
|
26
|
+
- Improved file name detection for `.yml` and `.yaml` formats, including names like `app.en.yml`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Default behaviour: conflicts are no longer skipped or raise errors, but are resolved by overwriting with source locale values.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
- **Rake task `i18nize:translate`** has been removed.
|
|
33
|
+
Use the CLI instead:
|
|
34
|
+
```bash
|
|
35
|
+
i18nize cs
|
|
36
|
+
i18nize cs --from en
|
|
37
|
+
i18nize cs --missing
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
* Eliminated `IndexError: string not matched` errors when attempting to insert into scalar values.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## \[0.4.4] - 2025-09-08
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
* CLI option `--missing [FROM]` to list missing keys without translating.
|
|
51
|
+
|
|
52
|
+
* `i18nize cs --missing` → shows missing Czech keys (from English).
|
|
53
|
+
* `i18nize cs --missing de` → shows missing Czech keys (from German).
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## \[0.4.2.1] - 2025-09-08
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
* Correct requires for `Translator` and `Inserter` inside CLI (prevented `NameError`).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## \[0.4.2] - 2025-09-08
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
* `.env` file support (via `dotenv` if present).
|
|
70
|
+
* Improved CLI error messages.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## \[0.4.1] - 2025-09-08
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
* File path normalization for top-level locales (`en.yml` → `cs.yml`).
|
|
79
|
+
* Safer regex for file name replacement.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## \[0.4.0] - 2025-08-25
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
* Standalone CLI (`i18nize <locale>`) — no need to call only via Rake.
|
|
88
|
+
* Basic `--from` option for source language.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## \[0.1.0] - 2025-07-30
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
|
|
96
|
+
* Initial release.
|
|
97
|
+
* DeepL translation of missing keys.
|
|
98
|
+
* Character count estimation and confirmation prompt.
|
|
99
|
+
* Automatic creation of target locale files.
|
|
100
|
+
* Rake task `i18nize:translate[locale]`.
|
data/README.md
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
+
|
|
1
2
|
# i18nize
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
[](https://github.com/nikolas2145/i18nize/actions/workflows/main.yml)
|
|
5
|
+
[](https://badge.fury.io/rb/i18nize)
|
|
5
6
|
|
|
7
|
+
🧠 Automatically fills in missing I18n YAML keys using DeepL translation API.
|
|
8
|
+
Designed to streamline multi-language support in Ruby and Rails projects.
|
|
6
9
|
|
|
7
10
|
## ✨ Features
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
* Detects missing or empty translation keys in locale files
|
|
13
|
+
* Supports multiple YAML files and nested directories (e.g. `config/locales/api/en.yml`)
|
|
14
|
+
* Automatically translates from a source locale (**default: `en`**) to a target (e.g. `cs`)
|
|
15
|
+
* **Placeholder mode** to fill only blank/missing keys without DeepL (`--only_blank`)
|
|
16
|
+
* Integrates with DeepL API (Free or Pro)
|
|
17
|
+
* Safely merges translations into corresponding `*.yml` files
|
|
18
|
+
* Handles structural conflicts by overwriting target with source truth
|
|
19
|
+
* Character count check with confirmation prompt
|
|
20
|
+
* CLI (`i18nize`)
|
|
17
21
|
|
|
18
22
|
## 🔧 Installation
|
|
19
23
|
|
|
@@ -35,61 +39,95 @@ Or install manually:
|
|
|
35
39
|
gem install i18nize
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
|
|
39
42
|
## 🚀 Usage
|
|
40
43
|
|
|
41
44
|
### 1. Set your DeepL API key
|
|
42
45
|
|
|
43
|
-
Create a `.env` or export it in your shell:
|
|
46
|
+
Create a `.env` file (loaded automatically if you use `dotenv`) or export it in your shell:
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
49
|
export DEEPL_API_KEY=your-api-key
|
|
47
50
|
export DEEPL_ENDPOINT=https://api.deepl.com/v2/translate # Only if you are using a DeepL Pro account
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
👉 You can use the [Free DeepL API](https://www.deepl.com/pro#developer).
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
### 2. Run the translation task
|
|
55
|
+
### 2. Run translations via CLI
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
|
-
|
|
58
|
+
# Translate from en → cs
|
|
59
|
+
i18nize cs
|
|
60
|
+
|
|
61
|
+
# Translate from de → fr
|
|
62
|
+
i18nize fr --from de
|
|
58
63
|
```
|
|
59
64
|
|
|
60
65
|
💬 This will:
|
|
61
66
|
|
|
62
|
-
* Scan config/locales/**/*en.yml (
|
|
63
|
-
* Detect which `
|
|
67
|
+
* Scan `config/locales/**/*en.yml` (or your chosen source language)
|
|
68
|
+
* Detect which `cs.yml` files are missing keys
|
|
64
69
|
* Use DeepL to translate missing values
|
|
65
|
-
* Write them to the appropriate files (
|
|
70
|
+
* Write them to the appropriate files (creating them if needed)
|
|
71
|
+
|
|
72
|
+
### 3. Show missing keys only
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Missing cs keys (source en)
|
|
76
|
+
i18nize cs --missing
|
|
77
|
+
|
|
78
|
+
# Missing cs keys (source de)
|
|
79
|
+
i18nize cs --missing de
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This mode only lists missing keys, no translations are written.
|
|
83
|
+
|
|
84
|
+
### 4. Fill placeholders for blank/missing keys (no DeepL)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Insert placeholders instead of translating
|
|
88
|
+
i18nize cs --only_blank
|
|
89
|
+
```
|
|
66
90
|
|
|
91
|
+
This scans all keys in the **source** locale (default `en`) and, for any **missing or blank** value in the target locale, inserts a placeholder:
|
|
67
92
|
|
|
93
|
+
```
|
|
94
|
+
<locale>.<full.dot.key> missing
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
en:
|
|
101
|
+
test:
|
|
102
|
+
title: "Testing"
|
|
103
|
+
cs:
|
|
104
|
+
test:
|
|
105
|
+
title: "cs.test.title missing"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Existing non-blank values in the target are left untouched.
|
|
68
109
|
|
|
69
110
|
## 🛡️ Character limit check
|
|
70
111
|
|
|
71
|
-
Before translating, the
|
|
112
|
+
Before translating, the CLI:
|
|
72
113
|
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
114
|
+
* Counts the total number of characters
|
|
115
|
+
* Warns if the count exceeds **500,000** (DeepL Free monthly limit)
|
|
116
|
+
* Asks for confirmation:
|
|
76
117
|
|
|
77
118
|
```text
|
|
78
119
|
[i18nize] Estimated character count: 43125
|
|
79
120
|
Proceed with translation? [y/N]:
|
|
80
121
|
```
|
|
81
122
|
|
|
82
|
-
|
|
83
|
-
|
|
84
123
|
## 🗂️ Locale file support
|
|
85
124
|
|
|
86
|
-
* Files are
|
|
125
|
+
* Files are matched by name:
|
|
87
126
|
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
* `config/locales/api/en.yml` → `config/locales/api/cs.yml`
|
|
128
|
+
* `config/locales/en.devise.yml` → `config/locales/cs.devise.yml`
|
|
90
129
|
* Missing files are created automatically
|
|
91
|
-
|
|
92
|
-
|
|
130
|
+
* If a scalar exists where a nested structure is required, **it is overwritten by source locale structure**
|
|
93
131
|
|
|
94
132
|
## 🧩 Example structure
|
|
95
133
|
|
|
@@ -102,7 +140,13 @@ config/locales/
|
|
|
102
140
|
│ └── cs.yml
|
|
103
141
|
```
|
|
104
142
|
|
|
105
|
-
After running
|
|
143
|
+
After running:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
i18nize cs
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
the gem will create:
|
|
106
150
|
|
|
107
151
|
```
|
|
108
152
|
config/locales/api/cs.yml
|
|
@@ -114,16 +158,22 @@ With keys translated from `api/en.yml`.
|
|
|
114
158
|
|
|
115
159
|
## 📦 Configuration
|
|
116
160
|
|
|
117
|
-
For now, configuration is via
|
|
118
|
-
Optional future support: `.i18nize.yml`.
|
|
161
|
+
For now, configuration is via CLI flags and environment variables:
|
|
119
162
|
|
|
163
|
+
* `--from LANG` (default `en`)
|
|
164
|
+
* `--missing [FROM]` (list missing keys only)
|
|
165
|
+
* `--only_blank` (fill only blank/missing keys with placeholders, no DeepL)
|
|
166
|
+
* `DEEPL_API_KEY` (required for translation mode)
|
|
167
|
+
* `DEEPL_ENDPOINT` (optional, only for Pro)
|
|
168
|
+
* `I18NIZE_ALLOW_EMPTY == "1"` → **skip empty/whitespace-only values** in the source when inserting or translating
|
|
169
|
+
(i.e., if a source value is `""` or `" "` it will be ignored and not propagated to the target)
|
|
120
170
|
|
|
171
|
+
---
|
|
121
172
|
|
|
122
173
|
## 🔍 TODO
|
|
123
174
|
|
|
124
|
-
*
|
|
125
|
-
* YAML key sorting
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
|
|
175
|
+
* RSpec test suite
|
|
176
|
+
* YAML key sorting / format preservation
|
|
177
|
+
* Richer conflict resolution strategies (configurable overwrite / skip)
|
|
178
|
+
* Use custom char limit
|
|
129
179
|
|
data/lib/i18nize/cli.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
-
|
|
5
4
|
require "i18nize/translator"
|
|
6
5
|
require "i18nize/inserter"
|
|
7
6
|
require "i18nize/comparer"
|
|
8
7
|
|
|
9
8
|
module I18nize
|
|
9
|
+
# CLI for i18nize
|
|
10
10
|
class CLI
|
|
11
11
|
def self.run(argv)
|
|
12
12
|
new(argv).run
|
|
@@ -16,107 +16,102 @@ module I18nize
|
|
|
16
16
|
@argv = argv.dup
|
|
17
17
|
@options = {
|
|
18
18
|
from: "en",
|
|
19
|
-
mode: :translate,
|
|
19
|
+
mode: :translate, # :translate | :missing | :only_blank
|
|
20
20
|
missing_from: nil
|
|
21
21
|
}
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def run
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
parse_flags!(@argv[1..] || [])
|
|
29
|
-
|
|
30
|
-
to_locale = parse_positional_locale!(first)
|
|
25
|
+
remaining = parse_flags!(@argv)
|
|
26
|
+
abort usage unless remaining.any?
|
|
27
|
+
to_locale = remaining.shift.strip
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
case @options[:mode]
|
|
30
|
+
when :missing
|
|
33
31
|
from = @options[:missing_from] || @options[:from]
|
|
34
32
|
show_missing_translations(from:, to: to_locale)
|
|
35
|
-
|
|
33
|
+
when :only_blank
|
|
34
|
+
run_only_blank(to_locale)
|
|
35
|
+
else
|
|
36
|
+
run_translation(to_locale)
|
|
36
37
|
end
|
|
37
|
-
|
|
38
|
-
run_translation(to_locale)
|
|
39
38
|
0
|
|
40
39
|
rescue OptionParser::InvalidOption => e
|
|
41
40
|
warn red(e.message)
|
|
42
|
-
|
|
41
|
+
puts usage
|
|
42
|
+
2
|
|
43
43
|
rescue SystemExit => e
|
|
44
44
|
raise e
|
|
45
45
|
rescue => e
|
|
46
46
|
warn red("Error: #{e.class}: #{e.message}")
|
|
47
47
|
e.backtrace.each { |l| warn l } if ENV["I18NIZE_DEBUG"] == "1"
|
|
48
|
-
|
|
48
|
+
1
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def parse_positional_locale!(arg)
|
|
58
|
-
abort yellow("Usage: i18nize <to_locale> [--from en] [--missing [FROM]]") unless arg
|
|
59
|
-
arg.strip
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def parse_flags!(rest)
|
|
63
|
-
OptionParser.new do |o|
|
|
53
|
+
def parse_flags!(args)
|
|
54
|
+
parser = OptionParser.new do |o|
|
|
64
55
|
o.banner = "Usage: i18nize <to_locale> [options]"
|
|
65
|
-
o.on("--from LANG", "Source language
|
|
66
|
-
|
|
56
|
+
o.on("--from LANG", "Source language (default: en)") { |v| @options[:from] = v }
|
|
67
57
|
o.on("--missing", "--missing [FROM]", "List missing keys only; optionally specify source locale") do |from|
|
|
68
58
|
@options[:mode] = :missing
|
|
69
59
|
@options[:missing_from] = from if from && !from.strip.empty?
|
|
70
60
|
end
|
|
61
|
+
o.on("--only_blank", "Fill only missing/blank keys with '<locale>.<full.key> missing'") do
|
|
62
|
+
@options[:mode] = :only_blank
|
|
63
|
+
end
|
|
64
|
+
o.on("-h", "--help", "Show help") { puts usage; exit 0 }
|
|
65
|
+
end
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
parser.parse!(args)
|
|
68
|
+
args
|
|
74
69
|
end
|
|
75
70
|
|
|
76
71
|
def show_missing_translations(from:, to:)
|
|
77
72
|
load_rails_if_available
|
|
78
73
|
puts cyan("🔍 Missing keys for #{to} (from #{from})")
|
|
79
|
-
|
|
80
74
|
missing = I18nize::Comparer.missing_translations(from_locale: from, to_locale: to)
|
|
81
|
-
|
|
82
75
|
if missing.empty?
|
|
83
76
|
puts green("✅ No missing translations.")
|
|
84
77
|
return
|
|
85
78
|
end
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
puts yellow("⚠️ #{total_keys} missing #{total_keys == 1 ? 'key' : 'keys'}:")
|
|
89
|
-
|
|
79
|
+
total = missing.total_keys
|
|
80
|
+
puts yellow("⚠️ #{total} missing #{total == 1 ? 'key' : 'keys'}:")
|
|
90
81
|
missing.values_by_file.each do |file, keys_hash|
|
|
91
82
|
puts " #{file}:"
|
|
92
|
-
keys_hash.keys.sort.each
|
|
93
|
-
puts " - #{k.sub(/^#{from}\./, '')}"
|
|
94
|
-
end
|
|
83
|
+
keys_hash.keys.sort.each { |k| puts " - #{k.sub(/^#{Regexp.escape(from)}\./, "")}" }
|
|
95
84
|
end
|
|
96
85
|
end
|
|
97
86
|
|
|
98
|
-
def
|
|
87
|
+
def run_only_blank(to_locale)
|
|
99
88
|
load_env_files
|
|
89
|
+
load_rails_if_available
|
|
90
|
+
puts cyan("Filling only blank/missing keys: from #{@options[:from]} → #{to_locale}")
|
|
91
|
+
I18nize::Inserter.insert_only_blank(
|
|
92
|
+
from_locale: @options[:from],
|
|
93
|
+
to_locale: to_locale
|
|
94
|
+
)
|
|
95
|
+
puts green("✅ Placeholders inserted for blank/missing keys.")
|
|
96
|
+
end
|
|
100
97
|
|
|
98
|
+
def run_translation(to_locale)
|
|
99
|
+
load_env_files
|
|
101
100
|
api_key = ENV["DEEPL_API_KEY"]
|
|
102
101
|
abort red("Missing DEEPL_API_KEY env variable") if api_key.to_s.strip.empty?
|
|
103
|
-
|
|
104
102
|
load_rails_if_available
|
|
105
103
|
|
|
106
104
|
puts cyan("Starting translation: from #{@options[:from]} → #{to_locale}")
|
|
107
|
-
|
|
108
105
|
translator = I18nize::Translator.new(
|
|
109
106
|
auth_key: api_key,
|
|
110
107
|
from_lang: @options[:from].upcase,
|
|
111
108
|
to_lang: to_locale.upcase
|
|
112
109
|
)
|
|
113
|
-
|
|
114
110
|
I18nize::Inserter.insert_missing(
|
|
115
111
|
from_locale: @options[:from],
|
|
116
112
|
to_locale: to_locale,
|
|
117
113
|
translator: translator
|
|
118
114
|
)
|
|
119
|
-
|
|
120
115
|
puts green("✅ Translation completed.")
|
|
121
116
|
end
|
|
122
117
|
|
|
@@ -124,18 +119,37 @@ module I18nize
|
|
|
124
119
|
begin
|
|
125
120
|
require "dotenv/load"
|
|
126
121
|
rescue LoadError
|
|
127
|
-
# Ignore
|
|
128
122
|
end
|
|
129
123
|
end
|
|
130
124
|
|
|
131
125
|
def load_rails_if_available
|
|
132
126
|
return if defined?(Rails)
|
|
133
127
|
|
|
134
|
-
if File.exist?("config/environment.rb")
|
|
135
|
-
|
|
136
|
-
end
|
|
128
|
+
require_relative "../../config/environment" if File.exist?("config/environment.rb")
|
|
129
|
+
|
|
137
130
|
rescue LoadError
|
|
138
|
-
#
|
|
131
|
+
# Ignored
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def usage
|
|
135
|
+
<<~HELP
|
|
136
|
+
Usage:
|
|
137
|
+
i18nize <to_locale> [options]
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
i18nize cs # translate missing from en -> cs
|
|
141
|
+
i18nize cs --from de # translate missing from de -> cs
|
|
142
|
+
i18nize --from de cs # flags can be before or after the locale
|
|
143
|
+
i18nize cs --missing # list missing keys for cs (from en)
|
|
144
|
+
i18nize cs --missing de # list missing keys for cs (from de)
|
|
145
|
+
i18nize cs --only_blank # fill only blank/missing keys with placeholders
|
|
146
|
+
|
|
147
|
+
Options:
|
|
148
|
+
--from LANG Source language (default: en)
|
|
149
|
+
--missing [FROM] Only list missing keys (optional override of source)
|
|
150
|
+
--only_blank Insert '<locale>.<full.key> missing' for blank/missing keys
|
|
151
|
+
-h, --help Show this help
|
|
152
|
+
HELP
|
|
139
153
|
end
|
|
140
154
|
|
|
141
155
|
def color(text, code) = "\e[#{code}m#{text}\e[0m"
|
|
@@ -147,24 +161,5 @@ module I18nize
|
|
|
147
161
|
def yellow(text) = color(text, 33)
|
|
148
162
|
|
|
149
163
|
def cyan(text) = color(text, 36)
|
|
150
|
-
|
|
151
|
-
def print_help_and_exit(code = 0)
|
|
152
|
-
puts <<~HELP
|
|
153
|
-
Usage:
|
|
154
|
-
i18nize <to_locale> [options]
|
|
155
|
-
|
|
156
|
-
Examples:
|
|
157
|
-
i18nize cs # translate missing from en -> cs
|
|
158
|
-
i18nize cs --from de # translate missing from de -> cs
|
|
159
|
-
i18nize cs --missing # list missing keys for cs (from en)
|
|
160
|
-
i18nize cs --missing de # list missing keys for cs (from de)
|
|
161
|
-
|
|
162
|
-
Options:
|
|
163
|
-
--from LANG Source language (default: en)
|
|
164
|
-
--missing [FROM] Only list missing keys (optional override of source)
|
|
165
|
-
-h, --help Show this help
|
|
166
|
-
HELP
|
|
167
|
-
exit code
|
|
168
|
-
end
|
|
169
164
|
end
|
|
170
165
|
end
|
data/lib/i18nize/comparer.rb
CHANGED
|
@@ -4,8 +4,9 @@ require_relative "loader"
|
|
|
4
4
|
require_relative "missing_set"
|
|
5
5
|
|
|
6
6
|
module I18nize
|
|
7
|
+
# Small utility to compare translations from one locale to another
|
|
7
8
|
class Comparer
|
|
8
|
-
def self.missing_translations(from_locale: "en",
|
|
9
|
+
def self.missing_translations(to_locale:, from_locale: "en") # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
9
10
|
from_data = Loader.load_per_file(locale: from_locale)
|
|
10
11
|
to_data = Loader.load_per_file(locale: to_locale)
|
|
11
12
|
|
|
@@ -37,7 +38,7 @@ module I18nize
|
|
|
37
38
|
MissingSet.new(from_locale: from_locale, to_locale: to_locale, data: result)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
def self.missing_keys(from_locale: "en"
|
|
41
|
+
def self.missing_keys(to_locale:, from_locale: "en")
|
|
41
42
|
missing_translations(from_locale: from_locale, to_locale: to_locale).keys_by_file
|
|
42
43
|
end
|
|
43
44
|
end
|
data/lib/i18nize/inserter.rb
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "byebug"
|
|
5
6
|
require_relative "comparer"
|
|
7
|
+
require_relative "loader"
|
|
6
8
|
|
|
7
9
|
module I18nize
|
|
8
10
|
class Inserter
|
|
9
11
|
CONFLICT_POLICY = :overwrite # :overwrite | :skip
|
|
10
12
|
|
|
11
|
-
def self.insert_missing(from_locale: "en",
|
|
13
|
+
def self.insert_missing(to_locale:, from_locale: "en", base_path: "config/locales", translator: nil)
|
|
12
14
|
missing_data = Comparer.missing_translations(from_locale: from_locale, to_locale: to_locale)
|
|
13
15
|
|
|
14
16
|
if translator
|
|
@@ -26,7 +28,7 @@ module I18nize
|
|
|
26
28
|
overwrites = []
|
|
27
29
|
|
|
28
30
|
missing_data.data.each do |from_rel_path, translations|
|
|
29
|
-
dirname
|
|
31
|
+
dirname = File.dirname(from_rel_path)
|
|
30
32
|
filename = File.basename(from_rel_path)
|
|
31
33
|
|
|
32
34
|
locale_re = /(^|[._])#{Regexp.escape(from_locale)}\.(ya?ml)$/
|
|
@@ -40,7 +42,7 @@ module I18nize
|
|
|
40
42
|
translation_map =
|
|
41
43
|
if translator
|
|
42
44
|
src_values = translations.values
|
|
43
|
-
translated_values = translator.translate_texts(src_values) #
|
|
45
|
+
translated_values = translator.translate_texts(src_values) # returns array
|
|
44
46
|
translations.keys.zip(translated_values).to_h
|
|
45
47
|
else
|
|
46
48
|
translations
|
|
@@ -57,15 +59,70 @@ module I18nize
|
|
|
57
59
|
puts "\e[32m[i18nize] Inserted #{translation_map.size} keys into #{to_full_path}\e[0m"
|
|
58
60
|
end
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
return unless overwrites.any?
|
|
61
63
|
puts "\n\e[33m[i18nize] Overwrote #{overwrites.size} scalar node(s) to create nested structure:\e[0m"
|
|
62
64
|
overwrites.each do |c|
|
|
63
65
|
puts " \e[33m- #{c[:file]}: #{c[:locale]}.#{c[:path].join('.')}\e[0m (was: #{c[:existing_class]})"
|
|
64
66
|
end
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# NEW: fill only missing/blank values with "<locale>.<full.key> missing"
|
|
71
|
+
def self.insert_only_blank(to_locale:, from_locale: "en", base_path: "config/locales")
|
|
72
|
+
from_data = Loader.load_per_file(locale: from_locale, base_path: base_path)
|
|
73
|
+
# to_data = Loader.load_per_file(locale: to_locale, base_path: base_path)
|
|
74
|
+
|
|
75
|
+
updated_files = 0
|
|
76
|
+
overwrites = []
|
|
77
|
+
|
|
78
|
+
from_data.each do |from_rel_path, from_keys|
|
|
79
|
+
dirname = File.dirname(from_rel_path)
|
|
80
|
+
filename = File.basename(from_rel_path)
|
|
81
|
+
locale_re = /(^|[._])#{Regexp.escape(from_locale)}\.(ya?ml)$/
|
|
82
|
+
new_filename = filename.sub(locale_re) { "#{Regexp.last_match(1)}#{to_locale}.#{Regexp.last_match(2)}" }
|
|
83
|
+
to_rel_path = dirname == "." || dirname.empty? ? new_filename : File.join(dirname, new_filename)
|
|
84
|
+
to_full_path = File.join(base_path, to_rel_path)
|
|
85
|
+
|
|
86
|
+
existing = File.exist?(to_full_path) ? YAML.load_file(to_full_path) : {}
|
|
87
|
+
existing[to_locale] ||= {}
|
|
88
|
+
|
|
89
|
+
changes = 0
|
|
90
|
+
|
|
91
|
+
from_keys.each_key do |from_full_key|
|
|
92
|
+
to_full_key = from_full_key.sub(/^#{Regexp.escape(from_locale)}\./, "#{to_locale}.")
|
|
93
|
+
path = to_full_key.sub(/^#{Regexp.escape(to_locale)}\./, "").split(".")
|
|
94
|
+
current = dig_nested(existing[to_locale], path)
|
|
95
|
+
next if current.to_s.strip.empty? && ENV["I18NIZE_ALLOW_EMPTY"] == "1"
|
|
96
|
+
next unless current.nil? || current.to_s.strip.empty?
|
|
97
|
+
|
|
98
|
+
placeholder = "#{to_full_key} missing"
|
|
99
|
+
insert_nested(existing[to_locale], path, placeholder, overwrites, to_rel_path, to_locale, :overwrite)
|
|
100
|
+
changes += 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
next unless changes.positive?
|
|
104
|
+
|
|
105
|
+
FileUtils.mkdir_p(File.dirname(to_full_path))
|
|
106
|
+
File.write(to_full_path, existing.to_yaml(line_width: -1))
|
|
107
|
+
updated_files += 1
|
|
108
|
+
puts "\e[32m[i18nize] Inserted #{changes} placeholder#{changes == 1 ? '' : 's'} into #{to_full_path}\e[0m"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
puts "\e[90m[i18nize] No blank or missing keys found.\e[0m" if updated_files.zero?
|
|
112
|
+
|
|
113
|
+
return unless overwrites.any?
|
|
114
|
+
|
|
115
|
+
puts "\n\e[33m[i18nize] Overwrote #{overwrites.size} scalar node(s) while creating nested structure:\e[0m"
|
|
116
|
+
overwrites.each do |c|
|
|
117
|
+
puts " \e[33m- #{c[:file]}: #{c[:locale]}.#{c[:path].join('.')}\e[0m (was: #{c[:existing_class]})"
|
|
65
118
|
end
|
|
66
119
|
end
|
|
67
120
|
|
|
68
|
-
|
|
121
|
+
def self.dig_nested(hash, path)
|
|
122
|
+
path.reduce(hash) { |h, k| h.is_a?(Hash) ? h[k] : nil }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# conflict_policy: :overwrite (replace scalar with {}), :skip
|
|
69
126
|
def self.insert_nested(base, keys, value, overwrites, file, locale, conflict_policy)
|
|
70
127
|
key = keys.shift
|
|
71
128
|
|
|
@@ -79,18 +136,15 @@ module I18nize
|
|
|
79
136
|
case cur
|
|
80
137
|
when nil
|
|
81
138
|
base[key] = {}
|
|
82
|
-
insert_nested(base[key], keys, value, overwrites, file, locale, conflict_policy)
|
|
139
|
+
return insert_nested(base[key], keys, value, overwrites, file, locale, conflict_policy)
|
|
83
140
|
when Hash
|
|
84
|
-
insert_nested(cur, keys, value, overwrites, file, locale, conflict_policy)
|
|
141
|
+
return insert_nested(cur, keys, value, overwrites, file, locale, conflict_policy)
|
|
85
142
|
else
|
|
86
|
-
|
|
143
|
+
return false unless conflict_policy == :overwrite
|
|
87
144
|
overwrites << { file: file, locale: locale, path: [key, *keys], existing_class: cur.class.to_s }
|
|
88
145
|
base[key] = {}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# :skip
|
|
92
|
-
false
|
|
93
|
-
end
|
|
146
|
+
return insert_nested(base[key], keys, value, overwrites, file, locale, conflict_policy)
|
|
147
|
+
|
|
94
148
|
end
|
|
95
149
|
end
|
|
96
150
|
end
|
data/lib/i18nize/loader.rb
CHANGED
data/lib/i18nize/missing_set.rb
CHANGED
data/lib/i18nize/translator.rb
CHANGED
|
@@ -4,14 +4,15 @@ require "uri"
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
module I18nize
|
|
7
|
+
# Translator for i18nize
|
|
7
8
|
class Translator
|
|
8
9
|
ENDPOINT = ENV.fetch("DEEPL_ENDPOINT", "https://api-free.deepl.com/v2/translate")
|
|
9
10
|
PLACEHOLDER_RE = /%{\s*[^}]+\s*}/
|
|
10
11
|
|
|
11
12
|
def initialize(auth_key:, from_lang: "EN", to_lang:)
|
|
12
|
-
@auth_key
|
|
13
|
+
@auth_key = auth_key
|
|
13
14
|
@from_lang = from_lang
|
|
14
|
-
@to_lang
|
|
15
|
+
@to_lang = to_lang
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def translate_texts(texts)
|
|
@@ -30,7 +31,7 @@ module I18nize
|
|
|
30
31
|
body_params = [
|
|
31
32
|
["auth_key", @auth_key],
|
|
32
33
|
["source_lang", @from_lang],
|
|
33
|
-
["target_lang", @to_lang]
|
|
34
|
+
["target_lang", @to_lang]
|
|
34
35
|
] + protected.map { |text| ["text", text] }
|
|
35
36
|
|
|
36
37
|
req = Net::HTTP::Post.new(uri)
|
|
@@ -43,7 +44,7 @@ module I18nize
|
|
|
43
44
|
json = JSON.parse(res.body)
|
|
44
45
|
out = json["translations"].map { |t| t["text"] }
|
|
45
46
|
|
|
46
|
-
out.map { |s| s.gsub(/__I18NIZE_PLH_(\d+)__/) { placeholders[
|
|
47
|
+
out.map { |s| s.gsub(/__I18NIZE_PLH_(\d+)__/) { placeholders[::Regexp.last_match(1).to_i] } }
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
end
|
data/lib/i18nize/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: i18nize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '0.
|
|
4
|
+
version: '0.6'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nikolas2145
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-09-
|
|
11
|
+
date: 2025-09-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description:
|
|
13
|
+
description: "i18nize helps Ruby and Rails projects stay multilingual by automatically
|
|
14
|
+
filling in \nmissing translations in I18n YAML locale files. It scans for missing
|
|
15
|
+
or empty keys \nacross nested locale structures, and uses the DeepL API (Free or
|
|
16
|
+
Pro) to translate \nthem from a chosen source locale (default: en).\n\nFeatures:\n-
|
|
17
|
+
Detects and lists missing translation keys\n- Automatically translates and inserts
|
|
18
|
+
values into the correct YAML files\n- Supports nested directories and multiple locale
|
|
19
|
+
files (e.g. config/locales/api/en.yml)\n- Preserves I18n placeholders such as %{count}\n-
|
|
20
|
+
Handles pluralization branches (one, other, etc.)\n- Source locale is treated as
|
|
21
|
+
the single source of truth (conflicts are resolved by overwrite)\n- Simple CLI:
|
|
22
|
+
`i18nize <locale>` or `i18nize <locale> --missing`\n\nThis gem streamlines the translation
|
|
23
|
+
workflow, making it easier to maintain \nconsistent, up-to-date locale files across
|
|
24
|
+
large Ruby on Rails applications.\n"
|
|
14
25
|
email:
|
|
15
26
|
- nikolas2145@gmail.com
|
|
16
27
|
executables:
|
|
@@ -18,11 +29,6 @@ executables:
|
|
|
18
29
|
extensions: []
|
|
19
30
|
extra_rdoc_files: []
|
|
20
31
|
files:
|
|
21
|
-
- ".idea/.gitignore"
|
|
22
|
-
- ".idea/git_toolbox_prj.xml"
|
|
23
|
-
- ".idea/i18nize.iml"
|
|
24
|
-
- ".idea/material_theme_project_new.xml"
|
|
25
|
-
- ".idea/vcs.xml"
|
|
26
32
|
- ".rspec"
|
|
27
33
|
- ".rubocop.yml"
|
|
28
34
|
- CHANGELOG.md
|
|
@@ -66,5 +72,5 @@ requirements: []
|
|
|
66
72
|
rubygems_version: 3.4.19
|
|
67
73
|
signing_key:
|
|
68
74
|
specification_version: 4
|
|
69
|
-
summary:
|
|
75
|
+
summary: Automatic DeepL-powered translations for I18n YAML files
|
|
70
76
|
test_files: []
|
data/.idea/.gitignore
DELETED
data/.idea/git_toolbox_prj.xml
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="GitToolBoxProjectSettings">
|
|
4
|
-
<option name="commitMessageIssueKeyValidationOverride">
|
|
5
|
-
<BoolValueOverride>
|
|
6
|
-
<option name="enabled" value="true" />
|
|
7
|
-
</BoolValueOverride>
|
|
8
|
-
</option>
|
|
9
|
-
<option name="commitMessageValidationEnabledOverride">
|
|
10
|
-
<BoolValueOverride>
|
|
11
|
-
<option name="enabled" value="true" />
|
|
12
|
-
</BoolValueOverride>
|
|
13
|
-
</option>
|
|
14
|
-
</component>
|
|
15
|
-
</project>
|
data/.idea/i18nize.iml
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<module version="4">
|
|
3
|
-
<component name="ModuleRunConfigurationManager">
|
|
4
|
-
<shared />
|
|
5
|
-
</component>
|
|
6
|
-
<component name="RakeTasksCache-v2">
|
|
7
|
-
<option name="myRootTask">
|
|
8
|
-
<RakeTaskImpl id="rake">
|
|
9
|
-
<subtasks>
|
|
10
|
-
<RakeTaskImpl description="Build i18nize-0.4.0.gem into the pkg directory" fullCommand="build" id="build" />
|
|
11
|
-
<RakeTaskImpl id="build">
|
|
12
|
-
<subtasks>
|
|
13
|
-
<RakeTaskImpl description="Generate SHA512 checksum of i18nize-0.4.0.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
|
|
14
|
-
</subtasks>
|
|
15
|
-
</RakeTaskImpl>
|
|
16
|
-
<RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
|
|
17
|
-
<RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
|
|
18
|
-
<RakeTaskImpl id="i18nize">
|
|
19
|
-
<subtasks>
|
|
20
|
-
<RakeTaskImpl description="Auto-translate missing I18n keys using DeepL (usage: rake 'i18nize:translate[cs]')" fullCommand="i18nize:translate[to_locale]" id="translate[to_locale]" />
|
|
21
|
-
<RakeTaskImpl description="" fullCommand="i18nize:translate" id="translate" />
|
|
22
|
-
</subtasks>
|
|
23
|
-
</RakeTaskImpl>
|
|
24
|
-
<RakeTaskImpl description="Build and install i18nize-0.4.0.gem into system gems" fullCommand="install" id="install" />
|
|
25
|
-
<RakeTaskImpl id="install">
|
|
26
|
-
<subtasks>
|
|
27
|
-
<RakeTaskImpl description="Build and install i18nize-0.4.0.gem into system gems without network access" fullCommand="install:local" id="local" />
|
|
28
|
-
</subtasks>
|
|
29
|
-
</RakeTaskImpl>
|
|
30
|
-
<RakeTaskImpl description="Create tag v0.4.0 and build and push i18nize-0.4.0.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
|
|
31
|
-
<RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
|
|
32
|
-
<RakeTaskImpl id="rubocop">
|
|
33
|
-
<subtasks>
|
|
34
|
-
<RakeTaskImpl description="Autocorrect RuboCop offenses (only when it's safe)" fullCommand="rubocop:autocorrect" id="autocorrect" />
|
|
35
|
-
<RakeTaskImpl description="Autocorrect RuboCop offenses (safe and unsafe)" fullCommand="rubocop:autocorrect_all" id="autocorrect_all" />
|
|
36
|
-
<RakeTaskImpl description="" fullCommand="rubocop:auto_correct" id="auto_correct" />
|
|
37
|
-
</subtasks>
|
|
38
|
-
</RakeTaskImpl>
|
|
39
|
-
<RakeTaskImpl description="Run RSpec code examples" fullCommand="spec" id="spec" />
|
|
40
|
-
<RakeTaskImpl description="" fullCommand="default" id="default" />
|
|
41
|
-
<RakeTaskImpl description="" fullCommand="release" id="release" />
|
|
42
|
-
<RakeTaskImpl id="release">
|
|
43
|
-
<subtasks>
|
|
44
|
-
<RakeTaskImpl description="" fullCommand="release:guard_clean" id="guard_clean" />
|
|
45
|
-
<RakeTaskImpl description="" fullCommand="release:rubygem_push" id="rubygem_push" />
|
|
46
|
-
<RakeTaskImpl description="" fullCommand="release:source_control_push" id="source_control_push" />
|
|
47
|
-
</subtasks>
|
|
48
|
-
</RakeTaskImpl>
|
|
49
|
-
</subtasks>
|
|
50
|
-
</RakeTaskImpl>
|
|
51
|
-
</option>
|
|
52
|
-
</component>
|
|
53
|
-
</module>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="MaterialThemeProjectNewConfig">
|
|
4
|
-
<option name="metadata">
|
|
5
|
-
<MTProjectMetadataState>
|
|
6
|
-
<option name="migrated" value="true" />
|
|
7
|
-
<option name="pristineConfig" value="false" />
|
|
8
|
-
<option name="userId" value="-151e60b4:18eb353ec10:-7ffc" />
|
|
9
|
-
</MTProjectMetadataState>
|
|
10
|
-
</option>
|
|
11
|
-
</component>
|
|
12
|
-
</project>
|