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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +28 -0
- data/.gitignore +3 -1
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile.lock +138 -0
- data/README.md +34 -35
- 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/lib/localio/processor.rb +2 -1
- data/lib/localio/processors/csv_processor.rb +12 -3
- data/lib/localio/processors/google_drive_processor.rb +36 -48
- data/lib/localio/processors/xls_processor.rb +12 -3
- data/lib/localio/processors/xlsx_processor.rb +12 -3
- 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/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_spec.rb +62 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/shared_terms.rb +35 -0
- 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 & 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('& 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
|