yury-twine 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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