better_translate 0.5.0 → 1.0.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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +8 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +125 -114
  7. data/CLAUDE.md +385 -0
  8. data/README.md +629 -244
  9. data/Rakefile +7 -1
  10. data/Steepfile +29 -0
  11. data/docs/implementation/00-overview.md +220 -0
  12. data/docs/implementation/01-setup_dependencies.md +668 -0
  13. data/docs/implementation/02-error_handling.md +65 -0
  14. data/docs/implementation/03-core_components.md +457 -0
  15. data/docs/implementation/03.5-variable_preservation.md +509 -0
  16. data/docs/implementation/04-provider_architecture.md +571 -0
  17. data/docs/implementation/05-translation_logic.md +1065 -0
  18. data/docs/implementation/06-main_module_api.md +122 -0
  19. data/docs/implementation/07-direct_translation_helpers.md +582 -0
  20. data/docs/implementation/08-rails_integration.md +323 -0
  21. data/docs/implementation/09-testing_suite.md +228 -0
  22. data/docs/implementation/10-documentation_examples.md +150 -0
  23. data/docs/implementation/11-quality_security.md +65 -0
  24. data/docs/implementation/12-cli_standalone.md +698 -0
  25. data/exe/better_translate +9 -0
  26. data/lib/better_translate/cache.rb +125 -0
  27. data/lib/better_translate/cli.rb +304 -0
  28. data/lib/better_translate/configuration.rb +201 -0
  29. data/lib/better_translate/direct_translator.rb +131 -0
  30. data/lib/better_translate/errors.rb +101 -0
  31. data/lib/better_translate/progress_tracker.rb +157 -0
  32. data/lib/better_translate/provider_factory.rb +45 -0
  33. data/lib/better_translate/providers/anthropic_provider.rb +154 -0
  34. data/lib/better_translate/providers/base_http_provider.rb +239 -0
  35. data/lib/better_translate/providers/chatgpt_provider.rb +138 -44
  36. data/lib/better_translate/providers/gemini_provider.rb +123 -61
  37. data/lib/better_translate/railtie.rb +18 -0
  38. data/lib/better_translate/rate_limiter.rb +90 -0
  39. data/lib/better_translate/strategies/base_strategy.rb +58 -0
  40. data/lib/better_translate/strategies/batch_strategy.rb +56 -0
  41. data/lib/better_translate/strategies/deep_strategy.rb +45 -0
  42. data/lib/better_translate/strategies/strategy_selector.rb +43 -0
  43. data/lib/better_translate/translator.rb +115 -284
  44. data/lib/better_translate/utils/hash_flattener.rb +104 -0
  45. data/lib/better_translate/validator.rb +105 -0
  46. data/lib/better_translate/variable_extractor.rb +259 -0
  47. data/lib/better_translate/version.rb +2 -9
  48. data/lib/better_translate/yaml_handler.rb +168 -0
  49. data/lib/better_translate.rb +97 -73
  50. data/lib/generators/better_translate/analyze/USAGE +12 -0
  51. data/lib/generators/better_translate/analyze/analyze_generator.rb +94 -0
  52. data/lib/generators/better_translate/install/USAGE +13 -0
  53. data/lib/generators/better_translate/install/install_generator.rb +71 -0
  54. data/lib/generators/better_translate/install/templates/README +20 -0
  55. data/lib/generators/better_translate/install/templates/initializer.rb.tt +47 -0
  56. data/lib/generators/better_translate/translate/USAGE +13 -0
  57. data/lib/generators/better_translate/translate/translate_generator.rb +114 -0
  58. data/lib/tasks/better_translate.rake +136 -0
  59. data/sig/better_translate/cache.rbs +28 -0
  60. data/sig/better_translate/cli.rbs +24 -0
  61. data/sig/better_translate/configuration.rbs +78 -0
  62. data/sig/better_translate/direct_translator.rbs +18 -0
  63. data/sig/better_translate/errors.rbs +46 -0
  64. data/sig/better_translate/progress_tracker.rbs +29 -0
  65. data/sig/better_translate/provider_factory.rbs +8 -0
  66. data/sig/better_translate/providers/anthropic_provider.rbs +27 -0
  67. data/sig/better_translate/providers/base_http_provider.rbs +44 -0
  68. data/sig/better_translate/providers/chatgpt_provider.rbs +25 -0
  69. data/sig/better_translate/providers/gemini_provider.rbs +22 -0
  70. data/sig/better_translate/railtie.rbs +7 -0
  71. data/sig/better_translate/rate_limiter.rbs +20 -0
  72. data/sig/better_translate/strategies/base_strategy.rbs +19 -0
  73. data/sig/better_translate/strategies/batch_strategy.rbs +13 -0
  74. data/sig/better_translate/strategies/deep_strategy.rbs +11 -0
  75. data/sig/better_translate/strategies/strategy_selector.rbs +10 -0
  76. data/sig/better_translate/translator.rbs +24 -0
  77. data/sig/better_translate/utils/hash_flattener.rbs +14 -0
  78. data/sig/better_translate/validator.rbs +14 -0
  79. data/sig/better_translate/variable_extractor.rbs +40 -0
  80. data/sig/better_translate/version.rbs +4 -0
  81. data/sig/better_translate/yaml_handler.rbs +29 -0
  82. data/sig/better_translate.rbs +32 -2
  83. data/sig/faraday.rbs +22 -0
  84. data/sig/generators/better_translate/analyze/analyze_generator.rbs +18 -0
  85. data/sig/generators/better_translate/install/install_generator.rbs +14 -0
  86. data/sig/generators/better_translate/translate/translate_generator.rbs +10 -0
  87. data/sig/optparse.rbs +9 -0
  88. data/sig/psych.rbs +5 -0
  89. data/sig/rails.rbs +34 -0
  90. metadata +89 -203
  91. data/lib/better_translate/helper.rb +0 -83
  92. data/lib/better_translate/providers/base_provider.rb +0 -102
  93. data/lib/better_translate/service.rb +0 -144
  94. data/lib/better_translate/similarity_analyzer.rb +0 -218
  95. data/lib/better_translate/utils.rb +0 -55
  96. data/lib/better_translate/writer.rb +0 -75
  97. data/lib/generators/better_translate/analyze_generator.rb +0 -57
  98. data/lib/generators/better_translate/install_generator.rb +0 -14
  99. data/lib/generators/better_translate/templates/better_translate.rb +0 -56
  100. data/lib/generators/better_translate/translate_generator.rb +0 -84
@@ -0,0 +1,509 @@
1
+ # 03.5 - Variable Preservation (Critical Feature)
2
+
3
+ [← Previous: 03-Core Components](./03-core_components.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 04-Provider Architecture →](./04-provider_architecture.md)
4
+
5
+ ---
6
+
7
+ ## Variable Preservation System
8
+
9
+ **PRIORITY: CRITICAL** - This feature prevents bugs by ensuring interpolation variables are not translated or modified.
10
+
11
+ ### Overview
12
+
13
+ When translating text that contains variables (e.g., `%{name}`, `{{user}}`), we must ensure these variables remain unchanged. This system extracts variables before translation, replaces them with safe placeholders, and restores them after translation.
14
+
15
+ ---
16
+
17
+ ## 3.5.1 `lib/better_translate/variable_extractor.rb`
18
+
19
+ ```ruby
20
+ # frozen_string_literal: true
21
+
22
+ module BetterTranslate
23
+ # Extracts and preserves interpolation variables during translation
24
+ #
25
+ # Supports multiple variable formats:
26
+ # - Rails I18n: %{name}, %{count}
27
+ # - I18n.js: {{user}}, {{email}}
28
+ # - ES6 templates: ${var}
29
+ # - Simple braces: {name}
30
+ #
31
+ # @example
32
+ # extractor = VariableExtractor.new("Hello %{name}, you have {{count}} messages")
33
+ # safe_text = extractor.extract
34
+ # #=> "Hello __VAR_0__, you have __VAR_1__ messages"
35
+ #
36
+ # translated = translate(safe_text) # "Ciao __VAR_0__, hai __VAR_1__ messaggi"
37
+ # final = extractor.restore(translated)
38
+ # #=> "Ciao %{name}, hai {{count}} messaggi"
39
+ #
40
+ class VariableExtractor
41
+ # Variable patterns to detect and preserve
42
+ VARIABLE_PATTERNS = {
43
+ rails: /%\{[^}]+\}/, # %{name}, %{count}
44
+ i18n_js: /\{\{[^}]+\}\}/, # {{user}}, {{email}}
45
+ es6: /\$\{[^}]+\}/, # ${var}
46
+ simple: /\{[a-zA-Z_][a-zA-Z0-9_]*\}/ # {name} but not {1,2,3}
47
+ }.freeze
48
+
49
+ # Combined pattern to match any variable format
50
+ COMBINED_PATTERN = Regexp.union(*VARIABLE_PATTERNS.values)
51
+
52
+ # Placeholder prefix
53
+ PLACEHOLDER_PREFIX = "__VAR_"
54
+ PLACEHOLDER_SUFFIX = "__"
55
+
56
+ # @return [String] Original text with variables
57
+ attr_reader :original_text
58
+
59
+ # @return [Array<String>] Extracted variables in order
60
+ attr_reader :variables
61
+
62
+ # @return [Hash<String, String>] Mapping of placeholders to original variables
63
+ attr_reader :placeholder_map
64
+
65
+ # Initialize extractor with text
66
+ #
67
+ # @param text [String] Text containing variables
68
+ def initialize(text)
69
+ @original_text = text
70
+ @variables = []
71
+ @placeholder_map = {}
72
+ @reverse_map = {}
73
+ end
74
+
75
+ # Extract variables and replace with placeholders
76
+ #
77
+ # @return [String] Text with variables replaced by placeholders
78
+ def extract
79
+ return original_text if original_text.nil? || original_text.empty?
80
+
81
+ result = original_text.dup
82
+ index = 0
83
+
84
+ # Find and replace all variables
85
+ result.gsub!(COMBINED_PATTERN) do |match|
86
+ placeholder = "#{PLACEHOLDER_PREFIX}#{index}#{PLACEHOLDER_SUFFIX}"
87
+ @variables << match
88
+ @placeholder_map[placeholder] = match
89
+ @reverse_map[match] = placeholder
90
+ index += 1
91
+ placeholder
92
+ end
93
+
94
+ result
95
+ end
96
+
97
+ # Restore variables from placeholders in translated text
98
+ #
99
+ # @param translated_text [String] Translated text with placeholders
100
+ # @param strict [Boolean] If true, raises error if variables are missing
101
+ # @return [String] Translated text with original variables restored
102
+ # @raise [ValidationError] if strict mode and variables are missing
103
+ def restore(translated_text, strict: true)
104
+ return translated_text if translated_text.nil? || translated_text.empty?
105
+ return translated_text if @placeholder_map.empty?
106
+
107
+ result = translated_text.dup
108
+
109
+ # Restore all placeholders
110
+ @placeholder_map.each do |placeholder, original_var|
111
+ result.gsub!(placeholder, original_var)
112
+ end
113
+
114
+ # Validate all variables are present
115
+ if strict
116
+ validate_variables!(result)
117
+ end
118
+
119
+ result
120
+ end
121
+
122
+ # Check if text contains variables
123
+ #
124
+ # @return [Boolean] true if variables are present
125
+ def variables?
126
+ !@variables.empty?
127
+ end
128
+
129
+ # Get count of variables
130
+ #
131
+ # @return [Integer] Number of variables
132
+ def variable_count
133
+ @variables.size
134
+ end
135
+
136
+ # Validate that all original variables are present in text
137
+ #
138
+ # @param text [String] Text to validate
139
+ # @raise [ValidationError] if variables are missing or modified
140
+ # @return [true] if all variables are present
141
+ def validate_variables!(text)
142
+ missing = []
143
+ extra = []
144
+
145
+ # Check for missing variables
146
+ @variables.each do |var|
147
+ unless text.include?(var)
148
+ missing << var
149
+ end
150
+ end
151
+
152
+ # Check for extra/unknown variables (potential corruption)
153
+ text.scan(COMBINED_PATTERN).each do |var|
154
+ unless @variables.include?(var)
155
+ extra << var
156
+ end
157
+ end
158
+
159
+ if missing.any? || extra.any?
160
+ error_msg = []
161
+ error_msg << "Missing variables: #{missing.join(', ')}" if missing.any?
162
+ error_msg << "Unexpected variables: #{extra.join(', ')}" if extra.any?
163
+
164
+ raise ValidationError.new(
165
+ "Variable validation failed: #{error_msg.join('; ')}",
166
+ context: {
167
+ original_variables: @variables,
168
+ missing: missing,
169
+ extra: extra,
170
+ text: text
171
+ }
172
+ )
173
+ end
174
+
175
+ true
176
+ end
177
+
178
+ # Extract variables from text without creating instance
179
+ #
180
+ # @param text [String] Text to analyze
181
+ # @return [Array<String>] List of variables found
182
+ def self.find_variables(text)
183
+ return [] if text.nil? || text.empty?
184
+ text.scan(COMBINED_PATTERN)
185
+ end
186
+
187
+ # Check if text contains variables
188
+ #
189
+ # @param text [String] Text to check
190
+ # @return [Boolean] true if variables are present
191
+ def self.contains_variables?(text)
192
+ return false if text.nil? || text.empty?
193
+ text.match?(COMBINED_PATTERN)
194
+ end
195
+ end
196
+ end
197
+ ```
198
+
199
+ ---
200
+
201
+ ## 3.5.2 Integration with BaseHttpProvider
202
+
203
+ Update `lib/better_translate/providers/base_http_provider.rb` to use VariableExtractor:
204
+
205
+ ```ruby
206
+ # Add to BaseHttpProvider class
207
+
208
+ # Translate text with variable preservation
209
+ #
210
+ # @param text [String] Text to translate
211
+ # @param target_lang_code [String] Target language code
212
+ # @param target_lang_name [String] Target language name
213
+ # @param preserve_variables [Boolean] Whether to preserve variables (default: true)
214
+ # @return [String] Translated text
215
+ # @api private
216
+ def translate_with_variable_preservation(text, target_lang_code, target_lang_name, preserve_variables: true)
217
+ # If no variables or preservation disabled, translate directly
218
+ unless preserve_variables && VariableExtractor.contains_variables?(text)
219
+ return translate_text_raw(text, target_lang_code, target_lang_name)
220
+ end
221
+
222
+ # Extract variables
223
+ extractor = VariableExtractor.new(text)
224
+ safe_text = extractor.extract
225
+
226
+ if config.verbose
227
+ puts "[VariablePreservation] Found #{extractor.variable_count} variables: #{extractor.variables.join(', ')}"
228
+ end
229
+
230
+ # Translate text with placeholders
231
+ translated = translate_text_raw(safe_text, target_lang_code, target_lang_name)
232
+
233
+ # Restore variables
234
+ begin
235
+ final_text = extractor.restore(translated, strict: true)
236
+
237
+ if config.verbose
238
+ puts "[VariablePreservation] ✓ All variables preserved"
239
+ end
240
+
241
+ final_text
242
+ rescue ValidationError => e
243
+ # Log warning but return translation anyway (non-strict mode fallback)
244
+ warn "[VariablePreservation] WARNING: #{e.message}"
245
+
246
+ # Attempt non-strict restore
247
+ extractor.restore(translated, strict: false)
248
+ end
249
+ end
250
+
251
+ # Raw translation without variable preservation (used internally)
252
+ #
253
+ # @param text [String] Text to translate
254
+ # @param target_lang_code [String] Target language code
255
+ # @param target_lang_name [String] Target language name
256
+ # @return [String] Translated text
257
+ # @api private
258
+ def translate_text_raw(text, target_lang_code, target_lang_name)
259
+ # Original translate_text implementation
260
+ # Subclasses implement this method
261
+ raise NotImplementedError, "#{self.class} must implement #translate_text_raw"
262
+ end
263
+ ```
264
+
265
+ **Note:** Each provider (ChatGPT, Gemini, Anthropic) should rename their `translate_text` to `translate_text_raw` and use `translate_with_variable_preservation` as the public interface.
266
+
267
+ ---
268
+
269
+ ## 3.5.3 Test: `spec/better_translate/variable_extractor_spec.rb`
270
+
271
+ ```ruby
272
+ # frozen_string_literal: true
273
+
274
+ RSpec.describe BetterTranslate::VariableExtractor do
275
+ describe "#extract" do
276
+ it "extracts Rails I18n variables" do
277
+ extractor = described_class.new("Hello %{name}, you have %{count} messages")
278
+ safe_text = extractor.extract
279
+
280
+ expect(safe_text).to eq("Hello __VAR_0__, you have __VAR_1__ messages")
281
+ expect(extractor.variables).to eq(["%{name}", "%{count}"])
282
+ expect(extractor.variable_count).to eq(2)
283
+ end
284
+
285
+ it "extracts I18n.js variables" do
286
+ extractor = described_class.new("Welcome {{user}}!")
287
+ safe_text = extractor.extract
288
+
289
+ expect(safe_text).to eq("Welcome __VAR_0__!")
290
+ expect(extractor.variables).to eq(["{{user}}"])
291
+ end
292
+
293
+ it "extracts ES6 template variables" do
294
+ extractor = described_class.new("Total: ${amount}")
295
+ safe_text = extractor.extract
296
+
297
+ expect(safe_text).to eq("Total: __VAR_0__")
298
+ expect(extractor.variables).to eq(["${amount}"])
299
+ end
300
+
301
+ it "extracts simple brace variables" do
302
+ extractor = described_class.new("Hello {name}")
303
+ safe_text = extractor.extract
304
+
305
+ expect(safe_text).to eq("Hello __VAR_0__")
306
+ expect(extractor.variables).to eq(["{name}"])
307
+ end
308
+
309
+ it "extracts mixed variable formats" do
310
+ text = "Hi %{name}, you have {{count}} items (${total})"
311
+ extractor = described_class.new(text)
312
+ safe_text = extractor.extract
313
+
314
+ expect(extractor.variables).to eq(["%{name}", "{{count}}", "${total}"])
315
+ expect(safe_text).to eq("Hi __VAR_0__, you have __VAR_1__ items (__VAR_2__)")
316
+ end
317
+
318
+ it "handles text without variables" do
319
+ extractor = described_class.new("Hello world")
320
+ safe_text = extractor.extract
321
+
322
+ expect(safe_text).to eq("Hello world")
323
+ expect(extractor.variables).to be_empty
324
+ end
325
+
326
+ it "handles empty text" do
327
+ extractor = described_class.new("")
328
+ expect(extractor.extract).to eq("")
329
+ expect(extractor.variables).to be_empty
330
+ end
331
+ end
332
+
333
+ describe "#restore" do
334
+ it "restores variables to original format" do
335
+ extractor = described_class.new("Hello %{name}")
336
+ safe_text = extractor.extract
337
+ translated = "Ciao __VAR_0__"
338
+
339
+ restored = extractor.restore(translated)
340
+ expect(restored).to eq("Ciao %{name}")
341
+ end
342
+
343
+ it "restores multiple variables in correct positions" do
344
+ extractor = described_class.new("You have %{count} messages from {{user}}")
345
+ safe_text = extractor.extract
346
+ # Simulated translation that reorders placeholders
347
+ translated = "{{user}} ti ha inviato %{count} messaggi"
348
+ # Replace placeholders
349
+ translated = "__VAR_1__ ti ha inviato __VAR_0__ messaggi"
350
+
351
+ restored = extractor.restore(translated)
352
+ expect(restored).to eq("{{user}} ti ha inviato %{count} messaggi")
353
+ end
354
+
355
+ it "raises error in strict mode if variable is missing" do
356
+ extractor = described_class.new("Hello %{name}")
357
+ extractor.extract
358
+ translated = "Ciao" # Missing variable
359
+
360
+ expect {
361
+ extractor.restore(translated, strict: true)
362
+ }.to raise_error(BetterTranslate::ValidationError, /Missing variables: %\{name\}/)
363
+ end
364
+
365
+ it "does not raise in non-strict mode if variable is missing" do
366
+ extractor = described_class.new("Hello %{name}")
367
+ extractor.extract
368
+ translated = "Ciao"
369
+
370
+ expect {
371
+ extractor.restore(translated, strict: false)
372
+ }.not_to raise_error
373
+
374
+ expect(extractor.restore(translated, strict: false)).to eq("Ciao")
375
+ end
376
+
377
+ it "detects unexpected variables" do
378
+ extractor = described_class.new("Hello world")
379
+ extractor.extract
380
+ translated = "Ciao %{name}" # Unexpected variable added
381
+
382
+ expect {
383
+ extractor.restore(translated, strict: true)
384
+ }.to raise_error(BetterTranslate::ValidationError, /Unexpected variables: %\{name\}/)
385
+ end
386
+ end
387
+
388
+ describe ".find_variables" do
389
+ it "finds all variables in text" do
390
+ text = "Hi %{name}, {{count}} items"
391
+ variables = described_class.find_variables(text)
392
+
393
+ expect(variables).to contain_exactly("%{name}", "{{count}}")
394
+ end
395
+
396
+ it "returns empty array for text without variables" do
397
+ expect(described_class.find_variables("Hello world")).to eq([])
398
+ end
399
+ end
400
+
401
+ describe ".contains_variables?" do
402
+ it "returns true if variables are present" do
403
+ expect(described_class.contains_variables?("Hello %{name}")).to be true
404
+ expect(described_class.contains_variables?("Hi {{user}}")).to be true
405
+ end
406
+
407
+ it "returns false if no variables" do
408
+ expect(described_class.contains_variables?("Hello world")).to be false
409
+ end
410
+ end
411
+
412
+ describe "#validate_variables!" do
413
+ it "passes validation if all variables are present" do
414
+ extractor = described_class.new("Hello %{name}")
415
+ extractor.extract
416
+
417
+ expect {
418
+ extractor.validate_variables!("Ciao %{name}")
419
+ }.not_to raise_error
420
+ end
421
+
422
+ it "fails validation if variables are missing" do
423
+ extractor = described_class.new("Hello %{name} and %{title}")
424
+ extractor.extract
425
+
426
+ expect {
427
+ extractor.validate_variables!("Ciao %{name}")
428
+ }.to raise_error(BetterTranslate::ValidationError)
429
+ end
430
+ end
431
+ end
432
+ ```
433
+
434
+ ---
435
+
436
+ ## 3.5.4 Configuration Option
437
+
438
+ Add to `lib/better_translate/configuration.rb`:
439
+
440
+ ```ruby
441
+ # @return [Boolean] Preserve interpolation variables during translation (default: true)
442
+ attr_accessor :preserve_variables
443
+
444
+ # In initialize method:
445
+ @preserve_variables = true
446
+ ```
447
+
448
+ ---
449
+
450
+ ## Usage Examples
451
+
452
+ ### Example 1: Automatic Variable Preservation
453
+
454
+ ```ruby
455
+ # Input YAML
456
+ en:
457
+ welcome: "Welcome %{name}!"
458
+ messages: "You have {{count}} new messages"
459
+
460
+ # After translation (Italian)
461
+ it:
462
+ welcome: "Benvenuto %{name}!"
463
+ messages: "Hai {{count}} nuovi messaggi"
464
+ ```
465
+
466
+ ### Example 2: Direct Translation with Variables
467
+
468
+ ```ruby
469
+ text = "Hello %{user}, your balance is ${amount}"
470
+ result = BetterTranslate.translate_text(text, from: "en", to: "it")
471
+ #=> "Ciao %{user}, il tuo saldo è ${amount}"
472
+
473
+ # Variables are automatically preserved!
474
+ ```
475
+
476
+ ### Example 3: Error Detection
477
+
478
+ ```ruby
479
+ # If translation removes a variable
480
+ text = "Total: %{amount}"
481
+ # Translation accidentally returns: "Totale:" (missing %{amount})
482
+ #=> ValidationError raised with details about missing variable
483
+ ```
484
+
485
+ ---
486
+
487
+ ## Benefits
488
+
489
+ 1. **Bug Prevention**: Eliminates broken interpolations in translations
490
+ 2. **Automatic**: Works transparently in all translation flows
491
+ 3. **Multiple Formats**: Supports Rails, I18n.js, ES6, and simple braces
492
+ 4. **Validation**: Detects missing or corrupted variables
493
+ 5. **Configurable**: Can be disabled if needed
494
+
495
+ ---
496
+
497
+ ## Implementation Checklist
498
+
499
+ - [ ] Create `lib/better_translate/variable_extractor.rb`
500
+ - [ ] Update `BaseHttpProvider` to use variable preservation
501
+ - [ ] Update all providers (ChatGPT, Gemini, Anthropic) to use `translate_text_raw`
502
+ - [ ] Add `preserve_variables` configuration option
503
+ - [ ] Create comprehensive test suite
504
+ - [ ] Update YARD documentation
505
+ - [ ] Add usage examples to README
506
+
507
+ ---
508
+
509
+ [← Previous: 03-Core Components](./03-core_components.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 04-Provider Architecture →](./04-provider_architecture.md)