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
@@ -5,6 +5,8 @@ require 'rexml/document'
5
5
  module Twine
6
6
  module Formatters
7
7
  class Android < Abstract
8
+ include Twine::Placeholders
9
+
8
10
  FORMAT_NAME = 'android'
9
11
  EXTENSION = '.xml'
10
12
  DEFAULT_FILE_NAME = 'strings.xml'
@@ -17,9 +19,6 @@ module Twine
17
19
  'nb' => 'no'
18
20
  # TODO: spanish
19
21
  ]
20
- DEFAULT_LANG_CODES = Hash[
21
- 'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
22
- ]
23
22
 
24
23
  def self.can_handle_directory?(path)
25
24
  Dir.entries(path).any? { |item| /^values.*$/.match(item) }
@@ -48,6 +47,16 @@ module Twine
48
47
  return
49
48
  end
50
49
 
50
+ def set_translation_for_key(key, lang, value)
51
+ value = CGI.unescapeHTML(value)
52
+ value.gsub!('\\\'', '\'')
53
+ value.gsub!('\\"', '"')
54
+ value = convert_placeholders_from_android_to_twine(value)
55
+ value.gsub!('\@', '@')
56
+ value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
57
+ super(key, lang, value)
58
+ end
59
+
51
60
  def read_file(path, lang)
52
61
  resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
53
62
  key_regex = /<string name="(\w+)">/
@@ -65,16 +74,8 @@ module Twine
65
74
  if key_match
66
75
  key = key_match[1]
67
76
  value_match = value_regex.match(line)
68
- if value_match
69
- value = value_match[1]
70
- value = CGI.unescapeHTML(value)
71
- value.gsub!('\\\'', '\'')
72
- value.gsub!('\\"', '"')
73
- value = iosify_substitutions(value)
74
- value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
75
- else
76
- value = ""
77
- end
77
+ value = value_match ? value_match[1] : ""
78
+
78
79
  set_translation_for_key(key, lang, value)
79
80
  if comment and comment.length > 0 and !comment.start_with?("SECTION:")
80
81
  set_comment_for_key(key, comment)
@@ -91,65 +92,46 @@ module Twine
91
92
  end
92
93
  end
93
94
 
94
- def write_file(path, lang)
95
- default_lang = nil
96
- if DEFAULT_LANG_CODES.has_key?(lang)
97
- default_lang = DEFAULT_LANG_CODES[lang]
98
- end
99
- File.open(path, 'w:UTF-8') do |f|
100
- f.puts "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
101
- f.write '<resources>'
102
- @strings.sections.each do |section|
103
- printed_section = false
104
- section.rows.each do |row|
105
- if row.matches_tags?(@options[:tags], @options[:untagged])
106
- if !printed_section
107
- f.puts ''
108
- if section.name && section.name.length > 0
109
- section_name = section.name.gsub('--', '—')
110
- f.puts "\t<!-- SECTION: #{section_name} -->"
111
- end
112
- printed_section = true
113
- end
95
+ def format_header(lang)
96
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
97
+ end
114
98
 
115
- key = row.key
99
+ def format_sections(strings, lang)
100
+ result = '<resources>'
101
+
102
+ result += super + "\n"
116
103
 
117
- value = row.translated_string_for_lang(lang, default_lang)
118
- if !value && @options[:include_untranslated]
119
- value = row.translated_string_for_lang(@strings.language_codes[0])
120
- end
104
+ result += '</resources>'
105
+ end
121
106
 
122
- if value # if values is nil, there was no appropriate translation, so let Android handle the defaulting
123
- value = String.new(value) # use a copy to prevent modifying the original
124
-
125
- # Android enforces the following rules on the values
126
- # 1) apostrophes and quotes must be escaped with a backslash
127
- value.gsub!('\'', '\\\\\'')
128
- value.gsub!('"', '\\\\"')
129
- # 2) HTML escape the string
130
- value = CGI.escapeHTML(value)
131
- # 3) fix substitutions (e.g. %s/%@)
132
- value = androidify_substitutions(value)
133
- # 4) replace beginning and end spaces with \0020. Otherwise Android strips them.
134
- value.gsub!(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
135
-
136
- comment = row.comment
137
- if comment
138
- comment = comment.gsub('--', '—')
139
- end
140
-
141
- if comment && comment.length > 0
142
- f.puts "\t<!-- #{comment} -->\n"
143
- end
144
- f.puts "\t<string name=\"#{key}\">#{value}</string>"
145
- end
146
- end
147
- end
148
- end
107
+ def format_section_header(section)
108
+ "\t<!-- SECTION: #{section.name} -->"
109
+ end
149
110
 
150
- f.puts '</resources>'
151
- end
111
+ def format_comment(row, lang)
112
+ "\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
152
113
  end
114
+
115
+ def key_value_pattern
116
+ "\t<string name=\"%{key}\">%{value}</string>"
117
+ end
118
+
119
+ def format_value(value)
120
+ # Android enforces the following rules on the values
121
+ # 1) apostrophes and quotes must be escaped with a backslash
122
+ value = escape_quotes(value)
123
+ value.gsub!("'", "\\\\'")
124
+ # 2) HTML escape the string
125
+ value = CGI.escapeHTML(value)
126
+ # 3) convert placeholders (e.g. %@ -> %s)
127
+ value = convert_placeholders_from_twine_to_android(value)
128
+ # 4) escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
129
+ resource_identifier_regex = /@(?!([a-z\.]+:)?[a-z+]+\/[a-zA-Z_]+)/ # @[<package_name>:]<resource_type>/<resource_name>
130
+ value.gsub!(resource_identifier_regex, '\@')
131
+ # 5) replace beginning and end spaces with \0020. Otherwise Android strips them.
132
+ value.gsub(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
133
+ end
134
+
153
135
  end
154
136
  end
155
137
  end
@@ -27,6 +27,10 @@ module Twine
27
27
  return
28
28
  end
29
29
 
30
+ def output_path_for_language(lang)
31
+ "#{lang}.lproj"
32
+ end
33
+
30
34
  def read_file(path, lang)
31
35
  encoding = Twine::Encoding.encoding_for_path(path)
32
36
  sep = nil
@@ -64,63 +68,45 @@ module Twine
64
68
  key.gsub!('\\"', '"')
65
69
  value = match[2]
66
70
  value.gsub!('\\"', '"')
67
- value = iosify_substitutions(value)
68
71
  set_translation_for_key(key, lang, value)
69
72
  if last_comment
70
73
  set_comment_for_key(key, last_comment)
71
74
  end
72
75
  end
73
- if @options[:consume_comments]
74
- match = /\/\* (.*) \*\//.match(line)
75
- if match
76
- last_comment = match[1]
77
- else
78
- last_comment = nil
79
- end
76
+
77
+ match = /\/\* (.*) \*\//.match(line)
78
+ if match
79
+ last_comment = match[1]
80
+ else
81
+ last_comment = nil
80
82
  end
83
+
81
84
  end
82
85
  end
83
86
  end
84
87
 
85
- def write_file(path, lang)
86
- default_lang = @strings.language_codes[0]
87
- encoding = @options[:output_encoding] || 'UTF-8'
88
- File.open(path, "w:#{encoding}") do |f|
89
- f.puts "/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
90
- @strings.sections.each do |section|
91
- printed_section = false
92
- section.rows.each do |row|
93
- if row.matches_tags?(@options[:tags], @options[:untagged])
94
- f.puts ''
95
- if !printed_section
96
- if section.name && section.name.length > 0
97
- f.print "/********** #{section.name} **********/\n\n"
98
- end
99
- printed_section = true
100
- end
101
-
102
- key = row.key
103
- key = key.gsub('"', '\\\\"')
104
-
105
- value = row.translated_string_for_lang(lang, default_lang)
106
- if value
107
- value = value.gsub('"', '\\\\"')
108
-
109
- comment = row.comment
110
- if comment
111
- comment = comment.gsub('*/', '* /')
112
- end
113
-
114
- if comment && comment.length > 0
115
- f.print "/* #{comment} */\n"
116
- end
117
-
118
- f.print "\"#{key}\" = \"#{value}\";\n"
119
- end
120
- end
121
- end
122
- end
123
- end
88
+ def format_header(lang)
89
+ "/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
90
+ end
91
+
92
+ def format_section_header(section)
93
+ "/********** #{section.name} **********/\n"
94
+ end
95
+
96
+ def key_value_pattern
97
+ "\"%{key}\" = \"%{value}\";\n"
98
+ end
99
+
100
+ def format_comment(row, lang)
101
+ "/* #{row.comment.gsub('*/', '* /')} */\n" if row.comment
102
+ end
103
+
104
+ def format_key(key)
105
+ escape_quotes(key)
106
+ end
107
+
108
+ def format_value(value)
109
+ escape_quotes(value)
124
110
  end
125
111
  end
126
112
  end
@@ -26,7 +26,7 @@ module Twine
26
26
  end
27
27
 
28
28
  def read_file(path, lang)
29
- comment_regex = /#.? *"(.*)"$/
29
+ comment_regex = /#\. *"?(.*)"?$/
30
30
  key_regex = /msgid *"(.*)"$/
31
31
  value_regex = /msgstr *"(.*)"$/m
32
32
 
@@ -60,14 +60,12 @@ module Twine
60
60
  line = Iconv.iconv('UTF-8', encoding, line).join
61
61
  end
62
62
  end
63
- if @options[:consume_comments]
64
- comment_match = comment_regex.match(line)
65
- if comment_match
66
- comment = comment_match[1]
67
- end
68
- else
69
- comment = nil
63
+
64
+ comment_match = comment_regex.match(line)
65
+ if comment_match
66
+ comment = comment_match[1]
70
67
  end
68
+
71
69
  key_match = key_regex.match(line)
72
70
  if key_match
73
71
  key = key_match[1].gsub('\\"', '"')
@@ -83,6 +81,8 @@ module Twine
83
81
  if comment and comment.length > 0 and !comment.start_with?("--------- ")
84
82
  set_comment_for_key(key, comment)
85
83
  end
84
+ key = nil
85
+ value = nil
86
86
  comment = nil
87
87
  end
88
88
 
@@ -90,53 +90,43 @@ module Twine
90
90
  end
91
91
  end
92
92
 
93
- def write_file(path, lang)
94
- default_lang = @strings.language_codes[0]
95
- encoding = @options[:output_encoding] || 'UTF-8'
96
- File.open(path, "w:#{encoding}") do |f|
97
- f.puts "##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n "
98
- @strings.sections.each do |section|
99
- printed_section = false
100
- section.rows.each do |row|
101
- if row.matches_tags?(@options[:tags], @options[:untagged])
102
- f.puts ''
103
- if !printed_section
104
- if section.name && section.name.length > 0
105
- f.print "#--------- #{section.name} ---------#\n\n"
106
- end
107
- printed_section = true
108
- end
109
-
110
- basetrans = row.translated_string_for_lang(default_lang)
93
+ def format_file(strings, lang)
94
+ @default_lang = strings.language_codes[0]
95
+ super
96
+ end
111
97
 
112
- key = row.key
113
- key = key.gsub('"', '\\\\"')
98
+ def format_header(lang)
99
+ "##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n"
100
+ end
114
101
 
115
- value = row.translated_string_for_lang(lang, default_lang)
116
- if value
117
- value = value.gsub('"', '\\\\"')
102
+ def format_section_header(section)
103
+ "#--------- #{section.name} ---------#\n"
104
+ end
118
105
 
119
- comment = row.comment
106
+ def row_pattern
107
+ "%{comment}%{base_translation}%{key_value}"
108
+ end
120
109
 
121
- if comment
122
- comment = comment.gsub('"', '\\\\"')
123
- end
110
+ def format_base_translation(row, lang)
111
+ base_translation = row.translations[@default_lang]
112
+ "# base translation: \"#{base_translation}\"\n" if base_translation
113
+ end
124
114
 
125
- if comment && comment.length > 0
126
- f.print "#. #{comment} \n"
127
- end
115
+ def key_value_pattern
116
+ "msgid \"%{key}\"\n" +
117
+ "msgstr \"%{value}\"\n"
118
+ end
128
119
 
129
- if basetrans && basetrans.length > 0
130
- f.print "# base translation: \"#{basetrans}\"\n"
131
- end
120
+ def format_comment(row, lang)
121
+ "#. #{escape_quotes(row.comment)}\n" if row.comment
122
+ end
132
123
 
133
- f.print "msgid \"#{key}\"\n"
134
- f.print "msgstr \"#{value}\"\n"
135
- end
136
- end
137
- end
138
- end
139
- end
124
+ def format_key(key)
125
+ escape_quotes(key)
126
+ end
127
+
128
+ def format_value(value)
129
+ escape_quotes(value)
140
130
  end
141
131
  end
142
132
  end
@@ -51,59 +51,44 @@ module Twine
51
51
  match = /((?:[^"\\]|\\.)+)\s*=\s*((?:[^"\\]|\\.)*)/.match(line)
52
52
  if match
53
53
  key = match[1]
54
- value = match[2]
54
+ value = match[2].strip
55
55
  value.gsub!(/\{[0-9]\}/, '%@')
56
56
  set_translation_for_key(key, lang, value)
57
57
  if last_comment
58
58
  set_comment_for_key(key, last_comment)
59
59
  end
60
60
  end
61
- if @options[:consume_comments]
62
- match = /#(.*)/.match(line)
63
- if match
64
- last_comment = match[1]
65
- else
66
- last_comment = nil
67
- end
61
+
62
+ match = /# *(.*)/.match(line)
63
+ if match
64
+ last_comment = match[1]
65
+ else
66
+ last_comment = nil
68
67
  end
68
+
69
69
  end
70
70
  end
71
71
  end
72
72
 
73
- def write_file(path, lang)
74
- default_lang = @strings.language_codes[0]
75
- encoding = @options[:output_encoding] || 'UTF-8'
76
- File.open(path, "w:#{encoding}") do |f|
77
- f.puts "## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}\n"
78
- @strings.sections.each do |section|
79
- printed_section = false
80
- section.rows.each do |row|
81
- if row.matches_tags?(@options[:tags], @options[:untagged])
82
- f.puts ''
83
- if !printed_section
84
- if section.name && section.name.length > 0
85
- f.print "## #{section.name} ##\n\n"
86
- end
87
- printed_section = true
88
- end
73
+ def format_header(lang)
74
+ "## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}"
75
+ end
89
76
 
90
- key = row.key
91
- value = row.translated_string_for_lang(lang, default_lang)
92
- if value
93
- placeHolderNumber = -1
94
- value = value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
95
-
96
- comment = row.comment
97
- if comment && comment.length > 0
98
- f.print "# #{comment}\n"
99
- end
77
+ def format_section_header(section)
78
+ "## #{section.name} ##\n"
79
+ end
100
80
 
101
- f.print "#{key}=#{value}"
102
- end
103
- end
104
- end
105
- end
106
- end
81
+ def format_comment(row, lang)
82
+ "# #{row.comment}\n" if row.comment
83
+ end
84
+
85
+ def key_value_pattern
86
+ "%{key}=%{value}"
87
+ end
88
+
89
+ def format_value(value)
90
+ placeHolderNumber = -1
91
+ value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
107
92
  end
108
93
  end
109
94
  end