localio 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +28 -0
  3. data/.gitignore +3 -1
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile.lock +138 -0
  7. data/README.md +34 -35
  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/lib/localio/processor.rb +2 -1
  12. data/lib/localio/processors/csv_processor.rb +12 -3
  13. data/lib/localio/processors/google_drive_processor.rb +36 -48
  14. data/lib/localio/processors/xls_processor.rb +12 -3
  15. data/lib/localio/processors/xlsx_processor.rb +12 -3
  16. data/lib/localio/template_handler.rb +3 -1
  17. data/lib/localio/templates/android_localizable.erb +14 -2
  18. data/lib/localio/templates/ios_constant_localizable.erb +16 -2
  19. data/lib/localio/templates/ios_localizable.erb +20 -5
  20. data/lib/localio/templates/java_properties_localizable.erb +16 -2
  21. data/lib/localio/templates/json_localizable.erb +6 -5
  22. data/lib/localio/templates/rails_localizable.erb +15 -3
  23. data/lib/localio/templates/resx_localizable.erb +14 -2
  24. data/lib/localio/templates/swift_constant_localizable.erb +15 -2
  25. data/lib/localio/version.rb +1 -1
  26. data/lib/localio/writers/ios_writer.rb +3 -3
  27. data/lib/localio/writers/swift_writer.rb +3 -3
  28. data/localio.gemspec +19 -25
  29. data/spec/fixtures/sample.csv +11 -0
  30. data/spec/localio/filter_spec.rb +40 -0
  31. data/spec/localio/formatter_spec.rb +32 -0
  32. data/spec/localio/processors/csv_processor_spec.rb +89 -0
  33. data/spec/localio/processors/google_drive_processor_spec.rb +107 -0
  34. data/spec/localio/processors/xls_processor_spec.rb +65 -0
  35. data/spec/localio/processors/xlsx_processor_spec.rb +59 -0
  36. data/spec/localio/segment_spec.rb +27 -0
  37. data/spec/localio/segments_list_holder_spec.rb +22 -0
  38. data/spec/localio/string_helper_spec.rb +49 -0
  39. data/spec/localio/template_handler_spec.rb +67 -0
  40. data/spec/localio/term_spec.rb +24 -0
  41. data/spec/localio/writers/android_writer_spec.rb +71 -0
  42. data/spec/localio/writers/ios_writer_spec.rb +63 -0
  43. data/spec/localio/writers/java_properties_writer_spec.rb +35 -0
  44. data/spec/localio/writers/json_writer_spec.rb +57 -0
  45. data/spec/localio/writers/rails_writer_spec.rb +47 -0
  46. data/spec/localio/writers/resx_writer_spec.rb +44 -0
  47. data/spec/localio/writers/swift_writer_spec.rb +42 -0
  48. data/spec/localio_spec.rb +62 -0
  49. data/spec/spec_helper.rb +24 -0
  50. data/spec/support/shared_terms.rb +35 -0
  51. metadata +60 -49
@@ -0,0 +1,89 @@
1
+ require 'localio/term'
2
+ require 'localio/string_helper'
3
+ require 'localio/processors/csv_processor'
4
+
5
+ RSpec.describe CsvProcessor do
6
+ let(:fixture_path) { File.expand_path('../../../fixtures/sample.csv', __FILE__) }
7
+ let(:platform_options) { {} }
8
+ let(:options) { { path: fixture_path } }
9
+
10
+ describe '.load_localizables' do
11
+ subject(:result) { CsvProcessor.load_localizables(platform_options, options) }
12
+
13
+ it 'returns languages en, es, fr' do
14
+ expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
15
+ end
16
+
17
+ it 'sets en as default language (marked with *)' do
18
+ expect(result[:default_language]).to eq('en')
19
+ end
20
+
21
+ it 'returns 8 terms between [key] and [end]' do
22
+ expect(result[:segments].count).to eq(8)
23
+ end
24
+
25
+ it 'parses term keywords correctly' do
26
+ keywords = result[:segments].map(&:keyword)
27
+ expect(keywords).to include('app_name', 'greeting', '[comment]', '[init-node]', '[end-node]')
28
+ end
29
+
30
+ it 'parses translations for each language' do
31
+ app_name = result[:segments].find { |t| t.keyword == 'app_name' }
32
+ expect(app_name.values['en']).to eq('My App')
33
+ expect(app_name.values['es']).to eq('Mi Aplicación')
34
+ expect(app_name.values['fr']).to eq('Mon Application')
35
+ end
36
+
37
+ it 'identifies comment rows' do
38
+ comment = result[:segments].find { |t| t.keyword == '[comment]' }
39
+ expect(comment.is_comment?).to be true
40
+ end
41
+
42
+ it 'raises ArgumentError when :path is missing' do
43
+ expect { CsvProcessor.load_localizables({}, {}) }
44
+ .to raise_error(ArgumentError, /:path attribute is missing/)
45
+ end
46
+
47
+ it 'raises IndexError when [key] marker is missing' do
48
+ Dir.mktmpdir do |tmpdir|
49
+ path = File.join(tmpdir, 'bad.csv')
50
+ File.write(path, "no,key,row\ndata,here,\n[end],,,\n")
51
+ expect { CsvProcessor.load_localizables({}, { path: path }) }
52
+ .to raise_error(IndexError, /Could not find any \[key\]/)
53
+ end
54
+ end
55
+
56
+ it 'raises IndexError when [end] marker is missing' do
57
+ Dir.mktmpdir do |tmpdir|
58
+ path = File.join(tmpdir, 'bad.csv')
59
+ File.write(path, "title,,,\n[key],*en,es,\ndata,val,val,\n")
60
+ expect { CsvProcessor.load_localizables({}, { path: path }) }
61
+ .to raise_error(IndexError, /Could not find any \[end\]/)
62
+ end
63
+ end
64
+
65
+ context 'with override_default option' do
66
+ let(:platform_options) { { override_default: 'es' } }
67
+
68
+ it 'uses the overridden default language' do
69
+ expect(result[:default_language]).to eq('es')
70
+ end
71
+ end
72
+
73
+ context 'with avoid_lang_downcase option' do
74
+ let(:tmpdir) { Dir.mktmpdir }
75
+ let(:options) do
76
+ path = File.join(tmpdir, 'upper.csv')
77
+ File.write(path, "Title,,,\n[key],*EN,ES,FR\napp_name,My App,Mi App,Mon App\n[end],,,\n")
78
+ { path: path }
79
+ end
80
+ let(:platform_options) { { avoid_lang_downcase: true } }
81
+
82
+ after { FileUtils.rm_rf(tmpdir) }
83
+
84
+ it 'preserves language case' do
85
+ expect(result[:languages].keys).to contain_exactly('EN', 'ES', 'FR')
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,107 @@
1
+ # google_drive transitively loads nokogiri. Nokogiri is now built for arm64
2
+ # and loads correctly, so we no longer need to stub it out. We only stub
3
+ # google_drive itself (which requires OAuth flow and network access) by:
4
+ # 1. Defining minimal stub modules for GoogleDrive before anything tries to
5
+ # reference them.
6
+ # 2. Pre-populating $LOADED_FEATURES with the google_drive gem paths so that
7
+ # every subsequent `require 'google_drive'` is treated as already loaded.
8
+ # All runtime calls are intercepted by RSpec doubles.
9
+
10
+ module GoogleDrive
11
+ module Session
12
+ def self.from_config(_config, _opts = {}); end
13
+ def self.from_service_account_key(_path, _scope = nil); end
14
+ end
15
+ end
16
+
17
+ begin
18
+ _gd_spec = Gem::Specification.find_by_name('google_drive')
19
+ _gd_base = File.join(_gd_spec.gem_dir, 'lib')
20
+ Dir["#{_gd_base}/**/*.rb"].sort.each do |f|
21
+ $LOADED_FEATURES << f unless $LOADED_FEATURES.include?(f)
22
+ end
23
+ ["#{_gd_base}/google_drive.rb"].each do |f|
24
+ $LOADED_FEATURES << f unless $LOADED_FEATURES.include?(f)
25
+ end
26
+ end
27
+
28
+ require 'localio/term'
29
+ require 'localio/string_helper'
30
+ require 'localio/processors/google_drive_processor'
31
+
32
+ RSpec.describe GoogleDriveProcessor do
33
+ let(:ws_data) do
34
+ {
35
+ [1, 1] => '[key]', [1, 2] => '*en', [1, 3] => 'es',
36
+ [2, 1] => 'app_name', [2, 2] => 'My App', [2, 3] => 'Mi Aplicación',
37
+ [3, 1] => 'greeting', [3, 2] => 'Hello', [3, 3] => 'Hola',
38
+ [4, 1] => '[end]', [4, 2] => '', [4, 3] => '',
39
+ }
40
+ end
41
+
42
+ let(:worksheet) do
43
+ ws = double('worksheet')
44
+ allow(ws).to receive(:[]) { |row, col| ws_data[[row, col]].to_s }
45
+ allow(ws).to receive(:max_rows).and_return(4)
46
+ allow(ws).to receive(:max_cols).and_return(3)
47
+ ws
48
+ end
49
+
50
+ let(:spreadsheet_double) { double('spreadsheet', title: 'My Translations') }
51
+ let(:session_double) { double('session') }
52
+
53
+ before do
54
+ allow(spreadsheet_double).to receive(:worksheets).and_return([worksheet])
55
+ allow(session_double).to receive(:spreadsheets).and_return([spreadsheet_double])
56
+
57
+ # Mock the google_drive 3.x session creation method used by the processor.
58
+ # from_config is used for the OAuth2 client_id/client_secret flow.
59
+ allow(GoogleDrive::Session).to receive(:from_config).and_return(session_double)
60
+
61
+ # Allow File.file? to return false for the :client_token option in OAuth tests
62
+ # so the processor takes the from_config path (not the service-account path).
63
+ allow(File).to receive(:file?).and_call_original
64
+ allow(File).to receive(:file?).with('token').and_return(false)
65
+ end
66
+
67
+ let(:options) do
68
+ { spreadsheet: 'My Translations', client_id: 'id', client_secret: 'secret', client_token: 'token', sheet: 0 }
69
+ end
70
+
71
+ describe '.load_localizables' do
72
+ subject(:result) { GoogleDriveProcessor.load_localizables({}, options) }
73
+
74
+ it 'raises when :spreadsheet is missing' do
75
+ expect { GoogleDriveProcessor.load_localizables({}, { client_id: 'a', client_secret: 'b' }) }
76
+ .to raise_error(ArgumentError, /:spreadsheet required/)
77
+ end
78
+
79
+ it 'raises when :login is provided (deprecated)' do
80
+ expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', login: 'u', client_id: 'a', client_secret: 'b' }) }
81
+ .to raise_error(ArgumentError, /:login is deprecated/)
82
+ end
83
+
84
+ it 'raises when :client_id is missing' do
85
+ expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', client_secret: 'b' }) }
86
+ .to raise_error(ArgumentError, /:client_id required/)
87
+ end
88
+
89
+ it 'raises when :client_secret is missing' do
90
+ expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', client_id: 'a' }) }
91
+ .to raise_error(ArgumentError, /:client_secret required/)
92
+ end
93
+
94
+ it 'returns languages en and es' do
95
+ expect(result[:languages].keys).to contain_exactly('en', 'es')
96
+ end
97
+
98
+ it 'sets en as default language' do
99
+ expect(result[:default_language]).to eq('en')
100
+ end
101
+
102
+ it 'parses translations' do
103
+ app_name = result[:segments].find { |t| t.keyword == 'app_name' }
104
+ expect(app_name.values['en']).to eq('My App')
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,65 @@
1
+ require 'localio/term'
2
+ require 'localio/string_helper'
3
+ require 'localio/processors/xls_processor'
4
+
5
+ RSpec.describe XlsProcessor do
6
+ let(:data) do
7
+ {
8
+ [0, 0] => '[key]', [0, 1] => '*en', [0, 2] => 'es', [0, 3] => 'fr',
9
+ [1, 0] => 'app_name', [1, 1] => 'My App', [1, 2] => 'Mi Aplicación', [1, 3] => 'Mon Application',
10
+ [2, 0] => 'greeting', [2, 1] => 'Hello', [2, 2] => 'Hola', [2, 3] => 'Bonjour',
11
+ [3, 0] => '[end]', [3, 1] => '', [3, 2] => '', [3, 3] => '',
12
+ }
13
+ end
14
+
15
+ let(:worksheet) do
16
+ ws = double('worksheet')
17
+ allow(ws).to receive(:[]) { |row, col| data[[row, col]].to_s }
18
+ allow(ws).to receive(:row_count).and_return(3)
19
+ allow(ws).to receive(:column_count).and_return(3)
20
+ ws
21
+ end
22
+
23
+ let(:book_double) { double('book') }
24
+
25
+ before do
26
+ allow(Spreadsheet).to receive(:client_encoding=)
27
+ allow(Spreadsheet).to receive(:open).and_return(book_double)
28
+ allow(book_double).to receive(:worksheet).with(0).and_return(worksheet)
29
+ end
30
+
31
+ let(:options) { { path: 'fake.xls' } }
32
+ let(:platform_options) { {} }
33
+
34
+ describe '.load_localizables' do
35
+ subject(:result) { XlsProcessor.load_localizables(platform_options, options) }
36
+
37
+ it 'raises ArgumentError when :path is missing' do
38
+ expect { XlsProcessor.load_localizables({}, {}) }
39
+ .to raise_error(ArgumentError, /:path attribute is missing/)
40
+ end
41
+
42
+ it 'returns languages en, es, fr' do
43
+ expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
44
+ end
45
+
46
+ it 'sets en as default language' do
47
+ expect(result[:default_language]).to eq('en')
48
+ end
49
+
50
+ it 'returns 2 terms' do
51
+ expect(result[:segments].count).to eq(2)
52
+ end
53
+
54
+ it 'parses translations correctly' do
55
+ app_name = result[:segments].find { |t| t.keyword == 'app_name' }
56
+ expect(app_name.values['en']).to eq('My App')
57
+ end
58
+
59
+ it 'raises when worksheet is nil' do
60
+ allow(book_double).to receive(:worksheet).and_return(nil)
61
+ expect { XlsProcessor.load_localizables({}, { path: 'fake.xls' }) }
62
+ .to raise_error(RuntimeError, /Unable to retrieve/)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ require 'localio/term'
2
+ require 'localio/string_helper'
3
+ require 'localio/processors/xlsx_processor'
4
+
5
+ RSpec.describe XlsxProcessor do
6
+ let(:rows) do
7
+ [
8
+ ['Title', nil, nil, nil],
9
+ ['[key]', '*en', 'es', 'fr'],
10
+ ['[comment]', 'Section General', 'Section General', 'Section General'],
11
+ ['app_name', 'My App', 'Mi Aplicación', 'Mon Application'],
12
+ ['greeting', 'Hello', 'Hola', 'Bonjour'],
13
+ ['[end]', nil, nil, nil],
14
+ ]
15
+ end
16
+
17
+ let(:sheet_double) { double('sheet', rows: rows) }
18
+ let(:book_double) { double('book', sheets: [sheet_double]) }
19
+
20
+ before { allow(SimpleXlsxReader).to receive(:open).and_return(book_double) }
21
+
22
+ let(:options) { { path: 'fake.xlsx', sheet: 0 } }
23
+ let(:platform_options) { {} }
24
+
25
+ describe '.load_localizables' do
26
+ subject(:result) { XlsxProcessor.load_localizables(platform_options, options) }
27
+
28
+ it 'raises ArgumentError when :path is missing' do
29
+ expect { XlsxProcessor.load_localizables({}, {}) }
30
+ .to raise_error(ArgumentError, /:path attribute is missing/)
31
+ end
32
+
33
+ it 'returns languages en, es, fr' do
34
+ expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
35
+ end
36
+
37
+ it 'sets en as default language' do
38
+ expect(result[:default_language]).to eq('en')
39
+ end
40
+
41
+ it 'returns 3 terms' do
42
+ expect(result[:segments].count).to eq(3)
43
+ end
44
+
45
+ it 'parses translations correctly' do
46
+ app_name = result[:segments].find { |t| t.keyword == 'app_name' }
47
+ expect(app_name.values['en']).to eq('My App')
48
+ expect(app_name.values['es']).to eq('Mi Aplicación')
49
+ end
50
+
51
+ it 'raises when sheet is nil' do
52
+ named_book = double('book', sheets: [])
53
+ allow(SimpleXlsxReader).to receive(:open).and_return(named_book)
54
+ allow(named_book).to receive(:sheets).and_return([])
55
+ expect { XlsxProcessor.load_localizables({}, { path: 'fake.xlsx', sheet: 'Missing' }) }
56
+ .to raise_error(RuntimeError, /Unable to retrieve/)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ require 'localio/string_helper'
2
+ require 'localio/segment'
3
+
4
+ RSpec.describe Segment do
5
+ subject(:segment) { Segment.new('app_name', 'My App', 'en') }
6
+
7
+ it 'stores key, translation, and language' do
8
+ expect(segment.key).to eq('app_name')
9
+ expect(segment.translation).to eq('My App')
10
+ expect(segment.language).to eq('en')
11
+ end
12
+
13
+ it 'processes translation through replace_escaped' do
14
+ seg = Segment.new('key', 'hello`+world', 'en')
15
+ expect(seg.translation).to eq('hello+world')
16
+ end
17
+
18
+ describe '#is_comment?' do
19
+ it 'returns true when key is nil' do
20
+ segment.key = nil
21
+ expect(segment.is_comment?).to be true
22
+ end
23
+ it 'returns false when key is set' do
24
+ expect(segment.is_comment?).to be false
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ require 'localio/segments_list_holder'
2
+
3
+ RSpec.describe SegmentsListHolder do
4
+ subject(:holder) { SegmentsListHolder.new('en') }
5
+
6
+ it { expect(holder.language).to eq('en') }
7
+ it { expect(holder.segments).to be_empty }
8
+
9
+ describe '#get_binding' do
10
+ it 'returns a Binding' do
11
+ expect(holder.get_binding).to be_a(Binding)
12
+ end
13
+
14
+ it 'exposes @language in the binding' do
15
+ expect(eval('@language', holder.get_binding)).to eq('en')
16
+ end
17
+
18
+ it 'exposes @segments in the binding' do
19
+ expect(eval('@segments', holder.get_binding)).to eq([])
20
+ end
21
+ end
22
+ end
@@ -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