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.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +28 -0
  3. data/.gitignore +4 -1
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile.lock +134 -0
  7. data/README.md +36 -34
  8. data/bin/localize +10 -7
  9. data/docs/plans/2026-02-23-modernization-design.md +91 -0
  10. data/docs/plans/2026-02-23-modernization.md +1699 -0
  11. data/docs/plans/2026-02-23-twine-writer-design.md +72 -0
  12. data/docs/plans/2026-02-23-twine-writer.md +267 -0
  13. data/lib/localio/localizable_writer.rb +4 -1
  14. data/lib/localio/processors/csv_processor.rb +1 -1
  15. data/lib/localio/processors/google_drive_processor.rb +19 -45
  16. data/lib/localio/processors/xlsx_processor.rb +1 -1
  17. data/lib/localio/template_handler.rb +3 -1
  18. data/lib/localio/templates/android_localizable.erb +14 -2
  19. data/lib/localio/templates/ios_constant_localizable.erb +16 -2
  20. data/lib/localio/templates/ios_localizable.erb +20 -5
  21. data/lib/localio/templates/java_properties_localizable.erb +16 -2
  22. data/lib/localio/templates/json_localizable.erb +6 -5
  23. data/lib/localio/templates/rails_localizable.erb +15 -3
  24. data/lib/localio/templates/resx_localizable.erb +14 -2
  25. data/lib/localio/templates/swift_constant_localizable.erb +15 -2
  26. data/lib/localio/version.rb +1 -1
  27. data/lib/localio/writers/ios_writer.rb +3 -3
  28. data/lib/localio/writers/swift_writer.rb +3 -3
  29. data/lib/localio/writers/twine_writer.rb +48 -0
  30. data/localio.gemspec +19 -25
  31. data/spec/fixtures/sample.csv +11 -0
  32. data/spec/localio/filter_spec.rb +40 -0
  33. data/spec/localio/formatter_spec.rb +32 -0
  34. data/spec/localio/processors/csv_processor_spec.rb +89 -0
  35. data/spec/localio/processors/google_drive_processor_spec.rb +107 -0
  36. data/spec/localio/processors/xls_processor_spec.rb +65 -0
  37. data/spec/localio/processors/xlsx_processor_spec.rb +59 -0
  38. data/spec/localio/segment_spec.rb +27 -0
  39. data/spec/localio/segments_list_holder_spec.rb +22 -0
  40. data/spec/localio/string_helper_spec.rb +49 -0
  41. data/spec/localio/template_handler_spec.rb +67 -0
  42. data/spec/localio/term_spec.rb +24 -0
  43. data/spec/localio/writers/android_writer_spec.rb +71 -0
  44. data/spec/localio/writers/ios_writer_spec.rb +63 -0
  45. data/spec/localio/writers/java_properties_writer_spec.rb +35 -0
  46. data/spec/localio/writers/json_writer_spec.rb +57 -0
  47. data/spec/localio/writers/rails_writer_spec.rb +47 -0
  48. data/spec/localio/writers/resx_writer_spec.rb +44 -0
  49. data/spec/localio/writers/swift_writer_spec.rb +42 -0
  50. data/spec/localio/writers/twine_writer_spec.rb +68 -0
  51. data/spec/localio_spec.rb +62 -0
  52. data/spec/spec_helper.rb +24 -0
  53. data/spec/support/shared_terms.rb +35 -0
  54. metadata +61 -46
@@ -0,0 +1,49 @@
1
+ require 'localio/string_helper'
2
+
3
+ RSpec.describe String do
4
+ describe '#space_to_underscore' do
5
+ it { expect('hello world'.space_to_underscore).to eq('hello_world') }
6
+ it { expect('hello'.space_to_underscore).to eq('hello') }
7
+ end
8
+
9
+ describe '#strip_tag' do
10
+ it 'strips single-letter bracket tags from the start' do
11
+ expect('[a]hello'.strip_tag).to eq('hello')
12
+ end
13
+ it 'does not strip multi-letter bracket tags' do
14
+ expect('[comment]hello'.strip_tag).to eq('[comment]hello')
15
+ end
16
+ it 'does not strip tags not at the start' do
17
+ expect('hello[a]'.strip_tag).to eq('hello[a]')
18
+ end
19
+ end
20
+
21
+ describe '#camel_case' do
22
+ it { expect('hello_world'.camel_case).to eq('HelloWorld') }
23
+ it { expect('HelloWorld'.camel_case).to eq('HelloWorld') }
24
+ end
25
+
26
+ describe '#replace_escaped' do
27
+ it { expect('a`+b'.replace_escaped).to eq('a+b') }
28
+ it { expect('a`=b'.replace_escaped).to eq('a=b') }
29
+ it { expect("a\\+b".replace_escaped).to eq('a+b') }
30
+ end
31
+
32
+ describe '#underscore' do
33
+ it { expect('HelloWorld'.underscore).to eq('hello_world') }
34
+ end
35
+
36
+ describe '#uncapitalize' do
37
+ it { expect('Hello'.uncapitalize).to eq('hello') }
38
+ end
39
+
40
+ describe '#blank?' do
41
+ it { expect(''.blank?).to be true }
42
+ it { expect('hello'.blank?).to be false }
43
+ end
44
+
45
+ describe '#green / #yellow / #red / #cyan' do
46
+ it { expect('ok'.green).to include("\e[32m") }
47
+ it { expect('ok'.yellow).to include("\e[33m") }
48
+ end
49
+ end
@@ -0,0 +1,67 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/segment'
3
+ require 'localio/segments_list_holder'
4
+ require 'localio/template_handler'
5
+
6
+ RSpec.describe TemplateHandler do
7
+ let(:holder) do
8
+ h = SegmentsListHolder.new('en')
9
+ h.segments << Segment.new('app_name', 'My App', 'en')
10
+ h.segments << Segment.new('greeting', 'Hello world', 'en')
11
+ h
12
+ end
13
+
14
+ describe '.process_template' do
15
+ it 'creates the output file in the target directory' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ workdir = File.join(tmpdir, 'work')
18
+ FileUtils.mkdir_p(workdir)
19
+ target = File.join(tmpdir, 'out')
20
+ Dir.chdir(workdir) do
21
+ TemplateHandler.process_template('rails_localizable.erb', target, 'en.yml', holder)
22
+ expect(File).to exist(File.join(target, 'en.yml'))
23
+ end
24
+ end
25
+ end
26
+
27
+ it 'renders ERB template content correctly' do
28
+ Dir.mktmpdir do |tmpdir|
29
+ workdir = File.join(tmpdir, 'work')
30
+ FileUtils.mkdir_p(workdir)
31
+ target = File.join(tmpdir, 'out')
32
+ Dir.chdir(workdir) do
33
+ TemplateHandler.process_template('rails_localizable.erb', target, 'en.yml', holder)
34
+ content = File.read(File.join(target, 'en.yml'))
35
+ expect(content).to include('en:')
36
+ expect(content).to include('app_name: "My App"')
37
+ expect(content).to include('greeting: "Hello world"')
38
+ end
39
+ end
40
+ end
41
+
42
+ it 'does not leave a stray temp file in the working directory' do
43
+ Dir.mktmpdir do |tmpdir|
44
+ workdir = File.join(tmpdir, 'work')
45
+ FileUtils.mkdir_p(workdir)
46
+ target = File.join(tmpdir, 'out')
47
+ Dir.chdir(workdir) do
48
+ TemplateHandler.process_template('rails_localizable.erb', target, 'en.yml', holder)
49
+ files = Dir.entries(workdir).reject { |f| f.start_with?('.') }
50
+ expect(files).to be_empty
51
+ end
52
+ end
53
+ end
54
+
55
+ it 'creates subdirectories as needed' do
56
+ Dir.mktmpdir do |tmpdir|
57
+ workdir = File.join(tmpdir, 'work')
58
+ FileUtils.mkdir_p(workdir)
59
+ Dir.chdir(workdir) do
60
+ subdir = File.join(tmpdir, 'out', 'values')
61
+ TemplateHandler.process_template('rails_localizable.erb', subdir, 'en.yml', holder)
62
+ expect(File).to exist(File.join(subdir, 'en.yml'))
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ require 'localio/term'
2
+
3
+ RSpec.describe Term do
4
+ subject(:term) { Term.new('app_name') }
5
+
6
+ it 'stores the keyword' do
7
+ expect(term.keyword).to eq('app_name')
8
+ end
9
+
10
+ it 'initializes with empty values hash' do
11
+ expect(term.values).to be_empty
12
+ end
13
+
14
+ it 'stores values by language' do
15
+ term.values['en'] = 'My App'
16
+ expect(term.values['en']).to eq('My App')
17
+ end
18
+
19
+ describe '#is_comment?' do
20
+ it { expect(Term.new('[comment]').is_comment?).to be true }
21
+ it { expect(Term.new('[COMMENT]').is_comment?).to be true }
22
+ it { expect(term.is_comment?).to be false }
23
+ end
24
+ end
@@ -0,0 +1,71 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/term'
3
+ require 'localio/segment'
4
+ require 'localio/segments_list_holder'
5
+ require 'localio/formatter'
6
+ require 'localio/template_handler'
7
+ require 'localio/writers/android_writer'
8
+
9
+ RSpec.describe AndroidWriter do
10
+ include_context 'standard terms'
11
+
12
+ let(:options) { { default_language: 'en' } }
13
+
14
+ describe '.write' do
15
+ it 'creates values/strings.xml for the default language' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
18
+ expect(File).to exist(File.join(tmpdir, 'values', 'strings.xml'))
19
+ end
20
+ end
21
+
22
+ it 'creates values-{lang}/strings.xml for non-default languages' do
23
+ Dir.mktmpdir do |tmpdir|
24
+ Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
25
+ expect(File).to exist(File.join(tmpdir, 'values-es', 'strings.xml'))
26
+ expect(File).to exist(File.join(tmpdir, 'values-fr', 'strings.xml'))
27
+ end
28
+ end
29
+
30
+ it 'converts & to &amp; in translations' do
31
+ Dir.mktmpdir do |tmpdir|
32
+ Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
33
+ content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
34
+ expect(content).to include('&amp; Jerry')
35
+ expect(content).not_to include('"Tom & Jerry"')
36
+ end
37
+ end
38
+
39
+ it 'converts ... to … in translations' do
40
+ Dir.mktmpdir do |tmpdir|
41
+ Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
42
+ content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
43
+ expect(content).to include('Wait…')
44
+ end
45
+ end
46
+
47
+ it 'converts %@ to %s in translations' do
48
+ Dir.mktmpdir do |tmpdir|
49
+ Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
50
+ content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
51
+ expect(content).to include('%s world')
52
+ end
53
+ end
54
+
55
+ it 'renders comment rows as XML comments' do
56
+ Dir.mktmpdir do |tmpdir|
57
+ Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
58
+ content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
59
+ expect(content).to include('<!-- Section General -->')
60
+ end
61
+ end
62
+
63
+ it 'includes app_name string resource' do
64
+ Dir.mktmpdir do |tmpdir|
65
+ Dir.chdir(tmpdir) { AndroidWriter.write(languages, terms, tmpdir, :smart, options) }
66
+ content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
67
+ expect(content).to include('<string name="app_name">My App</string>')
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,63 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/term'
3
+ require 'localio/segment'
4
+ require 'localio/segments_list_holder'
5
+ require 'localio/formatter'
6
+ require 'localio/template_handler'
7
+ require 'localio/writers/ios_writer'
8
+
9
+ RSpec.describe IosWriter do
10
+ include_context 'standard terms'
11
+
12
+ let(:options) { { default_language: 'en' } }
13
+
14
+ describe '.write' do
15
+ it 'creates Localizable.strings in {lang}.lproj/ for each language' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ workdir = File.join(tmpdir, 'work')
18
+ FileUtils.mkdir_p(workdir)
19
+ Dir.chdir(workdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
20
+ expect(File).to exist(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
21
+ expect(File).to exist(File.join(tmpdir, 'es.lproj', 'Localizable.strings'))
22
+ end
23
+ end
24
+
25
+ it 'creates LocalizableConstants.h by default' do
26
+ Dir.mktmpdir do |tmpdir|
27
+ workdir = File.join(tmpdir, 'work')
28
+ FileUtils.mkdir_p(workdir)
29
+ Dir.chdir(workdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
30
+ expect(File).to exist(File.join(tmpdir, 'LocalizableConstants.h'))
31
+ end
32
+ end
33
+
34
+ it 'skips LocalizableConstants.h when create_constants is false' do
35
+ Dir.mktmpdir do |tmpdir|
36
+ workdir = File.join(tmpdir, 'work')
37
+ FileUtils.mkdir_p(workdir)
38
+ Dir.chdir(workdir) { IosWriter.write(languages, terms, tmpdir, :smart, options.merge(create_constants: false)) }
39
+ expect(File).not_to exist(File.join(tmpdir, 'LocalizableConstants.h'))
40
+ end
41
+ end
42
+
43
+ it 'renders comment rows as line comments' do
44
+ Dir.mktmpdir do |tmpdir|
45
+ workdir = File.join(tmpdir, 'work')
46
+ FileUtils.mkdir_p(workdir)
47
+ Dir.chdir(workdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
48
+ content = File.read(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
49
+ expect(content).to include('// Section General')
50
+ end
51
+ end
52
+
53
+ it 'includes translation values' do
54
+ Dir.mktmpdir do |tmpdir|
55
+ workdir = File.join(tmpdir, 'work')
56
+ FileUtils.mkdir_p(workdir)
57
+ Dir.chdir(workdir) { IosWriter.write(languages, terms, tmpdir, :smart, options) }
58
+ content = File.read(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
59
+ expect(content).to include('My App')
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/term'
3
+ require 'localio/segment'
4
+ require 'localio/segments_list_holder'
5
+ require 'localio/formatter'
6
+ require 'localio/template_handler'
7
+ require 'localio/writers/java_properties_writer'
8
+
9
+ RSpec.describe JavaPropertiesWriter do
10
+ include_context 'standard terms'
11
+
12
+ let(:options) { { default_language: 'en' } }
13
+
14
+ describe '.write' do
15
+ it 'creates language_{lang}.properties for each language' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ workdir = File.join(tmpdir, 'work')
18
+ FileUtils.mkdir_p(workdir)
19
+ Dir.chdir(workdir) { JavaPropertiesWriter.write(languages, terms, tmpdir, :smart, options) }
20
+ expect(File).to exist(File.join(tmpdir, 'language_en.properties'))
21
+ expect(File).to exist(File.join(tmpdir, 'language_es.properties'))
22
+ end
23
+ end
24
+
25
+ it 'includes translation values' do
26
+ Dir.mktmpdir do |tmpdir|
27
+ workdir = File.join(tmpdir, 'work')
28
+ FileUtils.mkdir_p(workdir)
29
+ Dir.chdir(workdir) { JavaPropertiesWriter.write(languages, terms, tmpdir, :smart, options) }
30
+ content = File.read(File.join(tmpdir, 'language_en.properties'))
31
+ expect(content).to include('My App')
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ require 'json'
2
+ require 'localio/string_helper'
3
+ require 'localio/term'
4
+ require 'localio/segment'
5
+ require 'localio/segments_list_holder'
6
+ require 'localio/formatter'
7
+ require 'localio/template_handler'
8
+ require 'localio/writers/json_writer'
9
+
10
+ RSpec.describe JsonWriter do
11
+ include_context 'standard terms'
12
+
13
+ let(:options) { { default_language: 'en' } }
14
+
15
+ describe '.write' do
16
+ it 'creates strings-{lang}.json for each language' do
17
+ Dir.mktmpdir do |tmpdir|
18
+ workdir = File.join(tmpdir, 'work')
19
+ FileUtils.mkdir_p(workdir)
20
+ Dir.chdir(workdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
21
+ expect(File).to exist(File.join(tmpdir, 'strings-en.json'))
22
+ expect(File).to exist(File.join(tmpdir, 'strings-es.json'))
23
+ expect(File).to exist(File.join(tmpdir, 'strings-fr.json'))
24
+ end
25
+ end
26
+
27
+ it 'produces valid JSON' do
28
+ Dir.mktmpdir do |tmpdir|
29
+ workdir = File.join(tmpdir, 'work')
30
+ FileUtils.mkdir_p(workdir)
31
+ Dir.chdir(workdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
32
+ content = File.read(File.join(tmpdir, 'strings-en.json'))
33
+ expect { JSON.parse(content) }.not_to raise_error
34
+ end
35
+ end
36
+
37
+ it 'includes translation values' do
38
+ Dir.mktmpdir do |tmpdir|
39
+ workdir = File.join(tmpdir, 'work')
40
+ FileUtils.mkdir_p(workdir)
41
+ Dir.chdir(workdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
42
+ data = JSON.parse(File.read(File.join(tmpdir, 'strings-en.json')))
43
+ expect(data['translations']['app_name']).to eq('My App')
44
+ end
45
+ end
46
+
47
+ it 'sets correct language in meta' do
48
+ Dir.mktmpdir do |tmpdir|
49
+ workdir = File.join(tmpdir, 'work')
50
+ FileUtils.mkdir_p(workdir)
51
+ Dir.chdir(workdir) { JsonWriter.write(languages, terms, tmpdir, :smart, options) }
52
+ data = JSON.parse(File.read(File.join(tmpdir, 'strings-es.json')))
53
+ expect(data['meta']['language']).to eq('es')
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/term'
3
+ require 'localio/segment'
4
+ require 'localio/segments_list_holder'
5
+ require 'localio/formatter'
6
+ require 'localio/template_handler'
7
+ require 'localio/writers/rails_writer'
8
+
9
+ RSpec.describe RailsWriter do
10
+ include_context 'standard terms'
11
+
12
+ let(:options) { { default_language: 'en' } }
13
+
14
+ describe '.write' do
15
+ it 'creates {lang}.yml for each language' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ workdir = File.join(tmpdir, 'work')
18
+ FileUtils.mkdir_p(workdir)
19
+ Dir.chdir(workdir) { RailsWriter.write(languages, terms, tmpdir, :smart, options) }
20
+ expect(File).to exist(File.join(tmpdir, 'en.yml'))
21
+ expect(File).to exist(File.join(tmpdir, 'es.yml'))
22
+ expect(File).to exist(File.join(tmpdir, 'fr.yml'))
23
+ end
24
+ end
25
+
26
+ it 'starts YAML with the language key' do
27
+ Dir.mktmpdir do |tmpdir|
28
+ workdir = File.join(tmpdir, 'work')
29
+ FileUtils.mkdir_p(workdir)
30
+ Dir.chdir(workdir) { RailsWriter.write(languages, terms, tmpdir, :smart, options) }
31
+ content = File.read(File.join(tmpdir, 'en.yml'))
32
+ expect(content).to include('en:')
33
+ expect(content).to include('app_name: "My App"')
34
+ end
35
+ end
36
+
37
+ it 'renders comment rows as YAML comments' do
38
+ Dir.mktmpdir do |tmpdir|
39
+ workdir = File.join(tmpdir, 'work')
40
+ FileUtils.mkdir_p(workdir)
41
+ Dir.chdir(workdir) { RailsWriter.write(languages, terms, tmpdir, :smart, options) }
42
+ content = File.read(File.join(tmpdir, 'en.yml'))
43
+ expect(content).to include('# Section General')
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/term'
3
+ require 'localio/segment'
4
+ require 'localio/segments_list_holder'
5
+ require 'localio/formatter'
6
+ require 'localio/template_handler'
7
+ require 'localio/writers/resx_writer'
8
+
9
+ RSpec.describe ResXWriter do
10
+ include_context 'standard terms'
11
+
12
+ let(:options) { { default_language: 'en' } }
13
+
14
+ describe '.write' do
15
+ it 'creates Resources.resx for the default language' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ workdir = File.join(tmpdir, 'work')
18
+ FileUtils.mkdir_p(workdir)
19
+ Dir.chdir(workdir) { ResXWriter.write(languages, terms, tmpdir, :smart, options) }
20
+ expect(File).to exist(File.join(tmpdir, 'Resources.resx'))
21
+ end
22
+ end
23
+
24
+ it 'creates Resources.{lang}.resx for non-default languages' do
25
+ Dir.mktmpdir do |tmpdir|
26
+ workdir = File.join(tmpdir, 'work')
27
+ FileUtils.mkdir_p(workdir)
28
+ Dir.chdir(workdir) { ResXWriter.write(languages, terms, tmpdir, :smart, options) }
29
+ expect(File).to exist(File.join(tmpdir, 'Resources.es.resx'))
30
+ expect(File).to exist(File.join(tmpdir, 'Resources.fr.resx'))
31
+ end
32
+ end
33
+
34
+ it 'uses custom resource_file name when specified' do
35
+ Dir.mktmpdir do |tmpdir|
36
+ workdir = File.join(tmpdir, 'work')
37
+ FileUtils.mkdir_p(workdir)
38
+ Dir.chdir(workdir) { ResXWriter.write(languages, terms, tmpdir, :smart, options.merge(resource_file: 'Strings')) }
39
+ expect(File).to exist(File.join(tmpdir, 'Strings.resx'))
40
+ expect(File).to exist(File.join(tmpdir, 'Strings.es.resx'))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/term'
3
+ require 'localio/segment'
4
+ require 'localio/segments_list_holder'
5
+ require 'localio/formatter'
6
+ require 'localio/template_handler'
7
+ require 'localio/writers/swift_writer'
8
+
9
+ RSpec.describe SwiftWriter do
10
+ include_context 'standard terms'
11
+
12
+ let(:options) { { default_language: 'en' } }
13
+
14
+ describe '.write' do
15
+ it 'creates Localizable.strings in {lang}.lproj/' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ workdir = File.join(tmpdir, 'work')
18
+ FileUtils.mkdir_p(workdir)
19
+ Dir.chdir(workdir) { SwiftWriter.write(languages, terms, tmpdir, :smart, options) }
20
+ expect(File).to exist(File.join(tmpdir, 'en.lproj', 'Localizable.strings'))
21
+ end
22
+ end
23
+
24
+ it 'creates LocalizableConstants.swift by default' do
25
+ Dir.mktmpdir do |tmpdir|
26
+ workdir = File.join(tmpdir, 'work')
27
+ FileUtils.mkdir_p(workdir)
28
+ Dir.chdir(workdir) { SwiftWriter.write(languages, terms, tmpdir, :smart, options) }
29
+ expect(File).to exist(File.join(tmpdir, 'LocalizableConstants.swift'))
30
+ end
31
+ end
32
+
33
+ it 'skips LocalizableConstants.swift when create_constants is false' do
34
+ Dir.mktmpdir do |tmpdir|
35
+ workdir = File.join(tmpdir, 'work')
36
+ FileUtils.mkdir_p(workdir)
37
+ Dir.chdir(workdir) { SwiftWriter.write(languages, terms, tmpdir, :smart, options.merge(create_constants: false)) }
38
+ expect(File).not_to exist(File.join(tmpdir, 'LocalizableConstants.swift'))
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/term'
3
+ require 'localio/formatter'
4
+ require 'localio/writers/twine_writer'
5
+
6
+ RSpec.describe TwineWriter do
7
+ include_context 'standard terms'
8
+ # standard terms provides: languages {'en'=>1,'es'=>2,'fr'=>3},
9
+ # default_language 'en', and terms:
10
+ # [comment] "Section General", app_name, greeting, dots_test, ampersand_test
11
+
12
+ let(:options) { { default_language: 'en' } }
13
+
14
+ describe '.write' do
15
+ it 'creates strings.txt in the output path' do
16
+ Dir.mktmpdir do |tmpdir|
17
+ Dir.chdir(tmpdir) { TwineWriter.write(languages, terms, tmpdir, :smart, options) }
18
+ expect(File).to exist(File.join(tmpdir, 'strings.txt'))
19
+ end
20
+ end
21
+
22
+ it 'uses a custom filename when :output_file is specified' do
23
+ Dir.mktmpdir do |tmpdir|
24
+ Dir.chdir(tmpdir) do
25
+ TwineWriter.write(languages, terms, tmpdir, :smart, options.merge(output_file: 'translations.txt'))
26
+ end
27
+ expect(File).to exist(File.join(tmpdir, 'translations.txt'))
28
+ expect(File).not_to exist(File.join(tmpdir, 'strings.txt'))
29
+ end
30
+ end
31
+
32
+ it 'includes all languages for each key' do
33
+ Dir.mktmpdir do |tmpdir|
34
+ Dir.chdir(tmpdir) { TwineWriter.write(languages, terms, tmpdir, :smart, options) }
35
+ content = File.read(File.join(tmpdir, 'strings.txt'))
36
+ expect(content).to include('en = My App')
37
+ expect(content).to include('es = Mi Aplicación')
38
+ expect(content).to include('fr = Mon Application')
39
+ end
40
+ end
41
+
42
+ it 'writes [init-node] terms as [[section]] headers' do
43
+ section_terms = [
44
+ Term.new('[init-node]').tap { |t| t.values['en'] = 'General'; t.values['es'] = 'General'; t.values['fr'] = 'General' },
45
+ Term.new('app_name').tap { |t| t.values['en'] = 'My App'; t.values['es'] = 'Mi App'; t.values['fr'] = 'Mon App' },
46
+ Term.new('[end-node]').tap { |t| t.values['en'] = 'end'; t.values['es'] = 'end'; t.values['fr'] = 'end' },
47
+ ]
48
+ Dir.mktmpdir do |tmpdir|
49
+ Dir.chdir(tmpdir) { TwineWriter.write(languages, section_terms, tmpdir, :smart, options) }
50
+ content = File.read(File.join(tmpdir, 'strings.txt'))
51
+ expect(content).to include('[[General]]')
52
+ end
53
+ end
54
+
55
+ it 'attaches [comment] value as comment = on the following key' do
56
+ Dir.mktmpdir do |tmpdir|
57
+ Dir.chdir(tmpdir) { TwineWriter.write(languages, terms, tmpdir, :smart, options) }
58
+ content = File.read(File.join(tmpdir, 'strings.txt'))
59
+ # [comment] "Section General" appears before app_name and attaches to it
60
+ expect(content).to include("\t\tcomment = Section General")
61
+ # The comment block appears before the greeting key block
62
+ comment_pos = content.index('comment = Section General')
63
+ greeting_pos = content.index('[greeting]')
64
+ expect(comment_pos).to be < greeting_pos
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ require 'json'
2
+ require 'localio/string_helper'
3
+ require 'localio/term'
4
+ require 'localio/segment'
5
+ require 'localio/segments_list_holder'
6
+ require 'localio/filter'
7
+ require 'localio/formatter'
8
+ require 'localio/template_handler'
9
+ require 'localio/processors/csv_processor'
10
+ require 'localio/writers/android_writer'
11
+ require 'localio/writers/json_writer'
12
+
13
+ RSpec.describe 'Localio pipeline' do
14
+ let(:fixture_path) { File.expand_path('fixtures/sample.csv', __dir__) }
15
+
16
+ it 'processes CSV and writes Android strings.xml with correct transformations' do
17
+ Dir.mktmpdir do |tmpdir|
18
+ workdir = File.join(tmpdir, 'work')
19
+ FileUtils.mkdir_p(workdir)
20
+ Dir.chdir(workdir) do
21
+ result = CsvProcessor.load_localizables({}, { path: fixture_path })
22
+ AndroidWriter.write(result[:languages], result[:segments], tmpdir, :smart,
23
+ { default_language: result[:default_language] })
24
+
25
+ content = File.read(File.join(tmpdir, 'values', 'strings.xml'))
26
+ expect(content).to include('<string name="app_name">My App</string>')
27
+ expect(content).to include('&amp; Jerry')
28
+ expect(content).to include('Wait…')
29
+ expect(content).to include('%s world')
30
+ expect(content).to include('<!-- Section General -->')
31
+ end
32
+ end
33
+ end
34
+
35
+ it 'processes CSV and writes valid JSON with correct content' do
36
+ Dir.mktmpdir do |tmpdir|
37
+ workdir = File.join(tmpdir, 'work')
38
+ FileUtils.mkdir_p(workdir)
39
+ Dir.chdir(workdir) do
40
+ result = CsvProcessor.load_localizables({}, { path: fixture_path })
41
+ JsonWriter.write(result[:languages], result[:segments], tmpdir, :smart,
42
+ { default_language: result[:default_language] })
43
+
44
+ data = JSON.parse(File.read(File.join(tmpdir, 'strings-en.json')))
45
+ expect(data['translations']['app_name']).to eq('My App')
46
+ expect(data['meta']['language']).to eq('en')
47
+ end
48
+ end
49
+ end
50
+
51
+ it 'applies only filter before writing' do
52
+ result = CsvProcessor.load_localizables({}, { path: fixture_path })
53
+ filtered = Filter.apply_filter(result[:segments], { keys: 'app_' }, nil)
54
+ expect(filtered.map(&:keyword)).to eq(['app_name'])
55
+ end
56
+
57
+ it 'applies except filter before writing' do
58
+ result = CsvProcessor.load_localizables({}, { path: fixture_path })
59
+ filtered = Filter.apply_filter(result[:segments], nil, { keys: 'dots_|ampersand_' })
60
+ expect(filtered.map(&:keyword)).not_to include('dots_test', 'ampersand_test')
61
+ end
62
+ end
@@ -0,0 +1,24 @@
1
+ require 'tmpdir'
2
+ require 'fileutils'
3
+ # Load nokogiri eagerly so it is available before any spec file runs.
4
+ # Some specs stub $LOADED_FEATURES for google_drive (which also requires
5
+ # nokogiri internally); loading nokogiri here first ensures SimpleXlsxReader
6
+ # and android_writer can always find the Nokogiri constant.
7
+ require 'nokogiri'
8
+
9
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
10
+
11
+ Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
12
+
13
+ RSpec.configure do |config|
14
+ config.expect_with :rspec do |c|
15
+ c.include_chain_clauses_in_custom_matcher_descriptions = true
16
+ end
17
+ config.mock_with :rspec do |mocks|
18
+ mocks.verify_partial_doubles = true
19
+ end
20
+ config.shared_context_metadata_behavior = :apply_to_host_groups
21
+ config.filter_run_when_matching :focus
22
+ config.disable_monkey_patching!
23
+ config.order = :random
24
+ end