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.
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/**/*"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module BetterTranslate
6
4
  module Analyzer
7
5
  # Scans code files to find i18n key references
@@ -64,9 +64,12 @@ module BetterTranslate
64
64
  # #=> { "orphan_key" => "This is never used" }
65
65
  #
66
66
  def orphan_details
67
- @orphans.each_with_object({}) do |key, details|
68
- details[key] = all_keys[key]
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
@@ -87,7 +87,7 @@ module BetterTranslate
87
87
  # @return [String] Text report
88
88
  #
89
89
  def generate_text
90
- lines = []
90
+ lines = [] # : Array[String]
91
91
  lines << "=" * 60
92
92
  lines << "Orphan Keys Analysis Report"
93
93
  lines << "=" * 60
@@ -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
- raise ConfigurationError, "Input file does not exist: #{input_file}" unless File.exist?(input_file)
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
- output_path = build_output_path_for_file(config.input_file, target_lang_code)
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)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module BetterTranslate
4
4
  # Current version of BetterTranslate gem
5
- VERSION = "1.1.0"
5
+ VERSION = "1.1.1"
6
6
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "csv"
4
+
3
5
  require_relative "better_translate/version"
4
6
  require_relative "better_translate/errors"
5
7
  require_relative "better_translate/configuration"
@@ -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
- # Automatically uses Rails I18n configuration
18
- # To configure I18n in your Rails app, set in config/application.rb:
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, :es, :fr]
21
- config.source_language = I18n.default_locale.to_s
22
-
23
- # Target languages: automatically derived from I18n.available_locales
24
- # Excludes the source language from targets
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
- # Language name mapping for common languages
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
- if available_targets.any?
38
- # Use I18n available locales
39
- config.target_languages = available_targets.map do |locale|
40
- {
41
- short_name: locale,
42
- name: language_names[locale] || locale.capitalize
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
- config_file = Rails.root.join("config", "better_translate.yml")
34
-
35
- unless File.exist?(config_file)
36
- say "Config file not found at #{config_file}", :red
37
- say "Run 'rails generate better_translate:install' first"
38
- return
39
- end
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
- # Load configuration from YAML
42
- yaml_config = YAML.load_file(config_file)
43
-
44
- # Configure BetterTranslate
45
- BetterTranslate.configure do |config|
46
- config.provider = yaml_config["provider"]&.to_sym
47
- config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
48
- config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
49
- config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
50
-
51
- config.source_language = yaml_config["source_language"]
52
- config.target_languages = yaml_config["target_languages"]&.map do |lang|
53
- if lang.is_a?(Hash)
54
- { short_name: lang["short_name"], name: lang["name"] }
55
- else
56
- lang
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
- config.input_file = Rails.root.join(yaml_config["input_file"]).to_s
61
- config.output_folder = Rails.root.join(yaml_config["output_folder"]).to_s
62
- config.verbose = yaml_config.fetch("verbose", true)
63
- config.dry_run = options[:dry_run] || yaml_config.fetch("dry_run", false)
64
-
65
- # Map "full" to :override for backward compatibility
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
- config_file = Rails.root.join("config", "better_translate.yml")
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
- unless File.exist?(config_file)
18
- puts "Error: Configuration file not found at #{config_file}"
19
- puts "Run 'rake better_translate:config:generate' to create one."
20
- exit 1
21
- end
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
- # Load configuration from YAML
24
- yaml_config = YAML.load_file(config_file)
25
-
26
- # Configure BetterTranslate
27
- BetterTranslate.configure do |config|
28
- config.provider = yaml_config["provider"]&.to_sym
29
- config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
30
- config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
31
- config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
32
-
33
- config.source_language = yaml_config["source_language"]
34
- config.target_languages = yaml_config["target_languages"]&.map do |lang|
35
- if lang.is_a?(Hash)
36
- { short_name: lang["short_name"], name: lang["name"] }
37
- else
38
- lang
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
- config.input_file = yaml_config["input_file"]
43
- config.output_folder = yaml_config["output_folder"]
44
- config.verbose = yaml_config.fetch("verbose", true)
45
- config.dry_run = yaml_config.fetch("dry_run", false)
46
-
47
- # Map "full" to :override for backward compatibility
48
- translation_mode = yaml_config.fetch("translation_mode", "override")
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