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,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
|
|
@@ -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('& 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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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
|