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.
- checksums.yaml +4 -4
- data/.env.example +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +125 -114
- data/CLAUDE.md +385 -0
- data/README.md +629 -244
- data/Rakefile +7 -1
- data/Steepfile +29 -0
- data/docs/implementation/00-overview.md +220 -0
- data/docs/implementation/01-setup_dependencies.md +668 -0
- data/docs/implementation/02-error_handling.md +65 -0
- data/docs/implementation/03-core_components.md +457 -0
- data/docs/implementation/03.5-variable_preservation.md +509 -0
- data/docs/implementation/04-provider_architecture.md +571 -0
- data/docs/implementation/05-translation_logic.md +1065 -0
- data/docs/implementation/06-main_module_api.md +122 -0
- data/docs/implementation/07-direct_translation_helpers.md +582 -0
- data/docs/implementation/08-rails_integration.md +323 -0
- data/docs/implementation/09-testing_suite.md +228 -0
- data/docs/implementation/10-documentation_examples.md +150 -0
- data/docs/implementation/11-quality_security.md +65 -0
- data/docs/implementation/12-cli_standalone.md +698 -0
- data/exe/better_translate +9 -0
- data/lib/better_translate/cache.rb +125 -0
- data/lib/better_translate/cli.rb +304 -0
- data/lib/better_translate/configuration.rb +201 -0
- data/lib/better_translate/direct_translator.rb +131 -0
- data/lib/better_translate/errors.rb +101 -0
- data/lib/better_translate/progress_tracker.rb +157 -0
- data/lib/better_translate/provider_factory.rb +45 -0
- data/lib/better_translate/providers/anthropic_provider.rb +154 -0
- data/lib/better_translate/providers/base_http_provider.rb +239 -0
- data/lib/better_translate/providers/chatgpt_provider.rb +138 -44
- data/lib/better_translate/providers/gemini_provider.rb +123 -61
- data/lib/better_translate/railtie.rb +18 -0
- data/lib/better_translate/rate_limiter.rb +90 -0
- data/lib/better_translate/strategies/base_strategy.rb +58 -0
- data/lib/better_translate/strategies/batch_strategy.rb +56 -0
- data/lib/better_translate/strategies/deep_strategy.rb +45 -0
- data/lib/better_translate/strategies/strategy_selector.rb +43 -0
- data/lib/better_translate/translator.rb +115 -284
- data/lib/better_translate/utils/hash_flattener.rb +104 -0
- data/lib/better_translate/validator.rb +105 -0
- data/lib/better_translate/variable_extractor.rb +259 -0
- data/lib/better_translate/version.rb +2 -9
- data/lib/better_translate/yaml_handler.rb +168 -0
- data/lib/better_translate.rb +97 -73
- data/lib/generators/better_translate/analyze/USAGE +12 -0
- data/lib/generators/better_translate/analyze/analyze_generator.rb +94 -0
- data/lib/generators/better_translate/install/USAGE +13 -0
- data/lib/generators/better_translate/install/install_generator.rb +71 -0
- data/lib/generators/better_translate/install/templates/README +20 -0
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +47 -0
- data/lib/generators/better_translate/translate/USAGE +13 -0
- data/lib/generators/better_translate/translate/translate_generator.rb +114 -0
- data/lib/tasks/better_translate.rake +136 -0
- data/sig/better_translate/cache.rbs +28 -0
- data/sig/better_translate/cli.rbs +24 -0
- data/sig/better_translate/configuration.rbs +78 -0
- data/sig/better_translate/direct_translator.rbs +18 -0
- data/sig/better_translate/errors.rbs +46 -0
- data/sig/better_translate/progress_tracker.rbs +29 -0
- data/sig/better_translate/provider_factory.rbs +8 -0
- data/sig/better_translate/providers/anthropic_provider.rbs +27 -0
- data/sig/better_translate/providers/base_http_provider.rbs +44 -0
- data/sig/better_translate/providers/chatgpt_provider.rbs +25 -0
- data/sig/better_translate/providers/gemini_provider.rbs +22 -0
- data/sig/better_translate/railtie.rbs +7 -0
- data/sig/better_translate/rate_limiter.rbs +20 -0
- data/sig/better_translate/strategies/base_strategy.rbs +19 -0
- data/sig/better_translate/strategies/batch_strategy.rbs +13 -0
- data/sig/better_translate/strategies/deep_strategy.rbs +11 -0
- data/sig/better_translate/strategies/strategy_selector.rbs +10 -0
- data/sig/better_translate/translator.rbs +24 -0
- data/sig/better_translate/utils/hash_flattener.rbs +14 -0
- data/sig/better_translate/validator.rbs +14 -0
- data/sig/better_translate/variable_extractor.rbs +40 -0
- data/sig/better_translate/version.rbs +4 -0
- data/sig/better_translate/yaml_handler.rbs +29 -0
- data/sig/better_translate.rbs +32 -2
- data/sig/faraday.rbs +22 -0
- data/sig/generators/better_translate/analyze/analyze_generator.rbs +18 -0
- data/sig/generators/better_translate/install/install_generator.rbs +14 -0
- data/sig/generators/better_translate/translate/translate_generator.rbs +10 -0
- data/sig/optparse.rbs +9 -0
- data/sig/psych.rbs +5 -0
- data/sig/rails.rbs +34 -0
- metadata +89 -203
- data/lib/better_translate/helper.rb +0 -83
- data/lib/better_translate/providers/base_provider.rb +0 -102
- data/lib/better_translate/service.rb +0 -144
- data/lib/better_translate/similarity_analyzer.rb +0 -218
- data/lib/better_translate/utils.rb +0 -55
- data/lib/better_translate/writer.rb +0 -75
- data/lib/generators/better_translate/analyze_generator.rb +0 -57
- data/lib/generators/better_translate/install_generator.rb +0 -14
- data/lib/generators/better_translate/templates/better_translate.rb +0 -56
- 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
|