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,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterTranslate
4
+ # Extracts and preserves interpolation variables during translation
5
+ #
6
+ # Supports multiple variable formats:
7
+ # - Rails I18n: %{name}, %{count}
8
+ # - I18n.js: {{user}}, {{email}}
9
+ # - ES6 templates: ${var}
10
+ # - Simple braces: {name}
11
+ #
12
+ # Variables are extracted before translation, replaced with safe placeholders,
13
+ # and then restored after translation to ensure they remain unchanged.
14
+ #
15
+ # @example Basic usage
16
+ # extractor = VariableExtractor.new("Hello %{name}, you have {{count}} messages")
17
+ # safe_text = extractor.extract
18
+ # #=> "Hello __VAR_0__, you have __VAR_1__ messages"
19
+ #
20
+ # translated = translate(safe_text) # "Ciao __VAR_0__, hai __VAR_1__ messaggi"
21
+ # final = extractor.restore(translated)
22
+ # #=> "Ciao %{name}, hai {{count}} messaggi"
23
+ #
24
+ # @example Variable validation
25
+ # extractor = VariableExtractor.new("Total: %{amount}")
26
+ # extractor.extract
27
+ # extractor.validate_variables!("Totale: %{amount}") #=> true
28
+ # extractor.validate_variables!("Totale:") # raises ValidationError
29
+ #
30
+ class VariableExtractor
31
+ # Variable patterns to detect and preserve
32
+ VARIABLE_PATTERNS = {
33
+ rails_template: /%\{[^}]+\}/, # %{name}, %{count}
34
+ rails_annotated: /%<[^>]+>[a-z]/i, # %<name>s, %<count>d
35
+ i18n_js: /\{\{[^}]+\}\}/, # {{user}}, {{email}}
36
+ es6: /\$\{[^}]+\}/, # ${var}
37
+ simple: /\{[a-zA-Z_][a-zA-Z0-9_]*\}/ # {name} but not {1,2,3}
38
+ }.freeze
39
+
40
+ # Combined pattern to match any variable format
41
+ COMBINED_PATTERN = Regexp.union(*VARIABLE_PATTERNS.values).freeze
42
+
43
+ # Placeholder prefix
44
+ PLACEHOLDER_PREFIX = "__VAR_"
45
+
46
+ # Placeholder suffix
47
+ PLACEHOLDER_SUFFIX = "__"
48
+
49
+ # @return [String] Original text with variables
50
+ attr_reader :original_text
51
+
52
+ # @return [Array<String>] Extracted variables in order
53
+ attr_reader :variables
54
+
55
+ # @return [Hash<String, String>] Mapping of placeholders to original variables
56
+ attr_reader :placeholder_map
57
+
58
+ # Initialize extractor with text
59
+ #
60
+ # @param text [String] Text containing variables
61
+ #
62
+ # @example
63
+ # extractor = VariableExtractor.new("Hello %{name}")
64
+ #
65
+ def initialize(text)
66
+ @original_text = text
67
+ @variables = []
68
+ @placeholder_map = {}
69
+ @reverse_map = {}
70
+ end
71
+
72
+ # Extract variables and replace with placeholders
73
+ #
74
+ # Scans the text for all supported variable formats and replaces them
75
+ # with numbered placeholders (__VAR_0__, __VAR_1__, etc.).
76
+ #
77
+ # @return [String] Text with variables replaced by placeholders
78
+ #
79
+ # @example
80
+ # extractor = VariableExtractor.new("Hello %{name}")
81
+ # extractor.extract #=> "Hello __VAR_0__"
82
+ #
83
+ def extract
84
+ return "" if original_text.nil? || original_text.empty?
85
+
86
+ result = original_text.dup
87
+ index = 0
88
+
89
+ # Find and replace all variables
90
+ result.gsub!(COMBINED_PATTERN) do |match|
91
+ placeholder = "#{PLACEHOLDER_PREFIX}#{index}#{PLACEHOLDER_SUFFIX}"
92
+ @variables << match
93
+ @placeholder_map[placeholder] = match
94
+ @reverse_map[match] = placeholder
95
+ index += 1
96
+ placeholder
97
+ end
98
+
99
+ result
100
+ end
101
+
102
+ # Restore variables from placeholders in translated text
103
+ #
104
+ # Replaces all placeholders with their original variable formats.
105
+ # In strict mode, validates that all original variables are present.
106
+ #
107
+ # @param translated_text [String] Translated text with placeholders
108
+ # @param strict [Boolean] If true, raises error if variables are missing
109
+ # @return [String] Translated text with original variables restored
110
+ # @raise [ValidationError] if strict mode and variables are missing
111
+ #
112
+ # @example Successful restore
113
+ # extractor = VariableExtractor.new("Hello %{name}")
114
+ # extractor.extract
115
+ # extractor.restore("Ciao __VAR_0__") #=> "Ciao %{name}"
116
+ #
117
+ # @example Strict mode with missing variable
118
+ # extractor = VariableExtractor.new("Hello %{name}")
119
+ # extractor.extract
120
+ # extractor.restore("Ciao", strict: true) # raises ValidationError
121
+ #
122
+ def restore(translated_text, strict: true)
123
+ return "" if translated_text.nil? || translated_text.empty?
124
+
125
+ result = translated_text.dup
126
+
127
+ # Restore all placeholders
128
+ @placeholder_map.each do |placeholder, original_var|
129
+ result.gsub!(placeholder, original_var)
130
+ end
131
+
132
+ # Validate all variables are present
133
+ validate_variables!(result) if strict
134
+
135
+ result
136
+ end
137
+
138
+ # Check if text contains variables
139
+ #
140
+ # @return [Boolean] true if variables are present
141
+ #
142
+ # @example
143
+ # extractor = VariableExtractor.new("Hello %{name}")
144
+ # extractor.extract
145
+ # extractor.variables? #=> true
146
+ #
147
+ def variables?
148
+ !@variables.empty?
149
+ end
150
+
151
+ # Get count of variables
152
+ #
153
+ # @return [Integer] Number of variables
154
+ #
155
+ # @example
156
+ # extractor = VariableExtractor.new("Hi %{name}, {{count}} items")
157
+ # extractor.extract
158
+ # extractor.variable_count #=> 2
159
+ #
160
+ def variable_count
161
+ @variables.size
162
+ end
163
+
164
+ # Validate that all original variables are present in text
165
+ #
166
+ # Checks that:
167
+ # 1. All original variables are still present
168
+ # 2. No unexpected/extra variables have been added
169
+ #
170
+ # @param text [String] Text to validate
171
+ # @raise [ValidationError] if variables are missing or modified
172
+ # @return [true] if all variables are present
173
+ #
174
+ # @example Valid text
175
+ # extractor = VariableExtractor.new("Hello %{name}")
176
+ # extractor.extract
177
+ # extractor.validate_variables!("Ciao %{name}") #=> true
178
+ #
179
+ # @example Missing variable
180
+ # extractor = VariableExtractor.new("Hello %{name}")
181
+ # extractor.extract
182
+ # extractor.validate_variables!("Ciao") # raises ValidationError
183
+ #
184
+ def validate_variables!(text)
185
+ # @type var missing: Array[String]
186
+ missing = []
187
+ # @type var extra: Array[String]
188
+ extra = []
189
+
190
+ # Check for missing variables
191
+ @variables.each do |var|
192
+ var_str = var.is_a?(String) ? var : var.to_s
193
+ missing << var_str unless text.include?(var_str)
194
+ end
195
+
196
+ # Check for extra/unknown variables (potential corruption)
197
+ found_vars = text.scan(COMBINED_PATTERN)
198
+ found_vars.each do |var|
199
+ var_str = var.is_a?(String) ? var : var.to_s
200
+ extra << var_str unless @variables.include?(var_str)
201
+ end
202
+
203
+ if missing.any? || extra.any?
204
+ # @type var error_msg: Array[String]
205
+ error_msg = []
206
+ error_msg << "Missing variables: #{missing.join(", ")}" if missing.any?
207
+ error_msg << "Unexpected variables: #{extra.join(", ")}" if extra.any?
208
+
209
+ raise ValidationError.new(
210
+ "Variable validation failed: #{error_msg.join("; ")}",
211
+ context: {
212
+ original_variables: @variables,
213
+ missing: missing,
214
+ extra: extra,
215
+ text: text
216
+ }
217
+ )
218
+ end
219
+
220
+ true
221
+ end
222
+
223
+ # Extract variables from text without creating instance
224
+ #
225
+ # Static method to find all variables in text without needing
226
+ # to instantiate the extractor.
227
+ #
228
+ # @param text [String] Text to analyze
229
+ # @return [Array<String>] List of variables found
230
+ #
231
+ # @example
232
+ # VariableExtractor.find_variables("Hi %{name}, {{count}} items")
233
+ # #=> ["%{name}", "{{count}}"]
234
+ #
235
+ def self.find_variables(text)
236
+ return [] if text.nil? || text.empty?
237
+
238
+ text.scan(COMBINED_PATTERN)
239
+ end
240
+
241
+ # Check if text contains variables
242
+ #
243
+ # Static method to quickly check if text contains any supported
244
+ # variable format.
245
+ #
246
+ # @param text [String] Text to check
247
+ # @return [Boolean] true if variables are present
248
+ #
249
+ # @example
250
+ # VariableExtractor.contains_variables?("Hello %{name}") #=> true
251
+ # VariableExtractor.contains_variables?("Hello world") #=> false
252
+ #
253
+ def self.contains_variables?(text)
254
+ return false if text.nil? || text.empty?
255
+
256
+ text.match?(COMBINED_PATTERN)
257
+ end
258
+ end
259
+ end
@@ -1,13 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BetterTranslate
4
- # Current version of the BetterTranslate gem.
5
- #
6
- # The versioning follows Semantic Versioning 2.0.0 (https://semver.org/):
7
- # - MAJOR version for incompatible API changes
8
- # - MINOR version for backwards-compatible functionality additions
9
- # - PATCH version for backwards-compatible bug fixes
10
- #
11
- # @return [String] The current version in the format "MAJOR.MINOR.PATCH"
12
- VERSION = "0.5.0"
4
+ # Current version of BetterTranslate gem
5
+ VERSION = "1.0.0"
13
6
  end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module BetterTranslate
7
+ # Handles YAML file operations
8
+ #
9
+ # Provides methods for:
10
+ # - Reading and parsing YAML files
11
+ # - Writing YAML files with proper formatting
12
+ # - Merging translations (incremental mode)
13
+ # - Handling exclusions
14
+ # - Flattening/unflattening nested structures
15
+ #
16
+ # @example Reading a YAML file
17
+ # handler = YAMLHandler.new(config)
18
+ # data = handler.read_yaml("config/locales/en.yml")
19
+ #
20
+ # @example Writing translations
21
+ # handler.write_yaml("config/locales/it.yml", { "it" => { "greeting" => "Ciao" } })
22
+ #
23
+ class YAMLHandler
24
+ # @return [Configuration] Configuration object
25
+ attr_reader :config
26
+
27
+ # Initialize YAML handler
28
+ #
29
+ # @param config [Configuration] Configuration object
30
+ #
31
+ # @example
32
+ # config = Configuration.new
33
+ # handler = YAMLHandler.new(config)
34
+ #
35
+ def initialize(config)
36
+ @config = config
37
+ end
38
+
39
+ # Read and parse YAML file
40
+ #
41
+ # @param file_path [String] Path to YAML file
42
+ # @return [Hash] Parsed YAML content
43
+ # @raise [FileError] if file cannot be read
44
+ # @raise [YamlError] if YAML is invalid
45
+ #
46
+ # @example
47
+ # data = handler.read_yaml("config/locales/en.yml")
48
+ # #=> { "en" => { "greeting" => "Hello" } }
49
+ #
50
+ def read_yaml(file_path)
51
+ Validator.validate_file_exists!(file_path)
52
+
53
+ content = File.read(file_path)
54
+ YAML.safe_load(content) || {}
55
+ rescue Errno::ENOENT => e
56
+ raise FileError.new("File not found: #{file_path}", context: { error: e.message })
57
+ rescue Psych::SyntaxError => e
58
+ raise YamlError.new("Invalid YAML syntax in #{file_path}", context: { error: e.message })
59
+ end
60
+
61
+ # Write hash to YAML file
62
+ #
63
+ # @param file_path [String] Output file path
64
+ # @param data [Hash] Data to write
65
+ # @param diff_preview [DiffPreview, nil] Optional diff preview instance
66
+ # @return [Hash, nil] Summary hash if dry_run, nil otherwise
67
+ # @raise [FileError] if file cannot be written
68
+ #
69
+ # @example
70
+ # handler.write_yaml("config/locales/it.yml", { "it" => { "greeting" => "Ciao" } })
71
+ #
72
+ def write_yaml(file_path, data, diff_preview: nil)
73
+ summary = nil
74
+
75
+ # Show diff preview if in dry run mode
76
+ if config.dry_run && diff_preview
77
+ # @type var existing_data: Hash[String, untyped]
78
+ existing_data = File.exist?(file_path) ? read_yaml(file_path) : {}
79
+ summary = diff_preview.show_diff(existing_data, data, file_path)
80
+ end
81
+
82
+ return summary if config.dry_run
83
+
84
+ # Ensure output directory exists
85
+ FileUtils.mkdir_p(File.dirname(file_path))
86
+
87
+ File.write(file_path, YAML.dump(data))
88
+
89
+ nil
90
+ rescue Errno::EACCES => e
91
+ raise FileError.new("Permission denied: #{file_path}", context: { error: e.message })
92
+ rescue StandardError => e
93
+ raise FileError.new("Failed to write YAML: #{file_path}", context: { error: e.message })
94
+ end
95
+
96
+ # Get translatable strings from source YAML
97
+ #
98
+ # Reads the input file and returns a flattened hash of strings.
99
+ # Removes the root language key if present.
100
+ #
101
+ # @return [Hash] Flattened hash of translatable strings
102
+ #
103
+ # @example
104
+ # strings = handler.get_source_strings
105
+ # #=> { "greeting" => "Hello", "nav.home" => "Home" }
106
+ #
107
+ def get_source_strings
108
+ return {} unless config.input_file
109
+
110
+ source_data = read_yaml(config.input_file)
111
+ # Remove root language key if present (e.g., "en:")
112
+ source_data = source_data[config.source_language] || source_data
113
+
114
+ Utils::HashFlattener.flatten(source_data)
115
+ end
116
+
117
+ # Filter out excluded keys for a specific language
118
+ #
119
+ # @param strings [Hash] Flattened strings
120
+ # @param target_lang_code [String] Target language code
121
+ # @return [Hash] Filtered strings
122
+ #
123
+ # @example
124
+ # filtered = handler.filter_exclusions(strings, "it")
125
+ #
126
+ def filter_exclusions(strings, target_lang_code)
127
+ excluded_keys = config.global_exclusions.dup
128
+ excluded_keys += config.exclusions_per_language[target_lang_code] || []
129
+
130
+ strings.reject { |key, _| excluded_keys.include?(key) }
131
+ end
132
+
133
+ # Merge translated strings with existing file (incremental mode)
134
+ #
135
+ # @param file_path [String] Existing file path
136
+ # @param new_translations [Hash] New translations (flattened)
137
+ # @return [Hash] Merged translations (nested)
138
+ #
139
+ # @example
140
+ # merged = handler.merge_translations("config/locales/it.yml", new_translations)
141
+ #
142
+ def merge_translations(file_path, new_translations)
143
+ # @type var existing: Hash[String, untyped]
144
+ existing = File.exist?(file_path) ? read_yaml(file_path) : {}
145
+ existing_flat = Utils::HashFlattener.flatten(existing)
146
+
147
+ # Merge: existing takes precedence
148
+ merged = new_translations.merge(existing_flat)
149
+
150
+ Utils::HashFlattener.unflatten(merged)
151
+ end
152
+
153
+ # Build output file path for target language
154
+ #
155
+ # @param target_lang_code [String] Target language code
156
+ # @return [String] Output file path
157
+ #
158
+ # @example
159
+ # path = handler.build_output_path("it")
160
+ # #=> "config/locales/it.yml"
161
+ #
162
+ def build_output_path(target_lang_code)
163
+ return "#{target_lang_code}.yml" unless config.output_folder
164
+
165
+ File.join(config.output_folder, "#{target_lang_code}.yml")
166
+ end
167
+ end
168
+ end
@@ -1,99 +1,123 @@
1
1
  # frozen_string_literal: true
2
- require "ruby-progressbar"
3
2
 
4
- require "better_translate/version"
5
- require "better_translate/utils"
6
- require "better_translate/translator"
7
- require "better_translate/service"
8
- require "better_translate/writer"
9
- require "better_translate/helper"
10
- require "better_translate/similarity_analyzer"
3
+ require_relative "better_translate/version"
4
+ require_relative "better_translate/errors"
5
+ require_relative "better_translate/configuration"
6
+ require_relative "better_translate/cache"
7
+ require_relative "better_translate/rate_limiter"
8
+ require_relative "better_translate/validator"
9
+ require_relative "better_translate/variable_extractor"
10
+ require_relative "better_translate/utils/hash_flattener"
11
+ require_relative "better_translate/providers/base_http_provider"
12
+ require_relative "better_translate/providers/chatgpt_provider"
13
+ require_relative "better_translate/providers/gemini_provider"
14
+ require_relative "better_translate/providers/anthropic_provider"
15
+ require_relative "better_translate/provider_factory"
16
+ require_relative "better_translate/yaml_handler"
17
+ require_relative "better_translate/progress_tracker"
18
+ require_relative "better_translate/strategies/base_strategy"
19
+ require_relative "better_translate/strategies/deep_strategy"
20
+ require_relative "better_translate/strategies/batch_strategy"
21
+ require_relative "better_translate/strategies/strategy_selector"
22
+ require_relative "better_translate/translator"
23
+ require_relative "better_translate/direct_translator"
24
+ require_relative "better_translate/cli"
11
25
 
12
- require 'better_translate/providers/base_provider'
13
- require 'better_translate/providers/chatgpt_provider'
14
- require 'better_translate/providers/gemini_provider'
26
+ # Load Rails integration if Rails is present
27
+ require_relative "better_translate/railtie" if defined?(Rails::Railtie)
15
28
 
16
- require 'ostruct'
17
-
18
- # Main module for the BetterTranslate gem.
19
- # Provides functionality for translating YAML files using various AI providers.
29
+ # BetterTranslate - AI-powered YAML locale file translator
30
+ #
31
+ # Automatically translate YAML locale files using AI providers (ChatGPT, Gemini, Claude).
32
+ # Features intelligent caching, batch processing, and Rails integration.
20
33
  #
21
- # @example Basic configuration and usage
34
+ # @example Basic usage
22
35
  # BetterTranslate.configure do |config|
23
36
  # config.provider = :chatgpt
24
37
  # config.openai_key = ENV['OPENAI_API_KEY']
25
- # config.input_file = 'config/locales/en.yml'
26
- # config.target_languages = [{short_name: 'fr', name: 'French'}]
38
+ # config.source_language = "en"
39
+ # config.target_languages = [
40
+ # { short_name: "it", name: "Italian" },
41
+ # { short_name: "fr", name: "French" }
42
+ # ]
43
+ # config.input_file = "config/locales/en.yml"
44
+ # config.output_folder = "config/locales"
27
45
  # end
28
- #
29
- # BetterTranslate.magic # Start the translation process
46
+ #
47
+ # BetterTranslate.translate_files
48
+ #
30
49
  module BetterTranslate
31
50
  class << self
32
- # Configuration object for the gem
33
- # @return [OpenStruct] The configuration object
34
- attr_accessor :configuration
35
-
36
- # Configures the gem with the provided block.
37
- # Sets up the configuration object and yields it to the block.
51
+ # Configure BetterTranslate
52
+ #
53
+ # @yieldparam config [Configuration] Configuration object to modify
54
+ # @return [Configuration] The configuration object
55
+ #
56
+ # @example
57
+ # BetterTranslate.configure do |config|
58
+ # config.provider = :chatgpt
59
+ # config.openai_key = ENV['OPENAI_API_KEY']
60
+ # config.source_language = "en"
61
+ # config.target_languages = [{ short_name: "it", name: "Italian" }]
62
+ # end
38
63
  #
39
- # @yield [configuration] Yields the configuration object to the block
40
- # @yieldparam configuration [OpenStruct] The configuration object
41
- # @return [OpenStruct] The updated configuration object
42
64
  def configure
43
- self.configuration ||= OpenStruct.new
44
- yield(configuration) if block_given?
65
+ yield(configuration)
66
+ configuration
45
67
  end
46
68
 
47
- # Installs the gem configuration file (initializer) in a Rails application.
48
- # Copies the template initializer to the Rails config/initializers directory.
49
- # Only works in a Rails environment.
69
+ # Get current configuration
50
70
  #
51
- # @return [void]
52
- # @note This method will log an error if not called from a Rails application
53
- def install
54
- unless defined?(Rails) && Rails.respond_to?(:root)
55
- message = "The install method is only available in a Rails application."
56
- BetterTranslate::Utils.logger(message: message)
57
- return
58
- end
59
-
60
- # Builds the path to the template folder inside the gem
61
- source = File.expand_path("../generators/better_translate/templates/better_translate.rb", __dir__)
62
- destination = File.join(Rails.root, "config", "initializers", "better_translate.rb")
71
+ # @return [Configuration] Current configuration instance
72
+ #
73
+ # @example
74
+ # config = BetterTranslate.configuration
75
+ # config.provider #=> :chatgpt
76
+ #
77
+ def configuration
78
+ @configuration ||= Configuration.new
79
+ end
63
80
 
64
- if File.exist?(destination)
65
- message = "The initializer file already exists: #{destination}"
66
- BetterTranslate::Utils.logger(message: message)
67
- else
68
- FileUtils.mkdir_p(File.dirname(destination))
69
- FileUtils.cp(source, destination)
70
- message = "The initializer file already exists: #{destination}"
71
- BetterTranslate::Utils.logger(message: message)
81
+ # Translate files using current configuration
82
+ #
83
+ # @return [Hash] Results with :success_count, :failure_count, :errors
84
+ # @raise [ConfigurationError] if configuration is invalid or missing
85
+ #
86
+ # @example
87
+ # results = BetterTranslate.translate_files
88
+ # puts "Success: #{results[:success_count]}, Failures: #{results[:failure_count]}"
89
+ #
90
+ def translate_files
91
+ unless @configuration
92
+ raise ConfigurationError,
93
+ "BetterTranslate is not configured. Call BetterTranslate.configure first."
72
94
  end
95
+
96
+ translator = Translator.new(configuration)
97
+ translator.translate_all
73
98
  end
74
99
 
75
- # Starts the translation process using the configured settings.
76
- # This is the main entry point for the translation functionality.
77
- # Logs the start and completion of the translation process.
100
+ # Reset configuration
78
101
  #
79
102
  # @return [void]
103
+ #
80
104
  # @example
81
- # BetterTranslate.magic
82
- def magic
83
- message = "Magic method invoked: Translation will begin..."
84
- BetterTranslate::Utils.logger(message: message)
85
- # Utilizziamo il logger per tutti i messaggi
86
- message = "\n[BetterTranslate] Starting translation process...\n"
87
- BetterTranslate::Utils.logger(message: message)
88
-
89
- BetterTranslate::Translator.work
90
-
91
- message = "Magic method invoked: Translation completed successfully!"
92
- BetterTranslate::Utils.logger(message: message)
93
- # Utilizziamo il logger per tutti i messaggi
94
- message = "\n[BetterTranslate] Translation completed successfully!\n"
95
- BetterTranslate::Utils.logger(message: message)
105
+ # BetterTranslate.reset!
106
+ # BetterTranslate.configuration.provider #=> nil
107
+ #
108
+ def reset!
109
+ @configuration = nil
96
110
  end
97
111
 
112
+ # Get gem version
113
+ #
114
+ # @return [String] Version string
115
+ #
116
+ # @example
117
+ # BetterTranslate.version #=> "0.1.0"
118
+ #
119
+ def version
120
+ VERSION
121
+ end
98
122
  end
99
- end
123
+ end
@@ -0,0 +1,12 @@
1
+ Description:
2
+ Analyzes YAML locale file structure and provides statistics.
3
+
4
+ This will show you:
5
+ - Total number of translation strings
6
+ - File structure with nested keys
7
+ - String counts per section
8
+
9
+ Example:
10
+ bin/rails generate better_translate:analyze config/locales/en.yml
11
+
12
+ This will analyze the en.yml file and display statistics.