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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +163 -26
  3. data/lib/i18n_context_generator/android_resource.rb +209 -0
  4. data/lib/i18n_context_generator/apple_string_literal.rb +28 -0
  5. data/lib/i18n_context_generator/cache.rb +40 -8
  6. data/lib/i18n_context_generator/changed_location.rb +55 -0
  7. data/lib/i18n_context_generator/cli.rb +163 -76
  8. data/lib/i18n_context_generator/config/cli_values.rb +33 -0
  9. data/lib/i18n_context_generator/config/defaults.rb +35 -0
  10. data/lib/i18n_context_generator/config/schema.rb +225 -0
  11. data/lib/i18n_context_generator/config/serialization.rb +64 -0
  12. data/lib/i18n_context_generator/config/validation.rb +249 -0
  13. data/lib/i18n_context_generator/config.rb +293 -102
  14. data/lib/i18n_context_generator/context_extractor/cache_identity.rb +65 -0
  15. data/lib/i18n_context_generator/context_extractor/extraction_result.rb +46 -0
  16. data/lib/i18n_context_generator/context_extractor/run_logging.rb +45 -6
  17. data/lib/i18n_context_generator/context_extractor/source_entries.rb +103 -0
  18. data/lib/i18n_context_generator/context_extractor/source_filters.rb +51 -5
  19. data/lib/i18n_context_generator/context_extractor/translation_filters.rb +91 -0
  20. data/lib/i18n_context_generator/context_extractor/workflow.rb +118 -0
  21. data/lib/i18n_context_generator/context_extractor.rb +207 -216
  22. data/lib/i18n_context_generator/file_classifier.rb +72 -0
  23. data/lib/i18n_context_generator/generated_comment.rb +32 -0
  24. data/lib/i18n_context_generator/git_diff/xml_changes.rb +235 -0
  25. data/lib/i18n_context_generator/git_diff.rb +224 -95
  26. data/lib/i18n_context_generator/llm/anthropic.rb +65 -38
  27. data/lib/i18n_context_generator/llm/client.rb +294 -92
  28. data/lib/i18n_context_generator/llm/openai.rb +92 -51
  29. data/lib/i18n_context_generator/llm/openai_compatible.rb +20 -0
  30. data/lib/i18n_context_generator/llm/prompt_evidence.rb +47 -0
  31. data/lib/i18n_context_generator/llm/request_policy.rb +147 -0
  32. data/lib/i18n_context_generator/localization_syntax.rb +246 -0
  33. data/lib/i18n_context_generator/parsers/android_xml_parser.rb +47 -8
  34. data/lib/i18n_context_generator/parsers/base.rb +7 -4
  35. data/lib/i18n_context_generator/parsers/json_parser.rb +4 -0
  36. data/lib/i18n_context_generator/parsers/xcstrings_parser.rb +79 -0
  37. data/lib/i18n_context_generator/parsers/yaml_parser.rb +20 -7
  38. data/lib/i18n_context_generator/path_policy.rb +107 -0
  39. data/lib/i18n_context_generator/platform_validator.rb +24 -50
  40. data/lib/i18n_context_generator/run_metrics.rb +47 -0
  41. data/lib/i18n_context_generator/searcher/comment_masking.rb +115 -0
  42. data/lib/i18n_context_generator/searcher/match_filtering.rb +52 -0
  43. data/lib/i18n_context_generator/searcher/source_discovery.rb +109 -64
  44. data/lib/i18n_context_generator/searcher.rb +61 -205
  45. data/lib/i18n_context_generator/supplemental_context.rb +77 -0
  46. data/lib/i18n_context_generator/translation_comment_index.rb +121 -0
  47. data/lib/i18n_context_generator/version.rb +1 -1
  48. data/lib/i18n_context_generator/writers/android_xml_writer.rb +48 -18
  49. data/lib/i18n_context_generator/writers/atomic_file.rb +37 -0
  50. data/lib/i18n_context_generator/writers/csv_writer.rb +43 -18
  51. data/lib/i18n_context_generator/writers/helpers.rb +7 -29
  52. data/lib/i18n_context_generator/writers/json_writer.rb +31 -6
  53. data/lib/i18n_context_generator/writers/preview.rb +80 -0
  54. data/lib/i18n_context_generator/writers/result_serialization.rb +30 -0
  55. data/lib/i18n_context_generator/writers/strings_writer.rb +23 -12
  56. data/lib/i18n_context_generator/writers/swift_writer.rb +16 -54
  57. data/lib/i18n_context_generator/writers/xcstrings_writer.rb +57 -0
  58. data/lib/i18n_context_generator/xcstrings_document.rb +218 -0
  59. data/lib/i18n_context_generator/xml_scanner.rb +38 -0
  60. data/lib/i18n_context_generator.rb +20 -4
  61. metadata +59 -12
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../generated_comment'
4
+
3
5
  module I18nContextGenerator
4
6
  module Writers
5
7
  # Writer that updates iOS .strings files with context comments
@@ -15,6 +17,7 @@ module I18nContextGenerator
15
17
  def write(results, source_path)
16
18
  return unless File.exist?(source_path)
17
19
 
20
+ original_content = File.binread(source_path)
18
21
  # Parse the existing file
19
22
  original_file = DotStrings.parse_file(source_path, strict: false)
20
23
  results_by_key = results.each_with_object({}) do |result, lookup|
@@ -22,6 +25,7 @@ module I18nContextGenerator
22
25
 
23
26
  lookup[result.key] = result
24
27
  end
28
+ return false unless results_by_key.values.any? { |result| writable_result?(result) }
25
29
 
26
30
  # Build new file with updated comments (DotStrings::Item is immutable)
27
31
  new_file = DotStrings::File.new
@@ -43,24 +47,31 @@ module I18nContextGenerator
43
47
  new_file << new_item
44
48
  end
45
49
 
46
- # Write back to file
47
- File.write(source_path, new_file.to_s)
50
+ rendered = new_file.to_s
51
+ return false if rendered == original_content
52
+
53
+ AtomicFile.replace(source_path, rendered) do |candidate_path|
54
+ DotStrings.parse_file(candidate_path, strict: true)
55
+ end
56
+ true
48
57
  end
49
58
 
50
59
  private
51
60
 
52
61
  def build_comment(existing_comment, context_description)
53
- context_line = "#{@context_prefix}#{context_description}"
62
+ context_line = sanitize_comment("#{@context_prefix}#{context_description}")
63
+ context_prefix = sanitize_comment(@context_prefix)
64
+ GeneratedComment.merge(
65
+ existing: existing_comment,
66
+ generated: context_line,
67
+ prefix: context_prefix,
68
+ mode: @context_mode,
69
+ separator: "\n"
70
+ )
71
+ end
54
72
 
55
- if existing_comment.nil? || existing_comment.empty? || @context_mode == 'replace'
56
- context_line
57
- elsif !@context_prefix.empty? && existing_comment.include?(@context_prefix)
58
- # Replace existing context line (idempotent update)
59
- existing_comment.gsub(/#{Regexp.escape(@context_prefix)}[^\n]*/, context_line)
60
- else
61
- # Append context to existing comment
62
- "#{existing_comment}\n#{context_line}"
63
- end
73
+ def sanitize_comment(comment)
74
+ comment.gsub('*/', '* /')
64
75
  end
65
76
  end
66
77
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../generated_comment'
4
+ require_relative '../localization_syntax'
5
+
3
6
  module I18nContextGenerator
4
7
  module Writers
5
8
  # Writer that updates comment: parameters in Swift localization calls
@@ -7,20 +10,11 @@ module I18nContextGenerator
7
10
  class SwiftWriter
8
11
  include Helpers
9
12
 
10
- SWIFT_STRING_PATTERN = '"(?:\\\\.|[^"\\\\])*"'
11
13
  COMMENT_ARGUMENT_PATTERN = /comment:\s*"((?:\\.|[^"\\])*)"/
12
- private_constant :SWIFT_STRING_PATTERN, :COMMENT_ARGUMENT_PATTERN
13
-
14
- # Default patterns for Swift localization functions
15
- # Each pattern should capture: (prefix)(key)(middle)(comment_value)(suffix)
16
- DEFAULT_FUNCTIONS = %w[
17
- NSLocalizedString
18
- String(localized:
19
- Text(
20
- ].freeze
14
+ private_constant :COMMENT_ARGUMENT_PATTERN
21
15
 
22
16
  def initialize(functions: nil, context_prefix: 'Context: ', context_mode: 'replace')
23
- @functions = functions || DEFAULT_FUNCTIONS
17
+ @localization_syntax = LocalizationSyntax.new(swift_functions: functions)
24
18
  @context_prefix = context_prefix
25
19
  @context_mode = context_mode
26
20
  end
@@ -61,7 +55,7 @@ module I18nContextGenerator
61
55
  end
62
56
 
63
57
  if updated && content != original_content
64
- File.write(path, content)
58
+ AtomicFile.replace(path, content)
65
59
  true
66
60
  else
67
61
  false
@@ -72,46 +66,17 @@ module I18nContextGenerator
72
66
 
73
67
  # Update comment for a specific key in the content
74
68
  def update_comment_for_key(content, key, description)
75
- escaped_key = Regexp.escape(key)
76
-
77
- @functions.each do |func|
78
- # Build pattern based on function type
79
- pattern = build_pattern_for_function(func, escaped_key)
80
- next unless pattern
81
-
69
+ @localization_syntax.swift_writer_patterns(key).each do |pattern|
82
70
  # Try to match and replace
83
71
  content = content.gsub(pattern) do |match|
84
- update_match(match, func, key, description)
72
+ update_match(match, description)
85
73
  end
86
74
  end
87
75
 
88
76
  content
89
77
  end
90
78
 
91
- def build_pattern_for_function(func, escaped_key)
92
- comment_pattern = "comment:\\s*#{SWIFT_STRING_PATTERN}"
93
-
94
- case func
95
- when 'NSLocalizedString'
96
- # NSLocalizedString("key", comment: "...")
97
- # NSLocalizedString("key", value: "...", comment: "...")
98
- # NSLocalizedString("key", tableName: "...", comment: "...")
99
- Regexp.new("NSLocalizedString\\(\\s*\"#{escaped_key}\"[^)]*#{comment_pattern}[^)]*\\)", Regexp::MULTILINE)
100
- when 'String(localized:'
101
- # String(localized: "key", comment: "...")
102
- Regexp.new("String\\(\\s*localized:\\s*\"#{escaped_key}\"[^)]*#{comment_pattern}[^)]*\\)", Regexp::MULTILINE)
103
- when 'Text('
104
- # Text("key", comment: "...")
105
- # Text(LocalizedStringKey("key"), comment: "...")
106
- Regexp.new("Text\\([^)]*\"#{escaped_key}\"[^)]*#{comment_pattern}[^)]*\\)", Regexp::MULTILINE)
107
- else
108
- # Custom function - assume pattern like: func("key", ..., comment: "...")
109
- escaped_func = Regexp.escape(func)
110
- Regexp.new("#{escaped_func}\\([^)]*\"#{escaped_key}\"[^)]*#{comment_pattern}[^)]*\\)", Regexp::MULTILINE)
111
- end
112
- end
113
-
114
- def update_match(match, _func, _key, new_comment)
79
+ def update_match(match, new_comment)
115
80
  # Replace the comment value while preserving the rest of the call
116
81
  match.gsub(COMMENT_ARGUMENT_PATTERN) do |_comment_match|
117
82
  existing_comment = unescape_swift_string(Regexp.last_match(1))
@@ -122,16 +87,13 @@ module I18nContextGenerator
122
87
 
123
88
  def build_final_comment(existing_comment, new_context)
124
89
  context_line = "#{@context_prefix}#{new_context}"
125
-
126
- if existing_comment.nil? || existing_comment.empty? || @context_mode == 'replace'
127
- context_line
128
- elsif !@context_prefix.empty? && existing_comment.include?(@context_prefix)
129
- # Update existing context line (idempotent)
130
- existing_comment.gsub(/#{Regexp.escape(@context_prefix)}[^\n]*/, context_line)
131
- else
132
- # Append context to existing comment
133
- "#{existing_comment} #{context_line}"
134
- end
90
+ GeneratedComment.merge(
91
+ existing: existing_comment,
92
+ generated: context_line,
93
+ prefix: @context_prefix,
94
+ mode: @context_mode,
95
+ separator: ' '
96
+ )
135
97
  end
136
98
 
137
99
  def escape_swift_string(str)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../generated_comment'
4
+
5
+ module I18nContextGenerator
6
+ module Writers
7
+ # Updates developer comments in Apple string catalogs while leaving every
8
+ # localization and extraction attribute intact.
9
+ class XcstringsWriter
10
+ include Helpers
11
+
12
+ def initialize(context_prefix: 'Context: ', context_mode: 'replace')
13
+ @context_prefix = context_prefix
14
+ @context_mode = context_mode
15
+ end
16
+
17
+ def write(results, source_path)
18
+ return unless File.exist?(source_path)
19
+
20
+ original = File.binread(source_path)
21
+ document = XcstringsDocument.new(original, path: source_path)
22
+ catalog = document.catalog
23
+ Parsers::XcstringsParser.validate_catalog!(catalog, path: source_path)
24
+ results_by_key = results.each_with_object({}) do |result, lookup|
25
+ next unless result_matches_source_path?(result, source_path)
26
+ next unless writable_result?(result)
27
+
28
+ lookup[result.key] = result
29
+ end
30
+ return false if results_by_key.empty?
31
+
32
+ comments_by_key = catalog.fetch('strings').each_with_object({}) do |(key, entry), comments|
33
+ result = results_by_key[key]
34
+ next unless result
35
+
36
+ comments[key] = GeneratedComment.merge(
37
+ existing: entry['comment'],
38
+ generated: "#{@context_prefix}#{result.description}",
39
+ prefix: @context_prefix,
40
+ mode: @context_mode,
41
+ separator: "\n"
42
+ )
43
+ end
44
+ rendered = document.with_comments(comments_by_key)
45
+ return false if rendered == original
46
+
47
+ AtomicFile.replace(source_path, rendered) do |candidate_path|
48
+ candidate = Oj.load_file(candidate_path, mode: :strict)
49
+ Parsers::XcstringsParser.validate_catalog!(candidate, path: candidate_path)
50
+ end
51
+ true
52
+ rescue Oj::ParseError => e
53
+ raise Error, "Failed to parse Apple string catalog #{source_path}: #{e.message}"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'oj'
5
+
6
+ module I18nContextGenerator
7
+ # Structural index for an Apple string catalog that retains byte offsets in
8
+ # the original JSON. It lets write-back edit only owned comment values and
9
+ # lets diff handling map revision-specific line numbers without reformatting.
10
+ class XcstringsDocument
11
+ Member = Data.define(:key, :key_start, :key_end, :value_start, :value_end, :separator)
12
+
13
+ attr_reader :catalog
14
+
15
+ def initialize(content, path:)
16
+ @content = content
17
+ @path = path
18
+ @catalog = Oj.load(content, mode: :strict)
19
+ @line_starts = [0]
20
+ content.each_char.with_index { |character, index| @line_starts << (index + 1) if character == "\n" }
21
+ @root_members = object_members(skip_whitespace(0))
22
+ strings_member = @root_members.find { |member| member.key == 'strings' }
23
+ raise TypeError, 'strings must be a mapping' unless strings_member && @content[strings_member.value_start] == '{'
24
+
25
+ @strings_member = strings_member
26
+ @entry_members = object_members(strings_member.value_start)
27
+ @entries_by_key = @entry_members.to_h { |member| [member.key, member] }
28
+ rescue Oj::ParseError, TypeError => e
29
+ raise Error, "Failed to parse Apple string catalog #{path}: #{e.message}"
30
+ end
31
+
32
+ def line_index
33
+ @entry_members.each_with_object({}) do |member, index|
34
+ start_line = line_number(member.key_start)
35
+ end_line = line_number(member.value_end - 1)
36
+ (start_line..end_line).each { |line| index[line] = member.key }
37
+ end
38
+ end
39
+
40
+ def with_comments(comments_by_key)
41
+ edits = comments_by_key.filter_map do |key, comment|
42
+ entry_member = @entries_by_key[key]
43
+ next unless entry_member && @content[entry_member.value_start] == '{'
44
+ next if @catalog.dig('strings', key, 'comment') == comment
45
+
46
+ comment_edit(entry_member, comment)
47
+ end
48
+ return @content if edits.empty?
49
+
50
+ rendered = @content.dup
51
+ edits.sort_by(&:first).reverse_each do |start_offset, end_offset, replacement|
52
+ rendered[start_offset...end_offset] = replacement
53
+ end
54
+ rendered
55
+ end
56
+
57
+ private
58
+
59
+ def comment_edit(entry_member, comment)
60
+ children = object_members(entry_member.value_start)
61
+ existing = children.find { |member| member.key == 'comment' }
62
+ encoded_comment = JSON.generate(comment)
63
+ return [existing.value_start, existing.value_end, encoded_comment] if existing
64
+
65
+ insert_comment_edit(entry_member, children, encoded_comment)
66
+ end
67
+
68
+ def insert_comment_edit(entry_member, children, encoded_comment)
69
+ separator = children.first&.separator || entry_member.separator
70
+ if inline_member?(entry_member)
71
+ insertion_point = entry_member.value_start + 1
72
+ rendered_comment = "\"comment\"#{separator}#{encoded_comment}"
73
+ rendered_comment = "#{rendered_comment}," unless children.empty?
74
+ return [insertion_point, insertion_point, rendered_comment]
75
+ end
76
+
77
+ entry_indent = line_indent(entry_member.key_start)
78
+ child_indent = children.empty? ? "#{entry_indent}#{indent_unit}" : line_indent(children.first.key_start)
79
+ child_indent = "#{entry_indent}#{indent_unit}" unless child_indent.match?(/\A\s*\z/)
80
+ rendered_comment = "#{child_indent}\"comment\"#{separator}#{encoded_comment}"
81
+
82
+ if children.empty?
83
+ interior_start = entry_member.value_start + 1
84
+ interior_end = entry_member.value_end - 1
85
+ replacement = "#{newline}#{rendered_comment}#{newline}#{entry_indent}"
86
+ [interior_start, interior_end, replacement]
87
+ else
88
+ insertion_point = entry_member.value_start + 1
89
+ [insertion_point, insertion_point, "#{newline}#{rendered_comment},"]
90
+ end
91
+ end
92
+
93
+ def inline_member?(member)
94
+ !line_indent(member.key_start).match?(/\A[ \t]*\z/)
95
+ end
96
+
97
+ def indent_unit
98
+ @indent_unit ||= begin
99
+ strings_indent = line_indent(@strings_member.key_start)
100
+ block_entry = @entry_members.find { |member| !inline_member?(member) }
101
+ entry_indent = line_indent(block_entry&.key_start || @strings_member.key_start)
102
+ difference = entry_indent.delete_prefix(strings_indent)
103
+ difference.empty? || !difference.match?(/\A[ \t]*\z/) ? ' ' : difference
104
+ end
105
+ end
106
+
107
+ def newline
108
+ @newline ||= @content.include?("\r\n") ? "\r\n" : "\n"
109
+ end
110
+
111
+ def line_indent(offset)
112
+ line_start = @content.rindex("\n", offset - 1)
113
+ @content[(line_start ? line_start + 1 : 0)...offset]
114
+ end
115
+
116
+ def line_number(offset)
117
+ @line_starts.bsearch_index { |line_start| line_start > offset } || @line_starts.length
118
+ end
119
+
120
+ def object_members(object_start)
121
+ raise TypeError, "expected object at byte #{object_start}" unless @content[object_start] == '{'
122
+
123
+ members = []
124
+ cursor = skip_whitespace(object_start + 1)
125
+ return members if @content[cursor] == '}'
126
+
127
+ loop do
128
+ key_start = cursor
129
+ key_end = string_end(key_start)
130
+ key = JSON.parse(@content[key_start...key_end])
131
+ cursor = skip_whitespace(key_end)
132
+ raise TypeError, "expected ':' at byte #{cursor}" unless @content[cursor] == ':'
133
+
134
+ cursor = skip_whitespace(cursor + 1)
135
+ value_start = cursor
136
+ value_end = value_end(value_start)
137
+ members << Member.new(
138
+ key: key,
139
+ key_start: key_start,
140
+ key_end: key_end,
141
+ value_start: value_start,
142
+ value_end: value_end,
143
+ separator: @content[key_end...value_start]
144
+ )
145
+ cursor = skip_whitespace(value_end)
146
+ break if @content[cursor] == '}'
147
+
148
+ raise TypeError, "expected ',' at byte #{cursor}" unless @content[cursor] == ','
149
+
150
+ cursor = skip_whitespace(cursor + 1)
151
+ end
152
+
153
+ members
154
+ end
155
+
156
+ def value_end(start_offset)
157
+ case @content[start_offset]
158
+ when '"'
159
+ string_end(start_offset)
160
+ when '{'
161
+ collection_end(start_offset, '{', '}')
162
+ when '['
163
+ collection_end(start_offset, '[', ']')
164
+ else
165
+ primitive_end(start_offset)
166
+ end
167
+ end
168
+
169
+ def string_end(start_offset)
170
+ cursor = start_offset + 1
171
+ escaped = false
172
+ while cursor < @content.length
173
+ character = @content[cursor]
174
+ if escaped
175
+ escaped = false
176
+ elsif character == '\\'
177
+ escaped = true
178
+ elsif character == '"'
179
+ return cursor + 1
180
+ end
181
+ cursor += 1
182
+ end
183
+ raise TypeError, "unterminated string at byte #{start_offset}"
184
+ end
185
+
186
+ def collection_end(start_offset, opening, closing)
187
+ depth = 0
188
+ cursor = start_offset
189
+ while cursor < @content.length
190
+ character = @content[cursor]
191
+ case character
192
+ when '"'
193
+ cursor = string_end(cursor)
194
+ next
195
+ when opening
196
+ depth += 1
197
+ when closing
198
+ depth -= 1
199
+ return cursor + 1 if depth.zero?
200
+ end
201
+ cursor += 1
202
+ end
203
+ raise TypeError, "unterminated collection at byte #{start_offset}"
204
+ end
205
+
206
+ def primitive_end(start_offset)
207
+ cursor = start_offset
208
+ cursor += 1 while cursor < @content.length && !",}] \t\r\n".include?(@content[cursor])
209
+ cursor
210
+ end
211
+
212
+ def skip_whitespace(offset)
213
+ cursor = offset
214
+ cursor += 1 while cursor < @content.length && @content[cursor].match?(/\s/)
215
+ cursor
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nContextGenerator
4
+ # Small stateful XML scanning helpers for line-oriented diff and location code.
5
+ # This is intentionally not an XML parser; it only removes comments while
6
+ # preserving the remaining text and line boundaries.
7
+ module XmlScanner
8
+ module_function
9
+
10
+ def without_comments(line, state)
11
+ remaining = line.to_s
12
+ visible = +''
13
+ contained_comment = false
14
+
15
+ loop do
16
+ if state[:inside_comment]
17
+ contained_comment = true
18
+ comment_end = remaining.index('-->')
19
+ return [visible, contained_comment] unless comment_end
20
+
21
+ state[:inside_comment] = false
22
+ remaining = remaining[(comment_end + 3)..].to_s
23
+ else
24
+ comment_start = remaining.index('<!--')
25
+ unless comment_start
26
+ visible << remaining
27
+ return [visible, contained_comment]
28
+ end
29
+
30
+ visible << remaining[0...comment_start]
31
+ contained_comment = true
32
+ state[:inside_comment] = true
33
+ remaining = remaining[(comment_start + 4)..].to_s
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module I18nContextGenerator
4
+ class Error < StandardError; end
5
+ end
6
+
3
7
  require 'yaml'
4
8
  require 'csv'
5
9
  require 'digest'
@@ -10,27 +14,39 @@ require 'tty-progressbar'
10
14
  require 'dotstrings'
11
15
 
12
16
  require_relative 'i18n_context_generator/version'
17
+ require_relative 'i18n_context_generator/path_policy'
18
+ require_relative 'i18n_context_generator/file_classifier'
19
+ require_relative 'i18n_context_generator/supplemental_context'
20
+ require_relative 'i18n_context_generator/apple_string_literal'
21
+ require_relative 'i18n_context_generator/changed_location'
22
+ require_relative 'i18n_context_generator/localization_syntax'
23
+ require_relative 'i18n_context_generator/generated_comment'
24
+ require_relative 'i18n_context_generator/android_resource'
25
+ require_relative 'i18n_context_generator/xcstrings_document'
13
26
  require_relative 'i18n_context_generator/config'
14
27
  require_relative 'i18n_context_generator/parsers/base'
15
28
  require_relative 'i18n_context_generator/parsers/json_parser'
16
29
  require_relative 'i18n_context_generator/parsers/yaml_parser'
17
30
  require_relative 'i18n_context_generator/parsers/strings_parser'
31
+ require_relative 'i18n_context_generator/parsers/xcstrings_parser'
18
32
  require_relative 'i18n_context_generator/parsers/android_xml_parser'
19
33
  require_relative 'i18n_context_generator/searcher'
20
34
  require_relative 'i18n_context_generator/llm/client'
21
35
  require_relative 'i18n_context_generator/llm/anthropic'
22
36
  require_relative 'i18n_context_generator/llm/openai'
37
+ require_relative 'i18n_context_generator/llm/openai_compatible'
23
38
  require_relative 'i18n_context_generator/writers/helpers'
39
+ require_relative 'i18n_context_generator/writers/result_serialization'
40
+ require_relative 'i18n_context_generator/writers/atomic_file'
41
+ require_relative 'i18n_context_generator/writers/preview'
24
42
  require_relative 'i18n_context_generator/writers/csv_writer'
25
43
  require_relative 'i18n_context_generator/writers/json_writer'
26
44
  require_relative 'i18n_context_generator/writers/strings_writer'
45
+ require_relative 'i18n_context_generator/writers/xcstrings_writer'
27
46
  require_relative 'i18n_context_generator/writers/android_xml_writer'
28
47
  require_relative 'i18n_context_generator/writers/swift_writer'
29
48
  require_relative 'i18n_context_generator/cache'
30
49
  require_relative 'i18n_context_generator/git_diff'
31
50
  require_relative 'i18n_context_generator/platform_validator'
51
+ require_relative 'i18n_context_generator/run_metrics'
32
52
  require_relative 'i18n_context_generator/context_extractor'
33
-
34
- module I18nContextGenerator
35
- class Error < StandardError; end
36
- end