yury-twine 0.9.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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +30 -0
  4. data/README.md +230 -0
  5. data/bin/twine +7 -0
  6. data/lib/twine.rb +36 -0
  7. data/lib/twine/cli.rb +200 -0
  8. data/lib/twine/encoding.rb +22 -0
  9. data/lib/twine/formatters.rb +20 -0
  10. data/lib/twine/formatters/abstract.rb +187 -0
  11. data/lib/twine/formatters/android.rb +254 -0
  12. data/lib/twine/formatters/apple.rb +328 -0
  13. data/lib/twine/output_processor.rb +57 -0
  14. data/lib/twine/placeholders.rb +54 -0
  15. data/lib/twine/plugin.rb +62 -0
  16. data/lib/twine/runner.rb +332 -0
  17. data/lib/twine/twine_file.rb +266 -0
  18. data/lib/twine/version.rb +3 -0
  19. data/test/command_test.rb +14 -0
  20. data/test/fixtures/consume_loc_drop.zip +0 -0
  21. data/test/fixtures/enc_utf16be.dummy +0 -0
  22. data/test/fixtures/enc_utf16be_bom.dummy +0 -0
  23. data/test/fixtures/enc_utf16le.dummy +0 -0
  24. data/test/fixtures/enc_utf16le_bom.dummy +0 -0
  25. data/test/fixtures/enc_utf8.dummy +2 -0
  26. data/test/fixtures/formatter_android.xml +15 -0
  27. data/test/fixtures/formatter_apple.strings +20 -0
  28. data/test/fixtures/formatter_django.po +30 -0
  29. data/test/fixtures/formatter_flash.properties +15 -0
  30. data/test/fixtures/formatter_gettext.po +26 -0
  31. data/test/fixtures/formatter_jquery.json +7 -0
  32. data/test/fixtures/formatter_tizen.xml +15 -0
  33. data/test/fixtures/gettext_multiline.po +10 -0
  34. data/test/fixtures/twine_accent_values.txt +13 -0
  35. data/test/test_abstract_formatter.rb +165 -0
  36. data/test/test_cli.rb +304 -0
  37. data/test/test_consume_loc_drop.rb +27 -0
  38. data/test/test_consume_localization_file.rb +119 -0
  39. data/test/test_formatters.rb +363 -0
  40. data/test/test_generate_all_localization_files.rb +102 -0
  41. data/test/test_generate_loc_drop.rb +80 -0
  42. data/test/test_generate_localization_file.rb +91 -0
  43. data/test/test_output_processor.rb +85 -0
  44. data/test/test_placeholders.rb +84 -0
  45. data/test/test_twine_definition.rb +111 -0
  46. data/test/test_twine_file.rb +58 -0
  47. data/test/test_validate_twine_file.rb +61 -0
  48. data/test/twine_file_dsl.rb +46 -0
  49. data/test/twine_test.rb +48 -0
  50. metadata +179 -0
@@ -0,0 +1,102 @@
1
+ require 'command_test'
2
+
3
+ class TestGenerateAllLocalizationFiles < CommandTest
4
+ def new_runner(create_folders, twine_file = nil)
5
+ options = {}
6
+ options[:output_path] = @output_dir
7
+ options[:format] = 'apple'
8
+ options[:create_folders] = create_folders
9
+
10
+ unless twine_file
11
+ twine_file = build_twine_file 'en', 'es' do
12
+ add_section 'Section' do
13
+ add_definition key: 'value'
14
+ end
15
+ end
16
+ end
17
+
18
+ Twine::Runner.new(options, twine_file)
19
+ end
20
+
21
+ class TestDoNotCreateFolders < TestGenerateAllLocalizationFiles
22
+ def new_runner(twine_file = nil)
23
+ super(false, twine_file)
24
+ end
25
+
26
+ def test_fails_if_output_folder_does_not_exist
27
+ assert_raises Twine::Error do
28
+ new_runner.generate_all_localization_files
29
+ end
30
+ end
31
+
32
+ def test_does_not_create_language_folders
33
+ Dir.mkdir File.join @output_dir, 'en.lproj'
34
+ new_runner.generate_all_localization_files
35
+ refute File.exists?(File.join(@output_dir, 'es.lproj')), "language folder should not be created"
36
+ end
37
+
38
+ def test_prints_empty_file_warnings
39
+ Dir.mkdir File.join @output_dir, 'en.lproj'
40
+ empty_twine_file = build_twine_file('en') {}
41
+ new_runner(empty_twine_file).generate_all_localization_files
42
+ assert_match "Skipping file at path", Twine::stderr.string
43
+ end
44
+ end
45
+
46
+ class TestCreateFolders < TestGenerateAllLocalizationFiles
47
+ def new_runner(twine_file = nil)
48
+ super(true, twine_file)
49
+ end
50
+
51
+ def test_creates_output_folder
52
+ FileUtils.remove_entry_secure @output_dir
53
+ new_runner.generate_all_localization_files
54
+ assert File.exists? @output_dir
55
+ end
56
+
57
+ def test_creates_language_folders
58
+ new_runner.generate_all_localization_files
59
+ assert File.exists?(File.join(@output_dir, 'en.lproj')), "language folder 'en.lproj' should be created"
60
+ assert File.exists?(File.join(@output_dir, 'es.lproj')), "language folder 'es.lproj' should be created"
61
+ end
62
+
63
+ def test_prints_empty_file_warnings
64
+ empty_twine_file = build_twine_file('en') {}
65
+ new_runner(empty_twine_file).generate_all_localization_files
66
+
67
+ assert_match "Skipping file at path", Twine::stderr.string
68
+ end
69
+ end
70
+
71
+ class TestValidate < CommandTest
72
+ def new_runner(validate)
73
+ Dir.mkdir File.join @output_dir, 'values-en'
74
+
75
+ options = {}
76
+ options[:output_path] = @output_dir
77
+ options[:format] = 'android'
78
+ options[:validate] = validate
79
+
80
+ twine_file = build_twine_file 'en' do
81
+ add_section 'Section' do
82
+ add_definition key: 'value'
83
+ add_definition key: 'value'
84
+ end
85
+ end
86
+
87
+ Twine::Runner.new(options, twine_file)
88
+ end
89
+
90
+ def test_does_not_validate_twine_file
91
+ prepare_mock_formatter Twine::Formatters::Android
92
+
93
+ new_runner(false).generate_all_localization_files
94
+ end
95
+
96
+ def test_validates_twine_file_if_validate
97
+ assert_raises Twine::Error do
98
+ new_runner(true).generate_all_localization_files
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,80 @@
1
+ require 'command_test'
2
+
3
+ class TestGenerateLocDrop < CommandTest
4
+ def new_runner(twine_file = nil)
5
+ options = {}
6
+ options[:output_path] = @output_path
7
+ options[:format] = 'apple'
8
+
9
+ unless twine_file
10
+ twine_file = build_twine_file 'en', 'fr' do
11
+ add_section 'Section' do
12
+ add_definition key: 'value'
13
+ end
14
+ end
15
+ end
16
+
17
+ Twine::Runner.new(options, twine_file)
18
+ end
19
+
20
+ def test_generates_zip_file
21
+ new_runner.generate_loc_drop
22
+
23
+ assert File.exists?(@output_path), "zip file should exist"
24
+ end
25
+
26
+ def test_zip_file_structure
27
+ new_runner.generate_loc_drop
28
+
29
+ names = []
30
+ Zip::File.open(@output_path) do |zipfile|
31
+ zipfile.each do |entry|
32
+ names << entry.name
33
+ end
34
+ end
35
+ assert_equal ['Locales/', 'Locales/en.strings', 'Locales/fr.strings'], names
36
+ end
37
+
38
+ def test_uses_formatter
39
+ formatter = prepare_mock_formatter Twine::Formatters::Apple
40
+ formatter.expects(:format_file).twice
41
+
42
+ new_runner.generate_loc_drop
43
+ end
44
+
45
+ def test_prints_empty_file_warnings
46
+ empty_twine_file = build_twine_file('en') {}
47
+ new_runner(empty_twine_file).generate_loc_drop
48
+ assert_match "Skipping file", Twine::stderr.string
49
+ end
50
+
51
+ class TestValidate < CommandTest
52
+ def new_runner(validate)
53
+ options = {}
54
+ options[:output_path] = @output_path
55
+ options[:format] = 'android'
56
+ options[:validate] = validate
57
+
58
+ twine_file = build_twine_file 'en' do
59
+ add_section 'Section' do
60
+ add_definition key: 'value'
61
+ add_definition key: 'value'
62
+ end
63
+ end
64
+
65
+ Twine::Runner.new(options, twine_file)
66
+ end
67
+
68
+ def test_does_not_validate_twine_file
69
+ prepare_mock_formatter Twine::Formatters::Android
70
+
71
+ new_runner(false).generate_loc_drop
72
+ end
73
+
74
+ def test_validates_twine_file_if_validate
75
+ assert_raises Twine::Error do
76
+ new_runner(true).generate_loc_drop
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,91 @@
1
+ require 'command_test'
2
+
3
+ class TestGenerateLocalizationFile < CommandTest
4
+ def new_runner(language, file)
5
+ options = {}
6
+ options[:output_path] = File.join(@output_dir, file) if file
7
+ options[:languages] = language if language
8
+
9
+ twine_file = Twine::TwineFile.new
10
+ twine_file.language_codes.concat KNOWN_LANGUAGES
11
+
12
+ Twine::Runner.new(options, twine_file)
13
+ end
14
+
15
+ def prepare_mock_format_file_formatter(formatter_class)
16
+ formatter = prepare_mock_formatter(formatter_class)
17
+ formatter.expects(:format_file).returns(true)
18
+ end
19
+
20
+ def test_deducts_android_format_from_output_path
21
+ prepare_mock_format_file_formatter Twine::Formatters::Android
22
+
23
+ new_runner('fr', 'fr.xml').generate_localization_file
24
+ end
25
+
26
+ def test_deducts_apple_format_from_output_path
27
+ prepare_mock_format_file_formatter Twine::Formatters::Apple
28
+
29
+ new_runner('fr', 'fr.strings').generate_localization_file
30
+ end
31
+
32
+ def test_deducts_jquery_format_from_output_path
33
+ prepare_mock_format_file_formatter Twine::Formatters::JQuery
34
+
35
+ new_runner('fr', 'fr.json').generate_localization_file
36
+ end
37
+
38
+ def test_deducts_gettext_format_from_output_path
39
+ prepare_mock_format_file_formatter Twine::Formatters::Gettext
40
+
41
+ new_runner('fr', 'fr.po').generate_localization_file
42
+ end
43
+
44
+ def test_deducts_language_from_output_path
45
+ random_language = KNOWN_LANGUAGES.sample
46
+ formatter = prepare_mock_formatter Twine::Formatters::Android
47
+ formatter.expects(:format_file).with(random_language).returns(true)
48
+
49
+ new_runner(nil, "#{random_language}.xml").generate_localization_file
50
+ end
51
+
52
+ def test_returns_error_if_nothing_written
53
+ formatter = prepare_mock_formatter Twine::Formatters::Android
54
+ formatter.expects(:format_file).returns(false)
55
+
56
+ assert_raises Twine::Error do
57
+ new_runner('fr', 'fr.xml').generate_localization_file
58
+ end
59
+ end
60
+
61
+ class TestValidate < CommandTest
62
+ def new_runner(validate)
63
+ options = {}
64
+ options[:output_path] = @output_path
65
+ options[:languages] = ['en']
66
+ options[:format] = 'android'
67
+ options[:validate] = validate
68
+
69
+ twine_file = build_twine_file 'en' do
70
+ add_section 'Section' do
71
+ add_definition key: 'value'
72
+ add_definition key: 'value'
73
+ end
74
+ end
75
+
76
+ Twine::Runner.new(options, twine_file)
77
+ end
78
+
79
+ def test_does_not_validate_twine_file
80
+ prepare_mock_formatter Twine::Formatters::Android
81
+
82
+ new_runner(false).generate_localization_file
83
+ end
84
+
85
+ def test_validates_twine_file_if_validate
86
+ assert_raises Twine::Error do
87
+ new_runner(true).generate_localization_file
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,85 @@
1
+ require 'twine_test'
2
+
3
+ class TestOutputProcessor < TwineTest
4
+ def setup
5
+ super
6
+
7
+ @twine_file = build_twine_file 'en', 'fr' do
8
+ add_section 'Section' do
9
+ add_definition key1: 'value1', tags: ['tag1']
10
+ add_definition key2: 'value2', tags: ['tag1', 'tag2']
11
+ add_definition key3: 'value3', tags: ['tag2']
12
+ add_definition key4: { en: 'value4-en', fr: 'value4-fr' }
13
+ end
14
+ end
15
+ end
16
+
17
+ def test_includes_all_keys_by_default
18
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, {})
19
+ result = processor.process('en')
20
+
21
+ assert_equal %w(key1 key2 key3 key4), result.definitions_by_key.keys.sort
22
+ end
23
+
24
+ def test_filter_by_tag
25
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1']] })
26
+ result = processor.process('en')
27
+
28
+ assert_equal %w(key1 key2), result.definitions_by_key.keys.sort
29
+ end
30
+
31
+ def test_filter_by_multiple_tags
32
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1', 'tag2']] })
33
+ result = processor.process('en')
34
+
35
+ assert_equal %w(key1 key2 key3), result.definitions_by_key.keys.sort
36
+ end
37
+
38
+ def test_filter_untagged
39
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, { tags: [['tag1']], untagged: true })
40
+ result = processor.process('en')
41
+
42
+ assert_equal %w(key1 key2 key4), result.definitions_by_key.keys.sort
43
+ end
44
+
45
+ def test_include_translated
46
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, { include: :translated })
47
+ result = processor.process('fr')
48
+
49
+ assert_equal %w(key4), result.definitions_by_key.keys.sort
50
+ end
51
+
52
+ def test_include_untranslated
53
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, { include: :untranslated })
54
+ result = processor.process('fr')
55
+
56
+ assert_equal %w(key1 key2 key3), result.definitions_by_key.keys.sort
57
+ end
58
+
59
+ class TranslationFallback < TwineTest
60
+ def setup
61
+ super
62
+
63
+ @twine_file = build_twine_file 'en', 'fr', 'de' do
64
+ add_section 'Section' do
65
+ add_definition key1: { en: 'value1-en', fr: 'value1-fr' }
66
+ end
67
+ end
68
+ end
69
+
70
+ def test_fallback_to_default_language
71
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, {})
72
+ result = processor.process('de')
73
+
74
+ assert_equal 'value1-en', result.definitions_by_key['key1'].translations['de']
75
+ end
76
+
77
+ def test_fallback_to_developer_language
78
+ processor = Twine::Processors::OutputProcessor.new(@twine_file, {developer_language: 'fr'})
79
+ result = processor.process('de')
80
+
81
+ assert_equal 'value1-fr', result.definitions_by_key['key1'].translations['de']
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,84 @@
1
+ require 'twine_test'
2
+
3
+ class PlaceholderTest < TwineTest
4
+ def assert_starts_with(prefix, value)
5
+ msg = message(nil) { "Expected #{mu_pp(value)} to start with #{mu_pp(prefix)}" }
6
+ assert value.start_with?(prefix), msg
7
+ end
8
+
9
+ def placeholder(type = nil)
10
+ # %[parameter][flags][width][.precision][length]type (see https://en.wikipedia.org/wiki/Printf_format_string#Format_placeholder_specification)
11
+ lucky = lambda { rand > 0.5 }
12
+ placeholder = '%'
13
+ placeholder += (rand * 20).to_i.to_s + '$' if lucky.call
14
+ placeholder += '-+ 0#'.chars.to_a.sample if lucky.call
15
+ placeholder += (0.upto(20).map(&:to_s) << "*").sample if lucky.call
16
+ placeholder += '.' + (0.upto(20).map(&:to_s) << "*").sample if lucky.call
17
+ placeholder += %w(h hh l ll L z j t).sample if lucky.call
18
+ placeholder += type || 'diufFeEgGxXocpaA'.chars.to_a.sample # this does not contain s or @ because strings are a special case
19
+ end
20
+
21
+ class ToAndroid < PlaceholderTest
22
+ def to_android(value)
23
+ Twine::Placeholders.convert_placeholders_from_twine_to_android(value)
24
+ end
25
+
26
+ def test_replaces_string_placeholder
27
+ placeholder = placeholder('@')
28
+ expected = placeholder
29
+ expected[-1] = 's'
30
+ assert_equal "some #{expected} value", to_android("some #{placeholder} value")
31
+ end
32
+
33
+ def test_does_not_change_regular_at_signs
34
+ input = "some @ more @@ signs @"
35
+ assert_equal input, to_android(input)
36
+ end
37
+
38
+ def test_does_not_modify_single_percent_signs
39
+ assert_equal "some % value", to_android("some % value")
40
+ end
41
+
42
+ def test_escapes_single_percent_signs_if_placeholder_present
43
+ assert_starts_with "some %% v", to_android("some % value #{placeholder}")
44
+ end
45
+
46
+ def test_does_not_modify_double_percent_signs
47
+ assert_equal "some %% value", to_android("some %% value")
48
+ end
49
+
50
+ def test_does_not_modify_double_percent_signs_if_placeholder_present
51
+ assert_starts_with "some %% v", to_android("some %% value #{placeholder}")
52
+ end
53
+
54
+ def test_does_not_modify_single_placeholder
55
+ input = "some #{placeholder} text"
56
+ assert_equal input, to_android(input)
57
+ end
58
+
59
+ def test_numbers_multiple_placeholders
60
+ assert_equal "first %1$d second %2$f", to_android("first %d second %f")
61
+ end
62
+
63
+ def test_does_not_modify_numbered_placeholders
64
+ input = "second %2$f first %1$d"
65
+ assert_equal input, to_android(input)
66
+ end
67
+
68
+ def test_raises_an_error_when_mixing_numbered_and_non_numbered_placeholders
69
+ assert_raises Twine::Error do
70
+ to_android("some %d second %2$f")
71
+ end
72
+ end
73
+ end
74
+
75
+ class FromAndroid < PlaceholderTest
76
+ def from_android(value)
77
+ Twine::Placeholders.convert_placeholders_from_android_to_twine(value)
78
+ end
79
+
80
+ def test_replaces_string_placeholder
81
+ assert_equal "some %@ value", from_android("some %s value")
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,111 @@
1
+ require 'twine_test'
2
+
3
+ class TestTwineDefinition < TwineTest
4
+ class TestTags < TwineTest
5
+ def setup
6
+ super
7
+ @definition = Twine::TwineDefinition.new 'key'
8
+ end
9
+
10
+ def test_include_untagged
11
+ assert @definition.matches_tags?([[rand(100000).to_s]], true)
12
+ end
13
+
14
+ def test_matches_no_given_tags
15
+ assert @definition.matches_tags?([], false)
16
+ end
17
+
18
+ def test_matches_tag
19
+ @definition.tags = ['tag1']
20
+
21
+ assert @definition.matches_tags?([['tag1']], false)
22
+ end
23
+
24
+ def test_matches_any_tag
25
+ @definition.tags = ['tag1']
26
+
27
+ assert @definition.matches_tags?([['tag0', 'tag1', 'tag2']], false)
28
+ end
29
+
30
+ def test_matches_all_tags
31
+ @definition.tags = ['tag1', 'tag2']
32
+
33
+ assert @definition.matches_tags?([['tag1'], ['tag2']], false)
34
+ end
35
+
36
+ def test_does_not_match_all_tags
37
+ @definition.tags = ['tag1']
38
+
39
+ refute @definition.matches_tags?([['tag1'], ['tag2']], false)
40
+ end
41
+
42
+ def test_does_not_match_excluded_tag
43
+ @definition.tags = ['tag1']
44
+
45
+ refute @definition.matches_tags?([['~tag1']], false)
46
+ end
47
+
48
+ def test_matches_excluded_tag
49
+ @definition.tags = ['tag2']
50
+
51
+ assert @definition.matches_tags?([['~tag1']], false)
52
+ end
53
+
54
+ def test_complex_rules
55
+ @definition.tags = ['tag1', 'tag2', 'tag3']
56
+
57
+ assert @definition.matches_tags?([['tag1']], false)
58
+ assert @definition.matches_tags?([['tag1', 'tag4']], false)
59
+ assert @definition.matches_tags?([['tag1'], ['tag2'], ['tag3']], false)
60
+ refute @definition.matches_tags?([['tag1'], ['tag4']], false)
61
+
62
+ assert @definition.matches_tags?([['tag4', '~tag5']], false)
63
+ end
64
+ end
65
+
66
+ class TestReferences < TwineTest
67
+ def setup
68
+ super
69
+
70
+ @reference = Twine::TwineDefinition.new 'reference-key'
71
+ @reference.comment = 'reference comment'
72
+ @reference.tags = ['ref1']
73
+ @reference.translations['en'] = 'ref-value'
74
+
75
+ @definition = Twine::TwineDefinition.new 'key'
76
+ @definition.reference_key = @reference.key
77
+ @definition.reference = @reference
78
+ end
79
+
80
+ def test_reference_comment_used
81
+ assert_equal 'reference comment', @definition.comment
82
+ end
83
+
84
+ def test_reference_comment_override
85
+ @definition.comment = 'definition comment'
86
+
87
+ assert_equal 'definition comment', @definition.comment
88
+ end
89
+
90
+ def test_reference_tags_used
91
+ assert @definition.matches_tags?([['ref1']], false)
92
+ end
93
+
94
+ def test_reference_tags_override
95
+ @definition.tags = ['tag1']
96
+
97
+ refute @definition.matches_tags?([['ref1']], false)
98
+ assert @definition.matches_tags?([['tag1']], false)
99
+ end
100
+
101
+ def test_reference_translation_used
102
+ assert_equal 'ref-value', @definition.translation_for_lang('en')
103
+ end
104
+
105
+ def test_reference_translation_override
106
+ @definition.translations['en'] = 'value'
107
+
108
+ assert_equal 'value', @definition.translation_for_lang('en')
109
+ end
110
+ end
111
+ end