better_translate 1.0.0 → 1.0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +28 -1
- data/RELEASE_NOTES_v1.0.0.md +240 -0
- data/Steepfile +2 -2
- data/docs/implementation/00-overview.md +1 -1
- data/docs/implementation/04-provider_architecture.md +5 -5
- data/lib/better_translate/cache.rb +2 -1
- data/lib/better_translate/cli.rb +2 -2
- data/lib/better_translate/providers/anthropic_provider.rb +4 -3
- data/lib/better_translate/providers/chatgpt_provider.rb +2 -1
- data/lib/better_translate/providers/gemini_provider.rb +5 -4
- data/lib/better_translate/railtie.rb +2 -1
- data/lib/better_translate/rate_limiter.rb +4 -1
- data/lib/better_translate/strategies/batch_strategy.rb +1 -1
- data/lib/better_translate/strategies/deep_strategy.rb +1 -1
- data/lib/better_translate/translator.rb +5 -1
- data/lib/better_translate/utils/hash_flattener.rb +2 -2
- data/lib/better_translate/variable_extractor.rb +7 -7
- data/lib/better_translate/version.rb +1 -1
- data/lib/generators/better_translate/analyze/analyze_generator.rb +2 -1
- data/lib/generators/better_translate/install/install_generator.rb +4 -3
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +39 -7
- data/lib/generators/better_translate/translate/translate_generator.rb +2 -1
- data/regenerate_vcr.rb +47 -0
- data/sig/better_translate/configuration.rbs +7 -2
- data/sig/better_translate/providers/base_http_provider.rbs +1 -1
- data/sig/better_translate/variable_extractor.rbs +1 -1
- data/sig/better_translate.rbs +2 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b02561def13795980279169998c3a28170f0c2b1c4c7e4ffc58ae61b20b98ff5
|
|
4
|
+
data.tar.gz: d44be305f231f038918bb8dbf722db94328d9d9e71f1bcdd8bbe32aefceeb2f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98e5a8fb7c3103220b6b2102a2242a8bb4d103a8e3b2df24298e3fa4a3f8e75dae3b48ebbb578a59de84e7c547f3bc12cab71a77c789aa9ab347b7d424aea113
|
|
7
|
+
data.tar.gz: 6aeaafff6b3ff756c620e229d93582ab64a48dca322d21b32d5d38a408e5f6d085ef5ba280da141d0c65b02ca54c3b5559bda29e204aff81b0a6562fac0b860f
|
data/README.md
CHANGED
|
@@ -685,7 +685,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/alessi
|
|
|
685
685
|
5. **HTTP Client**: Use Faraday for all HTTP requests (never Net::HTTP or HTTParty)
|
|
686
686
|
6. **VCR Cassettes**: Record integration tests with real API responses for CI/CD
|
|
687
687
|
|
|
688
|
-
### Workflow
|
|
688
|
+
### Development Workflow
|
|
689
689
|
|
|
690
690
|
```bash
|
|
691
691
|
# 1. Clone and setup
|
|
@@ -714,6 +714,33 @@ git push origin my-feature
|
|
|
714
714
|
# 7. Create a Pull Request
|
|
715
715
|
```
|
|
716
716
|
|
|
717
|
+
### Release Workflow
|
|
718
|
+
|
|
719
|
+
Releases are automated via GitHub Actions:
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
# 1. Update version
|
|
723
|
+
vim lib/better_translate/version.rb # VERSION = "1.0.1"
|
|
724
|
+
|
|
725
|
+
# 2. Update CHANGELOG
|
|
726
|
+
vim CHANGELOG.md
|
|
727
|
+
|
|
728
|
+
# 3. Commit and tag
|
|
729
|
+
git add -A
|
|
730
|
+
git commit -m "chore: Release v1.0.1"
|
|
731
|
+
git tag v1.0.1
|
|
732
|
+
git push origin main
|
|
733
|
+
git push origin v1.0.1
|
|
734
|
+
|
|
735
|
+
# 4. GitHub Actions automatically:
|
|
736
|
+
# ✅ Runs tests
|
|
737
|
+
# ✅ Builds gem
|
|
738
|
+
# ✅ Publishes to RubyGems.org
|
|
739
|
+
# ✅ Creates GitHub Release
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Setup**: See [`.github/RUBYGEMS_SETUP.md`](.github/RUBYGEMS_SETUP.md) for configuring RubyGems trusted publishing (no API keys needed!).
|
|
743
|
+
|
|
717
744
|
## 📄 License
|
|
718
745
|
|
|
719
746
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# BetterTranslate v1.0.0 - Complete Release 🎉
|
|
2
|
+
|
|
3
|
+
**AI-powered YAML locale file translator for Rails and Ruby projects**
|
|
4
|
+
|
|
5
|
+
This is the first complete release of BetterTranslate, featuring multi-provider AI translation support, comprehensive testing infrastructure, and production-ready features.
|
|
6
|
+
|
|
7
|
+
## 🚀 Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install the gem
|
|
11
|
+
gem install better_translate
|
|
12
|
+
|
|
13
|
+
# Try the interactive demo
|
|
14
|
+
git clone https://github.com/alessiobussolari/better_translate.git
|
|
15
|
+
cd better_translate
|
|
16
|
+
bundle install
|
|
17
|
+
export OPENAI_API_KEY=your_key_here
|
|
18
|
+
ruby spec/dummy/demo_translation.rb
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## ✨ Key Features
|
|
22
|
+
|
|
23
|
+
### Multi-Provider AI Support
|
|
24
|
+
- **ChatGPT** (GPT-5-nano) - Fast, excellent quality
|
|
25
|
+
- **Google Gemini** (gemini-2.0-flash-exp) - Very fast, cost-effective
|
|
26
|
+
- **Anthropic Claude** (Claude 3.5) - Excellent quality (coming soon)
|
|
27
|
+
|
|
28
|
+
### Translation Features
|
|
29
|
+
- 🎯 **Smart Strategies**: Automatic selection between deep (<50 strings) and batch (≥50 strings) translation
|
|
30
|
+
- ⚡ **Intelligent Caching**: LRU cache with optional TTL reduces API costs
|
|
31
|
+
- 🔄 **Translation Modes**: Override (replace all) or Incremental (merge with existing)
|
|
32
|
+
- 🚫 **Flexible Exclusions**: Global + language-specific exclusion rules
|
|
33
|
+
- 🎨 **Domain Context**: Provide context for medical, legal, financial, or technical terminology
|
|
34
|
+
- 📊 **Similarity Analysis**: Built-in Levenshtein distance analyzer
|
|
35
|
+
|
|
36
|
+
### Production-Ready Quality
|
|
37
|
+
- 🧪 **259 Tests** passing with 90%+ coverage
|
|
38
|
+
- 🎬 **VCR Integration**: 18 cassettes (260KB) with real API responses
|
|
39
|
+
- 🏗️ **Rails Dummy App**: Interactive demo with real translations
|
|
40
|
+
- 📚 **Complete Documentation**: README, YARD docs, usage guides
|
|
41
|
+
- 🛡️ **Type-Safe**: RBS signatures with Steep type checking
|
|
42
|
+
- ✅ **RuboCop Compliant**: Clean, consistent code style
|
|
43
|
+
|
|
44
|
+
### Rails Integration
|
|
45
|
+
```bash
|
|
46
|
+
# Generate initializer
|
|
47
|
+
rails generate better_translate:install
|
|
48
|
+
|
|
49
|
+
# Configure in config/initializers/better_translate.rb
|
|
50
|
+
BetterTranslate.configure do |config|
|
|
51
|
+
config.provider = :chatgpt
|
|
52
|
+
config.openai_key = ENV["OPENAI_API_KEY"]
|
|
53
|
+
config.source_language = "en"
|
|
54
|
+
config.target_languages = [
|
|
55
|
+
{ short_name: "it", name: "Italian" },
|
|
56
|
+
{ short_name: "fr", name: "French" }
|
|
57
|
+
]
|
|
58
|
+
config.input_file = "config/locales/en.yml"
|
|
59
|
+
config.output_folder = "config/locales"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Translate all files
|
|
63
|
+
BetterTranslate.translate_all
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 📖 Documentation
|
|
67
|
+
|
|
68
|
+
- **[README.md](README.md)** - Complete feature documentation
|
|
69
|
+
- **[USAGE_GUIDE.md](spec/dummy/USAGE_GUIDE.md)** - Interactive demo guide
|
|
70
|
+
- **[VCR Testing Guide](spec/integration/README.md)** - Integration testing with VCR
|
|
71
|
+
- **[CLAUDE.md](CLAUDE.md)** - Developer guide for contributors
|
|
72
|
+
- **[CHANGELOG.md](CHANGELOG.md)** - Detailed changelog
|
|
73
|
+
|
|
74
|
+
## 🎬 Demo Output
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
# en.yml (input)
|
|
78
|
+
en:
|
|
79
|
+
hello: "Hello"
|
|
80
|
+
users:
|
|
81
|
+
greeting: "Hello %{name}"
|
|
82
|
+
messages:
|
|
83
|
+
success: "Operation completed successfully"
|
|
84
|
+
|
|
85
|
+
# it.yml (generated by BetterTranslate)
|
|
86
|
+
it:
|
|
87
|
+
hello: "Ciao"
|
|
88
|
+
users:
|
|
89
|
+
greeting: "Ciao %{name}" # ✅ Variables preserved!
|
|
90
|
+
messages:
|
|
91
|
+
success: "Operazione completata con successo"
|
|
92
|
+
|
|
93
|
+
# fr.yml (generated by BetterTranslate)
|
|
94
|
+
fr:
|
|
95
|
+
hello: "Bonjour"
|
|
96
|
+
users:
|
|
97
|
+
greeting: "Bonjour %{name}" # ✅ Variables preserved!
|
|
98
|
+
messages:
|
|
99
|
+
success: "Opération réussie"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 🏗️ Architecture Highlights
|
|
103
|
+
|
|
104
|
+
### Provider Architecture
|
|
105
|
+
```
|
|
106
|
+
BaseHttpProvider (abstract)
|
|
107
|
+
├── ChatGPTProvider (GPT-5-nano, temp=1.0)
|
|
108
|
+
├── GeminiProvider (gemini-2.0-flash-exp)
|
|
109
|
+
└── AnthropicProvider (coming soon)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**BaseHttpProvider features:**
|
|
113
|
+
- Faraday-based HTTP communication
|
|
114
|
+
- Retry logic with exponential backoff (3 attempts)
|
|
115
|
+
- Rate limiting (0.5s between requests, thread-safe)
|
|
116
|
+
- Configurable timeouts (default: 30s)
|
|
117
|
+
|
|
118
|
+
### Core Components
|
|
119
|
+
- **Configuration**: Type-safe config with validation
|
|
120
|
+
- **Cache**: Thread-safe LRU cache with optional TTL
|
|
121
|
+
- **RateLimiter**: Thread-safe request throttling
|
|
122
|
+
- **Validator**: Comprehensive input validation
|
|
123
|
+
- **HashFlattener**: Nested YAML ↔ flat structure conversion
|
|
124
|
+
- **VariableExtractor**: Preserves `%{name}` and `%<name>s` placeholders
|
|
125
|
+
- **ProgressTracker**: Real-time translation progress
|
|
126
|
+
|
|
127
|
+
### Error Hierarchy
|
|
128
|
+
```
|
|
129
|
+
BetterTranslate::Error (base)
|
|
130
|
+
├── ConfigurationError
|
|
131
|
+
├── ValidationError
|
|
132
|
+
├── TranslationError
|
|
133
|
+
├── ProviderError
|
|
134
|
+
├── ApiError
|
|
135
|
+
├── RateLimitError
|
|
136
|
+
├── FileError
|
|
137
|
+
├── YamlError
|
|
138
|
+
└── ProviderNotFoundError
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 🧪 Testing Infrastructure
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
spec/
|
|
145
|
+
├── better_translate/ # 259 unit tests
|
|
146
|
+
│ ├── cache_spec.rb
|
|
147
|
+
│ ├── providers/
|
|
148
|
+
│ └── ...
|
|
149
|
+
├── integration/ # VCR integration tests
|
|
150
|
+
│ ├── chatgpt_integration_spec.rb
|
|
151
|
+
│ ├── gemini_integration_spec.rb
|
|
152
|
+
│ └── rails_dummy_app_spec.rb
|
|
153
|
+
├── dummy/ # Rails dummy app
|
|
154
|
+
│ ├── demo_translation.rb # 🚀 Interactive demo
|
|
155
|
+
│ └── config/locales/
|
|
156
|
+
│ ├── en.yml # Source (16 keys)
|
|
157
|
+
│ ├── it.yml # Generated (519 bytes)
|
|
158
|
+
│ └── fr.yml # Generated (511 bytes)
|
|
159
|
+
└── vcr_cassettes/ # 18 cassettes (260KB)
|
|
160
|
+
├── chatgpt/ (7)
|
|
161
|
+
├── gemini/ (7)
|
|
162
|
+
└── rails/ (4)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 📦 Package Details
|
|
166
|
+
|
|
167
|
+
- **Gem Name**: `better_translate`
|
|
168
|
+
- **Version**: `1.0.0`
|
|
169
|
+
- **Size**: 83KB
|
|
170
|
+
- **Files**: 152 files
|
|
171
|
+
- **Lines of Code**: 23,654
|
|
172
|
+
- **Ruby Version**: >= 3.0.0
|
|
173
|
+
- **Dependencies**: Faraday ~> 2.0 (only runtime dependency)
|
|
174
|
+
|
|
175
|
+
## 🔧 Configuration Examples
|
|
176
|
+
|
|
177
|
+
### Medical Terminology
|
|
178
|
+
```ruby
|
|
179
|
+
config.translation_context = "Medical terminology for healthcare applications"
|
|
180
|
+
config.target_languages = [{ short_name: "es", name: "Spanish" }]
|
|
181
|
+
# "patient" → "paciente", "diagnosis" → "diagnóstico"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### E-commerce with Exclusions
|
|
185
|
+
```ruby
|
|
186
|
+
config.global_exclusions = ["brand_name", "sku"] # Never translate
|
|
187
|
+
config.exclusions_per_language = {
|
|
188
|
+
"de" => ["legal.terms"], # German legal terms manually translated
|
|
189
|
+
"fr" => ["marketing.slogan"] # French slogan crafted by marketing
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Incremental Mode
|
|
194
|
+
```ruby
|
|
195
|
+
config.translation_mode = :incremental
|
|
196
|
+
# Only translates missing keys, preserves manual corrections
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 🚦 Performance
|
|
200
|
+
|
|
201
|
+
**Demo Results** (16 keys, 2 languages):
|
|
202
|
+
- Italian: 16 strings in 1m 6s (~4s per string)
|
|
203
|
+
- French: 16 strings in 1m 7s (~4s per string)
|
|
204
|
+
- **Total**: 32 translations in 2m 13s
|
|
205
|
+
|
|
206
|
+
**With Caching Enabled**:
|
|
207
|
+
- Subsequent runs: < 1s (cache hits)
|
|
208
|
+
- API costs reduced by ~90%
|
|
209
|
+
|
|
210
|
+
## 🤝 Contributing
|
|
211
|
+
|
|
212
|
+
Contributions are welcome! Please see [CLAUDE.md](CLAUDE.md) for development guidelines.
|
|
213
|
+
|
|
214
|
+
**Development Requirements**:
|
|
215
|
+
1. TDD (Test-Driven Development) - Write tests first
|
|
216
|
+
2. YARD Documentation - Document all public methods
|
|
217
|
+
3. RuboCop Compliance - Clean code style
|
|
218
|
+
4. VCR Cassettes - Record integration tests
|
|
219
|
+
|
|
220
|
+
## 📜 License
|
|
221
|
+
|
|
222
|
+
MIT License - See [LICENSE.txt](LICENSE.txt)
|
|
223
|
+
|
|
224
|
+
## 🙏 Acknowledgments
|
|
225
|
+
|
|
226
|
+
Built with:
|
|
227
|
+
- **Faraday** - HTTP client
|
|
228
|
+
- **VCR** - HTTP interaction recording
|
|
229
|
+
- **RSpec** - Testing framework
|
|
230
|
+
- **RuboCop** - Code linting
|
|
231
|
+
- **Steep** - Type checking
|
|
232
|
+
- **YARD** - Documentation
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
**Made with ❤️ by [Alessio Bussolari](https://github.com/alessiobussolari)**
|
|
237
|
+
|
|
238
|
+
**Powered by Claude Code** 🤖
|
|
239
|
+
|
|
240
|
+
[Report Bug](https://github.com/alessiobussolari/better_translate/issues) · [Request Feature](https://github.com/alessiobussolari/better_translate/issues) · [Documentation](https://github.com/alessiobussolari/better_translate)
|
data/Steepfile
CHANGED
|
@@ -14,7 +14,7 @@ BetterTranslate is a powerful Ruby gem designed to automatically translate YAML
|
|
|
14
14
|
|
|
15
15
|
### Key Features
|
|
16
16
|
|
|
17
|
-
- **Multiple AI Providers**: ChatGPT (GPT-5-nano), Google Gemini (gemini-2.
|
|
17
|
+
- **Multiple AI Providers**: ChatGPT (GPT-5-nano), Google Gemini (gemini-2.5-flash-lite), Anthropic Claude (claude-haiku-4-5)
|
|
18
18
|
- **Smart Translation Strategies**: Automatic selection between Deep (< 50 strings) and Batch (≥ 50 strings) processing
|
|
19
19
|
- **Intelligent Caching**: LRU cache with configurable capacity and TTL
|
|
20
20
|
- **Rails Integration**: 3 generators (install, translate, analyze)
|
|
@@ -331,10 +331,10 @@ module BetterTranslate
|
|
|
331
331
|
module Providers
|
|
332
332
|
# Google Gemini translation provider
|
|
333
333
|
#
|
|
334
|
-
# Uses gemini-2.
|
|
334
|
+
# Uses gemini-2.5-flash-lite model
|
|
335
335
|
class GeminiProvider < BaseHttpProvider
|
|
336
|
-
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.
|
|
337
|
-
MODEL = "gemini-2.
|
|
336
|
+
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent"
|
|
337
|
+
MODEL = "gemini-2.5-flash-lite"
|
|
338
338
|
|
|
339
339
|
# Translate a single text
|
|
340
340
|
#
|
|
@@ -431,10 +431,10 @@ module BetterTranslate
|
|
|
431
431
|
module Providers
|
|
432
432
|
# Anthropic Claude translation provider
|
|
433
433
|
#
|
|
434
|
-
# Uses claude-
|
|
434
|
+
# Uses claude-haiku-4-5 model
|
|
435
435
|
class AnthropicProvider < BaseHttpProvider
|
|
436
436
|
API_URL = "https://api.anthropic.com/v1/messages"
|
|
437
|
-
MODEL = "claude-
|
|
437
|
+
MODEL = "claude-haiku-4-5"
|
|
438
438
|
API_VERSION = "2023-06-01"
|
|
439
439
|
|
|
440
440
|
# Translate a single text
|
data/lib/better_translate/cli.rb
CHANGED
|
@@ -221,8 +221,8 @@ module BetterTranslate
|
|
|
221
221
|
"dry_run" => false,
|
|
222
222
|
"translation_mode" => "override",
|
|
223
223
|
"preserve_variables" => true,
|
|
224
|
-
"global_exclusions" =>
|
|
225
|
-
"exclusions_per_language" =>
|
|
224
|
+
"global_exclusions" => [],
|
|
225
|
+
"exclusions_per_language" => {},
|
|
226
226
|
"model" => nil,
|
|
227
227
|
"temperature" => 0.3,
|
|
228
228
|
"max_tokens" => 2000,
|
|
@@ -4,7 +4,7 @@ module BetterTranslate
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Anthropic Claude translation provider
|
|
6
6
|
#
|
|
7
|
-
# Uses claude-
|
|
7
|
+
# Uses claude-haiku-4-5 model for fast, efficient translations.
|
|
8
8
|
#
|
|
9
9
|
# @example Basic usage
|
|
10
10
|
# config = Configuration.new
|
|
@@ -18,7 +18,7 @@ module BetterTranslate
|
|
|
18
18
|
API_URL = "https://api.anthropic.com/v1/messages"
|
|
19
19
|
|
|
20
20
|
# Model to use for translations
|
|
21
|
-
MODEL = "claude-
|
|
21
|
+
MODEL = "claude-haiku-4-5"
|
|
22
22
|
|
|
23
23
|
# API version
|
|
24
24
|
API_VERSION = "2023-06-01"
|
|
@@ -97,7 +97,8 @@ module BetterTranslate
|
|
|
97
97
|
#
|
|
98
98
|
def build_system_message(target_lang_name)
|
|
99
99
|
base_message = "You are a professional translator. Translate the following text to #{target_lang_name}. " \
|
|
100
|
-
"Return ONLY the translated text, without any explanations or additional text."
|
|
100
|
+
"Return ONLY the translated text, without any explanations or additional text. " \
|
|
101
|
+
"Words like VARIABLE_0, VARIABLE_1, etc. are placeholders and must be kept unchanged in the translation."
|
|
101
102
|
|
|
102
103
|
if config.translation_context && !config.translation_context.empty?
|
|
103
104
|
base_message += "\n\nContext: #{config.translation_context}"
|
|
@@ -95,7 +95,8 @@ module BetterTranslate
|
|
|
95
95
|
#
|
|
96
96
|
def build_system_message(target_lang_name)
|
|
97
97
|
base_message = "You are a professional translator. Translate the following text to #{target_lang_name}. " \
|
|
98
|
-
"Return ONLY the translated text, without any explanations or additional text."
|
|
98
|
+
"Return ONLY the translated text, without any explanations or additional text. " \
|
|
99
|
+
"Words like VARIABLE_0, VARIABLE_1, etc. are placeholders and must be kept unchanged in the translation."
|
|
99
100
|
|
|
100
101
|
if config.translation_context && !config.translation_context.empty?
|
|
101
102
|
base_message += "\n\nContext: #{config.translation_context}"
|
|
@@ -4,7 +4,7 @@ module BetterTranslate
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Google Gemini translation provider
|
|
6
6
|
#
|
|
7
|
-
# Uses gemini-2.
|
|
7
|
+
# Uses gemini-2.5-flash-lite model for fast, high-quality translations.
|
|
8
8
|
#
|
|
9
9
|
# @example Basic usage
|
|
10
10
|
# config = Configuration.new
|
|
@@ -15,10 +15,10 @@ module BetterTranslate
|
|
|
15
15
|
#
|
|
16
16
|
class GeminiProvider < BaseHttpProvider
|
|
17
17
|
# Google Gemini API endpoint
|
|
18
|
-
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.
|
|
18
|
+
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent"
|
|
19
19
|
|
|
20
20
|
# Model to use for translations
|
|
21
|
-
MODEL = "gemini-2.
|
|
21
|
+
MODEL = "gemini-2.5-flash-lite"
|
|
22
22
|
|
|
23
23
|
# Translate a single text
|
|
24
24
|
#
|
|
@@ -77,7 +77,8 @@ module BetterTranslate
|
|
|
77
77
|
#
|
|
78
78
|
def build_prompt(text, target_lang_name)
|
|
79
79
|
base_prompt = "Translate the following text to #{target_lang_name}. " \
|
|
80
|
-
"Return ONLY the translated text, without any explanations
|
|
80
|
+
"Return ONLY the translated text, without any explanations. " \
|
|
81
|
+
"Words like VARIABLE_0, VARIABLE_1, etc. are placeholders and must be kept unchanged in the translation.\n\n" \
|
|
81
82
|
"Text: #{text}"
|
|
82
83
|
|
|
83
84
|
if config.translation_context && !config.translation_context.empty?
|
|
@@ -11,7 +11,8 @@ module BetterTranslate
|
|
|
11
11
|
#
|
|
12
12
|
class Railtie < Rails::Railtie
|
|
13
13
|
rake_tasks do
|
|
14
|
-
|
|
14
|
+
dir = __dir__
|
|
15
|
+
rake_file = File.expand_path("../tasks/better_translate.rake", dir) if dir
|
|
15
16
|
load rake_file if rake_file
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -52,7 +52,10 @@ module BetterTranslate
|
|
|
52
52
|
@mutex.synchronize do
|
|
53
53
|
return if @last_request_time.nil?
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
last_time = @last_request_time
|
|
56
|
+
return unless last_time
|
|
57
|
+
|
|
58
|
+
elapsed = Time.now - last_time
|
|
56
59
|
sleep_time = @delay - elapsed.to_f
|
|
57
60
|
|
|
58
61
|
sleep(sleep_time) if sleep_time.positive?
|
|
@@ -37,7 +37,7 @@ module BetterTranslate
|
|
|
37
37
|
progress_tracker.update(
|
|
38
38
|
language: target_lang_name,
|
|
39
39
|
current_key: "Batch #{batch_index + 1}/#{total_batches}",
|
|
40
|
-
progress: ((batch_index + 1).to_f / total_batches * 100.0).round(1)
|
|
40
|
+
progress: ((batch_index + 1).to_f / total_batches * 100.0).round(1).to_f
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
translated_batch = provider.translate_batch(batch, target_lang_code, target_lang_name)
|
|
@@ -32,7 +32,7 @@ module BetterTranslate
|
|
|
32
32
|
progress_tracker.update(
|
|
33
33
|
language: target_lang_name,
|
|
34
34
|
current_key: key,
|
|
35
|
-
progress: ((index + 1).to_f / total * 100.0).round(1)
|
|
35
|
+
progress: ((index + 1).to_f / total * 100.0).round(1).to_f
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
translated[key] = provider.translate_text(value, target_lang_code, target_lang_name)
|
|
@@ -62,7 +62,11 @@ module BetterTranslate
|
|
|
62
62
|
rescue StandardError => e
|
|
63
63
|
results[:failure_count] += 1
|
|
64
64
|
# @type var error_context: Hash[Symbol, untyped]
|
|
65
|
-
error_context = e.
|
|
65
|
+
error_context = if e.is_a?(BetterTranslate::Error)
|
|
66
|
+
e.context
|
|
67
|
+
else
|
|
68
|
+
{}
|
|
69
|
+
end
|
|
66
70
|
results[:errors] << {
|
|
67
71
|
language: lang[:name],
|
|
68
72
|
error: e.message,
|
|
@@ -47,7 +47,7 @@ module BetterTranslate
|
|
|
47
47
|
# #=> { "config/database/host" => "localhost" }
|
|
48
48
|
#
|
|
49
49
|
def self.flatten(hash, parent_key = "", separator = ".")
|
|
50
|
-
initial_hash = {}
|
|
50
|
+
initial_hash = {} # : Hash[String, untyped]
|
|
51
51
|
hash.each_with_object(initial_hash) do |(key, value), result|
|
|
52
52
|
new_key = parent_key.empty? ? key.to_s : "#{parent_key}#{separator}#{key}"
|
|
53
53
|
|
|
@@ -85,7 +85,7 @@ module BetterTranslate
|
|
|
85
85
|
# #=> { "config" => { "database" => { "host" => "localhost" } } }
|
|
86
86
|
#
|
|
87
87
|
def self.unflatten(hash, separator = ".")
|
|
88
|
-
initial_hash = {}
|
|
88
|
+
initial_hash = {} # : Hash[String, untyped]
|
|
89
89
|
hash.each_with_object(initial_hash) do |(key, value), result|
|
|
90
90
|
keys = key.split(separator)
|
|
91
91
|
last_key = keys.pop
|
|
@@ -15,9 +15,9 @@ module BetterTranslate
|
|
|
15
15
|
# @example Basic usage
|
|
16
16
|
# extractor = VariableExtractor.new("Hello %{name}, you have {{count}} messages")
|
|
17
17
|
# safe_text = extractor.extract
|
|
18
|
-
# #=> "Hello
|
|
18
|
+
# #=> "Hello VARIABLE_0, you have VARIABLE_1 messages"
|
|
19
19
|
#
|
|
20
|
-
# translated = translate(safe_text) # "Ciao
|
|
20
|
+
# translated = translate(safe_text) # "Ciao VARIABLE_0, hai VARIABLE_1 messaggi"
|
|
21
21
|
# final = extractor.restore(translated)
|
|
22
22
|
# #=> "Ciao %{name}, hai {{count}} messaggi"
|
|
23
23
|
#
|
|
@@ -41,10 +41,10 @@ module BetterTranslate
|
|
|
41
41
|
COMBINED_PATTERN = Regexp.union(*VARIABLE_PATTERNS.values).freeze
|
|
42
42
|
|
|
43
43
|
# Placeholder prefix
|
|
44
|
-
PLACEHOLDER_PREFIX = "
|
|
44
|
+
PLACEHOLDER_PREFIX = "VARIABLE_"
|
|
45
45
|
|
|
46
46
|
# Placeholder suffix
|
|
47
|
-
PLACEHOLDER_SUFFIX = "
|
|
47
|
+
PLACEHOLDER_SUFFIX = ""
|
|
48
48
|
|
|
49
49
|
# @return [String] Original text with variables
|
|
50
50
|
attr_reader :original_text
|
|
@@ -72,13 +72,13 @@ module BetterTranslate
|
|
|
72
72
|
# Extract variables and replace with placeholders
|
|
73
73
|
#
|
|
74
74
|
# Scans the text for all supported variable formats and replaces them
|
|
75
|
-
# with numbered placeholders (
|
|
75
|
+
# with numbered placeholders (VARIABLE_0, VARIABLE_1, etc.).
|
|
76
76
|
#
|
|
77
77
|
# @return [String] Text with variables replaced by placeholders
|
|
78
78
|
#
|
|
79
79
|
# @example
|
|
80
80
|
# extractor = VariableExtractor.new("Hello %{name}")
|
|
81
|
-
# extractor.extract #=> "Hello
|
|
81
|
+
# extractor.extract #=> "Hello VARIABLE_0"
|
|
82
82
|
#
|
|
83
83
|
def extract
|
|
84
84
|
return "" if original_text.nil? || original_text.empty?
|
|
@@ -112,7 +112,7 @@ module BetterTranslate
|
|
|
112
112
|
# @example Successful restore
|
|
113
113
|
# extractor = VariableExtractor.new("Hello %{name}")
|
|
114
114
|
# extractor.extract
|
|
115
|
-
# extractor.restore("Ciao
|
|
115
|
+
# extractor.restore("Ciao VARIABLE_0") #=> "Ciao %{name}"
|
|
116
116
|
#
|
|
117
117
|
# @example Strict mode with missing variable
|
|
118
118
|
# extractor = VariableExtractor.new("Hello %{name}")
|
|
@@ -12,7 +12,8 @@ module BetterTranslate
|
|
|
12
12
|
# rails generate better_translate:analyze config/locales/en.yml
|
|
13
13
|
#
|
|
14
14
|
class AnalyzeGenerator < Rails::Generators::Base
|
|
15
|
-
|
|
15
|
+
dir = __dir__
|
|
16
|
+
source_root File.expand_path("templates", dir) if dir
|
|
16
17
|
|
|
17
18
|
desc "Analyze YAML locale file structure and statistics"
|
|
18
19
|
|
|
@@ -12,7 +12,8 @@ module BetterTranslate
|
|
|
12
12
|
# rails generate better_translate:install
|
|
13
13
|
#
|
|
14
14
|
class InstallGenerator < Rails::Generators::Base
|
|
15
|
-
|
|
15
|
+
dir = __dir__
|
|
16
|
+
source_root File.expand_path("templates", dir) if dir
|
|
16
17
|
|
|
17
18
|
desc "Creates BetterTranslate initializer and config files"
|
|
18
19
|
|
|
@@ -46,8 +47,8 @@ module BetterTranslate
|
|
|
46
47
|
"dry_run" => false,
|
|
47
48
|
"translation_mode" => "override",
|
|
48
49
|
"preserve_variables" => true,
|
|
49
|
-
"global_exclusions" =>
|
|
50
|
-
"exclusions_per_language" =>
|
|
50
|
+
"global_exclusions" => [],
|
|
51
|
+
"exclusions_per_language" => {},
|
|
51
52
|
"model" => nil,
|
|
52
53
|
"temperature" => 0.3,
|
|
53
54
|
"max_tokens" => 2000,
|
|
@@ -14,15 +14,47 @@ BetterTranslate.configure do |config|
|
|
|
14
14
|
config.anthropic_key = ENV["ANTHROPIC_API_KEY"]
|
|
15
15
|
|
|
16
16
|
# Source and target languages
|
|
17
|
-
|
|
18
|
-
config.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
# Automatically uses Rails I18n configuration
|
|
18
|
+
# To configure I18n in your Rails app, set in config/application.rb:
|
|
19
|
+
# config.i18n.default_locale = :it
|
|
20
|
+
# config.i18n.available_locales = [:it, :en, :es, :fr]
|
|
21
|
+
config.source_language = I18n.default_locale.to_s
|
|
22
|
+
|
|
23
|
+
# Target languages: automatically derived from I18n.available_locales
|
|
24
|
+
# Excludes the source language from targets
|
|
25
|
+
available_targets = (I18n.available_locales - [I18n.default_locale]).map(&:to_s)
|
|
26
|
+
|
|
27
|
+
# Language name mapping for common languages
|
|
28
|
+
language_names = {
|
|
29
|
+
"en" => "English", "it" => "Italian", "es" => "Spanish", "fr" => "French",
|
|
30
|
+
"de" => "German", "pt" => "Portuguese", "ru" => "Russian", "zh" => "Chinese",
|
|
31
|
+
"ja" => "Japanese", "ko" => "Korean", "ar" => "Arabic", "nl" => "Dutch",
|
|
32
|
+
"pl" => "Polish", "tr" => "Turkish", "sv" => "Swedish", "da" => "Danish",
|
|
33
|
+
"fi" => "Finnish", "no" => "Norwegian", "cs" => "Czech", "el" => "Greek",
|
|
34
|
+
"he" => "Hebrew", "hi" => "Hindi", "th" => "Thai", "vi" => "Vietnamese"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if available_targets.any?
|
|
38
|
+
# Use I18n available locales
|
|
39
|
+
config.target_languages = available_targets.map do |locale|
|
|
40
|
+
{
|
|
41
|
+
short_name: locale,
|
|
42
|
+
name: language_names[locale] || locale.capitalize
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
# Fallback: suggest common languages
|
|
47
|
+
# Uncomment and modify the languages you want to translate to
|
|
48
|
+
config.target_languages = [
|
|
49
|
+
{ short_name: "it", name: "Italian" },
|
|
50
|
+
{ short_name: "es", name: "Spanish" },
|
|
51
|
+
{ short_name: "fr", name: "French" }
|
|
52
|
+
]
|
|
53
|
+
end
|
|
23
54
|
|
|
24
55
|
# File paths
|
|
25
|
-
|
|
56
|
+
# Uses source_language for input file
|
|
57
|
+
config.input_file = Rails.root.join("config", "locales", "#{config.source_language}.yml").to_s
|
|
26
58
|
config.output_folder = Rails.root.join("config", "locales").to_s
|
|
27
59
|
|
|
28
60
|
# Options
|
|
@@ -15,7 +15,8 @@ module BetterTranslate
|
|
|
15
15
|
# rails generate better_translate:translate --dry-run
|
|
16
16
|
#
|
|
17
17
|
class TranslateGenerator < Rails::Generators::Base
|
|
18
|
-
|
|
18
|
+
dir = __dir__
|
|
19
|
+
source_root File.expand_path("templates", dir) if dir
|
|
19
20
|
|
|
20
21
|
desc "Run BetterTranslate translation task"
|
|
21
22
|
|
data/regenerate_vcr.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "dotenv/load"
|
|
6
|
+
require "vcr"
|
|
7
|
+
require "webmock/rspec"
|
|
8
|
+
require_relative "lib/better_translate"
|
|
9
|
+
require "tmpdir"
|
|
10
|
+
|
|
11
|
+
# Setup VCR
|
|
12
|
+
VCR.configure do |config|
|
|
13
|
+
config.cassette_library_dir = "spec/vcr_cassettes"
|
|
14
|
+
config.hook_into :webmock
|
|
15
|
+
config.filter_sensitive_data("<GEMINI_API_KEY>") { ENV["GEMINI_API_KEY"] }
|
|
16
|
+
config.default_cassette_options = { record: :all }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
test_dir = Dir.mktmpdir("gemini_test")
|
|
20
|
+
puts "Test output dir: #{test_dir}"
|
|
21
|
+
|
|
22
|
+
VCR.use_cassette("rails/dummy_app_gemini_translation", record: :all) do
|
|
23
|
+
config = BetterTranslate::Configuration.new
|
|
24
|
+
config.provider = :gemini
|
|
25
|
+
config.gemini_key = ENV["GEMINI_API_KEY"]
|
|
26
|
+
config.source_language = "en"
|
|
27
|
+
config.target_languages = [{ short_name: "fr", name: "French" }]
|
|
28
|
+
config.input_file = "spec/dummy/config/locales/en.yml"
|
|
29
|
+
config.output_folder = test_dir
|
|
30
|
+
config.cache_enabled = false
|
|
31
|
+
config.verbose = true
|
|
32
|
+
config.validate!
|
|
33
|
+
|
|
34
|
+
puts "Starting translation..."
|
|
35
|
+
translator = BetterTranslate::Translator.new(config)
|
|
36
|
+
results = translator.translate_all
|
|
37
|
+
|
|
38
|
+
puts "\nResults: #{results.inspect}"
|
|
39
|
+
puts "\nFiles created:"
|
|
40
|
+
Dir.entries(test_dir).each { |f| puts " - #{f}" unless f.start_with?(".") }
|
|
41
|
+
|
|
42
|
+
if results[:success_count].positive?
|
|
43
|
+
puts "\n✓ Successfully regenerated VCR cassette!"
|
|
44
|
+
else
|
|
45
|
+
puts "\n✗ Translation failed"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -31,8 +31,13 @@ module BetterTranslate
|
|
|
31
31
|
attr_accessor provider: (Symbol | nil)
|
|
32
32
|
attr_accessor openai_key: String?
|
|
33
33
|
attr_accessor google_gemini_key: String?
|
|
34
|
-
attr_accessor
|
|
35
|
-
|
|
34
|
+
attr_accessor claude_key: String?
|
|
35
|
+
|
|
36
|
+
# Aliases (these are methods created by alias in configuration.rb)
|
|
37
|
+
alias gemini_key google_gemini_key
|
|
38
|
+
alias gemini_key= google_gemini_key=
|
|
39
|
+
alias anthropic_key claude_key
|
|
40
|
+
alias anthropic_key= claude_key=
|
|
36
41
|
attr_accessor source_language: String?
|
|
37
42
|
attr_accessor target_languages: Array[target_language]
|
|
38
43
|
attr_accessor input_file: String?
|
|
@@ -36,7 +36,7 @@ module BetterTranslate
|
|
|
36
36
|
|
|
37
37
|
def http_client: () -> Faraday::Connection
|
|
38
38
|
|
|
39
|
-
def with_cache:
|
|
39
|
+
def with_cache: (String cache_key) { () -> String } -> String
|
|
40
40
|
|
|
41
41
|
def build_cache_key: (String text, String target_lang_code) -> String
|
|
42
42
|
end
|
|
@@ -33,7 +33,7 @@ module BetterTranslate
|
|
|
33
33
|
|
|
34
34
|
def validate_variables!: (String text) -> true
|
|
35
35
|
|
|
36
|
-
def self.find_variables: (String? text) -> Array[String]
|
|
36
|
+
def self.find_variables: (String? text) -> Array[String | Array[String?]]
|
|
37
37
|
|
|
38
38
|
def self.contains_variables?: (String? text) -> bool
|
|
39
39
|
end
|
data/sig/better_translate.rbs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
module BetterTranslate
|
|
6
6
|
type translation_results = { success_count: Integer, failure_count: Integer, errors: Array[Hash[Symbol, untyped]] }
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Module-level instance variable (for singleton methods)
|
|
9
|
+
self.@configuration: Configuration?
|
|
9
10
|
|
|
10
11
|
# Configure BetterTranslate
|
|
11
12
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_translate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0
|
|
4
|
+
version: 1.0.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- alessiobussolari
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- CODE_OF_CONDUCT.md
|
|
43
43
|
- LICENSE.txt
|
|
44
44
|
- README.md
|
|
45
|
+
- RELEASE_NOTES_v1.0.0.md
|
|
45
46
|
- Rakefile
|
|
46
47
|
- Steepfile
|
|
47
48
|
- docs/implementation/00-overview.md
|
|
@@ -92,6 +93,7 @@ files:
|
|
|
92
93
|
- lib/generators/better_translate/translate/USAGE
|
|
93
94
|
- lib/generators/better_translate/translate/translate_generator.rb
|
|
94
95
|
- lib/tasks/better_translate.rake
|
|
96
|
+
- regenerate_vcr.rb
|
|
95
97
|
- sig/better_translate.rbs
|
|
96
98
|
- sig/better_translate/cache.rbs
|
|
97
99
|
- sig/better_translate/cli.rbs
|