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,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ # Thread-safe rate limiter
5
+ #
6
+ # Ensures requests are spaced out by a minimum delay.
7
+ #
8
+ # @example Basic usage
9
+ # limiter = RateLimiter.new(delay: 0.5)
10
+ # limiter.wait # Waits if needed
11
+ # # Make API request
12
+ # limiter.record_request
13
+ #
14
+ # @example In a loop
15
+ # limiter = RateLimiter.new(delay: 1.0)
16
+ # translations.each do |text|
17
+ # limiter.wait
18
+ # result = translate_api_call(text)
19
+ # limiter.record_request
20
+ # end
21
+ #
22
+ class RateLimiter
23
+ # @return [Float] Delay between requests in seconds
24
+ attr_reader :delay
25
+
26
+ # Initialize a new rate limiter
27
+ #
28
+ # @param delay [Float] Delay in seconds between requests
29
+ #
30
+ # @example Create rate limiter with 1 second delay
31
+ # limiter = RateLimiter.new(delay: 1.0)
32
+ #
33
+ def initialize(delay: 0.5)
34
+ @delay = delay
35
+ @last_request_time = nil
36
+ @mutex = Mutex.new
37
+ end
38
+
39
+ # Wait if necessary to respect rate limit
40
+ #
41
+ # Calculates time elapsed since last request and sleeps
42
+ # for the remaining time if needed.
43
+ #
44
+ # @return [void]
45
+ #
46
+ # @example Wait before making request
47
+ # limiter.wait
48
+ # response = api_client.post(data)
49
+ # limiter.record_request
50
+ #
51
+ def wait
52
+ @mutex.synchronize do
53
+ return if @last_request_time.nil?
54
+
55
+ elapsed = Time.now - @last_request_time
56
+ sleep_time = @delay - elapsed.to_f
57
+
58
+ sleep(sleep_time) if sleep_time.positive?
59
+ end
60
+ end
61
+
62
+ # Record that a request was made
63
+ #
64
+ # Should be called immediately after making a request.
65
+ #
66
+ # @return [void]
67
+ #
68
+ # @example Record request timestamp
69
+ # response = api_client.post(data)
70
+ # limiter.record_request
71
+ #
72
+ def record_request
73
+ @mutex.synchronize { @last_request_time = Time.now }
74
+ end
75
+
76
+ # Reset the rate limiter
77
+ #
78
+ # Clears the last request time. Useful for testing or
79
+ # when switching contexts.
80
+ #
81
+ # @return [void]
82
+ #
83
+ # @example Reset limiter
84
+ # limiter.reset
85
+ #
86
+ def reset
87
+ @mutex.synchronize { @last_request_time = nil }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ # Translation strategy implementations
5
+ module Strategies
6
+ # Base class for translation strategies
7
+ #
8
+ # @abstract Subclasses must implement {#translate}
9
+ #
10
+ # @example Creating a custom strategy
11
+ # class MyStrategy < BaseStrategy
12
+ # def translate(strings, target_lang_code, target_lang_name)
13
+ # # Custom translation logic
14
+ # end
15
+ # end
16
+ #
17
+ class BaseStrategy
18
+ # @return [Configuration] Configuration object
19
+ attr_reader :config
20
+
21
+ # @return [Providers::BaseHttpProvider] Translation provider
22
+ attr_reader :provider
23
+
24
+ # @return [ProgressTracker] Progress tracker
25
+ attr_reader :progress_tracker
26
+
27
+ # Initialize the strategy
28
+ #
29
+ # @param config [Configuration] Configuration object
30
+ # @param provider [Providers::BaseHttpProvider] Translation provider
31
+ # @param progress_tracker [ProgressTracker] Progress tracker
32
+ #
33
+ # @example
34
+ # strategy = BaseStrategy.new(config, provider, tracker)
35
+ #
36
+ def initialize(config, provider, progress_tracker)
37
+ @config = config
38
+ @provider = provider
39
+ @progress_tracker = progress_tracker
40
+ end
41
+
42
+ # Translate strings
43
+ #
44
+ # @param strings [Hash] Flattened hash of strings to translate
45
+ # @param target_lang_code [String] Target language code
46
+ # @param target_lang_name [String] Target language name
47
+ # @return [Hash] Translated strings (flattened)
48
+ # @raise [NotImplementedError] Must be implemented by subclasses
49
+ #
50
+ # @example
51
+ # translated = strategy.translate(strings, "it", "Italian")
52
+ #
53
+ def translate(strings, target_lang_code, target_lang_name)
54
+ raise NotImplementedError, "#{self.class} must implement #translate"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ module Strategies
5
+ # Batch translation strategy
6
+ #
7
+ # Translates strings in batches for improved performance.
8
+ # Used for larger files (>= 50 strings).
9
+ #
10
+ # @example
11
+ # strategy = BatchStrategy.new(config, provider, tracker)
12
+ # translated = strategy.translate(strings, "it", "Italian")
13
+ #
14
+ class BatchStrategy < BaseStrategy
15
+ # Batch size for translation
16
+ BATCH_SIZE = 10
17
+
18
+ # Translate strings in batches
19
+ #
20
+ # @param strings [Hash] Flattened hash of strings to translate
21
+ # @param target_lang_code [String] Target language code
22
+ # @param target_lang_name [String] Target language name
23
+ # @return [Hash] Translated strings (flattened)
24
+ #
25
+ # @example
26
+ # strings = { "key1" => "value1", "key2" => "value2", ... }
27
+ # translated = strategy.translate(strings, "it", "Italian")
28
+ #
29
+ def translate(strings, target_lang_code, target_lang_name)
30
+ # @type var translated: Hash[String, String]
31
+ translated = {}
32
+ keys = strings.keys
33
+ values = strings.values
34
+ total_batches = (values.size.to_f / BATCH_SIZE).ceil
35
+
36
+ values.each_slice(BATCH_SIZE).with_index do |batch, batch_index|
37
+ progress_tracker.update(
38
+ language: target_lang_name,
39
+ current_key: "Batch #{batch_index + 1}/#{total_batches}",
40
+ progress: ((batch_index + 1).to_f / total_batches * 100.0).round(1)
41
+ )
42
+
43
+ translated_batch = provider.translate_batch(batch, target_lang_code, target_lang_name)
44
+
45
+ # Map back to keys
46
+ batch_keys = keys[batch_index * BATCH_SIZE, batch.size]
47
+ batch_keys&.each_with_index do |key, i|
48
+ translated[key] = translated_batch[i]
49
+ end
50
+ end
51
+
52
+ translated
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ module Strategies
5
+ # Deep translation strategy
6
+ #
7
+ # Translates each string individually with detailed progress tracking.
8
+ # Used for smaller files (< 50 strings) to provide more granular progress.
9
+ #
10
+ # @example
11
+ # strategy = DeepStrategy.new(config, provider, tracker)
12
+ # translated = strategy.translate(strings, "it", "Italian")
13
+ #
14
+ class DeepStrategy < BaseStrategy
15
+ # Translate strings individually
16
+ #
17
+ # @param strings [Hash] Flattened hash of strings to translate
18
+ # @param target_lang_code [String] Target language code
19
+ # @param target_lang_name [String] Target language name
20
+ # @return [Hash] Translated strings (flattened)
21
+ #
22
+ # @example
23
+ # translated = strategy.translate({ "greeting" => "Hello" }, "it", "Italian")
24
+ # #=> { "greeting" => "Ciao" }
25
+ #
26
+ def translate(strings, target_lang_code, target_lang_name)
27
+ # @type var translated: Hash[String, String]
28
+ translated = {}
29
+ total = strings.size
30
+
31
+ strings.each_with_index do |(key, value), index|
32
+ progress_tracker.update(
33
+ language: target_lang_name,
34
+ current_key: key,
35
+ progress: ((index + 1).to_f / total * 100.0).round(1)
36
+ )
37
+
38
+ translated[key] = provider.translate_text(value, target_lang_code, target_lang_name)
39
+ end
40
+
41
+ translated
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ module Strategies
5
+ # Selects the appropriate translation strategy based on content size
6
+ #
7
+ # @example
8
+ # strategy = StrategySelector.select(25, config, provider, tracker)
9
+ # #=> #<DeepStrategy>
10
+ #
11
+ # strategy = StrategySelector.select(100, config, provider, tracker)
12
+ # #=> #<BatchStrategy>
13
+ #
14
+ class StrategySelector
15
+ # Threshold for switching from deep to batch strategy
16
+ DEEP_STRATEGY_THRESHOLD = 50
17
+
18
+ # Select the appropriate strategy
19
+ #
20
+ # @param strings_count [Integer] Number of strings to translate
21
+ # @param config [Configuration] Configuration object
22
+ # @param provider [Providers::BaseHttpProvider] Translation provider
23
+ # @param progress_tracker [ProgressTracker] Progress tracker
24
+ # @return [BaseStrategy] Selected strategy instance
25
+ #
26
+ # @example Small file (deep strategy)
27
+ # strategy = StrategySelector.select(30, config, provider, tracker)
28
+ # #=> #<DeepStrategy>
29
+ #
30
+ # @example Large file (batch strategy)
31
+ # strategy = StrategySelector.select(200, config, provider, tracker)
32
+ # #=> #<BatchStrategy>
33
+ #
34
+ def self.select(strings_count, config, provider, progress_tracker)
35
+ if strings_count < DEEP_STRATEGY_THRESHOLD
36
+ DeepStrategy.new(config, provider, progress_tracker)
37
+ else
38
+ BatchStrategy.new(config, provider, progress_tracker)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end