localio 0.1.7 → 0.2.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 +5 -5
- data/.github/workflows/ci.yml +28 -0
- data/.gitignore +4 -1
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile.lock +134 -0
- data/README.md +36 -34
- data/bin/localize +10 -7
- data/docs/plans/2026-02-23-modernization-design.md +91 -0
- data/docs/plans/2026-02-23-modernization.md +1699 -0
- data/docs/plans/2026-02-23-twine-writer-design.md +72 -0
- data/docs/plans/2026-02-23-twine-writer.md +267 -0
- data/lib/localio/localizable_writer.rb +4 -1
- data/lib/localio/processors/csv_processor.rb +1 -1
- data/lib/localio/processors/google_drive_processor.rb +19 -45
- data/lib/localio/processors/xlsx_processor.rb +1 -1
- data/lib/localio/template_handler.rb +3 -1
- data/lib/localio/templates/android_localizable.erb +14 -2
- data/lib/localio/templates/ios_constant_localizable.erb +16 -2
- data/lib/localio/templates/ios_localizable.erb +20 -5
- data/lib/localio/templates/java_properties_localizable.erb +16 -2
- data/lib/localio/templates/json_localizable.erb +6 -5
- data/lib/localio/templates/rails_localizable.erb +15 -3
- data/lib/localio/templates/resx_localizable.erb +14 -2
- data/lib/localio/templates/swift_constant_localizable.erb +15 -2
- data/lib/localio/version.rb +1 -1
- data/lib/localio/writers/ios_writer.rb +3 -3
- data/lib/localio/writers/swift_writer.rb +3 -3
- data/lib/localio/writers/twine_writer.rb +48 -0
- data/localio.gemspec +19 -25
- data/spec/fixtures/sample.csv +11 -0
- data/spec/localio/filter_spec.rb +40 -0
- data/spec/localio/formatter_spec.rb +32 -0
- data/spec/localio/processors/csv_processor_spec.rb +89 -0
- data/spec/localio/processors/google_drive_processor_spec.rb +107 -0
- data/spec/localio/processors/xls_processor_spec.rb +65 -0
- data/spec/localio/processors/xlsx_processor_spec.rb +59 -0
- data/spec/localio/segment_spec.rb +27 -0
- data/spec/localio/segments_list_holder_spec.rb +22 -0
- data/spec/localio/string_helper_spec.rb +49 -0
- data/spec/localio/template_handler_spec.rb +67 -0
- data/spec/localio/term_spec.rb +24 -0
- data/spec/localio/writers/android_writer_spec.rb +71 -0
- data/spec/localio/writers/ios_writer_spec.rb +63 -0
- data/spec/localio/writers/java_properties_writer_spec.rb +35 -0
- data/spec/localio/writers/json_writer_spec.rb +57 -0
- data/spec/localio/writers/rails_writer_spec.rb +47 -0
- data/spec/localio/writers/resx_writer_spec.rb +44 -0
- data/spec/localio/writers/swift_writer_spec.rb +42 -0
- data/spec/localio/writers/twine_writer_spec.rb +68 -0
- data/spec/localio_spec.rb +62 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/shared_terms.rb +35 -0
- metadata +61 -46
|
@@ -0,0 +1,1699 @@
|
|
|
1
|
+
# Localio Modernization: Test Suite + Dependency Updates
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add comprehensive RSpec test coverage to the Localio gem, then update all dependencies to support Ruby 3.x.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Phase 1 writes tests against the existing codebase using fixtures and mocks. Phase 2 updates dependencies using the test suite as a safety net. All writer tests use `Dir.mktmpdir` + `Dir.chdir` for file isolation. External services (Google Drive, XLSX/XLS readers) are mocked at the library interface level.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby 3.x, RSpec 3.x, built-in CSV, mocked google_drive/spreadsheet/simple_xlsx_reader
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Phase 1: Test Suite
|
|
14
|
+
|
|
15
|
+
### Task 1: Bootstrap RSpec
|
|
16
|
+
|
|
17
|
+
**Files:**
|
|
18
|
+
- Create: `spec/spec_helper.rb`
|
|
19
|
+
- Create: `.rspec`
|
|
20
|
+
|
|
21
|
+
**Step 1: Create `.rspec`**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
--require spec_helper
|
|
25
|
+
--format documentation
|
|
26
|
+
--color
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Step 2: Create `spec/spec_helper.rb`**
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'tmpdir'
|
|
33
|
+
require 'fileutils'
|
|
34
|
+
|
|
35
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
36
|
+
|
|
37
|
+
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
|
|
38
|
+
|
|
39
|
+
RSpec.configure do |config|
|
|
40
|
+
config.expect_with :rspec do |c|
|
|
41
|
+
c.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
42
|
+
end
|
|
43
|
+
config.mock_with :rspec do |mocks|
|
|
44
|
+
mocks.verify_partial_doubles = true
|
|
45
|
+
end
|
|
46
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
|
47
|
+
config.filter_run_when_matching :focus
|
|
48
|
+
config.disable_monkey_patching!
|
|
49
|
+
config.order = :random
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Step 3: Run RSpec**
|
|
54
|
+
|
|
55
|
+
Run: `bundle exec rspec`
|
|
56
|
+
Expected: `0 examples, 0 failures`
|
|
57
|
+
|
|
58
|
+
**Step 4: Commit**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git add spec/spec_helper.rb .rspec
|
|
62
|
+
git commit -m "test: bootstrap RSpec with spec_helper"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### Task 2: Create fixture CSV and shared terms
|
|
68
|
+
|
|
69
|
+
**Files:**
|
|
70
|
+
- Create: `spec/fixtures/sample.csv`
|
|
71
|
+
- Create: `spec/support/shared_terms.rb`
|
|
72
|
+
|
|
73
|
+
**Step 1: Create `spec/fixtures/sample.csv`**
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Title Row,,,
|
|
77
|
+
[key],*en,es,fr
|
|
78
|
+
[comment],Section General,Section General,Section General
|
|
79
|
+
app_name,My App,Mi Aplicación,Mon Application
|
|
80
|
+
greeting,Hello %@ world,Hola %@ mundo,Bonjour %@ monde
|
|
81
|
+
dots_test,Wait...,Espera...,Attendez...
|
|
82
|
+
ampersand_test,Tom & Jerry,Tom & Jerry,Tom & Jerry
|
|
83
|
+
[init-node],module,module,module
|
|
84
|
+
nested_key,Module Key,Clave Módulo,Clé Module
|
|
85
|
+
[end-node],end,end,end
|
|
86
|
+
[end],,,
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This gives 8 terms between `[key]` and `[end]`. Languages: en (default, marked with `*`), es, fr.
|
|
90
|
+
All cells have values to avoid nil-translation bugs in writers.
|
|
91
|
+
|
|
92
|
+
**Step 2: Create `spec/support/shared_terms.rb`**
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
require 'localio/term'
|
|
96
|
+
|
|
97
|
+
RSpec.shared_context 'standard terms' do
|
|
98
|
+
let(:languages) { { 'en' => 1, 'es' => 2, 'fr' => 3 } }
|
|
99
|
+
let(:default_language) { 'en' }
|
|
100
|
+
let(:terms) do
|
|
101
|
+
[
|
|
102
|
+
Term.new('[comment]').tap do |t|
|
|
103
|
+
t.values['en'] = 'Section General'
|
|
104
|
+
t.values['es'] = 'Section General'
|
|
105
|
+
t.values['fr'] = 'Section General'
|
|
106
|
+
end,
|
|
107
|
+
Term.new('app_name').tap do |t|
|
|
108
|
+
t.values['en'] = 'My App'
|
|
109
|
+
t.values['es'] = 'Mi Aplicación'
|
|
110
|
+
t.values['fr'] = 'Mon Application'
|
|
111
|
+
end,
|
|
112
|
+
Term.new('greeting').tap do |t|
|
|
113
|
+
t.values['en'] = 'Hello %@ world'
|
|
114
|
+
t.values['es'] = 'Hola %@ mundo'
|
|
115
|
+
t.values['fr'] = 'Bonjour %@ monde'
|
|
116
|
+
end,
|
|
117
|
+
Term.new('dots_test').tap do |t|
|
|
118
|
+
t.values['en'] = 'Wait...'
|
|
119
|
+
t.values['es'] = 'Espera...'
|
|
120
|
+
t.values['fr'] = 'Attendez...'
|
|
121
|
+
end,
|
|
122
|
+
Term.new('ampersand_test').tap do |t|
|
|
123
|
+
t.values['en'] = 'Tom & Jerry'
|
|
124
|
+
t.values['es'] = 'Tom & Jerry'
|
|
125
|
+
t.values['fr'] = 'Tom & Jerry'
|
|
126
|
+
end,
|
|
127
|
+
]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Step 3: Commit**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git add spec/fixtures/sample.csv spec/support/shared_terms.rb
|
|
136
|
+
git commit -m "test: add sample CSV fixture and shared terms context"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Task 3: StringHelper tests
|
|
142
|
+
|
|
143
|
+
**Files:**
|
|
144
|
+
- Create: `spec/localio/string_helper_spec.rb`
|
|
145
|
+
|
|
146
|
+
**Step 1: Write test**
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
require 'localio/string_helper'
|
|
150
|
+
|
|
151
|
+
RSpec.describe String do
|
|
152
|
+
describe '#space_to_underscore' do
|
|
153
|
+
it { expect('hello world'.space_to_underscore).to eq('hello_world') }
|
|
154
|
+
it { expect('hello'.space_to_underscore).to eq('hello') }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe '#strip_tag' do
|
|
158
|
+
it 'strips single-letter bracket tags from the start' do
|
|
159
|
+
expect('[a]hello'.strip_tag).to eq('hello')
|
|
160
|
+
end
|
|
161
|
+
it 'does not strip multi-letter bracket tags' do
|
|
162
|
+
expect('[comment]hello'.strip_tag).to eq('[comment]hello')
|
|
163
|
+
end
|
|
164
|
+
it 'does not strip tags not at the start' do
|
|
165
|
+
expect('hello[a]'.strip_tag).to eq('hello[a]')
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
describe '#camel_case' do
|
|
170
|
+
it { expect('hello_world'.camel_case).to eq('HelloWorld') }
|
|
171
|
+
it { expect('HelloWorld'.camel_case).to eq('HelloWorld') }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe '#replace_escaped' do
|
|
175
|
+
it { expect('a`+b'.replace_escaped).to eq('a+b') }
|
|
176
|
+
it { expect('a`=b'.replace_escaped).to eq('a=b') }
|
|
177
|
+
it { expect("a\\+b".replace_escaped).to eq('a+b') }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
describe '#underscore' do
|
|
181
|
+
it { expect('HelloWorld'.underscore).to eq('hello_world') }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe '#uncapitalize' do
|
|
185
|
+
it { expect('Hello'.uncapitalize).to eq('hello') }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
describe '#blank?' do
|
|
189
|
+
it { expect(''.blank?).to be true }
|
|
190
|
+
it { expect('hello'.blank?).to be false }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
describe '#green / #yellow / #red / #cyan' do
|
|
194
|
+
it { expect('ok'.green).to include("\e[32m") }
|
|
195
|
+
it { expect('ok'.yellow).to include("\e[33m") }
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Step 2: Run test**
|
|
201
|
+
|
|
202
|
+
Run: `bundle exec rspec spec/localio/string_helper_spec.rb`
|
|
203
|
+
Expected: all pass
|
|
204
|
+
|
|
205
|
+
**Step 3: Commit**
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
git add spec/localio/string_helper_spec.rb
|
|
209
|
+
git commit -m "test: StringHelper string extension tests"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### Task 4: Term and Segment model tests
|
|
215
|
+
|
|
216
|
+
**Files:**
|
|
217
|
+
- Create: `spec/localio/term_spec.rb`
|
|
218
|
+
- Create: `spec/localio/segment_spec.rb`
|
|
219
|
+
|
|
220
|
+
**Step 1: Write `spec/localio/term_spec.rb`**
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
require 'localio/term'
|
|
224
|
+
|
|
225
|
+
RSpec.describe Term do
|
|
226
|
+
subject(:term) { Term.new('app_name') }
|
|
227
|
+
|
|
228
|
+
it 'stores the keyword' do
|
|
229
|
+
expect(term.keyword).to eq('app_name')
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'initializes with empty values hash' do
|
|
233
|
+
expect(term.values).to be_empty
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'stores values by language' do
|
|
237
|
+
term.values['en'] = 'My App'
|
|
238
|
+
expect(term.values['en']).to eq('My App')
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
describe '#is_comment?' do
|
|
242
|
+
it { expect(Term.new('[comment]').is_comment?).to be true }
|
|
243
|
+
it { expect(Term.new('[COMMENT]').is_comment?).to be true }
|
|
244
|
+
it { expect(term.is_comment?).to be false }
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Step 2: Write `spec/localio/segment_spec.rb`**
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
require 'localio/string_helper'
|
|
253
|
+
require 'localio/segment'
|
|
254
|
+
|
|
255
|
+
RSpec.describe Segment do
|
|
256
|
+
subject(:segment) { Segment.new('app_name', 'My App', 'en') }
|
|
257
|
+
|
|
258
|
+
it 'stores key, translation, and language' do
|
|
259
|
+
expect(segment.key).to eq('app_name')
|
|
260
|
+
expect(segment.translation).to eq('My App')
|
|
261
|
+
expect(segment.language).to eq('en')
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it 'processes translation through replace_escaped' do
|
|
265
|
+
seg = Segment.new('key', 'hello`+world', 'en')
|
|
266
|
+
expect(seg.translation).to eq('hello+world')
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
describe '#is_comment?' do
|
|
270
|
+
it 'returns true when key is nil' do
|
|
271
|
+
segment.key = nil
|
|
272
|
+
expect(segment.is_comment?).to be true
|
|
273
|
+
end
|
|
274
|
+
it 'returns false when key is set' do
|
|
275
|
+
expect(segment.is_comment?).to be false
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Step 3: Run tests**
|
|
282
|
+
|
|
283
|
+
Run: `bundle exec rspec spec/localio/term_spec.rb spec/localio/segment_spec.rb`
|
|
284
|
+
Expected: all pass
|
|
285
|
+
|
|
286
|
+
**Step 4: Commit**
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
git add spec/localio/term_spec.rb spec/localio/segment_spec.rb
|
|
290
|
+
git commit -m "test: Term and Segment model tests"
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### Task 5: SegmentsListHolder, Filter, and Formatter tests
|
|
296
|
+
|
|
297
|
+
**Files:**
|
|
298
|
+
- Create: `spec/localio/segments_list_holder_spec.rb`
|
|
299
|
+
- Create: `spec/localio/filter_spec.rb`
|
|
300
|
+
- Create: `spec/localio/formatter_spec.rb`
|
|
301
|
+
|
|
302
|
+
**Step 1: Write `spec/localio/segments_list_holder_spec.rb`**
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
require 'localio/segments_list_holder'
|
|
306
|
+
|
|
307
|
+
RSpec.describe SegmentsListHolder do
|
|
308
|
+
subject(:holder) { SegmentsListHolder.new('en') }
|
|
309
|
+
|
|
310
|
+
it { expect(holder.language).to eq('en') }
|
|
311
|
+
it { expect(holder.segments).to be_empty }
|
|
312
|
+
|
|
313
|
+
describe '#get_binding' do
|
|
314
|
+
it 'returns a Binding' do
|
|
315
|
+
expect(holder.get_binding).to be_a(Binding)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it 'exposes @language in the binding' do
|
|
319
|
+
expect(eval('@language', holder.get_binding)).to eq('en')
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it 'exposes @segments in the binding' do
|
|
323
|
+
expect(eval('@segments', holder.get_binding)).to eq([])
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Step 2: Write `spec/localio/filter_spec.rb`**
|
|
330
|
+
|
|
331
|
+
Note: `Filter` operates on `Term` objects (not `Segment`), matching against `term.keyword`.
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
require 'localio/term'
|
|
335
|
+
require 'localio/filter'
|
|
336
|
+
|
|
337
|
+
RSpec.describe Filter do
|
|
338
|
+
let(:terms) do
|
|
339
|
+
['app_name', 'app_title', 'settings_title', 'settings_back', '[comment]'].map { |kw| Term.new(kw) }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
describe '.apply_filter' do
|
|
343
|
+
it 'returns all terms when no filters set' do
|
|
344
|
+
expect(Filter.apply_filter(terms, nil, nil)).to eq(terms)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
context 'with only filter' do
|
|
348
|
+
it 'keeps terms matching the regex' do
|
|
349
|
+
result = Filter.apply_filter(terms, { keys: 'app_' }, nil)
|
|
350
|
+
expect(result.map(&:keyword)).to contain_exactly('app_name', 'app_title')
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
it 'returns empty array when nothing matches' do
|
|
354
|
+
expect(Filter.apply_filter(terms, { keys: 'nonexistent' }, nil)).to be_empty
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
context 'with except filter' do
|
|
359
|
+
it 'excludes terms matching the regex' do
|
|
360
|
+
result = Filter.apply_filter(terms, nil, { keys: 'settings_' })
|
|
361
|
+
expect(result.map(&:keyword)).not_to include('settings_title', 'settings_back')
|
|
362
|
+
expect(result.map(&:keyword)).to include('app_name', 'app_title')
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
context 'with both filters' do
|
|
367
|
+
it 'applies only first then except' do
|
|
368
|
+
result = Filter.apply_filter(terms, { keys: 'app_' }, { keys: 'title' })
|
|
369
|
+
expect(result.map(&:keyword)).to contain_exactly('app_name')
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Step 3: Write `spec/localio/formatter_spec.rb`**
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
require 'localio/string_helper'
|
|
380
|
+
require 'localio/formatter'
|
|
381
|
+
|
|
382
|
+
RSpec.describe Formatter do
|
|
383
|
+
let(:smart_callback) { ->(key) { key.upcase } }
|
|
384
|
+
|
|
385
|
+
describe '.format' do
|
|
386
|
+
it ':smart delegates to callback' do
|
|
387
|
+
expect(Formatter.format('hello', :smart, smart_callback)).to eq('HELLO')
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
it ':none returns key unchanged' do
|
|
391
|
+
expect(Formatter.format('Hello World', :none, smart_callback)).to eq('Hello World')
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
it ':camel_case converts to CamelCase' do
|
|
395
|
+
expect(Formatter.format('hello world', :camel_case, smart_callback)).to eq('HelloWorld')
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
it ':camel_case strips single-letter bracket tags' do
|
|
399
|
+
expect(Formatter.format('[a]hello', :camel_case, smart_callback)).to eq('Hello')
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
it ':snake_case converts spaces to underscores and downcases' do
|
|
403
|
+
expect(Formatter.format('Hello World', :snake_case, smart_callback)).to eq('hello_world')
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it 'raises ArgumentError for unknown formatter' do
|
|
407
|
+
expect { Formatter.format('key', :unknown, smart_callback) }.to raise_error(ArgumentError)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Step 4: Run tests**
|
|
414
|
+
|
|
415
|
+
Run: `bundle exec rspec spec/localio/segments_list_holder_spec.rb spec/localio/filter_spec.rb spec/localio/formatter_spec.rb`
|
|
416
|
+
Expected: all pass
|
|
417
|
+
|
|
418
|
+
**Step 5: Commit**
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
git add spec/localio/segments_list_holder_spec.rb spec/localio/filter_spec.rb spec/localio/formatter_spec.rb
|
|
422
|
+
git commit -m "test: SegmentsListHolder, Filter, and Formatter tests"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
### Task 6: TemplateHandler tests
|
|
428
|
+
|
|
429
|
+
**Files:**
|
|
430
|
+
- Create: `spec/localio/template_handler_spec.rb`
|
|
431
|
+
|
|
432
|
+
The `TemplateHandler` writes an intermediate file in cwd before copying to the target directory.
|
|
433
|
+
Use `Dir.chdir(tmpdir) { }` (block form, restores cwd on exit) to keep intermediate files in tmpdir.
|
|
434
|
+
|
|
435
|
+
**Step 1: Write test**
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
require 'localio/string_helper'
|
|
439
|
+
require 'localio/segment'
|
|
440
|
+
require 'localio/segments_list_holder'
|
|
441
|
+
require 'localio/template_handler'
|
|
442
|
+
|
|
443
|
+
RSpec.describe TemplateHandler do
|
|
444
|
+
let(:holder) do
|
|
445
|
+
h = SegmentsListHolder.new('en')
|
|
446
|
+
h.segments << Segment.new('app_name', 'My App', 'en')
|
|
447
|
+
h.segments << Segment.new('greeting', 'Hello world', 'en')
|
|
448
|
+
h
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
describe '.process_template' do
|
|
452
|
+
it 'creates the output file in the target directory' do
|
|
453
|
+
Dir.mktmpdir do |tmpdir|
|
|
454
|
+
Dir.chdir(tmpdir) do
|
|
455
|
+
TemplateHandler.process_template('rails_localizable.erb', tmpdir, 'en.yml', holder)
|
|
456
|
+
expect(File).to exist(File.join(tmpdir, 'en.yml'))
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it 'renders ERB template content correctly' do
|
|
462
|
+
Dir.mktmpdir do |tmpdir|
|
|
463
|
+
Dir.chdir(tmpdir) do
|
|
464
|
+
TemplateHandler.process_template('rails_localizable.erb', tmpdir, 'en.yml', holder)
|
|
465
|
+
content = File.read(File.join(tmpdir, 'en.yml'))
|
|
466
|
+
expect(content).to include('en:')
|
|
467
|
+
expect(content).to include('app_name: "My App"')
|
|
468
|
+
expect(content).to include('greeting: "Hello world"')
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
it 'does not leave a stray temp file in the working directory' do
|
|
474
|
+
Dir.mktmpdir do |tmpdir|
|
|
475
|
+
Dir.chdir(tmpdir) do
|
|
476
|
+
TemplateHandler.process_template('rails_localizable.erb', tmpdir, 'en.yml', holder)
|
|
477
|
+
files = Dir.entries(tmpdir).reject { |f| f.start_with?('.') }
|
|
478
|
+
expect(files).to eq(['en.yml'])
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
it 'creates subdirectories as needed' do
|
|
484
|
+
Dir.mktmpdir do |tmpdir|
|
|
485
|
+
Dir.chdir(tmpdir) do
|
|
486
|
+
subdir = File.join(tmpdir, 'values')
|
|
487
|
+
TemplateHandler.process_template('rails_localizable.erb', subdir, 'en.yml', holder)
|
|
488
|
+
expect(File).to exist(File.join(subdir, 'en.yml'))
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Step 2: Run test**
|
|
497
|
+
|
|
498
|
+
Run: `bundle exec rspec spec/localio/template_handler_spec.rb`
|
|
499
|
+
Expected: all pass
|
|
500
|
+
|
|
501
|
+
**Step 3: Commit**
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
git add spec/localio/template_handler_spec.rb
|
|
505
|
+
git commit -m "test: TemplateHandler tests"
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### Task 7: CsvProcessor tests
|
|
511
|
+
|
|
512
|
+
**Files:**
|
|
513
|
+
- Create: `spec/localio/processors/csv_processor_spec.rb`
|
|
514
|
+
|
|
515
|
+
**Step 1: Write test**
|
|
516
|
+
|
|
517
|
+
```ruby
|
|
518
|
+
require 'localio/term'
|
|
519
|
+
require 'localio/string_helper'
|
|
520
|
+
require 'localio/processors/csv_processor'
|
|
521
|
+
|
|
522
|
+
RSpec.describe CsvProcessor do
|
|
523
|
+
let(:fixture_path) { File.expand_path('../../../fixtures/sample.csv', __FILE__) }
|
|
524
|
+
let(:platform_options) { {} }
|
|
525
|
+
let(:options) { { path: fixture_path } }
|
|
526
|
+
|
|
527
|
+
describe '.load_localizables' do
|
|
528
|
+
subject(:result) { CsvProcessor.load_localizables(platform_options, options) }
|
|
529
|
+
|
|
530
|
+
it 'returns languages en, es, fr' do
|
|
531
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
it 'sets en as default language (marked with *)' do
|
|
535
|
+
expect(result[:default_language]).to eq('en')
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
it 'returns 8 terms between [key] and [end]' do
|
|
539
|
+
expect(result[:segments].count).to eq(8)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
it 'parses term keywords correctly' do
|
|
543
|
+
keywords = result[:segments].map(&:keyword)
|
|
544
|
+
expect(keywords).to include('app_name', 'greeting', '[comment]', '[init-node]', '[end-node]')
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
it 'parses translations for each language' do
|
|
548
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
549
|
+
expect(app_name.values['en']).to eq('My App')
|
|
550
|
+
expect(app_name.values['es']).to eq('Mi Aplicación')
|
|
551
|
+
expect(app_name.values['fr']).to eq('Mon Application')
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
it 'identifies comment rows' do
|
|
555
|
+
comment = result[:segments].find { |t| t.keyword == '[comment]' }
|
|
556
|
+
expect(comment.is_comment?).to be true
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
it 'raises ArgumentError when :path is missing' do
|
|
560
|
+
expect { CsvProcessor.load_localizables({}, {}) }
|
|
561
|
+
.to raise_error(ArgumentError, /:path attribute is missing/)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
it 'raises IndexError when [key] marker is missing' do
|
|
565
|
+
Dir.mktmpdir do |tmpdir|
|
|
566
|
+
path = File.join(tmpdir, 'bad.csv')
|
|
567
|
+
File.write(path, "no,key,row\ndata,here,\n[end],,,\n")
|
|
568
|
+
expect { CsvProcessor.load_localizables({}, { path: path }) }
|
|
569
|
+
.to raise_error(IndexError, /Could not find any \[key\]/)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it 'raises IndexError when [end] marker is missing' do
|
|
574
|
+
Dir.mktmpdir do |tmpdir|
|
|
575
|
+
path = File.join(tmpdir, 'bad.csv')
|
|
576
|
+
File.write(path, "title,,,\n[key],*en,es,\ndata,val,val,\n")
|
|
577
|
+
expect { CsvProcessor.load_localizables({}, { path: path }) }
|
|
578
|
+
.to raise_error(IndexError, /Could not find any \[end\]/)
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
context 'with override_default option' do
|
|
583
|
+
let(:platform_options) { { override_default: 'es' } }
|
|
584
|
+
|
|
585
|
+
it 'uses the overridden default language' do
|
|
586
|
+
expect(result[:default_language]).to eq('es')
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
context 'with avoid_lang_downcase option' do
|
|
591
|
+
let(:tmpdir) { Dir.mktmpdir }
|
|
592
|
+
let(:options) do
|
|
593
|
+
path = File.join(tmpdir, 'upper.csv')
|
|
594
|
+
File.write(path, "Title,,,\n[key],*EN,ES,FR\napp_name,My App,Mi App,Mon App\n[end],,,\n")
|
|
595
|
+
{ path: path }
|
|
596
|
+
end
|
|
597
|
+
let(:platform_options) { { avoid_lang_downcase: true } }
|
|
598
|
+
|
|
599
|
+
after { FileUtils.rm_rf(tmpdir) }
|
|
600
|
+
|
|
601
|
+
it 'preserves language case' do
|
|
602
|
+
expect(result[:languages].keys).to contain_exactly('EN', 'ES', 'FR')
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**Step 2: Run test**
|
|
610
|
+
|
|
611
|
+
Run: `bundle exec rspec spec/localio/processors/csv_processor_spec.rb`
|
|
612
|
+
Expected: all pass (CSV is stdlib, no external gem)
|
|
613
|
+
|
|
614
|
+
**Step 3: Commit**
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
git add spec/localio/processors/csv_processor_spec.rb
|
|
618
|
+
git commit -m "test: CsvProcessor tests"
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
### Task 8: XlsxProcessor tests (mocked)
|
|
624
|
+
|
|
625
|
+
**Files:**
|
|
626
|
+
- Create: `spec/localio/processors/xlsx_processor_spec.rb`
|
|
627
|
+
|
|
628
|
+
Mock `SimpleXlsxReader` to avoid needing a binary fixture file.
|
|
629
|
+
|
|
630
|
+
**Step 1: Write test**
|
|
631
|
+
|
|
632
|
+
```ruby
|
|
633
|
+
require 'localio/term'
|
|
634
|
+
require 'localio/string_helper'
|
|
635
|
+
require 'localio/processors/xlsx_processor'
|
|
636
|
+
|
|
637
|
+
RSpec.describe XlsxProcessor do
|
|
638
|
+
let(:rows) do
|
|
639
|
+
[
|
|
640
|
+
['Title', nil, nil, nil],
|
|
641
|
+
['[key]', '*en', 'es', 'fr'],
|
|
642
|
+
['[comment]', 'Section General', 'Section General', 'Section General'],
|
|
643
|
+
['app_name', 'My App', 'Mi Aplicación', 'Mon Application'],
|
|
644
|
+
['greeting', 'Hello', 'Hola', 'Bonjour'],
|
|
645
|
+
['[end]', nil, nil, nil],
|
|
646
|
+
]
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
let(:sheet_double) { double('sheet', rows: rows) }
|
|
650
|
+
let(:book_double) { double('book', sheets: [sheet_double]) }
|
|
651
|
+
|
|
652
|
+
before { allow(SimpleXlsxReader).to receive(:open).and_return(book_double) }
|
|
653
|
+
|
|
654
|
+
let(:options) { { path: 'fake.xlsx', sheet: 0 } }
|
|
655
|
+
let(:platform_options) { {} }
|
|
656
|
+
|
|
657
|
+
describe '.load_localizables' do
|
|
658
|
+
subject(:result) { XlsxProcessor.load_localizables(platform_options, options) }
|
|
659
|
+
|
|
660
|
+
it 'raises ArgumentError when :path is missing' do
|
|
661
|
+
expect { XlsxProcessor.load_localizables({}, {}) }
|
|
662
|
+
.to raise_error(ArgumentError, /:path attribute is missing/)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
it 'returns languages en, es, fr' do
|
|
666
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
it 'sets en as default language' do
|
|
670
|
+
expect(result[:default_language]).to eq('en')
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it 'returns 3 terms' do
|
|
674
|
+
expect(result[:segments].count).to eq(3)
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
it 'parses translations correctly' do
|
|
678
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
679
|
+
expect(app_name.values['en']).to eq('My App')
|
|
680
|
+
expect(app_name.values['es']).to eq('Mi Aplicación')
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
it 'selects sheet by string name' do
|
|
684
|
+
named_sheet = double('sheet', name: 'Translations', rows: rows)
|
|
685
|
+
allow(book_double).to receive(:sheets).and_return([named_sheet])
|
|
686
|
+
allow(book_double.sheets).to receive(:detect).and_call_original
|
|
687
|
+
result = XlsxProcessor.load_localizables({}, { path: 'fake.xlsx', sheet: 'Translations' })
|
|
688
|
+
expect(result[:languages].keys).to include('en')
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
it 'raises when sheet is nil' do
|
|
692
|
+
allow(book_double).to receive(:sheets).and_return([double('sheet', rows: rows)])
|
|
693
|
+
allow(book_double.sheets).to receive(:detect).and_return(nil)
|
|
694
|
+
expect { XlsxProcessor.load_localizables({}, { path: 'fake.xlsx', sheet: 'Missing' }) }
|
|
695
|
+
.to raise_error(RuntimeError, /Unable to retrieve/)
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Step 2: Run test**
|
|
702
|
+
|
|
703
|
+
Run: `bundle exec rspec spec/localio/processors/xlsx_processor_spec.rb`
|
|
704
|
+
Expected: pass if `simple_xlsx_reader` installs under current Ruby; note failures for Phase 2 if not
|
|
705
|
+
|
|
706
|
+
**Step 3: Commit**
|
|
707
|
+
|
|
708
|
+
```bash
|
|
709
|
+
git add spec/localio/processors/xlsx_processor_spec.rb
|
|
710
|
+
git commit -m "test: XlsxProcessor tests (mocked)"
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
### Task 9: XlsProcessor tests (mocked)
|
|
716
|
+
|
|
717
|
+
**Files:**
|
|
718
|
+
- Create: `spec/localio/processors/xls_processor_spec.rb`
|
|
719
|
+
|
|
720
|
+
Mock the `Spreadsheet` gem worksheet interface. XLS uses `worksheet[row, col]` (0-indexed rows).
|
|
721
|
+
|
|
722
|
+
**Step 1: Write test**
|
|
723
|
+
|
|
724
|
+
```ruby
|
|
725
|
+
require 'localio/term'
|
|
726
|
+
require 'localio/string_helper'
|
|
727
|
+
require 'localio/processors/xls_processor'
|
|
728
|
+
|
|
729
|
+
RSpec.describe XlsProcessor do
|
|
730
|
+
let(:data) do
|
|
731
|
+
{
|
|
732
|
+
[0, 0] => '[key]', [0, 1] => '*en', [0, 2] => 'es', [0, 3] => 'fr',
|
|
733
|
+
[1, 0] => 'app_name', [1, 1] => 'My App', [1, 2] => 'Mi Aplicación', [1, 3] => 'Mon Application',
|
|
734
|
+
[2, 0] => 'greeting', [2, 1] => 'Hello', [2, 2] => 'Hola', [2, 3] => 'Bonjour',
|
|
735
|
+
[3, 0] => '[end]', [3, 1] => '', [3, 2] => '', [3, 3] => '',
|
|
736
|
+
}
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
let(:worksheet) do
|
|
740
|
+
ws = double('worksheet')
|
|
741
|
+
allow(ws).to receive(:[]) { |row, col| data[[row, col]].to_s }
|
|
742
|
+
allow(ws).to receive(:row_count).and_return(3)
|
|
743
|
+
allow(ws).to receive(:column_count).and_return(3)
|
|
744
|
+
ws
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
let(:book_double) { double('book') }
|
|
748
|
+
|
|
749
|
+
before do
|
|
750
|
+
allow(Spreadsheet).to receive(:client_encoding=)
|
|
751
|
+
allow(Spreadsheet).to receive(:open).and_return(book_double)
|
|
752
|
+
allow(book_double).to receive(:worksheet).with(0).and_return(worksheet)
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
let(:options) { { path: 'fake.xls' } }
|
|
756
|
+
let(:platform_options) { {} }
|
|
757
|
+
|
|
758
|
+
describe '.load_localizables' do
|
|
759
|
+
subject(:result) { XlsProcessor.load_localizables(platform_options, options) }
|
|
760
|
+
|
|
761
|
+
it 'raises ArgumentError when :path is missing' do
|
|
762
|
+
expect { XlsProcessor.load_localizables({}, {}) }
|
|
763
|
+
.to raise_error(ArgumentError, /:path attribute is missing/)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
it 'returns languages en, es, fr' do
|
|
767
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
it 'sets en as default language' do
|
|
771
|
+
expect(result[:default_language]).to eq('en')
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
it 'returns 2 terms' do
|
|
775
|
+
expect(result[:segments].count).to eq(2)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
it 'parses translations correctly' do
|
|
779
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
780
|
+
expect(app_name.values['en']).to eq('My App')
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
it 'raises when worksheet is nil' do
|
|
784
|
+
allow(book_double).to receive(:worksheet).and_return(nil)
|
|
785
|
+
expect { XlsProcessor.load_localizables({}, { path: 'fake.xls' }) }
|
|
786
|
+
.to raise_error(RuntimeError, /Unable to retrieve/)
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**Step 2: Run test**
|
|
793
|
+
|
|
794
|
+
Run: `bundle exec rspec spec/localio/processors/xls_processor_spec.rb`
|
|
795
|
+
Expected: pass if `spreadsheet` gem installs; note failures for Phase 2 if not
|
|
796
|
+
|
|
797
|
+
**Step 3: Commit**
|
|
798
|
+
|
|
799
|
+
```bash
|
|
800
|
+
git add spec/localio/processors/xls_processor_spec.rb
|
|
801
|
+
git commit -m "test: XlsProcessor tests (mocked)"
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
### Task 10: GoogleDriveProcessor tests (mocked)
|
|
807
|
+
|
|
808
|
+
**Files:**
|
|
809
|
+
- Create: `spec/localio/processors/google_drive_processor_spec.rb`
|
|
810
|
+
|
|
811
|
+
Mock the entire GoogleDrive session and auth. Focus on argument validation and parsing logic.
|
|
812
|
+
|
|
813
|
+
**Step 1: Write test**
|
|
814
|
+
|
|
815
|
+
```ruby
|
|
816
|
+
require 'localio/term'
|
|
817
|
+
require 'localio/string_helper'
|
|
818
|
+
require 'localio/processors/google_drive_processor'
|
|
819
|
+
|
|
820
|
+
RSpec.describe GoogleDriveProcessor do
|
|
821
|
+
let(:ws_data) do
|
|
822
|
+
{
|
|
823
|
+
[1, 1] => '[key]', [1, 2] => '*en', [1, 3] => 'es',
|
|
824
|
+
[2, 1] => 'app_name', [2, 2] => 'My App', [2, 3] => 'Mi Aplicación',
|
|
825
|
+
[3, 1] => 'greeting', [3, 2] => 'Hello', [3, 3] => 'Hola',
|
|
826
|
+
[4, 1] => '[end]', [4, 2] => '', [4, 3] => '',
|
|
827
|
+
}
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
let(:worksheet) do
|
|
831
|
+
ws = double('worksheet')
|
|
832
|
+
allow(ws).to receive(:[]) { |row, col| ws_data[[row, col]].to_s }
|
|
833
|
+
allow(ws).to receive(:max_rows).and_return(4)
|
|
834
|
+
allow(ws).to receive(:max_cols).and_return(3)
|
|
835
|
+
ws
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
let(:spreadsheet_double) { double('spreadsheet', title: 'My Translations') }
|
|
839
|
+
let(:session_double) { double('session') }
|
|
840
|
+
|
|
841
|
+
before do
|
|
842
|
+
allow(spreadsheet_double).to receive(:worksheets).and_return([worksheet])
|
|
843
|
+
allow(session_double).to receive(:spreadsheets).and_return([spreadsheet_double])
|
|
844
|
+
allow(GoogleDrive).to receive(:login_with_oauth).and_return(session_double)
|
|
845
|
+
|
|
846
|
+
auth = double('auth',
|
|
847
|
+
authorization_uri: 'http://example.com',
|
|
848
|
+
access_token: 'token',
|
|
849
|
+
refresh_token: 'refresh'
|
|
850
|
+
)
|
|
851
|
+
allow(auth).to receive(:client_id=)
|
|
852
|
+
allow(auth).to receive(:client_secret=)
|
|
853
|
+
allow(auth).to receive(:scope=)
|
|
854
|
+
allow(auth).to receive(:redirect_uri=)
|
|
855
|
+
allow(auth).to receive(:refresh_token=)
|
|
856
|
+
allow(auth).to receive(:refresh!)
|
|
857
|
+
|
|
858
|
+
client = double('client', authorization: auth)
|
|
859
|
+
stub_const('Google::APIClient', double(new: client))
|
|
860
|
+
stub_const('Localio::VERSION', '0.1.7')
|
|
861
|
+
|
|
862
|
+
config = double('config', has?: false, get: nil)
|
|
863
|
+
allow(config).to receive(:store)
|
|
864
|
+
allow(config).to receive(:persist)
|
|
865
|
+
allow(ConfigStore).to receive(:new).and_return(config)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
let(:options) do
|
|
869
|
+
{ spreadsheet: 'My Translations', client_id: 'id', client_secret: 'secret', client_token: 'token', sheet: 0 }
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
describe '.load_localizables' do
|
|
873
|
+
subject(:result) { GoogleDriveProcessor.load_localizables({}, options) }
|
|
874
|
+
|
|
875
|
+
it 'raises when :spreadsheet is missing' do
|
|
876
|
+
expect { GoogleDriveProcessor.load_localizables({}, { client_id: 'a', client_secret: 'b' }) }
|
|
877
|
+
.to raise_error(ArgumentError, /:spreadsheet required/)
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
it 'raises when :login is provided (deprecated)' do
|
|
881
|
+
expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', login: 'u', client_id: 'a', client_secret: 'b' }) }
|
|
882
|
+
.to raise_error(ArgumentError, /:login is deprecated/)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
it 'raises when :client_id is missing' do
|
|
886
|
+
expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', client_secret: 'b' }) }
|
|
887
|
+
.to raise_error(ArgumentError, /:client_id required/)
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
it 'raises when :client_secret is missing' do
|
|
891
|
+
expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', client_id: 'a' }) }
|
|
892
|
+
.to raise_error(ArgumentError, /:client_secret required/)
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
it 'returns languages en and es' do
|
|
896
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es')
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
it 'sets en as default language' do
|
|
900
|
+
expect(result[:default_language]).to eq('en')
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
it 'parses translations' do
|
|
904
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
905
|
+
expect(app_name.values['en']).to eq('My App')
|
|
906
|
+
end
|
|
907
|
+
end
|
|
908
|
+
end
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**Step 2: Run test**
|
|
912
|
+
|
|
913
|
+
Run: `bundle exec rspec spec/localio/processors/google_drive_processor_spec.rb`
|
|
914
|
+
Expected: may fail if `google_drive ~> 1.0` does not install under Ruby 3.x — note for Phase 2
|
|
915
|
+
|
|
916
|
+
**Step 3: Commit**
|
|
917
|
+
|
|
918
|
+
```bash
|
|
919
|
+
git add spec/localio/processors/google_drive_processor_spec.rb
|
|
920
|
+
git commit -m "test: GoogleDriveProcessor tests (mocked)"
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
### Task 11: AndroidWriter tests
|
|
926
|
+
|
|
927
|
+
**Files:**
|
|
928
|
+
- Create: `spec/localio/writers/android_writer_spec.rb`
|
|
929
|
+
|
|
930
|
+
**Step 1: Write test**
|
|
931
|
+
|
|
932
|
+
```ruby
|
|
933
|
+
require 'localio/string_helper'
|
|
934
|
+
require 'localio/term'
|
|
935
|
+
require 'localio/segment'
|
|
936
|
+
require 'localio/segments_list_holder'
|
|
937
|
+
require 'localio/formatter'
|
|
938
|
+
require 'localio/template_handler'
|
|
939
|
+
require 'localio/writers/android_writer'
|
|
940
|
+
|
|
941
|
+
RSpec.describe AndroidWriter do
|
|
942
|
+
include_context 'standard terms'
|
|
943
|
+
|
|
944
|
+
let(:options) { { default_language: 'en' } }
|
|
945
|
+
|
|
946
|
+
describe '.write' do
|
|
947
|
+
it 'creates values/strings.xml for the default language' do
|
|
948
|
+
Dir.mktmpdir do |tmpdir|
|
|
949
|
+
Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
950
|
+
expect(File).to exist(File.join(tmpdir, 'values', 'strings.xml'))
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
it 'creates values-{lang}/strings.xml for non-default languages' do
|
|
955
|
+
Dir.mktmpdir do |tmpdir|
|
|
956
|
+
Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
957
|
+
expect(File).to exist(File.join(tmpdir, 'values-es', 'strings.xml'))
|
|
958
|
+
expect(File).to exist(File.join(tmpdir, 'values-fr', 'strings.xml'))
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
it 'converts & to & in translations' do
|
|
963
|
+
Dir.mktmpdir do |tmpdir|
|
|
964
|
+
Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
965
|
+
content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
|
|
966
|
+
expect(content).to include('& Jerry')
|
|
967
|
+
expect(content).not_to include('"Tom & Jerry"')
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
it 'converts ... to … in translations' do
|
|
972
|
+
Dir.mktmpdir do |tmpdir|
|
|
973
|
+
Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
974
|
+
content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
|
|
975
|
+
expect(content).to include('Wait…')
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
it 'converts %@ to %s in translations' do
|
|
980
|
+
Dir.mktmpdir do |tmpdir|
|
|
981
|
+
Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
982
|
+
content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
|
|
983
|
+
expect(content).to include('%s world')
|
|
984
|
+
end
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
it 'renders comment rows as XML comments' do
|
|
988
|
+
Dir.mktmpdir do |tmpdir|
|
|
989
|
+
Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
990
|
+
content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
|
|
991
|
+
expect(content).to include('<!-- Section General -->')
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
it 'includes app_name string resource' do
|
|
996
|
+
Dir.mktmpdir do |tmpdir|
|
|
997
|
+
Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
998
|
+
content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
|
|
999
|
+
expect(content).to include('<string name="app_name">My App</string>')
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
**Step 2: Run test**
|
|
1007
|
+
|
|
1008
|
+
Run: `bundle exec rspec spec/localio/writers/android_writer_spec.rb`
|
|
1009
|
+
Expected: all pass
|
|
1010
|
+
|
|
1011
|
+
**Step 3: Commit**
|
|
1012
|
+
|
|
1013
|
+
```bash
|
|
1014
|
+
git add spec/localio/writers/android_writer_spec.rb
|
|
1015
|
+
git commit -m "test: AndroidWriter tests"
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
### Task 12: IosWriter and SwiftWriter tests
|
|
1021
|
+
|
|
1022
|
+
**Files:**
|
|
1023
|
+
- Create: `spec/localio/writers/ios_writer_spec.rb`
|
|
1024
|
+
- Create: `spec/localio/writers/swift_writer_spec.rb`
|
|
1025
|
+
|
|
1026
|
+
**Step 1: Write `spec/localio/writers/ios_writer_spec.rb`**
|
|
1027
|
+
|
|
1028
|
+
```ruby
|
|
1029
|
+
require 'localio/string_helper'
|
|
1030
|
+
require 'localio/term'
|
|
1031
|
+
require 'localio/segment'
|
|
1032
|
+
require 'localio/segments_list_holder'
|
|
1033
|
+
require 'localio/formatter'
|
|
1034
|
+
require 'localio/template_handler'
|
|
1035
|
+
require 'localio/writers/ios_writer'
|
|
1036
|
+
|
|
1037
|
+
RSpec.describe IosWriter do
|
|
1038
|
+
include_context 'standard terms'
|
|
1039
|
+
|
|
1040
|
+
let(:options) { { default_language: 'en' } }
|
|
1041
|
+
|
|
1042
|
+
describe '.write' do
|
|
1043
|
+
it 'creates Localizable.strings in {lang}.lproj/ for each language' do
|
|
1044
|
+
Dir.mktmpdir do |tmpdir|
|
|
1045
|
+
Dir.chdir(tmpdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1046
|
+
expect(File).to exist(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
|
|
1047
|
+
expect(File).to exist(File.join(tmpdir, 'es.lproj', 'Localizable.strings'))
|
|
1048
|
+
end
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
it 'creates LocalizableConstants.h by default' do
|
|
1052
|
+
Dir.mktmpdir do |tmpdir|
|
|
1053
|
+
Dir.chdir(tmpdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1054
|
+
expect(File).to exist(File.join(tmpdir, 'LocalizableConstants.h'))
|
|
1055
|
+
end
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
it 'skips LocalizableConstants.h when create_constants is false' do
|
|
1059
|
+
Dir.mktmpdir do |tmpdir|
|
|
1060
|
+
Dir.chdir(tmpdir) { IosWriter.write(languages, terms, tmpdir, :smart, options.merge(create_constants: false)) }
|
|
1061
|
+
expect(File).not_to exist(File.join(tmpdir, 'LocalizableConstants.h'))
|
|
1062
|
+
end
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
it 'renders comment rows as line comments' do
|
|
1066
|
+
Dir.mktmpdir do |tmpdir|
|
|
1067
|
+
Dir.chdir(tmpdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1068
|
+
content = File.read(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
|
|
1069
|
+
expect(content).to include('// Section General')
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
it 'includes translation values' do
|
|
1074
|
+
Dir.mktmpdir do |tmpdir|
|
|
1075
|
+
Dir.chdir(tmpdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1076
|
+
content = File.read(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
|
|
1077
|
+
expect(content).to include('My App')
|
|
1078
|
+
end
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
**Step 2: Write `spec/localio/writers/swift_writer_spec.rb`**
|
|
1085
|
+
|
|
1086
|
+
```ruby
|
|
1087
|
+
require 'localio/string_helper'
|
|
1088
|
+
require 'localio/term'
|
|
1089
|
+
require 'localio/segment'
|
|
1090
|
+
require 'localio/segments_list_holder'
|
|
1091
|
+
require 'localio/formatter'
|
|
1092
|
+
require 'localio/template_handler'
|
|
1093
|
+
require 'localio/writers/swift_writer'
|
|
1094
|
+
|
|
1095
|
+
RSpec.describe SwiftWriter do
|
|
1096
|
+
include_context 'standard terms'
|
|
1097
|
+
|
|
1098
|
+
let(:options) { { default_language: 'en' } }
|
|
1099
|
+
|
|
1100
|
+
describe '.write' do
|
|
1101
|
+
it 'creates Localizable.strings in {lang}.lproj/' do
|
|
1102
|
+
Dir.mktmpdir do |tmpdir|
|
|
1103
|
+
Dir.chdir(tmpdir) { SwiftWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1104
|
+
expect(File).to exist(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
|
|
1105
|
+
end
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
it 'creates LocalizableConstants.swift by default' do
|
|
1109
|
+
Dir.mktmpdir do |tmpdir|
|
|
1110
|
+
Dir.chdir(tmpdir) { SwiftWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1111
|
+
expect(File).to exist(File.join(tmpdir, 'LocalizableConstants.swift'))
|
|
1112
|
+
end
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
it 'skips LocalizableConstants.swift when create_constants is false' do
|
|
1116
|
+
Dir.mktmpdir do |tmpdir|
|
|
1117
|
+
Dir.chdir(tmpdir) { SwiftWriter.write(languages, terms, tmpdir, :smart, options.merge(create_constants: false)) }
|
|
1118
|
+
expect(File).not_to exist(File.join(tmpdir, 'LocalizableConstants.swift'))
|
|
1119
|
+
end
|
|
1120
|
+
end
|
|
1121
|
+
end
|
|
1122
|
+
end
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
**Step 3: Run tests**
|
|
1126
|
+
|
|
1127
|
+
Run: `bundle exec rspec spec/localio/writers/ios_writer_spec.rb spec/localio/writers/swift_writer_spec.rb`
|
|
1128
|
+
Expected: all pass
|
|
1129
|
+
|
|
1130
|
+
**Step 4: Commit**
|
|
1131
|
+
|
|
1132
|
+
```bash
|
|
1133
|
+
git add spec/localio/writers/ios_writer_spec.rb spec/localio/writers/swift_writer_spec.rb
|
|
1134
|
+
git commit -m "test: IosWriter and SwiftWriter tests"
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
---
|
|
1138
|
+
|
|
1139
|
+
### Task 13: JsonWriter, RailsWriter, JavaPropertiesWriter, ResXWriter tests
|
|
1140
|
+
|
|
1141
|
+
**Files:**
|
|
1142
|
+
- Create: `spec/localio/writers/json_writer_spec.rb`
|
|
1143
|
+
- Create: `spec/localio/writers/rails_writer_spec.rb`
|
|
1144
|
+
- Create: `spec/localio/writers/java_properties_writer_spec.rb`
|
|
1145
|
+
- Create: `spec/localio/writers/resx_writer_spec.rb`
|
|
1146
|
+
|
|
1147
|
+
**Step 1: Write `spec/localio/writers/json_writer_spec.rb`**
|
|
1148
|
+
|
|
1149
|
+
```ruby
|
|
1150
|
+
require 'json'
|
|
1151
|
+
require 'localio/string_helper'
|
|
1152
|
+
require 'localio/term'
|
|
1153
|
+
require 'localio/segment'
|
|
1154
|
+
require 'localio/segments_list_holder'
|
|
1155
|
+
require 'localio/formatter'
|
|
1156
|
+
require 'localio/template_handler'
|
|
1157
|
+
require 'localio/writers/json_writer'
|
|
1158
|
+
|
|
1159
|
+
RSpec.describe JsonWriter do
|
|
1160
|
+
include_context 'standard terms'
|
|
1161
|
+
|
|
1162
|
+
let(:options) { { default_language: 'en' } }
|
|
1163
|
+
|
|
1164
|
+
describe '.write' do
|
|
1165
|
+
it 'creates strings-{lang}.json for each language' do
|
|
1166
|
+
Dir.mktmpdir do |tmpdir|
|
|
1167
|
+
Dir.chdir(tmpdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1168
|
+
expect(File).to exist(File.join(tmpdir, 'strings-en.json'))
|
|
1169
|
+
expect(File).to exist(File.join(tmpdir, 'strings-es.json'))
|
|
1170
|
+
expect(File).to exist(File.join(tmpdir, 'strings-fr.json'))
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
it 'produces valid JSON' do
|
|
1175
|
+
Dir.mktmpdir do |tmpdir|
|
|
1176
|
+
Dir.chdir(tmpdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1177
|
+
content = File.read(File.join(tmpdir, 'strings-en.json'))
|
|
1178
|
+
expect { JSON.parse(content) }.not_to raise_error
|
|
1179
|
+
end
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
it 'includes translation values' do
|
|
1183
|
+
Dir.mktmpdir do |tmpdir|
|
|
1184
|
+
Dir.chdir(tmpdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1185
|
+
data = JSON.parse(File.read(File.join(tmpdir, 'strings-en.json')))
|
|
1186
|
+
expect(data['translations']['app_name']).to eq('My App')
|
|
1187
|
+
end
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
it 'sets correct language in meta' do
|
|
1191
|
+
Dir.mktmpdir do |tmpdir|
|
|
1192
|
+
Dir.chdir(tmpdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1193
|
+
data = JSON.parse(File.read(File.join(tmpdir, 'strings-es.json')))
|
|
1194
|
+
expect(data['meta']['language']).to eq('es')
|
|
1195
|
+
end
|
|
1196
|
+
end
|
|
1197
|
+
end
|
|
1198
|
+
end
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
**Step 2: Write `spec/localio/writers/rails_writer_spec.rb`**
|
|
1202
|
+
|
|
1203
|
+
```ruby
|
|
1204
|
+
require 'localio/string_helper'
|
|
1205
|
+
require 'localio/term'
|
|
1206
|
+
require 'localio/segment'
|
|
1207
|
+
require 'localio/segments_list_holder'
|
|
1208
|
+
require 'localio/formatter'
|
|
1209
|
+
require 'localio/template_handler'
|
|
1210
|
+
require 'localio/writers/rails_writer'
|
|
1211
|
+
|
|
1212
|
+
RSpec.describe RailsWriter do
|
|
1213
|
+
include_context 'standard terms'
|
|
1214
|
+
|
|
1215
|
+
let(:options) { { default_language: 'en' } }
|
|
1216
|
+
|
|
1217
|
+
describe '.write' do
|
|
1218
|
+
it 'creates {lang}.yml for each language' do
|
|
1219
|
+
Dir.mktmpdir do |tmpdir|
|
|
1220
|
+
Dir.chdir(tmpdir) { RailsWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1221
|
+
expect(File).to exist(File.join(tmpdir, 'en.yml'))
|
|
1222
|
+
expect(File).to exist(File.join(tmpdir, 'es.yml'))
|
|
1223
|
+
expect(File).to exist(File.join(tmpdir, 'fr.yml'))
|
|
1224
|
+
end
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
it 'starts YAML with the language key' do
|
|
1228
|
+
Dir.mktmpdir do |tmpdir|
|
|
1229
|
+
Dir.chdir(tmpdir) { RailsWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1230
|
+
content = File.read(File.join(tmpdir, 'en.yml'))
|
|
1231
|
+
expect(content).to include('en:')
|
|
1232
|
+
expect(content).to include('app_name: "My App"')
|
|
1233
|
+
end
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
it 'renders comment rows as YAML comments' do
|
|
1237
|
+
Dir.mktmpdir do |tmpdir|
|
|
1238
|
+
Dir.chdir(tmpdir) { RailsWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1239
|
+
content = File.read(File.join(tmpdir, 'en.yml'))
|
|
1240
|
+
expect(content).to include('# Section General')
|
|
1241
|
+
end
|
|
1242
|
+
end
|
|
1243
|
+
end
|
|
1244
|
+
end
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
**Step 3: Write `spec/localio/writers/java_properties_writer_spec.rb`**
|
|
1248
|
+
|
|
1249
|
+
```ruby
|
|
1250
|
+
require 'localio/string_helper'
|
|
1251
|
+
require 'localio/term'
|
|
1252
|
+
require 'localio/segment'
|
|
1253
|
+
require 'localio/segments_list_holder'
|
|
1254
|
+
require 'localio/formatter'
|
|
1255
|
+
require 'localio/template_handler'
|
|
1256
|
+
require 'localio/writers/java_properties_writer'
|
|
1257
|
+
|
|
1258
|
+
RSpec.describe JavaPropertiesWriter do
|
|
1259
|
+
include_context 'standard terms'
|
|
1260
|
+
|
|
1261
|
+
let(:options) { { default_language: 'en' } }
|
|
1262
|
+
|
|
1263
|
+
describe '.write' do
|
|
1264
|
+
it 'creates language_{lang}.properties for each language' do
|
|
1265
|
+
Dir.mktmpdir do |tmpdir|
|
|
1266
|
+
Dir.chdir(tmpdir) { JavaPropertiesWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1267
|
+
expect(File).to exist(File.join(tmpdir, 'language_en.properties'))
|
|
1268
|
+
expect(File).to exist(File.join(tmpdir, 'language_es.properties'))
|
|
1269
|
+
end
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
it 'includes translation values' do
|
|
1273
|
+
Dir.mktmpdir do |tmpdir|
|
|
1274
|
+
Dir.chdir(tmpdir) { JavaPropertiesWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1275
|
+
content = File.read(File.join(tmpdir, 'language_en.properties'))
|
|
1276
|
+
expect(content).to include('My App')
|
|
1277
|
+
end
|
|
1278
|
+
end
|
|
1279
|
+
end
|
|
1280
|
+
end
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
**Step 4: Write `spec/localio/writers/resx_writer_spec.rb`**
|
|
1284
|
+
|
|
1285
|
+
```ruby
|
|
1286
|
+
require 'localio/string_helper'
|
|
1287
|
+
require 'localio/term'
|
|
1288
|
+
require 'localio/segment'
|
|
1289
|
+
require 'localio/segments_list_holder'
|
|
1290
|
+
require 'localio/formatter'
|
|
1291
|
+
require 'localio/template_handler'
|
|
1292
|
+
require 'localio/writers/resx_writer'
|
|
1293
|
+
|
|
1294
|
+
RSpec.describe ResXWriter do
|
|
1295
|
+
include_context 'standard terms'
|
|
1296
|
+
|
|
1297
|
+
let(:options) { { default_language: 'en' } }
|
|
1298
|
+
|
|
1299
|
+
describe '.write' do
|
|
1300
|
+
it 'creates Resources.resx for the default language' do
|
|
1301
|
+
Dir.mktmpdir do |tmpdir|
|
|
1302
|
+
Dir.chdir(tmpdir) { ResXWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1303
|
+
expect(File).to exist(File.join(tmpdir, 'Resources.resx'))
|
|
1304
|
+
end
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
it 'creates Resources.{lang}.resx for non-default languages' do
|
|
1308
|
+
Dir.mktmpdir do |tmpdir|
|
|
1309
|
+
Dir.chdir(tmpdir) { ResXWriter.write(languages, terms, tmpdir, :smart, options) }
|
|
1310
|
+
expect(File).to exist(File.join(tmpdir, 'Resources.es.resx'))
|
|
1311
|
+
expect(File).to exist(File.join(tmpdir, 'Resources.fr.resx'))
|
|
1312
|
+
end
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
it 'uses custom resource_file name when specified' do
|
|
1316
|
+
Dir.mktmpdir do |tmpdir|
|
|
1317
|
+
Dir.chdir(tmpdir) { ResXWriter.write(languages, terms, tmpdir, :smart, options.merge(resource_file: 'Strings')) }
|
|
1318
|
+
expect(File).to exist(File.join(tmpdir, 'Strings.resx'))
|
|
1319
|
+
expect(File).to exist(File.join(tmpdir, 'Strings.es.resx'))
|
|
1320
|
+
end
|
|
1321
|
+
end
|
|
1322
|
+
end
|
|
1323
|
+
end
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
**Step 5: Run all writer tests**
|
|
1327
|
+
|
|
1328
|
+
Run: `bundle exec rspec spec/localio/writers/`
|
|
1329
|
+
Expected: all pass
|
|
1330
|
+
|
|
1331
|
+
**Step 6: Commit**
|
|
1332
|
+
|
|
1333
|
+
```bash
|
|
1334
|
+
git add spec/localio/writers/json_writer_spec.rb spec/localio/writers/rails_writer_spec.rb spec/localio/writers/java_properties_writer_spec.rb spec/localio/writers/resx_writer_spec.rb
|
|
1335
|
+
git commit -m "test: Json, Rails, JavaProperties, and ResX writer tests"
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
---
|
|
1339
|
+
|
|
1340
|
+
### Task 14: Integration pipeline test
|
|
1341
|
+
|
|
1342
|
+
**Files:**
|
|
1343
|
+
- Create: `spec/localio_spec.rb`
|
|
1344
|
+
|
|
1345
|
+
**Step 1: Write test**
|
|
1346
|
+
|
|
1347
|
+
```ruby
|
|
1348
|
+
require 'json'
|
|
1349
|
+
require 'localio/string_helper'
|
|
1350
|
+
require 'localio/term'
|
|
1351
|
+
require 'localio/segment'
|
|
1352
|
+
require 'localio/segments_list_holder'
|
|
1353
|
+
require 'localio/filter'
|
|
1354
|
+
require 'localio/formatter'
|
|
1355
|
+
require 'localio/template_handler'
|
|
1356
|
+
require 'localio/processors/csv_processor'
|
|
1357
|
+
require 'localio/writers/android_writer'
|
|
1358
|
+
require 'localio/writers/json_writer'
|
|
1359
|
+
|
|
1360
|
+
RSpec.describe 'Localio pipeline' do
|
|
1361
|
+
let(:fixture_path) { File.expand_path('fixtures/sample.csv', __dir__) }
|
|
1362
|
+
|
|
1363
|
+
it 'processes CSV and writes Android strings.xml with correct transformations' do
|
|
1364
|
+
Dir.mktmpdir do |tmpdir|
|
|
1365
|
+
Dir.chdir(tmpdir) do
|
|
1366
|
+
result = CsvProcessor.load_localizables({}, { path: fixture_path })
|
|
1367
|
+
AndroidWriter.write(result[:languages], result[:segments], tmpdir, :smart,
|
|
1368
|
+
{ default_language: result[:default_language] })
|
|
1369
|
+
|
|
1370
|
+
content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
|
|
1371
|
+
expect(content).to include('<string name="app_name">My App</string>')
|
|
1372
|
+
expect(content).to include('& Jerry') # & converted
|
|
1373
|
+
expect(content).to include('Wait…') # ... converted
|
|
1374
|
+
expect(content).to include('%s world') # %@ converted
|
|
1375
|
+
expect(content).to include('<!-- Section General -->')
|
|
1376
|
+
end
|
|
1377
|
+
end
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
it 'processes CSV and writes valid JSON with correct content' do
|
|
1381
|
+
Dir.mktmpdir do |tmpdir|
|
|
1382
|
+
Dir.chdir(tmpdir) do
|
|
1383
|
+
result = CsvProcessor.load_localizables({}, { path: fixture_path })
|
|
1384
|
+
JsonWriter.write(result[:languages], result[:segments], tmpdir, :smart,
|
|
1385
|
+
{ default_language: result[:default_language] })
|
|
1386
|
+
|
|
1387
|
+
data = JSON.parse(File.read(File.join(tmpdir, 'strings-en.json')))
|
|
1388
|
+
expect(data['translations']['app_name']).to eq('My App')
|
|
1389
|
+
expect(data['meta']['language']).to eq('en')
|
|
1390
|
+
end
|
|
1391
|
+
end
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
it 'applies only filter before writing' do
|
|
1395
|
+
result = CsvProcessor.load_localizables({}, { path: fixture_path })
|
|
1396
|
+
filtered = Filter.apply_filter(result[:segments], { keys: 'app_' }, nil)
|
|
1397
|
+
expect(filtered.map(&:keyword)).to eq(['app_name'])
|
|
1398
|
+
end
|
|
1399
|
+
|
|
1400
|
+
it 'applies except filter before writing' do
|
|
1401
|
+
result = CsvProcessor.load_localizables({}, { path: fixture_path })
|
|
1402
|
+
filtered = Filter.apply_filter(result[:segments], nil, { keys: 'dots_|ampersand_' })
|
|
1403
|
+
expect(filtered.map(&:keyword)).not_to include('dots_test', 'ampersand_test')
|
|
1404
|
+
end
|
|
1405
|
+
end
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
**Step 2: Run full test suite**
|
|
1409
|
+
|
|
1410
|
+
Run: `bundle exec rspec`
|
|
1411
|
+
Expected: all tests pass, or failures are clearly gem-install issues (note for Phase 2)
|
|
1412
|
+
|
|
1413
|
+
**Step 3: Commit**
|
|
1414
|
+
|
|
1415
|
+
```bash
|
|
1416
|
+
git add spec/localio_spec.rb
|
|
1417
|
+
git commit -m "test: integration pipeline test (CSV → writers)"
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
---
|
|
1421
|
+
|
|
1422
|
+
## Phase 2: Dependency Updates
|
|
1423
|
+
|
|
1424
|
+
### Task 15: Audit gem compatibility
|
|
1425
|
+
|
|
1426
|
+
**Step 1: Attempt full install and check what breaks**
|
|
1427
|
+
|
|
1428
|
+
Run: `bundle install 2>&1`
|
|
1429
|
+
|
|
1430
|
+
Run: `bundle exec ruby -e "require 'google_drive'" 2>&1`
|
|
1431
|
+
Run: `bundle exec ruby -e "require 'simple_xlsx_reader'" 2>&1`
|
|
1432
|
+
Run: `bundle exec ruby -e "require 'spreadsheet'" 2>&1`
|
|
1433
|
+
Run: `bundle exec ruby -e "require 'micro-optparse'" 2>&1`
|
|
1434
|
+
|
|
1435
|
+
Run: `bundle exec rspec --format progress 2>&1 | tail -20`
|
|
1436
|
+
|
|
1437
|
+
Note all failures. These are the targets for the remaining tasks.
|
|
1438
|
+
|
|
1439
|
+
---
|
|
1440
|
+
|
|
1441
|
+
### Task 16: Update gemspec for Ruby 3.x
|
|
1442
|
+
|
|
1443
|
+
**Files:**
|
|
1444
|
+
- Modify: `localio.gemspec`
|
|
1445
|
+
|
|
1446
|
+
**Step 1: Replace gemspec content**
|
|
1447
|
+
|
|
1448
|
+
```ruby
|
|
1449
|
+
# coding: utf-8
|
|
1450
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
1451
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
1452
|
+
require 'localio/version'
|
|
1453
|
+
|
|
1454
|
+
Gem::Specification.new do |spec|
|
|
1455
|
+
spec.name = "localio"
|
|
1456
|
+
spec.version = Localio::VERSION
|
|
1457
|
+
spec.authors = ["Nacho Lopez"]
|
|
1458
|
+
spec.email = ["nacho@nlopez.io"]
|
|
1459
|
+
spec.description = %q{Automatic Localizable file generation for multiple platforms}
|
|
1460
|
+
spec.summary = %q{Generates Android, iOS, Rails, JSON, Java Properties, and .NET ResX localization files from spreadsheet sources.}
|
|
1461
|
+
spec.homepage = "http://github.com/mrmans0n/localio"
|
|
1462
|
+
spec.license = "MIT"
|
|
1463
|
+
|
|
1464
|
+
spec.files = `git ls-files`.split($/)
|
|
1465
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
1466
|
+
spec.require_paths = ["lib"]
|
|
1467
|
+
spec.executables << "localize"
|
|
1468
|
+
|
|
1469
|
+
spec.required_ruby_version = ">= 3.0"
|
|
1470
|
+
|
|
1471
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
1472
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
1473
|
+
spec.add_development_dependency "rake"
|
|
1474
|
+
|
|
1475
|
+
spec.add_dependency "google_drive", "~> 3.0"
|
|
1476
|
+
spec.add_dependency "spreadsheet", "~> 1.3"
|
|
1477
|
+
spec.add_dependency "simple_xlsx_reader", "~> 2.0"
|
|
1478
|
+
spec.add_dependency "nokogiri", "~> 1.16"
|
|
1479
|
+
end
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
Note: `micro-optparse` is removed — `bin/localize` will use stdlib `optparse` (Task 17).
|
|
1483
|
+
|
|
1484
|
+
**Step 2: Update bundle**
|
|
1485
|
+
|
|
1486
|
+
Run: `bundle update`
|
|
1487
|
+
Expected: updated Gemfile.lock
|
|
1488
|
+
|
|
1489
|
+
**Step 3: Run tests and note failures**
|
|
1490
|
+
|
|
1491
|
+
Run: `bundle exec rspec --format progress 2>&1 | tail -30`
|
|
1492
|
+
|
|
1493
|
+
**Step 4: Commit**
|
|
1494
|
+
|
|
1495
|
+
```bash
|
|
1496
|
+
git add localio.gemspec Gemfile.lock
|
|
1497
|
+
git commit -m "chore: update gemspec to Ruby 3.x, modernize dependencies"
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
---
|
|
1501
|
+
|
|
1502
|
+
### Task 17: Replace micro-optparse with stdlib optparse
|
|
1503
|
+
|
|
1504
|
+
**Files:**
|
|
1505
|
+
- Modify: `bin/localize`
|
|
1506
|
+
|
|
1507
|
+
**Step 1: Read current `bin/localize`**
|
|
1508
|
+
|
|
1509
|
+
Run: `cat bin/localize`
|
|
1510
|
+
|
|
1511
|
+
**Step 2: Replace with stdlib optparse**
|
|
1512
|
+
|
|
1513
|
+
```ruby
|
|
1514
|
+
#!/usr/bin/env ruby
|
|
1515
|
+
require 'optparse'
|
|
1516
|
+
require 'localio'
|
|
1517
|
+
|
|
1518
|
+
OptionParser.new do |opts|
|
|
1519
|
+
opts.banner = 'Usage: localize [Locfile]'
|
|
1520
|
+
opts.on('-v', '--version', 'Show version') do
|
|
1521
|
+
require 'localio/version'
|
|
1522
|
+
puts Localio::VERSION
|
|
1523
|
+
exit
|
|
1524
|
+
end
|
|
1525
|
+
end.parse!
|
|
1526
|
+
|
|
1527
|
+
Localio.from_cmdline(ARGV)
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
**Step 3: Run tests**
|
|
1531
|
+
|
|
1532
|
+
Run: `bundle exec rspec`
|
|
1533
|
+
Expected: no new failures
|
|
1534
|
+
|
|
1535
|
+
**Step 4: Commit**
|
|
1536
|
+
|
|
1537
|
+
```bash
|
|
1538
|
+
git add bin/localize
|
|
1539
|
+
git commit -m "chore: replace micro-optparse with stdlib optparse"
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
---
|
|
1543
|
+
|
|
1544
|
+
### Task 18: Fix google_drive 3.x API changes
|
|
1545
|
+
|
|
1546
|
+
**Files:**
|
|
1547
|
+
- Modify: `lib/localio/processors/google_drive_processor.rb`
|
|
1548
|
+
|
|
1549
|
+
The google_drive 3.x API changed significantly:
|
|
1550
|
+
- No more `Google::APIClient` — uses `googleauth` gem
|
|
1551
|
+
- `GoogleDrive.login_with_oauth(token)` → `GoogleDrive::Session.from_access_token(token)` or `from_credentials`
|
|
1552
|
+
|
|
1553
|
+
**Step 1: Read the new API**
|
|
1554
|
+
|
|
1555
|
+
Run: `bundle exec ruby -e "require 'google_drive'; puts GoogleDrive::Session.methods.sort"`
|
|
1556
|
+
|
|
1557
|
+
**Step 2: Update the authentication block**
|
|
1558
|
+
|
|
1559
|
+
Replace the `begin...rescue` auth block (lines 30–77 in the original) with google_drive 3.x compatible code:
|
|
1560
|
+
|
|
1561
|
+
```ruby
|
|
1562
|
+
puts 'Logging in to Google Drive...'
|
|
1563
|
+
begin
|
|
1564
|
+
config = ConfigStore.new
|
|
1565
|
+
|
|
1566
|
+
if options[:client_token]
|
|
1567
|
+
session = GoogleDrive::Session.from_service_account_key(options[:client_token])
|
|
1568
|
+
elsif config.has?(:refresh_token)
|
|
1569
|
+
session = GoogleDrive::Session.from_config_with_credentials(
|
|
1570
|
+
client_id: client_id,
|
|
1571
|
+
client_secret: client_secret,
|
|
1572
|
+
refresh_token: config.get(:refresh_token)
|
|
1573
|
+
)
|
|
1574
|
+
else
|
|
1575
|
+
session = GoogleDrive::Session.from_config_with_credentials(
|
|
1576
|
+
client_id: client_id,
|
|
1577
|
+
client_secret: client_secret
|
|
1578
|
+
) do |url|
|
|
1579
|
+
puts "1. Open this page:\n#{url}\n"
|
|
1580
|
+
puts '2. Enter the authorization code: '
|
|
1581
|
+
$stdin.gets.chomp
|
|
1582
|
+
end
|
|
1583
|
+
config.store(:refresh_token, session.refresh_token)
|
|
1584
|
+
config.persist
|
|
1585
|
+
end
|
|
1586
|
+
rescue => e
|
|
1587
|
+
raise "Couldn't access Google Drive: #{e.message}"
|
|
1588
|
+
end
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
Adjust method names based on what `GoogleDrive::Session.methods` shows in Step 1.
|
|
1592
|
+
|
|
1593
|
+
**Step 3: Update the GoogleDriveProcessor spec mock**
|
|
1594
|
+
|
|
1595
|
+
In `spec/localio/processors/google_drive_processor_spec.rb`, update the `before` block to mock `GoogleDrive::Session` instead of `Google::APIClient` and `GoogleDrive.login_with_oauth`.
|
|
1596
|
+
|
|
1597
|
+
**Step 4: Run processor spec**
|
|
1598
|
+
|
|
1599
|
+
Run: `bundle exec rspec spec/localio/processors/google_drive_processor_spec.rb`
|
|
1600
|
+
Expected: all pass
|
|
1601
|
+
|
|
1602
|
+
**Step 5: Commit**
|
|
1603
|
+
|
|
1604
|
+
```bash
|
|
1605
|
+
git add lib/localio/processors/google_drive_processor.rb spec/localio/processors/google_drive_processor_spec.rb
|
|
1606
|
+
git commit -m "fix: update GoogleDriveProcessor for google_drive 3.x API"
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
---
|
|
1610
|
+
|
|
1611
|
+
### Task 19: Fix simple_xlsx_reader 2.x API changes (if any)
|
|
1612
|
+
|
|
1613
|
+
**Files:**
|
|
1614
|
+
- Modify: `lib/localio/processors/xlsx_processor.rb` (if needed)
|
|
1615
|
+
|
|
1616
|
+
**Step 1: Run the spec and identify failures**
|
|
1617
|
+
|
|
1618
|
+
Run: `bundle exec rspec spec/localio/processors/xlsx_processor_spec.rb -f d`
|
|
1619
|
+
|
|
1620
|
+
**Step 2: Check changelog for breaking changes**
|
|
1621
|
+
|
|
1622
|
+
Run: `gem contents simple_xlsx_reader | xargs grep -l CHANGE 2>/dev/null | head -1 | xargs cat`
|
|
1623
|
+
|
|
1624
|
+
Common 2.x change: `SimpleXlsxReader.open(path)` may return a different object. The `book.sheets` array interface is usually stable but sheet row access may differ.
|
|
1625
|
+
|
|
1626
|
+
**Step 3: Fix any broken API calls in `xlsx_processor.rb`**
|
|
1627
|
+
|
|
1628
|
+
Update the mock in the spec to match the actual 2.x interface if needed.
|
|
1629
|
+
|
|
1630
|
+
**Step 4: Run tests**
|
|
1631
|
+
|
|
1632
|
+
Run: `bundle exec rspec spec/localio/processors/xlsx_processor_spec.rb`
|
|
1633
|
+
Expected: all pass
|
|
1634
|
+
|
|
1635
|
+
**Step 5: Commit**
|
|
1636
|
+
|
|
1637
|
+
```bash
|
|
1638
|
+
git add lib/localio/processors/xlsx_processor.rb
|
|
1639
|
+
git commit -m "fix: update XlsxProcessor for simple_xlsx_reader 2.x"
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
---
|
|
1643
|
+
|
|
1644
|
+
### Task 20: Final Ruby 3.x compatibility sweep
|
|
1645
|
+
|
|
1646
|
+
**Step 1: Run full suite and list all failures**
|
|
1647
|
+
|
|
1648
|
+
Run: `bundle exec rspec --format documentation 2>&1 | grep -E "FAILED|Error" | head -30`
|
|
1649
|
+
|
|
1650
|
+
**Common Ruby 3.x issues to look for:**
|
|
1651
|
+
|
|
1652
|
+
- `Hash.new('default')` used as a hash with string default value — the `languages = Hash.new('languages')` in processors may cause unexpected behavior when missing keys return `'languages'` instead of nil. This is existing code behaviour, not a breakage, but verify tests still pass.
|
|
1653
|
+
- Keyword argument separation: methods that used hash splatting (`**`) — check if any `options` hash calls trigger the "last hash argument" warning.
|
|
1654
|
+
- `$LOAD_PATH` and encoding — usually fine in 3.x.
|
|
1655
|
+
- `String#encode` changes — watch for encoding errors in CSV/XLS parsing with non-ASCII characters (the `Mi Aplicación` fixture tests this).
|
|
1656
|
+
|
|
1657
|
+
**Step 2: Fix any remaining failures one by one**
|
|
1658
|
+
|
|
1659
|
+
For each failure, read the error, identify the cause, fix minimally.
|
|
1660
|
+
|
|
1661
|
+
**Step 3: Run full suite — must be green**
|
|
1662
|
+
|
|
1663
|
+
Run: `bundle exec rspec`
|
|
1664
|
+
Expected: all tests pass
|
|
1665
|
+
|
|
1666
|
+
**Step 4: Commit any fixes**
|
|
1667
|
+
|
|
1668
|
+
```bash
|
|
1669
|
+
git add -p
|
|
1670
|
+
git commit -m "fix: Ruby 3.x compatibility fixes"
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
---
|
|
1674
|
+
|
|
1675
|
+
### Task 21: Version bump and final verification
|
|
1676
|
+
|
|
1677
|
+
**Files:**
|
|
1678
|
+
- Modify: `lib/localio/version.rb`
|
|
1679
|
+
- Modify: `README.md` (Ruby version requirement)
|
|
1680
|
+
|
|
1681
|
+
**Step 1: Bump the patch version**
|
|
1682
|
+
|
|
1683
|
+
In `lib/localio/version.rb`, change `VERSION = "0.1.7"` to `VERSION = "0.1.8"`.
|
|
1684
|
+
|
|
1685
|
+
**Step 2: Update README**
|
|
1686
|
+
|
|
1687
|
+
Find the Ruby version requirement mention in `README.md` and update it from `>= 1.9.2` to `>= 3.0`.
|
|
1688
|
+
|
|
1689
|
+
**Step 3: Final full suite run**
|
|
1690
|
+
|
|
1691
|
+
Run: `bundle exec rspec --format documentation`
|
|
1692
|
+
Expected: all tests documented and passing
|
|
1693
|
+
|
|
1694
|
+
**Step 4: Commit**
|
|
1695
|
+
|
|
1696
|
+
```bash
|
|
1697
|
+
git add lib/localio/version.rb README.md
|
|
1698
|
+
git commit -m "chore: bump version to 0.1.8, require Ruby 3.0+"
|
|
1699
|
+
```
|