twine 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -0
  3. data/lib/twine.rb +21 -0
  4. data/lib/twine/cli.rb +68 -91
  5. data/lib/twine/formatters/abstract.rb +133 -82
  6. data/lib/twine/formatters/android.rb +49 -67
  7. data/lib/twine/formatters/apple.rb +33 -47
  8. data/lib/twine/formatters/django.rb +38 -48
  9. data/lib/twine/formatters/flash.rb +25 -40
  10. data/lib/twine/formatters/gettext.rb +37 -44
  11. data/lib/twine/formatters/jquery.rb +31 -33
  12. data/lib/twine/formatters/tizen.rb +38 -55
  13. data/lib/twine/output_processor.rb +57 -0
  14. data/lib/twine/placeholders.rb +54 -0
  15. data/lib/twine/runner.rb +78 -115
  16. data/lib/twine/stringsfile.rb +83 -60
  17. data/lib/twine/version.rb +1 -1
  18. data/test/command_test_case.rb +12 -0
  19. data/test/fixtures/consume_loc_drop.zip +0 -0
  20. data/test/fixtures/formatter_android.xml +15 -0
  21. data/test/fixtures/formatter_apple.strings +20 -0
  22. data/test/fixtures/formatter_django.po +28 -0
  23. data/test/fixtures/formatter_flash.properties +15 -0
  24. data/test/fixtures/formatter_gettext.po +26 -0
  25. data/test/fixtures/formatter_jquery.json +7 -0
  26. data/test/fixtures/formatter_tizen.xml +15 -0
  27. data/test/fixtures/gettext_multiline.po +10 -0
  28. data/test/fixtures/twine_accent_values.txt +13 -0
  29. data/test/test_abstract_formatter.rb +152 -0
  30. data/test/test_cli.rb +288 -0
  31. data/test/test_consume_loc_drop.rb +27 -0
  32. data/test/test_consume_string_file.rb +53 -0
  33. data/test/test_formatters.rb +236 -0
  34. data/test/test_generate_all_string_files.rb +44 -0
  35. data/test/test_generate_loc_drop.rb +44 -0
  36. data/test/test_generate_string_file.rb +51 -0
  37. data/test/test_output_processor.rb +85 -0
  38. data/test/test_placeholders.rb +86 -0
  39. data/test/test_strings_file.rb +58 -0
  40. data/test/test_strings_row.rb +47 -0
  41. data/test/test_validate_strings_file.rb +55 -0
  42. data/test/twine_file_dsl.rb +46 -0
  43. data/test/twine_test_case.rb +44 -0
  44. metadata +80 -37
  45. data/test/fixtures/en-1.json +0 -5
  46. data/test/fixtures/en-1.po +0 -16
  47. data/test/fixtures/en-1.strings +0 -10
  48. data/test/fixtures/en-2.po +0 -23
  49. data/test/fixtures/en-3.xml +0 -8
  50. data/test/fixtures/fr-1.xml +0 -10
  51. data/test/fixtures/strings-1.txt +0 -17
  52. data/test/fixtures/strings-2.txt +0 -5
  53. data/test/fixtures/strings-3.txt +0 -5
  54. data/test/fixtures/test-json-line-breaks/consumed.txt +0 -5
  55. data/test/fixtures/test-json-line-breaks/generated.json +0 -3
  56. data/test/fixtures/test-json-line-breaks/line-breaks.json +0 -3
  57. data/test/fixtures/test-json-line-breaks/line-breaks.txt +0 -4
  58. data/test/fixtures/test-output-1.txt +0 -12
  59. data/test/fixtures/test-output-10.txt +0 -9
  60. data/test/fixtures/test-output-11.txt +0 -9
  61. data/test/fixtures/test-output-12.txt +0 -12
  62. data/test/fixtures/test-output-2.txt +0 -12
  63. data/test/fixtures/test-output-3.txt +0 -18
  64. data/test/fixtures/test-output-4.txt +0 -21
  65. data/test/fixtures/test-output-5.txt +0 -4
  66. data/test/fixtures/test-output-6.txt +0 -10
  67. data/test/fixtures/test-output-7.txt +0 -16
  68. data/test/fixtures/test-output-8.txt +0 -9
  69. data/test/fixtures/test-output-9.txt +0 -21
  70. data/test/twine_test.rb +0 -134
@@ -0,0 +1,44 @@
1
+ require 'command_test_case'
2
+
3
+ class TestGenerateAllStringFiles < CommandTestCase
4
+ class TestCreateFolders < CommandTestCase
5
+ def new_runner(create_folders)
6
+ options = {}
7
+ options[:output_path] = @output_dir
8
+ options[:format] = 'apple'
9
+ options[:create_folders] = create_folders
10
+
11
+ @twine_file = build_twine_file 'en', 'es' do
12
+ add_section 'Section' do
13
+ add_row key: 'value'
14
+ end
15
+ end
16
+
17
+ Twine::Runner.new(options, @twine_file)
18
+ end
19
+
20
+ def test_fails_if_output_folder_does_not_exist
21
+ assert_raises Twine::Error do
22
+ new_runner(false).generate_all_string_files
23
+ end
24
+ end
25
+
26
+ def test_creates_output_folder
27
+ FileUtils.remove_entry_secure @output_dir
28
+ new_runner(true).generate_all_string_files
29
+ assert File.exists? @output_dir
30
+ end
31
+
32
+ def test_does_not_create_language_folders_by_default
33
+ Dir.mkdir File.join @output_dir, 'en.lproj'
34
+ new_runner(false).generate_all_string_files
35
+ refute File.exists?(File.join(@output_dir, 'es.lproj')), "language folder should not be created"
36
+ end
37
+
38
+ def test_creates_language_folders
39
+ new_runner(true).generate_all_string_files
40
+ assert File.exists?(File.join(@output_dir, 'en.lproj')), "language folder 'en.lproj' should be created"
41
+ assert File.exists?(File.join(@output_dir, 'es.lproj')), "language folder 'es.lproj' should be created"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ require 'command_test_case'
2
+
3
+ class TestGenerateLocDrop < CommandTestCase
4
+ def setup
5
+ super
6
+
7
+ options = {}
8
+ options[:output_path] = @output_path
9
+ options[:format] = 'apple'
10
+
11
+ @twine_file = build_twine_file 'en', 'fr' do
12
+ add_section 'Section' do
13
+ add_row key: 'value'
14
+ end
15
+ end
16
+
17
+ @runner = Twine::Runner.new(options, @twine_file)
18
+ end
19
+
20
+ def test_generates_zip_file
21
+ @runner.generate_loc_drop
22
+
23
+ assert File.exists?(@output_path), "language folder should not be created"
24
+ end
25
+
26
+ def test_zip_file_structure
27
+ @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(:write_file).twice.with() { |path, lang| FileUtils.touch path }
41
+
42
+ @runner.generate_loc_drop
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ require 'command_test_case'
2
+
3
+ class TestGenerateStringFile < CommandTestCase
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
+ @strings = Twine::StringsFile.new
10
+ @strings.language_codes.concat KNOWN_LANGUAGES
11
+
12
+ Twine::Runner.new(options, @strings)
13
+ end
14
+
15
+ def prepare_mock_write_file_formatter(formatter_class)
16
+ formatter = prepare_mock_formatter(formatter_class)
17
+ formatter.expects(:write_file)
18
+ end
19
+
20
+ def test_deducts_android_format_from_output_path
21
+ prepare_mock_write_file_formatter Twine::Formatters::Android
22
+
23
+ new_runner('fr', 'fr.xml').generate_string_file
24
+ end
25
+
26
+ def test_deducts_apple_format_from_output_path
27
+ prepare_mock_write_file_formatter Twine::Formatters::Apple
28
+
29
+ new_runner('fr', 'fr.strings').generate_string_file
30
+ end
31
+
32
+ def test_deducts_jquery_format_from_output_path
33
+ prepare_mock_write_file_formatter Twine::Formatters::JQuery
34
+
35
+ new_runner('fr', 'fr.json').generate_string_file
36
+ end
37
+
38
+ def test_deducts_gettext_format_from_output_path
39
+ prepare_mock_write_file_formatter Twine::Formatters::Gettext
40
+
41
+ new_runner('fr', 'fr.po').generate_string_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(:write_file).with(anything, random_language)
48
+
49
+ new_runner(nil, "#{random_language}.xml").generate_string_file
50
+ end
51
+ end
@@ -0,0 +1,85 @@
1
+ require 'twine_test_case'
2
+
3
+ class TestOutputProcessor < TwineTestCase
4
+ def setup
5
+ super
6
+
7
+ @strings = build_twine_file 'en', 'fr' do
8
+ add_section 'Section' do
9
+ add_row key1: 'value1', tags: ['tag1']
10
+ add_row key2: 'value2', tags: ['tag1', 'tag2']
11
+ add_row key3: 'value3', tags: ['tag2']
12
+ add_row 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(@strings, {})
19
+ result = processor.process('en')
20
+
21
+ assert_equal %w(key1 key2 key3 key4), result.strings_map.keys.sort
22
+ end
23
+
24
+ def test_filter_by_tag
25
+ processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1'] })
26
+ result = processor.process('en')
27
+
28
+ assert_equal %w(key1 key2), result.strings_map.keys.sort
29
+ end
30
+
31
+ def test_filter_by_multiple_tags
32
+ processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1', 'tag2'] })
33
+ result = processor.process('en')
34
+
35
+ assert_equal %w(key1 key2 key3), result.strings_map.keys.sort
36
+ end
37
+
38
+ def test_filter_untagged
39
+ processor = Twine::Processors::OutputProcessor.new(@strings, { tags: ['tag1'], untagged: true })
40
+ result = processor.process('en')
41
+
42
+ assert_equal %w(key1 key2 key4), result.strings_map.keys.sort
43
+ end
44
+
45
+ def test_include_translated
46
+ processor = Twine::Processors::OutputProcessor.new(@strings, { include: 'translated' })
47
+ result = processor.process('fr')
48
+
49
+ assert_equal %w(key4), result.strings_map.keys.sort
50
+ end
51
+
52
+ def test_include_untranslated
53
+ processor = Twine::Processors::OutputProcessor.new(@strings, { include: 'untranslated' })
54
+ result = processor.process('fr')
55
+
56
+ assert_equal %w(key1 key2 key3), result.strings_map.keys.sort
57
+ end
58
+
59
+ class TranslationFallback < TwineTestCase
60
+ def setup
61
+ super
62
+
63
+ @strings = build_twine_file 'en', 'fr', 'de' do
64
+ add_section 'Section' do
65
+ add_row 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(@strings, {})
72
+ result = processor.process('de')
73
+
74
+ assert_equal 'value1-en', result.strings_map['key1'].translations['de']
75
+ end
76
+
77
+ def test_fallback_to_developer_language
78
+ processor = Twine::Processors::OutputProcessor.new(@strings, {developer_language: 'fr'})
79
+ result = processor.process('de')
80
+
81
+ assert_equal 'value1-fr', result.strings_map['key1'].translations['de']
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,86 @@
1
+ require 'twine_test_case'
2
+
3
+ class PlaceholderTestCase < TwineTestCase
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
+ end
21
+
22
+ class PlaceholderTest < TwineTestCase
23
+ class ToAndroid < PlaceholderTestCase
24
+ def to_android(value)
25
+ Twine::Placeholders.convert_placeholders_from_twine_to_android(value)
26
+ end
27
+
28
+ def test_replaces_string_placeholder
29
+ placeholder = placeholder('@')
30
+ expected = placeholder
31
+ expected[-1] = 's'
32
+ assert_equal "some #{expected} value", to_android("some #{placeholder} value")
33
+ end
34
+
35
+ def test_does_not_change_regular_at_signs
36
+ input = "some @ more @@ signs @"
37
+ assert_equal input, to_android(input)
38
+ end
39
+
40
+ def test_does_not_modify_single_percent_signs
41
+ assert_equal "some % value", to_android("some % value")
42
+ end
43
+
44
+ def test_escapes_single_percent_signs_if_placeholder_present
45
+ assert_starts_with "some %% v", to_android("some % value #{placeholder}")
46
+ end
47
+
48
+ def test_does_not_modify_double_percent_signs
49
+ assert_equal "some %% value", to_android("some %% value")
50
+ end
51
+
52
+ def test_does_not_modify_double_percent_signs_if_placeholder_present
53
+ assert_starts_with "some %% v", to_android("some %% value #{placeholder}")
54
+ end
55
+
56
+ def test_does_not_modify_single_placeholder
57
+ input = "some #{placeholder} text"
58
+ assert_equal input, to_android(input)
59
+ end
60
+
61
+ def test_numbers_multiple_placeholders
62
+ assert_equal "first %1$d second %2$f", to_android("first %d second %f")
63
+ end
64
+
65
+ def test_does_not_modify_numbered_placeholders
66
+ input = "second %2$f first %1$d"
67
+ assert_equal input, to_android(input)
68
+ end
69
+
70
+ def test_raises_an_error_when_mixing_numbered_and_non_numbered_placeholders
71
+ assert_raises Twine::Error do
72
+ to_android("some %d second %2$f")
73
+ end
74
+ end
75
+ end
76
+
77
+ class FromAndroid < PlaceholderTestCase
78
+ def from_android(value)
79
+ Twine::Placeholders.convert_placeholders_from_android_to_twine(value)
80
+ end
81
+
82
+ def test_replaces_string_placeholder
83
+ assert_equal "some %@ value", from_android("some %s value")
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,58 @@
1
+ require 'twine_test_case'
2
+
3
+ class TestStringsFile < TwineTestCase
4
+ class Reading < TwineTestCase
5
+ def setup
6
+ super
7
+
8
+ @strings = Twine::StringsFile.new
9
+ @strings.read fixture('twine_accent_values.txt')
10
+ end
11
+
12
+ def test_reading_keeps_leading_accent
13
+ assert_equal '`value', @strings.strings_map['value_with_leading_accent'].translations['en']
14
+ end
15
+
16
+ def test_reading_keeps_trailing_accent
17
+ assert_equal 'value`', @strings.strings_map['value_with_trailing_accent'].translations['en']
18
+ end
19
+
20
+ def test_reading_keeps_leading_space
21
+ assert_equal ' value', @strings.strings_map['value_with_leading_space'].translations['en']
22
+ end
23
+
24
+ def test_reading_keeps_trailing_space
25
+ assert_equal 'value ', @strings.strings_map['value_with_trailing_space'].translations['en']
26
+ end
27
+
28
+ def test_reading_keeps_wrapping_spaces
29
+ assert_equal ' value ', @strings.strings_map['value_wrapped_by_spaces'].translations['en']
30
+ end
31
+
32
+ def test_reading_keeps_wrapping_accents
33
+ assert_equal '`value`', @strings.strings_map['value_wrapped_by_accents'].translations['en']
34
+ end
35
+ end
36
+
37
+ class Writing < TwineTestCase
38
+
39
+ def test_accent_wrapping
40
+ @strings = build_twine_file 'en' do
41
+ add_section 'Section' do
42
+ add_row value_with_leading_accent: '`value'
43
+ add_row value_with_trailing_accent: 'value`'
44
+ add_row value_with_leading_space: ' value'
45
+ add_row value_with_trailing_space: 'value '
46
+ add_row value_wrapped_by_spaces: ' value '
47
+ add_row value_wrapped_by_accents: '`value`'
48
+ end
49
+ end
50
+
51
+ @strings.write @output_path
52
+
53
+ assert_equal content('twine_accent_values.txt'), output_content
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,47 @@
1
+ require 'twine_test_case'
2
+
3
+ class TestStringsRow < TwineTestCase
4
+ def setup
5
+ super
6
+
7
+ @reference = Twine::StringsRow.new 'reference-key'
8
+ @reference.comment = 'reference comment'
9
+ @reference.tags = ['ref1']
10
+ @reference.translations['en'] = 'ref-value'
11
+
12
+ @row = Twine::StringsRow.new 'key'
13
+ @row.reference_key = @reference.key
14
+ @row.reference = @reference
15
+ end
16
+
17
+ def test_reference_comment_used
18
+ assert_equal 'reference comment', @row.comment
19
+ end
20
+
21
+ def test_reference_comment_override
22
+ @row.comment = 'row comment'
23
+
24
+ assert_equal 'row comment', @row.comment
25
+ end
26
+
27
+ def test_reference_tags_used
28
+ assert @row.matches_tags?(['ref1'], false)
29
+ end
30
+
31
+ def test_reference_tags_override
32
+ @row.tags = ['tag1']
33
+
34
+ refute @row.matches_tags?(['ref1'], false)
35
+ assert @row.matches_tags?(['tag1'], false)
36
+ end
37
+
38
+ def test_reference_translation_used
39
+ assert_equal 'ref-value', @row.translated_string_for_lang('en')
40
+ end
41
+
42
+ def test_reference_translation_override
43
+ @row.translations['en'] = 'value'
44
+
45
+ assert_equal 'value', @row.translated_string_for_lang('en')
46
+ end
47
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ require 'command_test_case'
4
+
5
+ class TestValidateStringsFile < CommandTestCase
6
+ def setup
7
+ super
8
+ @options = { strings_file: 'input.txt' }
9
+
10
+ @twine_file = build_twine_file 'en' do
11
+ add_section 'Section 1' do
12
+ add_row key1: 'value1', tags: ['tag1']
13
+ add_row key2: 'value2', tags: ['tag1']
14
+ end
15
+
16
+ add_section 'Section 2' do
17
+ add_row key3: 'value3', tags: ['tag1', 'tag2']
18
+ add_row key4: 'value4', tags: ['tag2']
19
+ end
20
+ end
21
+ end
22
+
23
+ def random_row
24
+ @twine_file.strings_map[@twine_file.strings_map.keys.sample]
25
+ end
26
+
27
+ def test_recognizes_valid_file
28
+ Twine::Runner.new(@options, @twine_file).validate_strings_file
29
+ assert_equal "input.txt is valid.\n", Twine::stdout.string
30
+ end
31
+
32
+ def test_reports_duplicate_keys
33
+ @twine_file.sections[0].rows << random_row
34
+
35
+ assert_raises Twine::Error do
36
+ Twine::Runner.new(@options, @twine_file).validate_strings_file
37
+ end
38
+ end
39
+
40
+ def test_reports_missing_tags
41
+ random_row.tags.clear
42
+
43
+ assert_raises Twine::Error do
44
+ Twine::Runner.new(@options, @twine_file).validate_strings_file
45
+ end
46
+ end
47
+
48
+ def test_reports_invalid_characters_in_keys
49
+ random_row.key[0] = "!?;:,^`´'\"\\|/(){}[]~-+*=#$%".chars.to_a.sample
50
+
51
+ assert_raises Twine::Error do
52
+ Twine::Runner.new(@options, @twine_file).validate_strings_file
53
+ end
54
+ end
55
+ end