i18n-context-generator 0.4.0 → 0.5.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/README.md +163 -26
- data/lib/i18n_context_generator/android_resource.rb +209 -0
- data/lib/i18n_context_generator/apple_string_literal.rb +28 -0
- data/lib/i18n_context_generator/cache.rb +40 -8
- data/lib/i18n_context_generator/changed_location.rb +55 -0
- data/lib/i18n_context_generator/cli.rb +163 -76
- data/lib/i18n_context_generator/config/cli_values.rb +33 -0
- data/lib/i18n_context_generator/config/defaults.rb +35 -0
- data/lib/i18n_context_generator/config/schema.rb +225 -0
- data/lib/i18n_context_generator/config/serialization.rb +64 -0
- data/lib/i18n_context_generator/config/validation.rb +249 -0
- data/lib/i18n_context_generator/config.rb +293 -102
- data/lib/i18n_context_generator/context_extractor/cache_identity.rb +65 -0
- data/lib/i18n_context_generator/context_extractor/extraction_result.rb +46 -0
- data/lib/i18n_context_generator/context_extractor/run_logging.rb +45 -6
- data/lib/i18n_context_generator/context_extractor/source_entries.rb +103 -0
- data/lib/i18n_context_generator/context_extractor/source_filters.rb +51 -5
- data/lib/i18n_context_generator/context_extractor/translation_filters.rb +91 -0
- data/lib/i18n_context_generator/context_extractor/workflow.rb +118 -0
- data/lib/i18n_context_generator/context_extractor.rb +207 -216
- data/lib/i18n_context_generator/file_classifier.rb +72 -0
- data/lib/i18n_context_generator/generated_comment.rb +32 -0
- data/lib/i18n_context_generator/git_diff/xml_changes.rb +235 -0
- data/lib/i18n_context_generator/git_diff.rb +224 -95
- data/lib/i18n_context_generator/llm/anthropic.rb +65 -38
- data/lib/i18n_context_generator/llm/client.rb +294 -92
- data/lib/i18n_context_generator/llm/openai.rb +92 -51
- data/lib/i18n_context_generator/llm/openai_compatible.rb +20 -0
- data/lib/i18n_context_generator/llm/prompt_evidence.rb +47 -0
- data/lib/i18n_context_generator/llm/request_policy.rb +147 -0
- data/lib/i18n_context_generator/localization_syntax.rb +246 -0
- data/lib/i18n_context_generator/parsers/android_xml_parser.rb +47 -8
- data/lib/i18n_context_generator/parsers/base.rb +7 -4
- data/lib/i18n_context_generator/parsers/json_parser.rb +4 -0
- data/lib/i18n_context_generator/parsers/xcstrings_parser.rb +79 -0
- data/lib/i18n_context_generator/parsers/yaml_parser.rb +20 -7
- data/lib/i18n_context_generator/path_policy.rb +107 -0
- data/lib/i18n_context_generator/platform_validator.rb +24 -50
- data/lib/i18n_context_generator/run_metrics.rb +47 -0
- data/lib/i18n_context_generator/searcher/comment_masking.rb +115 -0
- data/lib/i18n_context_generator/searcher/match_filtering.rb +52 -0
- data/lib/i18n_context_generator/searcher/source_discovery.rb +109 -64
- data/lib/i18n_context_generator/searcher.rb +61 -205
- data/lib/i18n_context_generator/supplemental_context.rb +77 -0
- data/lib/i18n_context_generator/translation_comment_index.rb +121 -0
- data/lib/i18n_context_generator/version.rb +1 -1
- data/lib/i18n_context_generator/writers/android_xml_writer.rb +48 -18
- data/lib/i18n_context_generator/writers/atomic_file.rb +37 -0
- data/lib/i18n_context_generator/writers/csv_writer.rb +43 -18
- data/lib/i18n_context_generator/writers/helpers.rb +7 -29
- data/lib/i18n_context_generator/writers/json_writer.rb +31 -6
- data/lib/i18n_context_generator/writers/preview.rb +80 -0
- data/lib/i18n_context_generator/writers/result_serialization.rb +30 -0
- data/lib/i18n_context_generator/writers/strings_writer.rb +23 -12
- data/lib/i18n_context_generator/writers/swift_writer.rb +16 -54
- data/lib/i18n_context_generator/writers/xcstrings_writer.rb +57 -0
- data/lib/i18n_context_generator/xcstrings_document.rb +218 -0
- data/lib/i18n_context_generator/xml_scanner.rb +38 -0
- data/lib/i18n_context_generator.rb +20 -4
- metadata +59 -12
|
@@ -8,18 +8,57 @@ module I18nContextGenerator
|
|
|
8
8
|
|
|
9
9
|
def log_empty_entries_message
|
|
10
10
|
if @config.diff_base && translation_backed_discovery?
|
|
11
|
-
|
|
11
|
+
log "No changed translation keys found in #{@config.diff_base}...#{@config.diff_head}."
|
|
12
12
|
elsif @config.diff_base && source_discovery_filtered_by_diff?
|
|
13
|
-
|
|
13
|
+
log "No changed source localization entries found in #{@config.diff_base}...#{@config.diff_head}."
|
|
14
14
|
else
|
|
15
|
-
|
|
15
|
+
log "No #{entry_label_for_logging} found."
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def log_loaded_entries(count)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
log "Loaded #{count} #{entry_label_for_logging}"
|
|
21
|
+
range = "#{@config.diff_base}...#{@config.diff_head}"
|
|
22
|
+
log "(filtered to changes in #{range})" if @config.diff_base && translation_backed_discovery?
|
|
23
|
+
log "(filtered to source changes in #{range})" if source_discovery_filtered_by_diff?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resolved_model
|
|
27
|
+
LLM::Client.default_model_for(@config.provider, configured_model: @config.model)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def log_metrics
|
|
31
|
+
summary = [
|
|
32
|
+
"Requests: #{@metrics.request_count}",
|
|
33
|
+
"cache hits: #{@metrics.cache_hits}",
|
|
34
|
+
"tokens: #{@metrics.input_tokens} in / #{@metrics.output_tokens} out",
|
|
35
|
+
"retries: #{@metrics.retry_count}"
|
|
36
|
+
].join(', ')
|
|
37
|
+
if @metrics.estimated_cost_usd
|
|
38
|
+
summary += format(
|
|
39
|
+
', estimated cost: $%<cost>.6f (standard list prices as of %<date>s)',
|
|
40
|
+
cost: @metrics.estimated_cost_usd,
|
|
41
|
+
date: @metrics.cost_pricing_as_of
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
log summary
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def log(message = '')
|
|
48
|
+
return if @quiet
|
|
49
|
+
|
|
50
|
+
log_output.puts(message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def log_output
|
|
54
|
+
@configured_log_output ||
|
|
55
|
+
(@config.output_stdout || workflow_stage == 'preview_diff' ? $stderr : $stdout)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def write_patch(patch)
|
|
59
|
+
output = @patch_output || $stdout
|
|
60
|
+
output.write(patch)
|
|
61
|
+
output.write("\n") unless patch.end_with?("\n")
|
|
23
62
|
end
|
|
24
63
|
end
|
|
25
64
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18nContextGenerator
|
|
4
|
+
class ContextExtractor
|
|
5
|
+
# Loads source-discovered entries and hydrates them from translation files.
|
|
6
|
+
module SourceEntries
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def load_entries
|
|
10
|
+
case normalized_discovery_mode
|
|
11
|
+
when 'source'
|
|
12
|
+
load_source_entries
|
|
13
|
+
when 'translations'
|
|
14
|
+
load_translations
|
|
15
|
+
else
|
|
16
|
+
auto_discovery_entries
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def auto_discovery_entries
|
|
21
|
+
return load_source_entries if @config.translations.empty?
|
|
22
|
+
|
|
23
|
+
load_translations
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_source_entries
|
|
27
|
+
discovered_entries = filter_source_entries(searcher.discover_localization_entries)
|
|
28
|
+
|
|
29
|
+
discovered_entries.flat_map do |entry|
|
|
30
|
+
hydrated_entries = translation_entries_for_discovered(entry)
|
|
31
|
+
hydrated_entries = [nil] if hydrated_entries.empty?
|
|
32
|
+
|
|
33
|
+
hydrated_entries.map { |hydrated_entry| build_source_entry(entry, hydrated_entry) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_source_entry(entry, hydrated_entry)
|
|
38
|
+
translation_comment = hydrated_entry&.metadata&.dig(:comment)
|
|
39
|
+
source_comment = entry.comment
|
|
40
|
+
metadata = hydrated_entry&.metadata&.dup || {}
|
|
41
|
+
metadata[:comment] = translation_comment || source_comment if translation_comment || source_comment
|
|
42
|
+
metadata[:source_location] = "#{entry.file}:#{entry.line}"
|
|
43
|
+
metadata[:source_locations] = entry.locations
|
|
44
|
+
metadata[:source_location_groups] = entry.location_groups
|
|
45
|
+
metadata[:resource_type] = entry.resource_type unless entry.resource_type == :string
|
|
46
|
+
|
|
47
|
+
Parsers::TranslationEntry.new(
|
|
48
|
+
key: hydrated_entry&.key || entry.key,
|
|
49
|
+
text: hydrated_entry&.text || entry.text || entry.key,
|
|
50
|
+
source_file: hydrated_entry&.source_file,
|
|
51
|
+
metadata: metadata
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_translation_lookup
|
|
56
|
+
return @load_translation_lookup if defined?(@load_translation_lookup)
|
|
57
|
+
|
|
58
|
+
empty_lookup = Hash.new { |hash, key| hash[key] = [] }
|
|
59
|
+
@load_translation_lookup = load_translations.each_with_object(empty_lookup) do |entry, lookup|
|
|
60
|
+
lookup[entry.key] << entry
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def load_translation_resource_lookup
|
|
65
|
+
return @load_translation_resource_lookup if defined?(@load_translation_resource_lookup)
|
|
66
|
+
|
|
67
|
+
empty_lookup = Hash.new { |hash, key| hash[key] = [] }
|
|
68
|
+
@load_translation_resource_lookup = load_translations.each_with_object(empty_lookup) do |entry, lookup|
|
|
69
|
+
metadata = entry.metadata || {}
|
|
70
|
+
if metadata[:plural]
|
|
71
|
+
lookup[[:plural, metadata[:plural]]] << entry
|
|
72
|
+
elsif metadata[:array]
|
|
73
|
+
lookup[[:array, metadata[:array]]] << entry
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def translation_entries_for_discovered(entry)
|
|
79
|
+
case entry.resource_type
|
|
80
|
+
when :plural, :array
|
|
81
|
+
load_translation_resource_lookup[[entry.resource_type, entry.key]]
|
|
82
|
+
else
|
|
83
|
+
load_translation_lookup[entry.key]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def normalized_discovery_mode
|
|
88
|
+
@config.discovery_mode.to_s.downcase
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def translation_backed_discovery?
|
|
92
|
+
return true if normalized_discovery_mode == 'translations'
|
|
93
|
+
return false if normalized_discovery_mode == 'source'
|
|
94
|
+
|
|
95
|
+
@config.translations.any?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def entry_label_for_logging
|
|
99
|
+
translation_backed_discovery? ? 'translation keys' : 'source localization entries'
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
3
5
|
module I18nContextGenerator
|
|
4
6
|
class ContextExtractor
|
|
5
7
|
# Helpers for filtering source-discovered entries without narrowing the full
|
|
@@ -21,7 +23,9 @@ module I18nContextGenerator
|
|
|
21
23
|
return [] if line_filter.empty?
|
|
22
24
|
|
|
23
25
|
entries.select do |entry|
|
|
24
|
-
|
|
26
|
+
entry.locations.any? do |location|
|
|
27
|
+
changed_location?(location, line_filter)
|
|
28
|
+
end
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
|
|
@@ -31,11 +35,41 @@ module I18nContextGenerator
|
|
|
31
35
|
@source_line_filter =
|
|
32
36
|
if @config.source_line_filter
|
|
33
37
|
normalize_source_line_filter(@config.source_line_filter)
|
|
34
|
-
elsif @config.diff_base
|
|
35
|
-
|
|
38
|
+
elsif @config.diff_base
|
|
39
|
+
git_diff.changed_lines(@config.source_paths)
|
|
36
40
|
end
|
|
37
41
|
end
|
|
38
42
|
|
|
43
|
+
def changed_result_locations_for(locations)
|
|
44
|
+
line_filter = source_line_filter
|
|
45
|
+
return [] unless line_filter
|
|
46
|
+
|
|
47
|
+
locations.select do |location|
|
|
48
|
+
changed_location?(location, line_filter)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def changed_result_location_groups_for(entry, changed_locations)
|
|
53
|
+
return [] if changed_locations.empty?
|
|
54
|
+
|
|
55
|
+
location_groups = entry.metadata&.dig(:source_location_groups)
|
|
56
|
+
return changed_locations.map { |location| [location] } unless location_groups&.any?
|
|
57
|
+
|
|
58
|
+
changed_location_set = changed_locations.to_set
|
|
59
|
+
location_groups.filter_map do |group|
|
|
60
|
+
changed_group = Array(group).select { |location| changed_location_set.include?(location) }
|
|
61
|
+
changed_group unless changed_group.empty?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def changed_location_attributes_for(entry, locations)
|
|
66
|
+
changed_locations = changed_result_locations_for(locations)
|
|
67
|
+
{
|
|
68
|
+
changed_locations: changed_locations,
|
|
69
|
+
changed_location_groups: changed_result_location_groups_for(entry, changed_locations)
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
39
73
|
def normalize_source_line_filter(filter)
|
|
40
74
|
filter.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |(file, lines), normalized_filter|
|
|
41
75
|
next if file.nil?
|
|
@@ -43,13 +77,25 @@ module I18nContextGenerator
|
|
|
43
77
|
normalized_lines = Array(lines).filter_map { |line| Integer(line, exception: false) }
|
|
44
78
|
next if normalized_lines.empty?
|
|
45
79
|
|
|
46
|
-
normalized_filter[file
|
|
80
|
+
normalized_filter[normalize_source_file(file)].merge(normalized_lines)
|
|
47
81
|
end
|
|
48
82
|
end
|
|
49
83
|
|
|
84
|
+
def changed_location?(location, line_filter)
|
|
85
|
+
file, _, line = location.rpartition(':')
|
|
86
|
+
line_filter.fetch(normalize_source_file(file), Set.new).include?(line.to_i)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalize_source_file(file)
|
|
90
|
+
Pathname.new(file.to_s).cleanpath.to_s
|
|
91
|
+
end
|
|
92
|
+
|
|
50
93
|
def result_locations_for(entry, matches)
|
|
94
|
+
source_locations = entry.metadata&.dig(:source_locations)
|
|
95
|
+
return source_locations if source_locations&.any?
|
|
96
|
+
|
|
51
97
|
source_location = entry.metadata&.dig(:source_location)
|
|
52
|
-
return
|
|
98
|
+
return Array(source_location) if source_location
|
|
53
99
|
|
|
54
100
|
matches.map { |m| "#{m.file}:#{m.line}" }
|
|
55
101
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require_relative '../android_resource'
|
|
5
|
+
|
|
6
|
+
module I18nContextGenerator
|
|
7
|
+
class ContextExtractor
|
|
8
|
+
# Filters translation-backed entries and retains their changed inline locations.
|
|
9
|
+
module TranslationFilters
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def filter_by_diff(entries)
|
|
13
|
+
@android_collection_members_by_location = {}
|
|
14
|
+
@android_resource_indexes_by_file = {}
|
|
15
|
+
@changed_translation_locations = git_diff.changed_key_locations(@config.translations)
|
|
16
|
+
|
|
17
|
+
return [] if @changed_translation_locations.empty?
|
|
18
|
+
|
|
19
|
+
log "Found #{@changed_translation_locations.size} changed translation entries in git diff"
|
|
20
|
+
|
|
21
|
+
entries.select { |entry| changed_translation_locations_for(entry).any? }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Extract the base resource name from composite Android keys.
|
|
25
|
+
def android_base_key(key)
|
|
26
|
+
AndroidResource.base_key(key)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def translation_key_for(entry)
|
|
30
|
+
metadata = entry.metadata || {}
|
|
31
|
+
metadata[:plural] || metadata[:array] || android_base_key(entry.key)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def changed_translation_locations_for(entry)
|
|
35
|
+
return [] unless @changed_translation_locations
|
|
36
|
+
return [] unless entry.source_file
|
|
37
|
+
|
|
38
|
+
source_file = Pathname.new(entry.source_file).cleanpath.to_s
|
|
39
|
+
locations = @changed_translation_locations.fetch([source_file, translation_key_for(entry)], [])
|
|
40
|
+
narrow_android_collection_locations(entry, locations)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def narrow_android_collection_locations(entry, locations)
|
|
44
|
+
metadata = entry.metadata || {}
|
|
45
|
+
return locations unless metadata[:plural] || metadata[:array]
|
|
46
|
+
|
|
47
|
+
members_by_location = locations.to_h do |location|
|
|
48
|
+
changed_location = ChangedLocation.parse(location)
|
|
49
|
+
line_number = changed_location.right? ? changed_location.line : changed_location.fallback_line
|
|
50
|
+
member = if line_number && metadata[:line_span]&.cover?(line_number)
|
|
51
|
+
entry.key
|
|
52
|
+
else
|
|
53
|
+
android_collection_member_at(changed_location, translation_key_for(entry))
|
|
54
|
+
end
|
|
55
|
+
[location, member]
|
|
56
|
+
end
|
|
57
|
+
return locations if members_by_location.values.compact.empty?
|
|
58
|
+
|
|
59
|
+
locations.select { |location| members_by_location[location] == entry.key }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def android_collection_member_at(location, expected_parent)
|
|
63
|
+
cache_key = [expected_parent, location]
|
|
64
|
+
return @android_collection_members_by_location[cache_key] if @android_collection_members_by_location&.key?(cache_key)
|
|
65
|
+
|
|
66
|
+
changed_location = ChangedLocation.parse(location)
|
|
67
|
+
file = changed_location.file
|
|
68
|
+
target_line = changed_location.right? ? changed_location.line : changed_location.fallback_line
|
|
69
|
+
return unless target_line
|
|
70
|
+
|
|
71
|
+
member = scan_android_collection_members(file, target_line, expected_parent) if File.file?(file)
|
|
72
|
+
@android_collection_members_by_location ||= {}
|
|
73
|
+
@android_collection_members_by_location[cache_key] = member
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def scan_android_collection_members(file, target_line, expected_parent)
|
|
77
|
+
android_resource_index(file).member_at(target_line, parent: expected_parent)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def android_resource_index(file)
|
|
81
|
+
@android_resource_indexes_by_file ||= {}
|
|
82
|
+
@android_resource_indexes_by_file[file] ||=
|
|
83
|
+
AndroidResource.index(File.read(file, encoding: 'UTF-8'))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def git_diff
|
|
87
|
+
@git_diff ||= GitDiff.new(base_ref: @config.diff_base, head_ref: @config.diff_head)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18nContextGenerator
|
|
4
|
+
class ContextExtractor
|
|
5
|
+
# Explicit non-mutating and mutating workflow stages shared by the CLI and
|
|
6
|
+
# programmatic callers.
|
|
7
|
+
module Workflow
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def workflow_stage
|
|
11
|
+
@config.dry_run ? 'plan' : @config.workflow_stage
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def pre_extraction_stage_handled?(entries)
|
|
15
|
+
case workflow_stage
|
|
16
|
+
when 'plan'
|
|
17
|
+
log_plan(entries)
|
|
18
|
+
true
|
|
19
|
+
when 'check'
|
|
20
|
+
check_entries(entries)
|
|
21
|
+
true
|
|
22
|
+
else
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log_plan(entries)
|
|
28
|
+
label = @config.dry_run ? 'Dry run' : 'Plan'
|
|
29
|
+
log "\n#{label} - would process these keys:"
|
|
30
|
+
entries.first(20).each { |entry| log " - #{entry.key}: #{truncate(entry.text, 50)}" }
|
|
31
|
+
log " ... and #{entries.size - 20} more" if entries.size > 20
|
|
32
|
+
destinations = []
|
|
33
|
+
destinations << (@config.output_stdout ? 'structured stdout' : @config.output_path) if @config.output_path
|
|
34
|
+
destinations << 'translation write-back' if @config.write_back
|
|
35
|
+
destinations << 'Swift code write-back' if @config.write_back_to_code
|
|
36
|
+
log "Destinations: #{destinations.empty? ? 'none' : destinations.join(', ')}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check_entries(entries)
|
|
40
|
+
without_usage = entries.count do |entry|
|
|
41
|
+
resource_type = entry.metadata&.dig(:resource_type)
|
|
42
|
+
searcher.search(entry.key, resource_type: resource_type).empty?
|
|
43
|
+
end
|
|
44
|
+
log "Check passed: #{entries.size} entries parsed; #{without_usage} without source usage."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deliver_results
|
|
48
|
+
if workflow_stage == 'preview_diff'
|
|
49
|
+
preview_changes
|
|
50
|
+
else
|
|
51
|
+
apply_results
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def apply_results
|
|
56
|
+
write_configured_output
|
|
57
|
+
write_back_to_source if @config.write_back
|
|
58
|
+
write_back_to_code if @config.write_back_to_code
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def write_configured_output
|
|
62
|
+
return unless @config.output_path
|
|
63
|
+
|
|
64
|
+
write_output
|
|
65
|
+
log "\nWrote #{@results.size} results to #{@config.output_stdout ? 'stdout' : @config.output_path}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def preview_changes
|
|
69
|
+
preview_translation_changes if @config.write_back
|
|
70
|
+
preview_code_changes if @config.write_back_to_code
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def preview_translation_changes
|
|
74
|
+
@config.translations.each do |path|
|
|
75
|
+
writer = source_writer_for(path)
|
|
76
|
+
next unless writer
|
|
77
|
+
|
|
78
|
+
relevant_results = @results.select { |result| result_matches_source_path?(result, path) }
|
|
79
|
+
next if relevant_results.empty?
|
|
80
|
+
|
|
81
|
+
patch = Writers::Preview.render(path) do |candidate_path|
|
|
82
|
+
candidate_results = relevant_results.map do |result|
|
|
83
|
+
ExtractionResult.new(**result.to_h.except(:source_file), source_file: candidate_path)
|
|
84
|
+
end
|
|
85
|
+
writer.write(candidate_results, candidate_path)
|
|
86
|
+
end
|
|
87
|
+
write_patch(patch) if patch
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def preview_code_changes
|
|
92
|
+
results_by_key = build_results_by_key_for_code_write_back
|
|
93
|
+
return if results_by_key.empty?
|
|
94
|
+
|
|
95
|
+
swift_files_for_write_back.each do |swift_file|
|
|
96
|
+
patch = Writers::Preview.render(swift_file) do |candidate_path|
|
|
97
|
+
swift_writer.update_file(candidate_path, results_by_key)
|
|
98
|
+
end
|
|
99
|
+
write_patch(patch) if patch
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def swift_writer
|
|
104
|
+
@swift_writer ||= Writers::SwiftWriter.new(
|
|
105
|
+
functions: @config.swift_functions,
|
|
106
|
+
context_prefix: @config.context_prefix,
|
|
107
|
+
context_mode: @config.context_mode
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def swift_files_for_write_back
|
|
112
|
+
@config.source_paths.flat_map do |source_path|
|
|
113
|
+
find_swift_files(source_path, ignore_patterns: @config.ignore_patterns)
|
|
114
|
+
end.uniq
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|