i18n-context-generator 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6588c09e273cb74c40dba8cc26798e46b82b5d47e5c840f6e4609843bb1f5ed8
4
- data.tar.gz: 9b3e309fd4030a99e065a2c7693240f09d205f9d95323a9744ecb3eaa1b239b1
3
+ metadata.gz: 2109020f4ff761dcf5fdd9817499b551619dc8ee17cef9e88acc881ac5ad647f
4
+ data.tar.gz: 26c9e86a187bbe4f2a0523754a7ee78912d96d9e532e261143ce905106cfd3da
5
5
  SHA512:
6
- metadata.gz: 2a7b9df974f135375341c7c1a5f73e40575f10c92d11a72ecc976ecef9ae6b25aac32351f1468c79671c870d87c2df576664a96b32ef00128815fc4ff016de1c
7
- data.tar.gz: 0cc9db25973ffe209dea8cb4197df899310320361f0a8da2e45fa597fc592867bea22cdd5ad5337907c18cbdf824e269955fe79550baedd23f0d919fdef73c00
6
+ metadata.gz: ed14b199d897b8ee27dbd793c372df01b72efc4a9b6a1122cd9fa724abb76030b93e1c967f82c02c5fafb4e104fba3b8abe10b9b1d672d95521b9b9f04d6b555
7
+ data.tar.gz: e13a1a8d4c17823597727717afb4f5950d0790109c35a124262989504123165a2111482fe9d27bf9baff3de930c9bc8fb6159edb1dee1ed9a8ad863beb6c346c
@@ -38,8 +38,18 @@ module I18nContextGenerator
38
38
  required: %w[description ui_element tone max_length confidence ambiguity_reason],
39
39
  properties: {
40
40
  description: { type: 'string' },
41
- ui_element: { type: %w[string null], enum: UI_ELEMENTS + [nil] },
42
- tone: { type: %w[string null], enum: TONES + [nil] },
41
+ ui_element: {
42
+ anyOf: [
43
+ { type: 'string', enum: UI_ELEMENTS },
44
+ { type: 'null' }
45
+ ]
46
+ },
47
+ tone: {
48
+ anyOf: [
49
+ { type: 'string', enum: TONES },
50
+ { type: 'null' }
51
+ ]
52
+ },
43
53
  max_length: { type: %w[integer null] },
44
54
  confidence: { type: 'string', enum: CONFIDENCE_LEVELS },
45
55
  ambiguity_reason: {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nContextGenerator
4
- VERSION = '0.5.0'
4
+ VERSION = '0.5.1'
5
5
  end
@@ -9,6 +9,25 @@ module I18nContextGenerator
9
9
  # lets diff handling map revision-specific line numbers without reformatting.
10
10
  class XcstringsDocument
11
11
  Member = Data.define(:key, :key_start, :key_end, :value_start, :value_end, :separator)
12
+ BYTE = {
13
+ backslash: '\\'.ord,
14
+ close_array: ']'.ord,
15
+ close_object: '}'.ord,
16
+ colon: ':'.ord,
17
+ comma: ','.ord,
18
+ newline: "\n".ord,
19
+ open_array: '['.ord,
20
+ open_object: '{'.ord,
21
+ quote: '"'.ord
22
+ }.freeze
23
+ JSON_WHITESPACE_BYTES = ["\t".ord, "\n".ord, "\r".ord, ' '.ord].freeze
24
+ PRIMITIVE_TERMINATOR_BYTES = [
25
+ BYTE[:comma],
26
+ BYTE[:close_object],
27
+ BYTE[:close_array],
28
+ *JSON_WHITESPACE_BYTES
29
+ ].freeze
30
+ private_constant :BYTE, :JSON_WHITESPACE_BYTES, :PRIMITIVE_TERMINATOR_BYTES
12
31
 
13
32
  attr_reader :catalog
14
33
 
@@ -17,10 +36,11 @@ module I18nContextGenerator
17
36
  @path = path
18
37
  @catalog = Oj.load(content, mode: :strict)
19
38
  @line_starts = [0]
20
- content.each_char.with_index { |character, index| @line_starts << (index + 1) if character == "\n" }
39
+ content.each_byte.with_index { |byte, index| @line_starts << (index + 1) if byte == BYTE[:newline] }
21
40
  @root_members = object_members(skip_whitespace(0))
22
41
  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] == '{'
42
+ strings_object = strings_member && byte_at(strings_member.value_start) == BYTE[:open_object]
43
+ raise TypeError, 'strings must be a mapping' unless strings_object
24
44
 
25
45
  @strings_member = strings_member
26
46
  @entry_members = object_members(strings_member.value_start)
@@ -40,18 +60,19 @@ module I18nContextGenerator
40
60
  def with_comments(comments_by_key)
41
61
  edits = comments_by_key.filter_map do |key, comment|
42
62
  entry_member = @entries_by_key[key]
43
- next unless entry_member && @content[entry_member.value_start] == '{'
63
+ next unless entry_member && byte_at(entry_member.value_start) == BYTE[:open_object]
44
64
  next if @catalog.dig('strings', key, 'comment') == comment
45
65
 
46
66
  comment_edit(entry_member, comment)
47
67
  end
48
68
  return @content if edits.empty?
49
69
 
50
- rendered = @content.dup
70
+ original_encoding = @content.encoding
71
+ rendered = @content.b
51
72
  edits.sort_by(&:first).reverse_each do |start_offset, end_offset, replacement|
52
- rendered[start_offset...end_offset] = replacement
73
+ rendered[start_offset...end_offset] = replacement.b
53
74
  end
54
- rendered
75
+ rendered.force_encoding(original_encoding)
55
76
  end
56
77
 
57
78
  private
@@ -109,8 +130,9 @@ module I18nContextGenerator
109
130
  end
110
131
 
111
132
  def line_indent(offset)
112
- line_start = @content.rindex("\n", offset - 1)
113
- @content[(line_start ? line_start + 1 : 0)...offset]
133
+ following_line = @line_starts.bsearch_index { |line_start| line_start > offset }
134
+ line_start = following_line ? @line_starts.fetch(following_line - 1) : @line_starts.last
135
+ byte_slice(line_start, offset)
114
136
  end
115
137
 
116
138
  def line_number(offset)
@@ -118,18 +140,18 @@ module I18nContextGenerator
118
140
  end
119
141
 
120
142
  def object_members(object_start)
121
- raise TypeError, "expected object at byte #{object_start}" unless @content[object_start] == '{'
143
+ raise TypeError, "expected object at byte #{object_start}" unless byte_at(object_start) == BYTE[:open_object]
122
144
 
123
145
  members = []
124
146
  cursor = skip_whitespace(object_start + 1)
125
- return members if @content[cursor] == '}'
147
+ return members if byte_at(cursor) == BYTE[:close_object]
126
148
 
127
149
  loop do
128
150
  key_start = cursor
129
151
  key_end = string_end(key_start)
130
- key = JSON.parse(@content[key_start...key_end])
152
+ key = JSON.parse(byte_slice(key_start, key_end))
131
153
  cursor = skip_whitespace(key_end)
132
- raise TypeError, "expected ':' at byte #{cursor}" unless @content[cursor] == ':'
154
+ raise TypeError, "expected ':' at byte #{cursor}" unless byte_at(cursor) == BYTE[:colon]
133
155
 
134
156
  cursor = skip_whitespace(cursor + 1)
135
157
  value_start = cursor
@@ -140,12 +162,12 @@ module I18nContextGenerator
140
162
  key_end: key_end,
141
163
  value_start: value_start,
142
164
  value_end: value_end,
143
- separator: @content[key_end...value_start]
165
+ separator: byte_slice(key_end, value_start)
144
166
  )
145
167
  cursor = skip_whitespace(value_end)
146
- break if @content[cursor] == '}'
168
+ break if byte_at(cursor) == BYTE[:close_object]
147
169
 
148
- raise TypeError, "expected ',' at byte #{cursor}" unless @content[cursor] == ','
170
+ raise TypeError, "expected ',' at byte #{cursor}" unless byte_at(cursor) == BYTE[:comma]
149
171
 
150
172
  cursor = skip_whitespace(cursor + 1)
151
173
  end
@@ -154,13 +176,13 @@ module I18nContextGenerator
154
176
  end
155
177
 
156
178
  def value_end(start_offset)
157
- case @content[start_offset]
158
- when '"'
179
+ case byte_at(start_offset)
180
+ when BYTE[:quote]
159
181
  string_end(start_offset)
160
- when '{'
161
- collection_end(start_offset, '{', '}')
162
- when '['
163
- collection_end(start_offset, '[', ']')
182
+ when BYTE[:open_object]
183
+ collection_end(start_offset, BYTE[:open_object], BYTE[:close_object])
184
+ when BYTE[:open_array]
185
+ collection_end(start_offset, BYTE[:open_array], BYTE[:close_array])
164
186
  else
165
187
  primitive_end(start_offset)
166
188
  end
@@ -169,13 +191,13 @@ module I18nContextGenerator
169
191
  def string_end(start_offset)
170
192
  cursor = start_offset + 1
171
193
  escaped = false
172
- while cursor < @content.length
173
- character = @content[cursor]
194
+ while cursor < @content.bytesize
195
+ byte = byte_at(cursor)
174
196
  if escaped
175
197
  escaped = false
176
- elsif character == '\\'
198
+ elsif byte == BYTE[:backslash]
177
199
  escaped = true
178
- elsif character == '"'
200
+ elsif byte == BYTE[:quote]
179
201
  return cursor + 1
180
202
  end
181
203
  cursor += 1
@@ -186,10 +208,10 @@ module I18nContextGenerator
186
208
  def collection_end(start_offset, opening, closing)
187
209
  depth = 0
188
210
  cursor = start_offset
189
- while cursor < @content.length
190
- character = @content[cursor]
191
- case character
192
- when '"'
211
+ while cursor < @content.bytesize
212
+ byte = byte_at(cursor)
213
+ case byte
214
+ when BYTE[:quote]
193
215
  cursor = string_end(cursor)
194
216
  next
195
217
  when opening
@@ -205,14 +227,22 @@ module I18nContextGenerator
205
227
 
206
228
  def primitive_end(start_offset)
207
229
  cursor = start_offset
208
- cursor += 1 while cursor < @content.length && !",}] \t\r\n".include?(@content[cursor])
230
+ cursor += 1 while cursor < @content.bytesize && !PRIMITIVE_TERMINATOR_BYTES.include?(byte_at(cursor))
209
231
  cursor
210
232
  end
211
233
 
212
234
  def skip_whitespace(offset)
213
235
  cursor = offset
214
- cursor += 1 while cursor < @content.length && @content[cursor].match?(/\s/)
236
+ cursor += 1 while cursor < @content.bytesize && JSON_WHITESPACE_BYTES.include?(byte_at(cursor))
215
237
  cursor
216
238
  end
239
+
240
+ def byte_at(offset)
241
+ @content.getbyte(offset)
242
+ end
243
+
244
+ def byte_slice(start_offset, end_offset)
245
+ @content.byteslice(start_offset...end_offset)
246
+ end
217
247
  end
218
248
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-context-generator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Automattic
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-23 00:00:00.000000000 Z
11
+ date: 2026-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby