better_translate 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +291 -0
- data/CHANGELOG.md +88 -0
- data/README.md +262 -2
- data/RELEASE_NOTES_v1.0.0.md +240 -0
- data/Steepfile +2 -2
- data/docs/implementation/00-overview.md +1 -1
- data/docs/implementation/04-provider_architecture.md +5 -5
- data/lib/better_translate/analyzer/code_scanner.rb +151 -0
- data/lib/better_translate/analyzer/key_scanner.rb +109 -0
- data/lib/better_translate/analyzer/orphan_detector.rb +88 -0
- data/lib/better_translate/analyzer/reporter.rb +155 -0
- data/lib/better_translate/cache.rb +2 -1
- data/lib/better_translate/cli.rb +81 -2
- data/lib/better_translate/configuration.rb +48 -2
- data/lib/better_translate/errors.rb +9 -0
- data/lib/better_translate/json_handler.rb +227 -0
- data/lib/better_translate/providers/anthropic_provider.rb +4 -3
- data/lib/better_translate/providers/chatgpt_provider.rb +2 -1
- data/lib/better_translate/providers/gemini_provider.rb +5 -4
- data/lib/better_translate/railtie.rb +2 -1
- data/lib/better_translate/rate_limiter.rb +4 -1
- data/lib/better_translate/strategies/batch_strategy.rb +1 -1
- data/lib/better_translate/strategies/deep_strategy.rb +1 -1
- data/lib/better_translate/translator.rb +204 -19
- data/lib/better_translate/utils/hash_flattener.rb +2 -2
- data/lib/better_translate/variable_extractor.rb +7 -7
- data/lib/better_translate/version.rb +1 -1
- data/lib/better_translate/yaml_handler.rb +59 -0
- data/lib/better_translate.rb +5 -0
- data/lib/generators/better_translate/analyze/analyze_generator.rb +2 -1
- data/lib/generators/better_translate/install/install_generator.rb +4 -3
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +39 -7
- data/lib/generators/better_translate/translate/translate_generator.rb +2 -1
- data/regenerate_vcr.rb +47 -0
- data/sig/better_translate/configuration.rbs +13 -2
- data/sig/better_translate/errors.rbs +4 -0
- data/sig/better_translate/providers/base_http_provider.rbs +1 -1
- data/sig/better_translate/translator.rbs +12 -1
- data/sig/better_translate/variable_extractor.rbs +1 -1
- data/sig/better_translate/yaml_handler.rbs +6 -0
- data/sig/better_translate.rbs +2 -1
- metadata +9 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module BetterTranslate
|
|
6
|
+
module Analyzer
|
|
7
|
+
# Scans code files to find i18n key references
|
|
8
|
+
#
|
|
9
|
+
# Supports multiple patterns:
|
|
10
|
+
# - t('key') / t("key")
|
|
11
|
+
# - I18n.t(:key) / I18n.t('key')
|
|
12
|
+
# - <%= t('key') %> in ERB
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# scanner = CodeScanner.new("app/")
|
|
16
|
+
# keys = scanner.scan
|
|
17
|
+
# #=> #<Set: {"users.greeting", "products.list"}>
|
|
18
|
+
#
|
|
19
|
+
class CodeScanner
|
|
20
|
+
# I18n patterns to match
|
|
21
|
+
#
|
|
22
|
+
# Matches:
|
|
23
|
+
# - t('users.greeting')
|
|
24
|
+
# - t("users.greeting")
|
|
25
|
+
# - I18n.t(:users.greeting)
|
|
26
|
+
# - I18n.t('users.greeting')
|
|
27
|
+
# - I18n.translate('users.greeting')
|
|
28
|
+
I18N_PATTERNS = [
|
|
29
|
+
/\bt\(['"]([a-z0-9_.]+)['"]/i, # t('key') or t("key")
|
|
30
|
+
/\bI18n\.t\(:([a-z0-9_.]+)/i, # I18n.t(:key)
|
|
31
|
+
/\bI18n\.t\(['"]([a-z0-9_.]+)['"]/i, # I18n.t('key')
|
|
32
|
+
/\bI18n\.translate\(['"]([a-z0-9_.]+)['"]/i # I18n.translate('key')
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# File extensions to scan
|
|
36
|
+
SCANNABLE_EXTENSIONS = %w[.rb .erb .html.erb .haml .slim].freeze
|
|
37
|
+
|
|
38
|
+
# @return [String] Path to scan (file or directory)
|
|
39
|
+
attr_reader :path
|
|
40
|
+
|
|
41
|
+
# @return [Set] Found i18n keys
|
|
42
|
+
attr_reader :keys
|
|
43
|
+
|
|
44
|
+
# @return [Array<String>] List of scanned files
|
|
45
|
+
attr_reader :files_scanned
|
|
46
|
+
|
|
47
|
+
# Initialize scanner with path
|
|
48
|
+
#
|
|
49
|
+
# @param path [String] File or directory path to scan
|
|
50
|
+
#
|
|
51
|
+
def initialize(path)
|
|
52
|
+
@path = path
|
|
53
|
+
@keys = Set.new
|
|
54
|
+
@files_scanned = []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Scan path and extract i18n keys
|
|
58
|
+
#
|
|
59
|
+
# @return [Set] Set of found i18n keys
|
|
60
|
+
# @raise [FileError] if path does not exist
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# scanner = CodeScanner.new("app/")
|
|
64
|
+
# keys = scanner.scan
|
|
65
|
+
# #=> #<Set: {"users.greeting"}>
|
|
66
|
+
#
|
|
67
|
+
def scan
|
|
68
|
+
validate_path!
|
|
69
|
+
|
|
70
|
+
files = collect_files
|
|
71
|
+
files.each do |file|
|
|
72
|
+
scan_file(file)
|
|
73
|
+
@files_scanned << file
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@keys
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get count of unique keys found
|
|
80
|
+
#
|
|
81
|
+
# @return [Integer] Number of unique keys
|
|
82
|
+
#
|
|
83
|
+
def key_count
|
|
84
|
+
@keys.size
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# Validate that path exists
|
|
90
|
+
#
|
|
91
|
+
# @raise [FileError] if path does not exist
|
|
92
|
+
#
|
|
93
|
+
def validate_path!
|
|
94
|
+
return if File.exist?(path)
|
|
95
|
+
|
|
96
|
+
raise FileError.new(
|
|
97
|
+
"Path does not exist: #{path}",
|
|
98
|
+
context: { path: path }
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Collect all scannable files from path
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<String>] List of file paths
|
|
105
|
+
#
|
|
106
|
+
def collect_files
|
|
107
|
+
if File.file?(path)
|
|
108
|
+
return [path] if scannable_file?(path)
|
|
109
|
+
|
|
110
|
+
return []
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
Dir.glob(File.join(path, "**", "*")).select do |file|
|
|
114
|
+
File.file?(file) && scannable_file?(file)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check if file should be scanned
|
|
119
|
+
#
|
|
120
|
+
# @param file [String] File path
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
#
|
|
123
|
+
def scannable_file?(file)
|
|
124
|
+
SCANNABLE_EXTENSIONS.any? { |ext| file.end_with?(ext) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Scan single file and extract keys
|
|
128
|
+
#
|
|
129
|
+
# @param file [String] File path
|
|
130
|
+
#
|
|
131
|
+
def scan_file(file)
|
|
132
|
+
content = File.read(file)
|
|
133
|
+
|
|
134
|
+
# Remove commented lines to avoid false positives
|
|
135
|
+
lines = content.split("\n")
|
|
136
|
+
active_lines = lines.reject { |line| line.strip.start_with?("#", "//", "<!--") }
|
|
137
|
+
active_content = active_lines.join("\n")
|
|
138
|
+
|
|
139
|
+
I18N_PATTERNS.each do |pattern|
|
|
140
|
+
active_content.scan(pattern) do |match|
|
|
141
|
+
key = match.is_a?(Array) ? match.first : match
|
|
142
|
+
@keys.add(key) if key
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
# Skip files that can't be read
|
|
147
|
+
warn "Warning: Could not scan #{file}: #{e.message}" if ENV["VERBOSE"]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module BetterTranslate
|
|
6
|
+
module Analyzer
|
|
7
|
+
# Scans YAML translation files and extracts all keys in flatten format
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# scanner = KeyScanner.new("config/locales/en.yml")
|
|
11
|
+
# keys = scanner.scan
|
|
12
|
+
# #=> { "users.greeting" => "Hello", "users.welcome" => "Welcome %{name}" }
|
|
13
|
+
#
|
|
14
|
+
class KeyScanner
|
|
15
|
+
# @return [String] Path to the YAML file
|
|
16
|
+
attr_reader :file_path
|
|
17
|
+
|
|
18
|
+
# @return [Hash] Flatten keys extracted from YAML
|
|
19
|
+
attr_reader :keys
|
|
20
|
+
|
|
21
|
+
# Initialize scanner with YAML file path
|
|
22
|
+
#
|
|
23
|
+
# @param file_path [String] Path to YAML file
|
|
24
|
+
#
|
|
25
|
+
def initialize(file_path)
|
|
26
|
+
@file_path = file_path
|
|
27
|
+
@keys = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Scan YAML file and extract all flatten keys
|
|
31
|
+
#
|
|
32
|
+
# @return [Hash] Flatten keys with their values
|
|
33
|
+
# @raise [FileError] if file does not exist
|
|
34
|
+
# @raise [YamlError] if YAML is invalid
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# scanner = KeyScanner.new("en.yml")
|
|
38
|
+
# keys = scanner.scan
|
|
39
|
+
# #=> { "users.greeting" => "Hello" }
|
|
40
|
+
#
|
|
41
|
+
def scan
|
|
42
|
+
validate_file!
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
content = YAML.load_file(file_path)
|
|
46
|
+
|
|
47
|
+
# Skip root language key (en, it, fr, etc.) and start from its content
|
|
48
|
+
if content.is_a?(Hash) && content.size == 1
|
|
49
|
+
root_key = content.keys.first.to_s
|
|
50
|
+
content = content[root_key] || {} if root_key.match?(/^[a-z]{2}(-[A-Z]{2})?$/)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
flatten_keys(content)
|
|
54
|
+
rescue Psych::SyntaxError => e
|
|
55
|
+
raise YamlError.new(
|
|
56
|
+
"Invalid YAML syntax in #{file_path}",
|
|
57
|
+
context: { file: file_path, error: e.message }
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@keys
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get total count of keys
|
|
65
|
+
#
|
|
66
|
+
# @return [Integer] Number of keys
|
|
67
|
+
#
|
|
68
|
+
def key_count
|
|
69
|
+
@keys.size
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Validate that file exists
|
|
75
|
+
#
|
|
76
|
+
# @raise [FileError] if file does not exist
|
|
77
|
+
#
|
|
78
|
+
def validate_file!
|
|
79
|
+
return if File.exist?(file_path)
|
|
80
|
+
|
|
81
|
+
raise FileError.new(
|
|
82
|
+
"Translation file does not exist: #{file_path}",
|
|
83
|
+
context: { file: file_path }
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Flatten nested hash into dot-notation keys
|
|
88
|
+
#
|
|
89
|
+
# @param hash [Hash] Nested hash to flatten
|
|
90
|
+
# @param prefix [String] Prefix for current level
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# flatten_keys({ "users" => { "greeting" => "Hello" } })
|
|
94
|
+
# #=> { "users.greeting" => "Hello" }
|
|
95
|
+
#
|
|
96
|
+
def flatten_keys(hash, prefix = nil)
|
|
97
|
+
hash.each do |key, value|
|
|
98
|
+
current_key = prefix ? "#{prefix}.#{key}" : key.to_s
|
|
99
|
+
|
|
100
|
+
if value.is_a?(Hash)
|
|
101
|
+
flatten_keys(value, current_key)
|
|
102
|
+
else
|
|
103
|
+
@keys[current_key] = value
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterTranslate
|
|
4
|
+
module Analyzer
|
|
5
|
+
# Detects orphan i18n keys (keys defined but never used in code)
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# all_keys = { "users.greeting" => "Hello", "orphan" => "Unused" }
|
|
9
|
+
# used_keys = Set.new(["users.greeting"])
|
|
10
|
+
#
|
|
11
|
+
# detector = OrphanDetector.new(all_keys, used_keys)
|
|
12
|
+
# orphans = detector.detect
|
|
13
|
+
# #=> ["orphan"]
|
|
14
|
+
#
|
|
15
|
+
class OrphanDetector
|
|
16
|
+
# @return [Hash] All translation keys with their values
|
|
17
|
+
attr_reader :all_keys
|
|
18
|
+
|
|
19
|
+
# @return [Set] Keys that are used in code
|
|
20
|
+
attr_reader :used_keys
|
|
21
|
+
|
|
22
|
+
# @return [Array<String>] Orphan keys (not used in code)
|
|
23
|
+
attr_reader :orphans
|
|
24
|
+
|
|
25
|
+
# Initialize detector
|
|
26
|
+
#
|
|
27
|
+
# @param all_keys [Hash] All translation keys from YAML files
|
|
28
|
+
# @param used_keys [Set] Keys found in code
|
|
29
|
+
#
|
|
30
|
+
def initialize(all_keys, used_keys)
|
|
31
|
+
@all_keys = all_keys
|
|
32
|
+
@used_keys = used_keys
|
|
33
|
+
@orphans = []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Detect orphan keys
|
|
37
|
+
#
|
|
38
|
+
# Compares all keys with used keys and identifies those that are never referenced.
|
|
39
|
+
#
|
|
40
|
+
# @return [Array<String>] List of orphan key names
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# detector.detect
|
|
44
|
+
# #=> ["orphan_key", "another.orphan"]
|
|
45
|
+
#
|
|
46
|
+
def detect
|
|
47
|
+
@orphans = all_keys.keys.reject { |key| used_keys.include?(key) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get count of orphan keys
|
|
51
|
+
#
|
|
52
|
+
# @return [Integer] Number of orphan keys
|
|
53
|
+
#
|
|
54
|
+
def orphan_count
|
|
55
|
+
@orphans.size
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get details of orphan keys with their values
|
|
59
|
+
#
|
|
60
|
+
# @return [Hash] Hash of orphan keys and their translation values
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# detector.orphan_details
|
|
64
|
+
# #=> { "orphan_key" => "This is never used" }
|
|
65
|
+
#
|
|
66
|
+
def orphan_details
|
|
67
|
+
@orphans.each_with_object({}) do |key, details|
|
|
68
|
+
details[key] = all_keys[key]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Calculate usage percentage
|
|
73
|
+
#
|
|
74
|
+
# @return [Float] Percentage of keys that are used (0.0 to 100.0)
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# detector.usage_percentage
|
|
78
|
+
# #=> 75.0 # 6 out of 8 keys are used
|
|
79
|
+
#
|
|
80
|
+
def usage_percentage
|
|
81
|
+
return 0.0 if all_keys.empty?
|
|
82
|
+
|
|
83
|
+
used_count = all_keys.size - @orphans.size
|
|
84
|
+
(used_count.to_f / all_keys.size * 100).round(1)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "csv"
|
|
5
|
+
|
|
6
|
+
module BetterTranslate
|
|
7
|
+
module Analyzer
|
|
8
|
+
# Generates reports for orphan key analysis
|
|
9
|
+
#
|
|
10
|
+
# Supports multiple output formats: text, JSON, CSV
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# reporter = Reporter.new(
|
|
14
|
+
# orphans: ["orphan_key"],
|
|
15
|
+
# orphan_details: { "orphan_key" => "Unused" },
|
|
16
|
+
# total_keys: 10,
|
|
17
|
+
# used_keys: 9,
|
|
18
|
+
# usage_percentage: 90.0,
|
|
19
|
+
# format: :text
|
|
20
|
+
# )
|
|
21
|
+
# puts reporter.generate
|
|
22
|
+
#
|
|
23
|
+
class Reporter
|
|
24
|
+
# @return [Array<String>] List of orphan keys
|
|
25
|
+
attr_reader :orphans
|
|
26
|
+
|
|
27
|
+
# @return [Hash] Orphan keys with their values
|
|
28
|
+
attr_reader :orphan_details
|
|
29
|
+
|
|
30
|
+
# @return [Integer] Total number of keys
|
|
31
|
+
attr_reader :total_keys
|
|
32
|
+
|
|
33
|
+
# @return [Integer] Number of used keys
|
|
34
|
+
attr_reader :used_keys
|
|
35
|
+
|
|
36
|
+
# @return [Float] Usage percentage
|
|
37
|
+
attr_reader :usage_percentage
|
|
38
|
+
|
|
39
|
+
# @return [Symbol] Output format (:text, :json, :csv)
|
|
40
|
+
attr_reader :format
|
|
41
|
+
|
|
42
|
+
# Initialize reporter
|
|
43
|
+
#
|
|
44
|
+
# @param orphans [Array<String>] List of orphan keys
|
|
45
|
+
# @param orphan_details [Hash] Orphan keys with values
|
|
46
|
+
# @param total_keys [Integer] Total number of keys
|
|
47
|
+
# @param used_keys [Integer] Number of used keys
|
|
48
|
+
# @param usage_percentage [Float] Usage percentage
|
|
49
|
+
# @param format [Symbol] Output format (:text, :json, :csv)
|
|
50
|
+
#
|
|
51
|
+
def initialize(orphans:, orphan_details:, total_keys:, used_keys:, usage_percentage:, format: :text)
|
|
52
|
+
@orphans = orphans
|
|
53
|
+
@orphan_details = orphan_details
|
|
54
|
+
@total_keys = total_keys
|
|
55
|
+
@used_keys = used_keys
|
|
56
|
+
@usage_percentage = usage_percentage
|
|
57
|
+
@format = format
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Generate report in specified format
|
|
61
|
+
#
|
|
62
|
+
# @return [String] Generated report
|
|
63
|
+
#
|
|
64
|
+
def generate
|
|
65
|
+
case format
|
|
66
|
+
when :json
|
|
67
|
+
generate_json
|
|
68
|
+
when :csv
|
|
69
|
+
generate_csv
|
|
70
|
+
else
|
|
71
|
+
generate_text
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Save report to file
|
|
76
|
+
#
|
|
77
|
+
# @param file_path [String] Output file path
|
|
78
|
+
#
|
|
79
|
+
def save_to_file(file_path)
|
|
80
|
+
File.write(file_path, generate)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Generate text format report
|
|
86
|
+
#
|
|
87
|
+
# @return [String] Text report
|
|
88
|
+
#
|
|
89
|
+
def generate_text
|
|
90
|
+
lines = []
|
|
91
|
+
lines << "=" * 60
|
|
92
|
+
lines << "Orphan Keys Analysis Report"
|
|
93
|
+
lines << "=" * 60
|
|
94
|
+
lines << ""
|
|
95
|
+
lines << "Statistics:"
|
|
96
|
+
lines << " Total keys: #{total_keys}"
|
|
97
|
+
lines << " Used keys: #{used_keys}"
|
|
98
|
+
lines << " Orphan keys: #{orphans.size}"
|
|
99
|
+
lines << " Usage: #{usage_percentage}%"
|
|
100
|
+
lines << ""
|
|
101
|
+
|
|
102
|
+
if orphans.empty?
|
|
103
|
+
lines << "✓ No orphan keys found! All translation keys are being used."
|
|
104
|
+
else
|
|
105
|
+
lines << "Orphan Keys (#{orphans.size}):"
|
|
106
|
+
lines << "-" * 60
|
|
107
|
+
|
|
108
|
+
orphans.each do |key|
|
|
109
|
+
value = orphan_details[key]
|
|
110
|
+
lines << ""
|
|
111
|
+
lines << " Key: #{key}"
|
|
112
|
+
lines << " Value: #{value}" if value
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
lines << ""
|
|
117
|
+
lines << "=" * 60
|
|
118
|
+
|
|
119
|
+
lines.join("\n")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Generate JSON format report
|
|
123
|
+
#
|
|
124
|
+
# @return [String] JSON report
|
|
125
|
+
#
|
|
126
|
+
def generate_json
|
|
127
|
+
data = {
|
|
128
|
+
orphans: orphans,
|
|
129
|
+
orphan_details: orphan_details,
|
|
130
|
+
orphan_count: orphans.size,
|
|
131
|
+
total_keys: total_keys,
|
|
132
|
+
used_keys: used_keys,
|
|
133
|
+
usage_percentage: usage_percentage
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
JSON.pretty_generate(data)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Generate CSV format report
|
|
140
|
+
#
|
|
141
|
+
# @return [String] CSV report
|
|
142
|
+
#
|
|
143
|
+
def generate_csv
|
|
144
|
+
CSV.generate do |csv|
|
|
145
|
+
csv << %w[Key Value]
|
|
146
|
+
|
|
147
|
+
orphans.each do |key|
|
|
148
|
+
value = orphan_details[key]
|
|
149
|
+
csv << [key, value]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/better_translate/cli.rb
CHANGED
|
@@ -53,6 +53,8 @@ module BetterTranslate
|
|
|
53
53
|
run_generate
|
|
54
54
|
when "direct"
|
|
55
55
|
run_direct
|
|
56
|
+
when "analyze"
|
|
57
|
+
run_analyze
|
|
56
58
|
when "--version", "-v"
|
|
57
59
|
puts "BetterTranslate version #{VERSION}"
|
|
58
60
|
when "--help", "-h", nil
|
|
@@ -79,6 +81,7 @@ module BetterTranslate
|
|
|
79
81
|
translate Translate YAML files using config file
|
|
80
82
|
generate OUTPUT_FILE Generate sample config file
|
|
81
83
|
direct TEXT Translate text directly
|
|
84
|
+
analyze Analyze YAML files for orphan keys
|
|
82
85
|
|
|
83
86
|
Options:
|
|
84
87
|
--help, -h Show this help message
|
|
@@ -88,6 +91,7 @@ module BetterTranslate
|
|
|
88
91
|
better_translate translate --config config.yml
|
|
89
92
|
better_translate generate config.yml
|
|
90
93
|
better_translate direct "Hello" --to it --provider chatgpt --api-key KEY
|
|
94
|
+
better_translate analyze --source config/locales/en.yml --scan-path app/
|
|
91
95
|
HELP
|
|
92
96
|
end
|
|
93
97
|
|
|
@@ -221,8 +225,8 @@ module BetterTranslate
|
|
|
221
225
|
"dry_run" => false,
|
|
222
226
|
"translation_mode" => "override",
|
|
223
227
|
"preserve_variables" => true,
|
|
224
|
-
"global_exclusions" =>
|
|
225
|
-
"exclusions_per_language" =>
|
|
228
|
+
"global_exclusions" => [],
|
|
229
|
+
"exclusions_per_language" => {},
|
|
226
230
|
"model" => nil,
|
|
227
231
|
"temperature" => 0.3,
|
|
228
232
|
"max_tokens" => 2000,
|
|
@@ -300,5 +304,80 @@ module BetterTranslate
|
|
|
300
304
|
rescue StandardError => e
|
|
301
305
|
puts "Error: #{e.message}"
|
|
302
306
|
end
|
|
307
|
+
|
|
308
|
+
# Run analyze command
|
|
309
|
+
#
|
|
310
|
+
# @return [void]
|
|
311
|
+
# @api private
|
|
312
|
+
#
|
|
313
|
+
def run_analyze
|
|
314
|
+
# @type var options: Hash[Symbol, String]
|
|
315
|
+
options = {}
|
|
316
|
+
OptionParser.new do |opts|
|
|
317
|
+
opts.on("--source FILE", "Source YAML file path") { |v| options[:source] = v }
|
|
318
|
+
opts.on("--scan-path PATH", "Path to scan for code files") { |v| options[:scan_path] = v }
|
|
319
|
+
opts.on("--format FORMAT", "Output format (text, json, csv)") { |v| options[:format] = v }
|
|
320
|
+
opts.on("--output FILE", "Output file path") { |v| options[:output] = v }
|
|
321
|
+
end.parse!(args[1..])
|
|
322
|
+
|
|
323
|
+
# Validate required options
|
|
324
|
+
unless options[:source]
|
|
325
|
+
puts "Error: --source is required"
|
|
326
|
+
return
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
unless options[:scan_path]
|
|
330
|
+
puts "Error: --scan-path is required"
|
|
331
|
+
return
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Validate paths exist
|
|
335
|
+
unless File.exist?(options[:source])
|
|
336
|
+
puts "Error: Source file not found: #{options[:source]}"
|
|
337
|
+
return
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
unless File.exist?(options[:scan_path])
|
|
341
|
+
puts "Error: Scan path not found: #{options[:scan_path]}"
|
|
342
|
+
return
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Default format
|
|
346
|
+
format = (options[:format] || "text").to_sym
|
|
347
|
+
|
|
348
|
+
# Scan keys from YAML
|
|
349
|
+
key_scanner = Analyzer::KeyScanner.new(options[:source])
|
|
350
|
+
all_keys = key_scanner.scan
|
|
351
|
+
|
|
352
|
+
# Scan code for used keys
|
|
353
|
+
code_scanner = Analyzer::CodeScanner.new(options[:scan_path])
|
|
354
|
+
used_keys = code_scanner.scan
|
|
355
|
+
|
|
356
|
+
# Detect orphans
|
|
357
|
+
detector = Analyzer::OrphanDetector.new(all_keys, used_keys)
|
|
358
|
+
orphans = detector.detect
|
|
359
|
+
|
|
360
|
+
# Generate report
|
|
361
|
+
reporter = Analyzer::Reporter.new(
|
|
362
|
+
orphans: orphans,
|
|
363
|
+
orphan_details: detector.orphan_details,
|
|
364
|
+
total_keys: all_keys.size,
|
|
365
|
+
used_keys: used_keys.size,
|
|
366
|
+
usage_percentage: detector.usage_percentage,
|
|
367
|
+
format: format
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
report = reporter.generate
|
|
371
|
+
|
|
372
|
+
# Output or save
|
|
373
|
+
if options[:output]
|
|
374
|
+
reporter.save_to_file(options[:output])
|
|
375
|
+
puts "Report saved to #{options[:output]}"
|
|
376
|
+
else
|
|
377
|
+
puts report
|
|
378
|
+
end
|
|
379
|
+
rescue StandardError => e
|
|
380
|
+
puts "Error: #{e.message}"
|
|
381
|
+
end
|
|
303
382
|
end
|
|
304
383
|
end
|