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,65 @@
1
+ # 02 - Error Handling
2
+
3
+ [← Previous: 01-Setup Dependencies](./01-setup_dependencies.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 03-Core Components →](./03-core_components.md)
4
+
5
+ ---
6
+
7
+ ## Error Handling
8
+
9
+ ### 2.1 `lib/better_translate/errors.rb`
10
+
11
+ ```ruby
12
+ # frozen_string_literal: true
13
+
14
+ module BetterTranslate
15
+ # Base error class for all BetterTranslate errors
16
+ #
17
+ # @abstract
18
+ class Error < StandardError
19
+ # @return [Hash] Additional context about the error
20
+ attr_reader :context
21
+
22
+ # Initialize a new error with optional context
23
+ #
24
+ # @param message [String] The error message
25
+ # @param context [Hash] Additional context information
26
+ def initialize(message = nil, context: {})
27
+ @context = context
28
+ super(message)
29
+ end
30
+ end
31
+
32
+ # Raised when configuration is invalid or incomplete
33
+ class ConfigurationError < Error; end
34
+
35
+ # Raised when input validation fails
36
+ class ValidationError < Error; end
37
+
38
+ # Raised when translation fails
39
+ class TranslationError < Error; end
40
+
41
+ # Raised when a provider encounters an error
42
+ class ProviderError < Error; end
43
+
44
+ # Raised when an API call fails
45
+ class ApiError < Error; end
46
+
47
+ # Raised when rate limit is exceeded
48
+ class RateLimitError < ApiError; end
49
+
50
+ # Raised when file operations fail
51
+ class FileError < Error; end
52
+
53
+ # Raised when YAML parsing fails
54
+ class YamlError < Error; end
55
+
56
+ # Raised when a provider is not found
57
+ class ProviderNotFoundError < Error; end
58
+ end
59
+ ```
60
+
61
+ ---
62
+
63
+ ---
64
+
65
+ [← Previous: 01-Setup Dependencies](./01-setup_dependencies.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 03-Core Components →](./03-core_components.md)
@@ -0,0 +1,457 @@
1
+ # 03 - Core Components
2
+
3
+ [← Previous: 02-Error Handling](./02-error_handling.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 04-Provider Architecture →](./04-provider_architecture.md)
4
+
5
+ ---
6
+
7
+ ## Core Components
8
+
9
+ ### 3.1 `lib/better_translate/configuration.rb`
10
+
11
+ ```ruby
12
+ # frozen_string_literal: true
13
+
14
+ module BetterTranslate
15
+ # Configuration class for BetterTranslate
16
+ #
17
+ # Manages all configuration options with type safety and validation.
18
+ #
19
+ # @example Basic configuration
20
+ # config = Configuration.new
21
+ # config.provider = :chatgpt
22
+ # config.openai_key = ENV['OPENAI_API_KEY']
23
+ # config.source_language = "en"
24
+ # config.target_languages = [{ short_name: "it", name: "Italian" }]
25
+ # config.validate!
26
+ #
27
+ class Configuration
28
+ # @return [Symbol] The translation provider (:chatgpt, :gemini, etc.)
29
+ attr_accessor :provider
30
+
31
+ # @return [String, nil] OpenAI API key
32
+ attr_accessor :openai_key
33
+
34
+ # @return [String, nil] Google Gemini API key
35
+ attr_accessor :google_gemini_key
36
+
37
+ # @return [String, nil] Anthropic API key
38
+ attr_accessor :anthropic_key
39
+
40
+ # @return [String] Source language code (e.g., "en")
41
+ attr_accessor :source_language
42
+
43
+ # @return [Array<Hash>] Target languages with :short_name and :name
44
+ attr_accessor :target_languages
45
+
46
+ # @return [String] Path to input YAML file
47
+ attr_accessor :input_file
48
+
49
+ # @return [String] Output folder for translated files
50
+ attr_accessor :output_folder
51
+
52
+ # @return [Symbol] Translation mode (:override or :incremental)
53
+ attr_accessor :translation_mode
54
+
55
+ # @return [String, nil] Translation context for domain-specific terminology
56
+ attr_accessor :translation_context
57
+
58
+ # @return [Integer] Maximum concurrent requests
59
+ attr_accessor :max_concurrent_requests
60
+
61
+ # @return [Integer] Request timeout in seconds
62
+ attr_accessor :request_timeout
63
+
64
+ # @return [Integer] Maximum number of retries
65
+ attr_accessor :max_retries
66
+
67
+ # @return [Float] Retry delay in seconds
68
+ attr_accessor :retry_delay
69
+
70
+ # @return [Boolean] Enable/disable caching
71
+ attr_accessor :cache_enabled
72
+
73
+ # @return [Integer] Cache size (LRU capacity)
74
+ attr_accessor :cache_size
75
+
76
+ # @return [Integer, nil] Cache TTL in seconds (nil = no expiration)
77
+ attr_accessor :cache_ttl
78
+
79
+ # @return [Boolean] Verbose logging
80
+ attr_accessor :verbose
81
+
82
+ # @return [Boolean] Dry run mode (no files written)
83
+ attr_accessor :dry_run
84
+
85
+ # @return [Array<String>] Global exclusions (apply to all languages)
86
+ attr_accessor :global_exclusions
87
+
88
+ # @return [Hash] Language-specific exclusions
89
+ attr_accessor :exclusions_per_language
90
+
91
+ # Initialize a new configuration with defaults
92
+ def initialize
93
+ @translation_mode = :override
94
+ @max_concurrent_requests = 3
95
+ @request_timeout = 30
96
+ @max_retries = 3
97
+ @retry_delay = 2.0
98
+ @cache_enabled = true
99
+ @cache_size = 1000
100
+ @cache_ttl = nil
101
+ @verbose = false
102
+ @dry_run = false
103
+ @global_exclusions = []
104
+ @exclusions_per_language = {}
105
+ @target_languages = []
106
+ end
107
+
108
+ # Validate the configuration
109
+ #
110
+ # @raise [ConfigurationError] if configuration is invalid
111
+ # @return [true] if configuration is valid
112
+ def validate!
113
+ validate_provider!
114
+ validate_api_keys!
115
+ validate_languages!
116
+ validate_files!
117
+ validate_optional_settings!
118
+ true
119
+ end
120
+
121
+ private
122
+
123
+ def validate_provider!
124
+ raise ConfigurationError, "Provider must be set" if provider.nil?
125
+ raise ConfigurationError, "Provider must be a Symbol" unless provider.is_a?(Symbol)
126
+ end
127
+
128
+ def validate_api_keys!
129
+ case provider
130
+ when :chatgpt
131
+ raise ConfigurationError, "OpenAI API key is required for ChatGPT provider" if openai_key.nil? || openai_key.empty?
132
+ when :gemini
133
+ raise ConfigurationError, "Google Gemini API key is required for Gemini provider" if google_gemini_key.nil? || google_gemini_key.empty?
134
+ when :anthropic
135
+ raise ConfigurationError, "Anthropic API key is required for Anthropic provider" if anthropic_key.nil? || anthropic_key.empty?
136
+ end
137
+ end
138
+
139
+ def validate_languages!
140
+ raise ConfigurationError, "Source language must be set" if source_language.nil? || source_language.empty?
141
+ raise ConfigurationError, "Target languages must be an array" unless target_languages.is_a?(Array)
142
+ raise ConfigurationError, "At least one target language is required" if target_languages.empty?
143
+
144
+ target_languages.each do |lang|
145
+ raise ConfigurationError, "Each target language must be a Hash" unless lang.is_a?(Hash)
146
+ raise ConfigurationError, "Target language must have :short_name" unless lang.key?(:short_name)
147
+ raise ConfigurationError, "Target language must have :name" unless lang.key?(:name)
148
+ end
149
+ end
150
+
151
+ def validate_files!
152
+ raise ConfigurationError, "Input file must be set" if input_file.nil? || input_file.empty?
153
+ raise ConfigurationError, "Output folder must be set" if output_folder.nil? || output_folder.empty?
154
+ raise ConfigurationError, "Input file does not exist: #{input_file}" unless File.exist?(input_file)
155
+ end
156
+
157
+ def validate_optional_settings!
158
+ valid_modes = [:override, :incremental]
159
+ raise ConfigurationError, "Translation mode must be :override or :incremental" unless valid_modes.include?(translation_mode)
160
+
161
+ raise ConfigurationError, "Max concurrent requests must be positive" if max_concurrent_requests <= 0
162
+ raise ConfigurationError, "Request timeout must be positive" if request_timeout <= 0
163
+ raise ConfigurationError, "Max retries must be non-negative" if max_retries < 0
164
+ raise ConfigurationError, "Cache size must be positive" if cache_size <= 0
165
+ end
166
+ end
167
+ end
168
+ ```
169
+
170
+ ### 3.2 `lib/better_translate/cache.rb`
171
+
172
+ ```ruby
173
+ # frozen_string_literal: true
174
+
175
+ module BetterTranslate
176
+ # LRU (Least Recently Used) Cache implementation
177
+ #
178
+ # Thread-safe cache with configurable capacity and optional TTL.
179
+ #
180
+ # @example Basic usage
181
+ # cache = Cache.new(capacity: 100)
182
+ # cache.set("hello:it", "ciao")
183
+ # cache.get("hello:it") #=> "ciao"
184
+ #
185
+ class Cache
186
+ # @return [Integer] Maximum number of items in cache
187
+ attr_reader :capacity
188
+
189
+ # @return [Integer, nil] Time to live in seconds
190
+ attr_reader :ttl
191
+
192
+ # Initialize a new cache
193
+ #
194
+ # @param capacity [Integer] Maximum cache size
195
+ # @param ttl [Integer, nil] Time to live in seconds
196
+ def initialize(capacity: 1000, ttl: nil)
197
+ @capacity = capacity
198
+ @ttl = ttl
199
+ @cache = {}
200
+ @mutex = Mutex.new
201
+ end
202
+
203
+ # Get a value from the cache
204
+ #
205
+ # @param key [String] Cache key
206
+ # @return [String, nil] Cached value or nil if not found/expired
207
+ def get(key)
208
+ @mutex.synchronize do
209
+ return nil unless @cache.key?(key)
210
+
211
+ entry = @cache[key]
212
+
213
+ # Check TTL
214
+ if @ttl && Time.now - entry[:timestamp] > @ttl
215
+ @cache.delete(key)
216
+ return nil
217
+ end
218
+
219
+ # Move to end (most recently used)
220
+ @cache.delete(key)
221
+ @cache[key] = entry
222
+ entry[:value]
223
+ end
224
+ end
225
+
226
+ # Set a value in the cache
227
+ #
228
+ # @param key [String] Cache key
229
+ # @param value [String] Value to cache
230
+ # @return [String] The cached value
231
+ def set(key, value)
232
+ @mutex.synchronize do
233
+ # Remove oldest entry if at capacity
234
+ @cache.shift if @cache.size >= @capacity && !@cache.key?(key)
235
+
236
+ @cache[key] = {
237
+ value: value,
238
+ timestamp: Time.now
239
+ }
240
+ value
241
+ end
242
+ end
243
+
244
+ # Clear the cache
245
+ #
246
+ # @return [void]
247
+ def clear
248
+ @mutex.synchronize { @cache.clear }
249
+ end
250
+
251
+ # Get cache size
252
+ #
253
+ # @return [Integer] Number of items in cache
254
+ def size
255
+ @mutex.synchronize { @cache.size }
256
+ end
257
+
258
+ # Check if key exists in cache
259
+ #
260
+ # @param key [String] Cache key
261
+ # @return [Boolean] true if key exists and not expired
262
+ def key?(key)
263
+ !get(key).nil?
264
+ end
265
+ end
266
+ end
267
+ ```
268
+
269
+ ### 3.3 `lib/better_translate/rate_limiter.rb`
270
+
271
+ ```ruby
272
+ # frozen_string_literal: true
273
+
274
+ module BetterTranslate
275
+ # Thread-safe rate limiter
276
+ #
277
+ # Ensures requests are spaced out by a minimum delay.
278
+ #
279
+ # @example
280
+ # limiter = RateLimiter.new(delay: 0.5)
281
+ # limiter.wait # Waits if needed
282
+ #
283
+ class RateLimiter
284
+ # @return [Float] Delay between requests in seconds
285
+ attr_reader :delay
286
+
287
+ # Initialize a new rate limiter
288
+ #
289
+ # @param delay [Float] Delay in seconds between requests
290
+ def initialize(delay: 0.5)
291
+ @delay = delay
292
+ @last_request_time = nil
293
+ @mutex = Mutex.new
294
+ end
295
+
296
+ # Wait if necessary to respect rate limit
297
+ #
298
+ # @return [void]
299
+ def wait
300
+ @mutex.synchronize do
301
+ return if @last_request_time.nil?
302
+
303
+ elapsed = Time.now - @last_request_time
304
+ sleep_time = @delay - elapsed
305
+
306
+ sleep(sleep_time) if sleep_time > 0
307
+ end
308
+ end
309
+
310
+ # Record that a request was made
311
+ #
312
+ # @return [void]
313
+ def record_request
314
+ @mutex.synchronize { @last_request_time = Time.now }
315
+ end
316
+
317
+ # Reset the rate limiter
318
+ #
319
+ # @return [void]
320
+ def reset
321
+ @mutex.synchronize { @last_request_time = nil }
322
+ end
323
+ end
324
+ end
325
+ ```
326
+
327
+ ### 3.4 `lib/better_translate/validator.rb`
328
+
329
+ ```ruby
330
+ # frozen_string_literal: true
331
+
332
+ module BetterTranslate
333
+ # Input validation utilities
334
+ #
335
+ # Validates language codes, text, paths, and other inputs.
336
+ class Validator
337
+ # Validate a language code
338
+ #
339
+ # @param code [String] Language code to validate
340
+ # @raise [ValidationError] if code is invalid
341
+ # @return [true] if valid
342
+ def self.validate_language_code!(code)
343
+ raise ValidationError, "Language code cannot be nil" if code.nil?
344
+ raise ValidationError, "Language code must be a String" unless code.is_a?(String)
345
+ raise ValidationError, "Language code cannot be empty" if code.empty?
346
+ raise ValidationError, "Language code must be 2 letters" unless code.match?(/^[a-z]{2}$/i)
347
+ true
348
+ end
349
+
350
+ # Validate text for translation
351
+ #
352
+ # @param text [String] Text to validate
353
+ # @raise [ValidationError] if text is invalid
354
+ # @return [true] if valid
355
+ def self.validate_text!(text)
356
+ raise ValidationError, "Text cannot be nil" if text.nil?
357
+ raise ValidationError, "Text must be a String" unless text.is_a?(String)
358
+ raise ValidationError, "Text cannot be empty" if text.strip.empty?
359
+ true
360
+ end
361
+
362
+ # Validate a file path exists
363
+ #
364
+ # @param path [String] File path to validate
365
+ # @raise [FileError] if path is invalid
366
+ # @return [true] if valid
367
+ def self.validate_file_exists!(path)
368
+ raise FileError, "File path cannot be nil" if path.nil?
369
+ raise FileError, "File path must be a String" unless path.is_a?(String)
370
+ raise FileError, "File does not exist: #{path}" unless File.exist?(path)
371
+ true
372
+ end
373
+
374
+ # Validate an API key
375
+ #
376
+ # @param key [String] API key to validate
377
+ # @param provider [Symbol] Provider name for error message
378
+ # @raise [ConfigurationError] if key is invalid
379
+ # @return [true] if valid
380
+ def self.validate_api_key!(key, provider:)
381
+ raise ConfigurationError, "API key for #{provider} cannot be nil" if key.nil?
382
+ raise ConfigurationError, "API key for #{provider} must be a String" unless key.is_a?(String)
383
+ raise ConfigurationError, "API key for #{provider} cannot be empty" if key.strip.empty?
384
+ true
385
+ end
386
+ end
387
+ end
388
+ ```
389
+
390
+ ### 3.5 `lib/better_translate/utils/hash_flattener.rb`
391
+
392
+ ```ruby
393
+ # frozen_string_literal: true
394
+
395
+ module BetterTranslate
396
+ module Utils
397
+ # Utilities for flattening and unflattening nested hashes
398
+ #
399
+ # Used to convert nested YAML structures to flat key-value pairs
400
+ # and back again.
401
+ #
402
+ # @example
403
+ # nested = { "user" => { "name" => "John", "age" => 30 } }
404
+ # flat = HashFlattener.flatten(nested)
405
+ # #=> { "user.name" => "John", "user.age" => 30 }
406
+ #
407
+ # HashFlattener.unflatten(flat)
408
+ # #=> { "user" => { "name" => "John", "age" => 30 } }
409
+ #
410
+ class HashFlattener
411
+ # Flatten a nested hash to dot-notation keys
412
+ #
413
+ # @param hash [Hash] Nested hash to flatten
414
+ # @param parent_key [String] Parent key prefix
415
+ # @param separator [String] Key separator
416
+ # @return [Hash] Flattened hash
417
+ def self.flatten(hash, parent_key = "", separator = ".")
418
+ hash.each_with_object({}) do |(key, value), result|
419
+ new_key = parent_key.empty? ? key.to_s : "#{parent_key}#{separator}#{key}"
420
+
421
+ if value.is_a?(Hash)
422
+ result.merge!(flatten(value, new_key, separator))
423
+ else
424
+ result[new_key] = value
425
+ end
426
+ end
427
+ end
428
+
429
+ # Unflatten a hash with dot-notation keys to nested structure
430
+ #
431
+ # @param hash [Hash] Flattened hash
432
+ # @param separator [String] Key separator
433
+ # @return [Hash] Nested hash
434
+ def self.unflatten(hash, separator = ".")
435
+ hash.each_with_object({}) do |(key, value), result|
436
+ keys = key.split(separator)
437
+ last_key = keys.pop
438
+
439
+ # Build nested structure
440
+ nested = keys.reduce(result) do |memo, k|
441
+ memo[k] ||= {}
442
+ memo[k]
443
+ end
444
+
445
+ nested[last_key] = value
446
+ end
447
+ end
448
+ end
449
+ end
450
+ end
451
+ ```
452
+
453
+ ---
454
+
455
+ ---
456
+
457
+ [← Previous: 02-Error Handling](./02-error_handling.md) | [Back to Index](../../IMPLEMENTATION_PLAN.md) | [Next: 04-Provider Architecture →](./04-provider_architecture.md)