twine 0.7.0 → 0.8.0

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