better_translate 1.1.0 → 1.1.1
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/CLAUDE.md +12 -7
- data/CONTRIBUTING.md +432 -0
- data/README.md +7 -1
- data/Rakefile +14 -1
- data/SECURITY.md +160 -0
- data/Steepfile +0 -1
- data/brakeman.yml +37 -0
- data/codecov.yml +34 -0
- data/lib/better_translate/analyzer/code_scanner.rb +0 -2
- data/lib/better_translate/analyzer/orphan_detector.rb +6 -3
- data/lib/better_translate/analyzer/reporter.rb +1 -1
- data/lib/better_translate/cli.rb +2 -2
- data/lib/better_translate/configuration.rb +28 -1
- data/lib/better_translate/json_handler.rb +2 -2
- data/lib/better_translate/translator.rb +3 -2
- data/lib/better_translate/version.rb +1 -1
- data/lib/better_translate.rb +2 -0
- data/lib/generators/better_translate/install/install_generator.rb +2 -2
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +22 -34
- data/lib/generators/better_translate/translate/translate_generator.rb +65 -46
- data/lib/tasks/better_translate.rake +62 -45
- data/sig/better_translate/analyzer/code_scanner.rbs +59 -0
- data/sig/better_translate/analyzer/key_scanner.rbs +40 -0
- data/sig/better_translate/analyzer/orphan_detector.rbs +43 -0
- data/sig/better_translate/analyzer/reporter.rbs +70 -0
- data/sig/better_translate/cli.rbs +2 -0
- data/sig/better_translate/json_handler.rbs +65 -0
- data/sig/better_translate/progress_tracker.rbs +1 -1
- data/sig/better_translate.rbs +4 -0
- data/sig/csv.rbs +16 -0
- metadata +26 -3
- data/regenerate_vcr.rb +0 -47
    
        data/brakeman.yml
    ADDED
    
    | @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            # Brakeman configuration file
         | 
| 2 | 
            +
            # https://brakemanscanner.org/docs/options/
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            # Set application path (defaults to current directory)
         | 
| 5 | 
            +
            :app_path: "."
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            # Set Rails version (detected automatically)
         | 
| 8 | 
            +
            # :rails_version: "8.1.0"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            # Enable additional security checks
         | 
| 11 | 
            +
            :force_scan: true
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            # Show all files processed
         | 
| 14 | 
            +
            :report_progress: true
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            # Skip certain checks (none skipped by default)
         | 
| 17 | 
            +
            :skip_checks: []
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            # Only run specific checks (empty means run all)
         | 
| 20 | 
            +
            :run_checks: []
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            # Set confidence levels to report (1=High, 2=Medium, 3=Weak)
         | 
| 23 | 
            +
            :min_confidence: 2
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            # Ignore specific warnings
         | 
| 26 | 
            +
            :ignore_file: ".brakeman.ignore"
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            # Additional paths to scan
         | 
| 29 | 
            +
            :additional_checks_path: []
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            # Paths to exclude from scanning
         | 
| 32 | 
            +
            :skip_files:
         | 
| 33 | 
            +
              - "spec/"
         | 
| 34 | 
            +
              - "test/"
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            # Exit with error code if warnings found
         | 
| 37 | 
            +
            :exit_on_warn: true
         | 
    
        data/codecov.yml
    ADDED
    
    | @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            # Codecov configuration
         | 
| 2 | 
            +
            # Documentation: https://docs.codecov.com/docs/codecov-yaml
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            codecov:
         | 
| 5 | 
            +
              require_ci_to_pass: yes
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            coverage:
         | 
| 8 | 
            +
              precision: 2
         | 
| 9 | 
            +
              round: down
         | 
| 10 | 
            +
              range: "70...100"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              status:
         | 
| 13 | 
            +
                project:
         | 
| 14 | 
            +
                  default:
         | 
| 15 | 
            +
                    target: 90%
         | 
| 16 | 
            +
                    threshold: 1%
         | 
| 17 | 
            +
                    if_ci_failed: error
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                patch:
         | 
| 20 | 
            +
                  default:
         | 
| 21 | 
            +
                    target: 90%
         | 
| 22 | 
            +
                    threshold: 1%
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            comment:
         | 
| 25 | 
            +
              layout: "reach,diff,flags,tree,footer"
         | 
| 26 | 
            +
              behavior: default
         | 
| 27 | 
            +
              require_changes: false
         | 
| 28 | 
            +
              require_base: false
         | 
| 29 | 
            +
              require_head: true
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            ignore:
         | 
| 32 | 
            +
              - "spec/**/*"
         | 
| 33 | 
            +
              - "vendor/**/*"
         | 
| 34 | 
            +
              - "gemfiles/**/*"
         | 
| @@ -64,9 +64,12 @@ module BetterTranslate | |
| 64 64 | 
             
                  #   #=> { "orphan_key" => "This is never used" }
         | 
| 65 65 | 
             
                  #
         | 
| 66 66 | 
             
                  def orphan_details
         | 
| 67 | 
            -
                    @ | 
| 68 | 
            -
             | 
| 67 | 
            +
                    # @type var result: Hash[String, untyped]
         | 
| 68 | 
            +
                    result = {}
         | 
| 69 | 
            +
                    @orphans.each do |key|
         | 
| 70 | 
            +
                      result[key] = all_keys[key]
         | 
| 69 71 | 
             
                    end
         | 
| 72 | 
            +
                    result
         | 
| 70 73 | 
             
                  end
         | 
| 71 74 |  | 
| 72 75 | 
             
                  # Calculate usage percentage
         | 
| @@ -81,7 +84,7 @@ module BetterTranslate | |
| 81 84 | 
             
                    return 0.0 if all_keys.empty?
         | 
| 82 85 |  | 
| 83 86 | 
             
                    used_count = all_keys.size - @orphans.size
         | 
| 84 | 
            -
                    (used_count.to_f / all_keys.size * 100).round(1)
         | 
| 87 | 
            +
                    (used_count.to_f / all_keys.size * 100).round(1).to_f
         | 
| 85 88 | 
             
                  end
         | 
| 86 89 | 
             
                end
         | 
| 87 90 | 
             
              end
         | 
    
        data/lib/better_translate/cli.rb
    CHANGED
    
    | @@ -225,8 +225,8 @@ module BetterTranslate | |
| 225 225 | 
             
                    "dry_run" => false,
         | 
| 226 226 | 
             
                    "translation_mode" => "override",
         | 
| 227 227 | 
             
                    "preserve_variables" => true,
         | 
| 228 | 
            -
                    "global_exclusions" => [],
         | 
| 229 | 
            -
                    "exclusions_per_language" => {},
         | 
| 228 | 
            +
                    "global_exclusions" => [], # : Array[String]
         | 
| 229 | 
            +
                    "exclusions_per_language" => {}, # : Hash[String, Array[String]]
         | 
| 230 230 | 
             
                    "model" => nil,
         | 
| 231 231 | 
             
                    "temperature" => 0.3,
         | 
| 232 232 | 
             
                    "max_tokens" => 2000,
         | 
| @@ -1,5 +1,8 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require "fileutils"
         | 
| 4 | 
            +
            require "json"
         | 
| 5 | 
            +
             | 
| 3 6 | 
             
            module BetterTranslate
         | 
| 4 7 | 
             
              # Configuration class for BetterTranslate
         | 
| 5 8 | 
             
              #
         | 
| @@ -216,7 +219,12 @@ module BetterTranslate | |
| 216 219 |  | 
| 217 220 | 
             
                  # Only validate input_file exists if using single file mode (not glob pattern or array)
         | 
| 218 221 | 
             
                  return unless input_file && !input_file.empty? && !input_files
         | 
| 219 | 
            -
             | 
| 222 | 
            +
             | 
| 223 | 
            +
                  # Create input file if it doesn't exist
         | 
| 224 | 
            +
                  return if File.exist?(input_file)
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                  create_default_input_file!(input_file)
         | 
| 227 | 
            +
                  puts "Created empty input file: #{input_file}" if verbose
         | 
| 220 228 | 
             
                end
         | 
| 221 229 |  | 
| 222 230 | 
             
                # Validate optional settings (timeouts, retries, cache, etc.)
         | 
| @@ -243,5 +251,24 @@ module BetterTranslate | |
| 243 251 | 
             
                  # Validate max_tokens is positive
         | 
| 244 252 | 
             
                  raise ConfigurationError, "Max tokens must be positive" if max_tokens && max_tokens <= 0
         | 
| 245 253 | 
             
                end
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                # Create a default input file with root language key
         | 
| 256 | 
            +
                #
         | 
| 257 | 
            +
                # @param file_path [String] Path to the input file
         | 
| 258 | 
            +
                # @return [void]
         | 
| 259 | 
            +
                # @api private
         | 
| 260 | 
            +
                def create_default_input_file!(file_path)
         | 
| 261 | 
            +
                  # Create directory if needed
         | 
| 262 | 
            +
                  FileUtils.mkdir_p(File.dirname(file_path))
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                  # Determine file format (YAML or JSON)
         | 
| 265 | 
            +
                  content = if file_path.end_with?(".json")
         | 
| 266 | 
            +
                              JSON.pretty_generate({ source_language => {} })
         | 
| 267 | 
            +
                            else
         | 
| 268 | 
            +
                              { source_language => {} }.to_yaml
         | 
| 269 | 
            +
                            end
         | 
| 270 | 
            +
             | 
| 271 | 
            +
                  File.write(file_path, content)
         | 
| 272 | 
            +
                end
         | 
| 246 273 | 
             
              end
         | 
| 247 274 | 
             
            end
         | 
| @@ -76,7 +76,7 @@ module BetterTranslate | |
| 76 76 |  | 
| 77 77 | 
             
                  # Show diff preview if in dry run mode
         | 
| 78 78 | 
             
                  if config.dry_run && diff_preview
         | 
| 79 | 
            -
                    existing_data = File.exist?(file_path) ? read_json(file_path) : {}
         | 
| 79 | 
            +
                    existing_data = File.exist?(file_path) ? read_json(file_path) : {} # : Hash[untyped, untyped]
         | 
| 80 80 | 
             
                    summary = diff_preview.show_diff(existing_data, data, file_path)
         | 
| 81 81 | 
             
                  end
         | 
| 82 82 |  | 
| @@ -151,7 +151,7 @@ module BetterTranslate | |
| 151 151 | 
             
                    target_lang = config.target_languages.first[:short_name]
         | 
| 152 152 | 
             
                    existing = existing[target_lang] || existing
         | 
| 153 153 | 
             
                  else
         | 
| 154 | 
            -
                    existing = {}
         | 
| 154 | 
            +
                    existing = {} # : Hash[untyped, untyped]
         | 
| 155 155 | 
             
                  end
         | 
| 156 156 |  | 
| 157 157 | 
             
                  existing_flat = Utils::HashFlattener.flatten(existing)
         | 
| @@ -116,7 +116,7 @@ module BetterTranslate | |
| 116 116 | 
             
                            # Backward compatibility with single input_file
         | 
| 117 117 | 
             
                            [config.input_file]
         | 
| 118 118 | 
             
                          else
         | 
| 119 | 
            -
                            []
         | 
| 119 | 
            +
                            [] # : Array[String]
         | 
| 120 120 | 
             
                          end
         | 
| 121 121 |  | 
| 122 122 | 
             
                  # Validate files exist (unless glob pattern that found nothing)
         | 
| @@ -247,7 +247,8 @@ module BetterTranslate | |
| 247 247 | 
             
                  translated = strategy.translate(strings_to_translate, target_lang_code, target_lang_name)
         | 
| 248 248 |  | 
| 249 249 | 
             
                  # Save - generate output path with proper filename
         | 
| 250 | 
            -
                   | 
| 250 | 
            +
                  current_input_file = config.input_file or raise "No input file set"
         | 
| 251 | 
            +
                  output_path = build_output_path_for_file(current_input_file, target_lang_code)
         | 
| 251 252 |  | 
| 252 253 | 
             
                  final_translations = if config.translation_mode == :incremental
         | 
| 253 254 | 
             
                                         handler.merge_translations(output_path, translated)
         | 
    
        data/lib/better_translate.rb
    CHANGED
    
    
| @@ -47,8 +47,8 @@ module BetterTranslate | |
| 47 47 | 
             
                      "dry_run" => false,
         | 
| 48 48 | 
             
                      "translation_mode" => "override",
         | 
| 49 49 | 
             
                      "preserve_variables" => true,
         | 
| 50 | 
            -
                      "global_exclusions" => [],
         | 
| 51 | 
            -
                      "exclusions_per_language" => {},
         | 
| 50 | 
            +
                      "global_exclusions" => [], # : Array[String]
         | 
| 51 | 
            +
                      "exclusions_per_language" => {}, # : Hash[String, Array[String]]
         | 
| 52 52 | 
             
                      "model" => nil,
         | 
| 53 53 | 
             
                      "temperature" => 0.3,
         | 
| 54 54 | 
             
                      "max_tokens" => 2000,
         | 
| @@ -2,6 +2,10 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            # BetterTranslate Configuration
         | 
| 4 4 | 
             
            #
         | 
| 5 | 
            +
            # IMPORTANT: I18n configuration is not yet available when initializers load.
         | 
| 6 | 
            +
            # You must manually set source_language and target_languages to match your
         | 
| 7 | 
            +
            # Rails I18n configuration in config/application.rb
         | 
| 8 | 
            +
            #
         | 
| 5 9 | 
             
            # For more configuration options, see config/better_translate.yml
         | 
| 6 10 |  | 
| 7 11 | 
             
            BetterTranslate.configure do |config|
         | 
| @@ -14,43 +18,27 @@ BetterTranslate.configure do |config| | |
| 14 18 | 
             
              config.anthropic_key = ENV["ANTHROPIC_API_KEY"]
         | 
| 15 19 |  | 
| 16 20 | 
             
              # Source and target languages
         | 
| 17 | 
            -
              # | 
| 18 | 
            -
              #  | 
| 21 | 
            +
              #
         | 
| 22 | 
            +
              # IMPORTANT: These must be set manually to match your Rails I18n config
         | 
| 23 | 
            +
              # (I18n.default_locale and I18n.available_locales are not yet available)
         | 
| 24 | 
            +
              #
         | 
| 25 | 
            +
              # Example: If your config/application.rb has:
         | 
| 19 26 | 
             
              #   config.i18n.default_locale = :it
         | 
| 20 | 
            -
              #   config.i18n.available_locales = [:it, :en, : | 
| 21 | 
            -
               | 
| 22 | 
            -
             | 
| 23 | 
            -
              #  | 
| 24 | 
            -
              #  | 
| 25 | 
            -
              available_targets = (I18n.available_locales - [I18n.default_locale]).map(&:to_s)
         | 
| 27 | 
            +
              #   config.i18n.available_locales = [:it, :en, :fr, :ja, :ru]
         | 
| 28 | 
            +
              #
         | 
| 29 | 
            +
              # Then set:
         | 
| 30 | 
            +
              #   config.source_language = "it"  # matches default_locale
         | 
| 31 | 
            +
              #   config.target_languages = [...]  # matches available_locales (excluding source)
         | 
| 26 32 |  | 
| 27 | 
            -
              #  | 
| 28 | 
            -
              language_names = {
         | 
| 29 | 
            -
                "en" => "English", "it" => "Italian", "es" => "Spanish", "fr" => "French",
         | 
| 30 | 
            -
                "de" => "German", "pt" => "Portuguese", "ru" => "Russian", "zh" => "Chinese",
         | 
| 31 | 
            -
                "ja" => "Japanese", "ko" => "Korean", "ar" => "Arabic", "nl" => "Dutch",
         | 
| 32 | 
            -
                "pl" => "Polish", "tr" => "Turkish", "sv" => "Swedish", "da" => "Danish",
         | 
| 33 | 
            -
                "fi" => "Finnish", "no" => "Norwegian", "cs" => "Czech", "el" => "Greek",
         | 
| 34 | 
            -
                "he" => "Hebrew", "hi" => "Hindi", "th" => "Thai", "vi" => "Vietnamese"
         | 
| 35 | 
            -
              }
         | 
| 33 | 
            +
              config.source_language = "en" # TODO: Change to match your default_locale
         | 
| 36 34 |  | 
| 37 | 
            -
               | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
                end
         | 
| 45 | 
            -
              else
         | 
| 46 | 
            -
                # Fallback: suggest common languages
         | 
| 47 | 
            -
                # Uncomment and modify the languages you want to translate to
         | 
| 48 | 
            -
                config.target_languages = [
         | 
| 49 | 
            -
                  { short_name: "it", name: "Italian" },
         | 
| 50 | 
            -
                  { short_name: "es", name: "Spanish" },
         | 
| 51 | 
            -
                  { short_name: "fr", name: "French" }
         | 
| 52 | 
            -
                ]
         | 
| 53 | 
            -
              end
         | 
| 35 | 
            +
              # Target languages (excluding source language)
         | 
| 36 | 
            +
              # TODO: Change to match your available_locales
         | 
| 37 | 
            +
              config.target_languages = [
         | 
| 38 | 
            +
                { short_name: "it", name: "Italian" },
         | 
| 39 | 
            +
                { short_name: "es", name: "Spanish" },
         | 
| 40 | 
            +
                { short_name: "fr", name: "French" }
         | 
| 41 | 
            +
              ]
         | 
| 54 42 |  | 
| 55 43 | 
             
              # File paths
         | 
| 56 44 | 
             
              # Uses source_language for input file
         | 
| @@ -29,57 +29,75 @@ module BetterTranslate | |
| 29 29 | 
             
                  #
         | 
| 30 30 | 
             
                  # @return [void]
         | 
| 31 31 | 
             
                  #
         | 
| 32 | 
            +
                  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
         | 
| 32 33 | 
             
                  def run_translation
         | 
| 33 | 
            -
                     | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
                       | 
| 37 | 
            -
             | 
| 38 | 
            -
                       | 
| 39 | 
            -
             | 
| 34 | 
            +
                    # Check if configuration is already loaded (from initializer)
         | 
| 35 | 
            +
                    if BetterTranslate.configuration.provider.nil?
         | 
| 36 | 
            +
                      # No initializer configuration found, try loading from YAML
         | 
| 37 | 
            +
                      config_file = Rails.root.join("config", "better_translate.yml")
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      unless File.exist?(config_file)
         | 
| 40 | 
            +
                        say "No configuration found", :red
         | 
| 41 | 
            +
                        say "Either:"
         | 
| 42 | 
            +
                        say "  1. Create config/initializers/better_translate.rb (recommended)"
         | 
| 43 | 
            +
                        say "  2. Run 'rails generate better_translate:install' to create YAML config"
         | 
| 44 | 
            +
                        return
         | 
| 45 | 
            +
                      end
         | 
| 40 46 |  | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 47 | 
            +
                      # Load configuration from YAML
         | 
| 48 | 
            +
                      yaml_config = YAML.load_file(config_file)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                      # Configure BetterTranslate
         | 
| 51 | 
            +
                      BetterTranslate.configure do |config|
         | 
| 52 | 
            +
                        config.provider = yaml_config["provider"]&.to_sym
         | 
| 53 | 
            +
                        config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
         | 
| 54 | 
            +
                        config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
         | 
| 55 | 
            +
                        config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                        config.source_language = yaml_config["source_language"]
         | 
| 58 | 
            +
                        config.target_languages = yaml_config["target_languages"]&.map do |lang|
         | 
| 59 | 
            +
                          if lang.is_a?(Hash)
         | 
| 60 | 
            +
                            { short_name: lang["short_name"], name: lang["name"] }
         | 
| 61 | 
            +
                          else
         | 
| 62 | 
            +
                            lang
         | 
| 63 | 
            +
                          end
         | 
| 57 64 | 
             
                        end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                        config.input_file = Rails.root.join(yaml_config["input_file"]).to_s
         | 
| 67 | 
            +
                        config.output_folder = Rails.root.join(yaml_config["output_folder"]).to_s
         | 
| 68 | 
            +
                        config.verbose = yaml_config.fetch("verbose", true)
         | 
| 69 | 
            +
                        config.dry_run = options[:dry_run] || yaml_config.fetch("dry_run", false)
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                        # Map "full" to :override for backward compatibility
         | 
| 72 | 
            +
                        translation_mode = yaml_config.fetch("translation_mode", "override")
         | 
| 73 | 
            +
                        translation_mode = "override" if translation_mode == "full"
         | 
| 74 | 
            +
                        config.translation_mode = translation_mode.to_sym
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                        config.preserve_variables = yaml_config.fetch("preserve_variables", true)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                        # Exclusions
         | 
| 79 | 
            +
                        config.global_exclusions = yaml_config["global_exclusions"] || []
         | 
| 80 | 
            +
                        config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                        # Provider options
         | 
| 83 | 
            +
                        config.model = yaml_config["model"] if yaml_config["model"]
         | 
| 84 | 
            +
                        config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
         | 
| 85 | 
            +
                        config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
         | 
| 86 | 
            +
                        config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
         | 
| 87 | 
            +
                        config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
         | 
| 88 | 
            +
                        config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
         | 
| 58 89 | 
             
                      end
         | 
| 90 | 
            +
                    elsif options[:dry_run]
         | 
| 91 | 
            +
                      # Configuration from initializer exists, but apply dry_run option if provided
         | 
| 92 | 
            +
                      BetterTranslate.configuration.dry_run = true
         | 
| 93 | 
            +
                    end
         | 
| 59 94 |  | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
                       | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
                       | 
| 66 | 
            -
                      translation_mode = yaml_config.fetch("translation_mode", "override")
         | 
| 67 | 
            -
                      translation_mode = "override" if translation_mode == "full"
         | 
| 68 | 
            -
                      config.translation_mode = translation_mode.to_sym
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                      config.preserve_variables = yaml_config.fetch("preserve_variables", true)
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                      # Exclusions
         | 
| 73 | 
            -
                      config.global_exclusions = yaml_config["global_exclusions"] || []
         | 
| 74 | 
            -
                      config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
         | 
| 75 | 
            -
             | 
| 76 | 
            -
                      # Provider options
         | 
| 77 | 
            -
                      config.model = yaml_config["model"] if yaml_config["model"]
         | 
| 78 | 
            -
                      config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
         | 
| 79 | 
            -
                      config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
         | 
| 80 | 
            -
                      config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
         | 
| 81 | 
            -
                      config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
         | 
| 82 | 
            -
                      config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
         | 
| 95 | 
            +
                    # Validate configuration (whether from initializer or YAML)
         | 
| 96 | 
            +
                    begin
         | 
| 97 | 
            +
                      BetterTranslate.configuration.validate!
         | 
| 98 | 
            +
                    rescue BetterTranslate::ConfigurationError => e
         | 
| 99 | 
            +
                      say "Invalid configuration: #{e.message}", :red
         | 
| 100 | 
            +
                      return
         | 
| 83 101 | 
             
                    end
         | 
| 84 102 |  | 
| 85 103 | 
             
                    # Perform translation
         | 
| @@ -110,6 +128,7 @@ module BetterTranslate | |
| 110 128 | 
             
                      say "  - #{error[:language]}: #{error[:error]}", :red
         | 
| 111 129 | 
             
                    end
         | 
| 112 130 | 
             
                  end
         | 
| 131 | 
            +
                  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
         | 
| 113 132 | 
             
                end
         | 
| 114 133 | 
             
              end
         | 
| 115 134 | 
             
            end
         | 
| @@ -9,59 +9,75 @@ unless Rake::Task.task_defined?(:environment) | |
| 9 9 | 
             
              end
         | 
| 10 10 | 
             
            end
         | 
| 11 11 |  | 
| 12 | 
            +
            # rubocop:disable Metrics/BlockLength
         | 
| 12 13 | 
             
            namespace :better_translate do
         | 
| 13 14 | 
             
              desc "Translate YAML locale files using AI providers"
         | 
| 14 15 | 
             
              task translate: :environment do
         | 
| 15 | 
            -
                 | 
| 16 | 
            +
                # Check if configuration is already loaded (from initializer)
         | 
| 17 | 
            +
                if BetterTranslate.configuration.provider.nil?
         | 
| 18 | 
            +
                  # No initializer configuration found, try loading from YAML
         | 
| 19 | 
            +
                  config_file = Rails.root.join("config", "better_translate.yml")
         | 
| 16 20 |  | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 21 | 
            +
                  unless File.exist?(config_file)
         | 
| 22 | 
            +
                    puts "Error: No configuration found"
         | 
| 23 | 
            +
                    puts "Either:"
         | 
| 24 | 
            +
                    puts "  1. Create config/initializers/better_translate.rb (recommended)"
         | 
| 25 | 
            +
                    puts "  2. Run 'rake better_translate:config:generate' to create YAML config"
         | 
| 26 | 
            +
                    exit 1
         | 
| 27 | 
            +
                  end
         | 
| 22 28 |  | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 29 | 
            +
                  # Load configuration from YAML
         | 
| 30 | 
            +
                  yaml_config = YAML.load_file(config_file)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  # Configure BetterTranslate
         | 
| 33 | 
            +
                  BetterTranslate.configure do |config|
         | 
| 34 | 
            +
                    config.provider = yaml_config["provider"]&.to_sym
         | 
| 35 | 
            +
                    config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
         | 
| 36 | 
            +
                    config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
         | 
| 37 | 
            +
                    config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    config.source_language = yaml_config["source_language"]
         | 
| 40 | 
            +
                    config.target_languages = yaml_config["target_languages"]&.map do |lang|
         | 
| 41 | 
            +
                      if lang.is_a?(Hash)
         | 
| 42 | 
            +
                        { short_name: lang["short_name"], name: lang["name"] }
         | 
| 43 | 
            +
                      else
         | 
| 44 | 
            +
                        lang
         | 
| 45 | 
            +
                      end
         | 
| 39 46 | 
             
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    config.input_file = yaml_config["input_file"]
         | 
| 49 | 
            +
                    config.output_folder = yaml_config["output_folder"]
         | 
| 50 | 
            +
                    config.verbose = yaml_config.fetch("verbose", true)
         | 
| 51 | 
            +
                    config.dry_run = yaml_config.fetch("dry_run", false)
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    # Map "full" to :override for backward compatibility
         | 
| 54 | 
            +
                    translation_mode = yaml_config.fetch("translation_mode", "override")
         | 
| 55 | 
            +
                    translation_mode = "override" if translation_mode == "full"
         | 
| 56 | 
            +
                    config.translation_mode = translation_mode.to_sym
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    config.preserve_variables = yaml_config.fetch("preserve_variables", true)
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    # Exclusions
         | 
| 61 | 
            +
                    config.global_exclusions = yaml_config["global_exclusions"] || []
         | 
| 62 | 
            +
                    config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    # Provider options
         | 
| 65 | 
            +
                    config.model = yaml_config["model"] if yaml_config["model"]
         | 
| 66 | 
            +
                    config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
         | 
| 67 | 
            +
                    config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
         | 
| 68 | 
            +
                    config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
         | 
| 69 | 
            +
                    config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
         | 
| 70 | 
            +
                    config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
         | 
| 40 71 | 
             
                  end
         | 
| 72 | 
            +
                end
         | 
| 41 73 |  | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
                   | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
                   | 
| 48 | 
            -
                   | 
| 49 | 
            -
                  translation_mode = "override" if translation_mode == "full"
         | 
| 50 | 
            -
                  config.translation_mode = translation_mode.to_sym
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                  config.preserve_variables = yaml_config.fetch("preserve_variables", true)
         | 
| 53 | 
            -
             | 
| 54 | 
            -
                  # Exclusions
         | 
| 55 | 
            -
                  config.global_exclusions = yaml_config["global_exclusions"] || []
         | 
| 56 | 
            -
                  config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                  # Provider options
         | 
| 59 | 
            -
                  config.model = yaml_config["model"] if yaml_config["model"]
         | 
| 60 | 
            -
                  config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
         | 
| 61 | 
            -
                  config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
         | 
| 62 | 
            -
                  config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
         | 
| 63 | 
            -
                  config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
         | 
| 64 | 
            -
                  config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
         | 
| 74 | 
            +
                # Validate configuration (whether from initializer or YAML)
         | 
| 75 | 
            +
                begin
         | 
| 76 | 
            +
                  BetterTranslate.configuration.validate!
         | 
| 77 | 
            +
                rescue BetterTranslate::ConfigurationError => e
         | 
| 78 | 
            +
                  puts "Error: Invalid configuration"
         | 
| 79 | 
            +
                  puts e.message
         | 
| 80 | 
            +
                  exit 1
         | 
| 65 81 | 
             
                end
         | 
| 66 82 |  | 
| 67 83 | 
             
                # Perform translation
         | 
| @@ -134,3 +150,4 @@ namespace :better_translate do | |
| 134 150 | 
             
                end
         | 
| 135 151 | 
             
              end
         | 
| 136 152 | 
             
            end
         | 
| 153 | 
            +
            # rubocop:enable Metrics/BlockLength
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module BetterTranslate
         | 
| 4 | 
            +
              module Analyzer
         | 
| 5 | 
            +
                # Scans code files to find i18n key references
         | 
| 6 | 
            +
                class CodeScanner
         | 
| 7 | 
            +
                  # I18n patterns to match
         | 
| 8 | 
            +
                  I18N_PATTERNS: Array[Regexp]
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  # File extensions to scan
         | 
| 11 | 
            +
                  SCANNABLE_EXTENSIONS: Array[String]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  # Path to scan (file or directory)
         | 
| 14 | 
            +
                  attr_reader path: String
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # Found i18n keys
         | 
| 17 | 
            +
                  attr_reader keys: Set[String]
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  # List of scanned files
         | 
| 20 | 
            +
                  attr_reader files_scanned: Array[String]
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  # Initialize scanner with path
         | 
| 23 | 
            +
                  #
         | 
| 24 | 
            +
                  # @param path File or directory path to scan
         | 
| 25 | 
            +
                  def initialize: (String path) -> void
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  # Scan path and extract i18n keys
         | 
| 28 | 
            +
                  #
         | 
| 29 | 
            +
                  # @return Set of found i18n keys
         | 
| 30 | 
            +
                  def scan: () -> Set[String]
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  # Get count of unique keys found
         | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  # @return Number of unique keys
         | 
| 35 | 
            +
                  def key_count: () -> Integer
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  private
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  # Validate that path exists
         | 
| 40 | 
            +
                  def validate_path!: () -> void
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  # Collect all scannable files from path
         | 
| 43 | 
            +
                  #
         | 
| 44 | 
            +
                  # @return List of file paths
         | 
| 45 | 
            +
                  def collect_files: () -> Array[String]
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  # Check if file should be scanned
         | 
| 48 | 
            +
                  #
         | 
| 49 | 
            +
                  # @param file File path
         | 
| 50 | 
            +
                  # @return Boolean
         | 
| 51 | 
            +
                  def scannable_file?: (String file) -> bool
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  # Scan single file and extract keys
         | 
| 54 | 
            +
                  #
         | 
| 55 | 
            +
                  # @param file File path
         | 
| 56 | 
            +
                  def scan_file: (String file) -> void
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         |