twine 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -69
  3. data/lib/twine.rb +11 -3
  4. data/lib/twine/cli.rb +375 -155
  5. data/lib/twine/formatters.rb +0 -5
  6. data/lib/twine/formatters/abstract.rb +43 -43
  7. data/lib/twine/formatters/android.rb +58 -59
  8. data/lib/twine/formatters/apple.rb +3 -3
  9. data/lib/twine/formatters/django.rb +15 -21
  10. data/lib/twine/formatters/flash.rb +17 -20
  11. data/lib/twine/formatters/gettext.rb +11 -15
  12. data/lib/twine/formatters/jquery.rb +8 -11
  13. data/lib/twine/formatters/tizen.rb +4 -4
  14. data/lib/twine/output_processor.rb +15 -15
  15. data/lib/twine/placeholders.rb +26 -6
  16. data/lib/twine/runner.rb +95 -95
  17. data/lib/twine/{stringsfile.rb → twine_file.rb} +53 -48
  18. data/lib/twine/version.rb +1 -1
  19. data/test/{command_test_case.rb → command_test.rb} +5 -5
  20. data/test/fixtures/{consume_loc_drop.zip → consume_localization_archive.zip} +0 -0
  21. data/test/fixtures/formatter_django.po +3 -1
  22. data/test/test_abstract_formatter.rb +40 -40
  23. data/test/test_cli.rb +313 -211
  24. data/test/test_consume_localization_archive.rb +27 -0
  25. data/test/{test_consume_string_file.rb → test_consume_localization_file.rb} +19 -19
  26. data/test/test_formatters.rb +108 -43
  27. data/test/{test_generate_all_string_files.rb → test_generate_all_localization_files.rb} +18 -18
  28. data/test/{test_generate_loc_drop.rb → test_generate_localization_archive.rb} +14 -14
  29. data/test/{test_generate_string_file.rb → test_generate_localization_file.rb} +18 -18
  30. data/test/test_output_processor.rb +26 -26
  31. data/test/test_placeholders.rb +44 -9
  32. data/test/test_twine_definition.rb +111 -0
  33. data/test/test_twine_file.rb +58 -0
  34. data/test/test_validate_twine_file.rb +61 -0
  35. data/test/twine_file_dsl.rb +12 -12
  36. data/test/{twine_test_case.rb → twine_test.rb} +1 -1
  37. metadata +23 -23
  38. data/test/test_consume_loc_drop.rb +0 -27
  39. data/test/test_strings_file.rb +0 -58
  40. data/test/test_strings_row.rb +0 -47
  41. data/test/test_validate_strings_file.rb +0 -61
@@ -18,8 +18,3 @@ module Twine
18
18
  end
19
19
  end
20
20
  end
21
-
22
- require File.join(File.dirname(__FILE__), 'formatters', 'abstract.rb')
23
- Dir[File.join(File.dirname(__FILE__), 'formatters', '*.rb')].each do |file|
24
- require file
25
- end
@@ -3,11 +3,11 @@ require 'fileutils'
3
3
  module Twine
4
4
  module Formatters
5
5
  class Abstract
6
- attr_accessor :strings
6
+ attr_accessor :twine_file
7
7
  attr_accessor :options
8
8
 
9
9
  def initialize
10
- @strings = StringsFile.new
10
+ @twine_file = TwineFile.new
11
11
  @options = {}
12
12
  end
13
13
 
@@ -20,7 +20,7 @@ module Twine
20
20
  end
21
21
 
22
22
  def can_handle_directory?(path)
23
- raise NotImplementedError.new("You must implement can_handle_directory? in your formatter class.")
23
+ Dir.entries(path).any? { |item| /^.+#{Regexp.escape(extension)}$/.match(item) }
24
24
  end
25
25
 
26
26
  def default_file_name
@@ -30,47 +30,47 @@ module Twine
30
30
  def set_translation_for_key(key, lang, value)
31
31
  value = value.gsub("\n", "\\n")
32
32
 
33
- if @strings.strings_map.include?(key)
34
- row = @strings.strings_map[key]
35
- reference = @strings.strings_map[row.reference_key] if row.reference_key
33
+ if @twine_file.definitions_by_key.include?(key)
34
+ definition = @twine_file.definitions_by_key[key]
35
+ reference = @twine_file.definitions_by_key[definition.reference_key] if definition.reference_key
36
36
 
37
37
  if !reference or value != reference.translations[lang]
38
- row.translations[lang] = value
38
+ definition.translations[lang] = value
39
39
  end
40
40
  elsif @options[:consume_all]
41
- Twine::stderr.puts "Adding new string '#{key}' to strings data file."
42
- current_section = @strings.sections.find { |s| s.name == 'Uncategorized' }
41
+ Twine::stderr.puts "Adding new definition '#{key}' to twine file."
42
+ current_section = @twine_file.sections.find { |s| s.name == 'Uncategorized' }
43
43
  unless current_section
44
- current_section = StringsSection.new('Uncategorized')
45
- @strings.sections.insert(0, current_section)
44
+ current_section = TwineSection.new('Uncategorized')
45
+ @twine_file.sections.insert(0, current_section)
46
46
  end
47
- current_row = StringsRow.new(key)
48
- current_section.rows << current_row
47
+ current_definition = TwineDefinition.new(key)
48
+ current_section.definitions << current_definition
49
49
 
50
50
  if @options[:tags] && @options[:tags].length > 0
51
- current_row.tags = @options[:tags]
51
+ current_definition.tags = @options[:tags]
52
52
  end
53
53
 
54
- @strings.strings_map[key] = current_row
55
- @strings.strings_map[key].translations[lang] = value
54
+ @twine_file.definitions_by_key[key] = current_definition
55
+ @twine_file.definitions_by_key[key].translations[lang] = value
56
56
  else
57
- Twine::stderr.puts "Warning: '#{key}' not found in strings data file."
57
+ Twine::stderr.puts "Warning: '#{key}' not found in twine file."
58
58
  end
59
- if !@strings.language_codes.include?(lang)
60
- @strings.add_language_code(lang)
59
+ if !@twine_file.language_codes.include?(lang)
60
+ @twine_file.add_language_code(lang)
61
61
  end
62
62
  end
63
63
 
64
64
  def set_comment_for_key(key, comment)
65
65
  return unless @options[:consume_comments]
66
66
 
67
- if @strings.strings_map.include?(key)
68
- row = @strings.strings_map[key]
67
+ if @twine_file.definitions_by_key.include?(key)
68
+ definition = @twine_file.definitions_by_key[key]
69
69
 
70
- reference = @strings.strings_map[row.reference_key] if row.reference_key
70
+ reference = @twine_file.definitions_by_key[definition.reference_key] if definition.reference_key
71
71
 
72
72
  if !reference or comment != reference.raw_comment
73
- row.comment = comment
73
+ definition.comment = comment
74
74
  end
75
75
  end
76
76
  end
@@ -88,35 +88,35 @@ module Twine
88
88
  end
89
89
 
90
90
  def format_file(lang)
91
- output_processor = Processors::OutputProcessor.new(@strings, @options)
92
- processed_strings = output_processor.process(lang)
91
+ output_processor = Processors::OutputProcessor.new(@twine_file, @options)
92
+ processed_twine_file = output_processor.process(lang)
93
93
 
94
- return nil if processed_strings.strings_map.empty?
94
+ return nil if processed_twine_file.definitions_by_key.empty?
95
95
 
96
96
  header = format_header(lang)
97
97
  result = ""
98
98
  result += header + "\n" if header
99
- result += format_sections(processed_strings, lang)
99
+ result += format_sections(processed_twine_file, lang)
100
100
  end
101
101
 
102
102
  def format_header(lang)
103
103
  end
104
104
 
105
- def format_sections(strings, lang)
106
- sections = strings.sections.map { |section| format_section(section, lang) }
105
+ def format_sections(twine_file, lang)
106
+ sections = twine_file.sections.map { |section| format_section(section, lang) }
107
107
  sections.compact.join("\n")
108
108
  end
109
109
 
110
110
  def format_section_header(section)
111
111
  end
112
112
 
113
- def should_include_row(row, lang)
114
- row.translated_string_for_lang(lang)
113
+ def should_include_definition(definition, lang)
114
+ return !definition.translation_for_lang(lang).nil?
115
115
  end
116
116
 
117
117
  def format_section(section, lang)
118
- rows = section.rows.select { |row| should_include_row(row, lang) }
119
- return if rows.empty?
118
+ definitions = section.definitions.select { |definition| should_include_definition(definition, lang) }
119
+ return if definitions.empty?
120
120
 
121
121
  result = ""
122
122
 
@@ -125,22 +125,22 @@ module Twine
125
125
  result += "\n#{section_header}" if section_header
126
126
  end
127
127
 
128
- rows.map! { |row| format_row(row, lang) }
129
- rows.compact! # remove nil entries
130
- rows.map! { |row| "\n#{row}" } # prepend newline
131
- result += rows.join
128
+ definitions.map! { |definition| format_definition(definition, lang) }
129
+ definitions.compact! # remove nil definitions
130
+ definitions.map! { |definition| "\n#{definition}" } # prepend newline
131
+ result += definitions.join
132
132
  end
133
133
 
134
- def format_row(row, lang)
135
- [format_comment(row, lang), format_key_value(row, lang)].compact.join
134
+ def format_definition(definition, lang)
135
+ [format_comment(definition, lang), format_key_value(definition, lang)].compact.join
136
136
  end
137
137
 
138
- def format_comment(row, lang)
138
+ def format_comment(definition, lang)
139
139
  end
140
140
 
141
- def format_key_value(row, lang)
142
- value = row.translated_string_for_lang(lang)
143
- key_value_pattern % { key: format_key(row.key.dup), value: format_value(value.dup) }
141
+ def format_key_value(definition, lang)
142
+ value = definition.translation_for_lang(lang)
143
+ key_value_pattern % { key: format_key(definition.key.dup), value: format_value(value.dup) }
144
144
  end
145
145
 
146
146
  def key_value_pattern
@@ -7,16 +7,6 @@ module Twine
7
7
  class Android < Abstract
8
8
  include Twine::Placeholders
9
9
 
10
- LANG_MAPPINGS = Hash[
11
- 'zh-rCN' => 'zh-Hans',
12
- 'zh-rHK' => 'zh-Hant',
13
- 'en-rGB' => 'en-UK',
14
- 'zh' => 'zh-Hans',
15
- 'in' => 'id',
16
- 'nb' => 'no'
17
- # TODO: spanish
18
- ]
19
-
20
10
  def format_name
21
11
  'android'
22
12
  end
@@ -30,24 +20,20 @@ module Twine
30
20
  end
31
21
 
32
22
  def default_file_name
33
- return 'strings.xml'
23
+ 'strings.xml'
34
24
  end
35
25
 
36
26
  def determine_language_given_path(path)
37
27
  path_arr = path.split(File::SEPARATOR)
38
28
  path_arr.each do |segment|
39
29
  if segment == 'values'
40
- return @strings.language_codes[0]
30
+ return @twine_file.language_codes[0]
41
31
  else
42
32
  # The language is defined by a two-letter ISO 639-1 language code, optionally followed by a two letter ISO 3166-1-alpha-2 region code (preceded by lowercase "r").
43
33
  # see http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources
44
34
  match = /^values-([a-z]{2}(-r[a-z]{2})?)$/i.match(segment)
45
- if match
46
- lang = match[1]
47
- lang = LANG_MAPPINGS.fetch(lang, lang)
48
- lang.sub!('-r', '-')
49
- return lang
50
- end
35
+
36
+ return match[1].sub('-r', '-') if match
51
37
  end
52
38
  end
53
39
 
@@ -55,7 +41,7 @@ module Twine
55
41
  end
56
42
 
57
43
  def output_path_for_language(lang)
58
- "values-" + (LANG_MAPPINGS.key(lang) || lang)
44
+ "values-#{lang}"
59
45
  end
60
46
 
61
47
  def set_translation_for_key(key, lang, value)
@@ -69,35 +55,23 @@ module Twine
69
55
  end
70
56
 
71
57
  def read(io, lang)
72
- resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
73
- key_regex = /<string name="(\w+)">/
74
- comment_regex = /<!-- (.*) -->/
75
- value_regex = /<string name="\w+">(.*)<\/string>/
76
- key = nil
77
- value = nil
58
+ document = REXML::Document.new io, :compress_whitespace => %w{ string }
59
+
78
60
  comment = nil
61
+ document.root.children.each do |child|
62
+ if child.is_a? REXML::Comment
63
+ content = child.string.strip
64
+ comment = content if content.length > 0 and not content.start_with?("SECTION:")
65
+ elsif child.is_a? REXML::Element
66
+ next unless child.name == 'string'
79
67
 
80
- content_match = resources_regex.match(io.read)
81
- if content_match
82
- for line in content_match[1].split(/\r?\n/)
83
- key_match = key_regex.match(line)
84
- if key_match
85
- key = key_match[1]
86
- value_match = value_regex.match(line)
87
- value = value_match ? value_match[1] : ""
88
-
89
- set_translation_for_key(key, lang, value)
90
- if comment and comment.length > 0 and !comment.start_with?("SECTION:")
91
- set_comment_for_key(key, comment)
92
- end
93
- comment = nil
94
- end
95
-
96
- comment_match = comment_regex.match(line)
97
- if comment_match
98
- comment = comment_match[1]
99
- end
100
- end
68
+ key = child.attributes['name']
69
+
70
+ set_translation_for_key(key, lang, child.text)
71
+ set_comment_for_key(key, comment) if comment
72
+
73
+ comment = nil
74
+ end
101
75
  end
102
76
  end
103
77
 
@@ -105,7 +79,7 @@ module Twine
105
79
  "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
106
80
  end
107
81
 
108
- def format_sections(strings, lang)
82
+ def format_sections(twine_file, lang)
109
83
  result = '<resources>'
110
84
 
111
85
  result += super + "\n"
@@ -117,27 +91,52 @@ module Twine
117
91
  "\t<!-- SECTION: #{section.name} -->"
118
92
  end
119
93
 
120
- def format_comment(row, lang)
121
- "\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
94
+ def format_comment(definition, lang)
95
+ "\t<!-- #{definition.comment.gsub('--', '—')} -->\n" if definition.comment
122
96
  end
123
97
 
124
98
  def key_value_pattern
125
99
  "\t<string name=\"%{key}\">%{value}</string>"
126
100
  end
127
101
 
128
- def format_value(value)
129
- # Android enforces the following rules on the values
130
- # 1) apostrophes and quotes must be escaped with a backslash
102
+ def escape_value(value)
103
+ # escape double and single quotes, & signs and tags
131
104
  value = escape_quotes(value)
132
105
  value.gsub!("'", "\\\\'")
133
- # 2) HTML escape the string
134
- value = CGI.escapeHTML(value)
135
- # 3) convert placeholders (e.g. %@ -> %s)
136
- value = convert_placeholders_from_twine_to_android(value)
137
- # 4) escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
106
+ value.gsub!(/&/, '&amp;')
107
+ value.gsub!('<', '&lt;')
108
+
109
+ # escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
138
110
  resource_identifier_regex = /@(?!([a-z\.]+:)?[a-z+]+\/[a-zA-Z_]+)/ # @[<package_name>:]<resource_type>/<resource_name>
139
- value.gsub!(resource_identifier_regex, '\@')
140
- # 5) replace beginning and end spaces with \0020. Otherwise Android strips them.
111
+ value.gsub(resource_identifier_regex, '\@')
112
+ end
113
+
114
+ # see http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
115
+ # however unescaped HTML markup like in "Welcome to <b>Android</b>!" is stripped when retrieved with getString() (http://stackoverflow.com/questions/9891996/)
116
+ def format_value(value)
117
+ value = value.dup
118
+
119
+ # convert placeholders (e.g. %@ -> %s)
120
+ value = convert_placeholders_from_twine_to_android(value)
121
+
122
+ # capture xliff tags and replace them with a placeholder
123
+ xliff_tags = []
124
+ value.gsub! /<xliff:g.+?<\/xliff:g>/ do
125
+ xliff_tags << $&
126
+ 'TWINE_XLIFF_TAG_PLACEHOLDER'
127
+ end
128
+
129
+ # escape everything outside xliff tags
130
+ value = escape_value(value)
131
+
132
+ # put xliff tags back into place
133
+ xliff_tags.each do |xliff_tag|
134
+ # escape content of xliff tags
135
+ xliff_tag.gsub! /(<xliff:g.*?>)(.*)(<\/xliff:g>)/ do "#{$1}#{escape_value($2)}#{$3}" end
136
+ value.sub! 'TWINE_XLIFF_TAG_PLACEHOLDER', xliff_tag
137
+ end
138
+
139
+ # replace beginning and end spaces with \u0020. Otherwise Android strips them.
141
140
  value.gsub(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
142
141
  end
143
142
 
@@ -14,7 +14,7 @@ module Twine
14
14
  end
15
15
 
16
16
  def default_file_name
17
- return 'Localizable.strings'
17
+ 'Localizable.strings'
18
18
  end
19
19
 
20
20
  def determine_language_given_path(path)
@@ -73,8 +73,8 @@ module Twine
73
73
  "\"%{key}\" = \"%{value}\";\n"
74
74
  end
75
75
 
76
- def format_comment(row, lang)
77
- "/* #{row.comment.gsub('*/', '* /')} */\n" if row.comment
76
+ def format_comment(definition, lang)
77
+ "/* #{definition.comment.gsub('*/', '* /')} */\n" if definition.comment
78
78
  end
79
79
 
80
80
  def format_key(key)
@@ -9,23 +9,17 @@ module Twine
9
9
  '.po'
10
10
  end
11
11
 
12
- def can_handle_directory?(path)
13
- Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
14
- end
15
-
16
12
  def default_file_name
17
- return 'strings.po'
13
+ 'strings.po'
18
14
  end
19
15
 
20
16
  def determine_language_given_path(path)
21
- path_arr = path.split(File::SEPARATOR)
22
- path_arr.each do |segment|
23
- match = /(..)\.po$/.match(segment)
24
- if match
25
- return match[1]
26
- end
27
- end
28
-
17
+ path_arr = path.split(File::SEPARATOR)
18
+ path_arr.each do |segment|
19
+ match = /(..)\.po$/.match(segment)
20
+ return match[1] if match
21
+ end
22
+
29
23
  return
30
24
  end
31
25
 
@@ -63,26 +57,26 @@ module Twine
63
57
  end
64
58
 
65
59
  def format_file(lang)
66
- @default_lang = @strings.language_codes[0]
60
+ @default_lang = @twine_file.language_codes[0]
67
61
  result = super
68
62
  @default_lang = nil
69
63
  result
70
64
  end
71
65
 
72
66
  def format_header(lang)
73
- "##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n"
67
+ "##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
74
68
  end
75
69
 
76
70
  def format_section_header(section)
77
71
  "#--------- #{section.name} ---------#\n"
78
72
  end
79
73
 
80
- def format_row(row, lang)
81
- [format_comment(row, lang), format_base_translation(row), format_key_value(row, lang)].compact.join
74
+ def format_definition(definition, lang)
75
+ [format_comment(definition, lang), format_base_translation(definition), format_key_value(definition, lang)].compact.join
82
76
  end
83
77
 
84
- def format_base_translation(row)
85
- base_translation = row.translations[@default_lang]
78
+ def format_base_translation(definition)
79
+ base_translation = definition.translations[@default_lang]
86
80
  "# base translation: \"#{base_translation}\"\n" if base_translation
87
81
  end
88
82
 
@@ -91,8 +85,8 @@ module Twine
91
85
  "msgstr \"%{value}\"\n"
92
86
  end
93
87
 
94
- def format_comment(row, lang)
95
- "#. #{escape_quotes(row.comment)}\n" if row.comment
88
+ def format_comment(definition, lang)
89
+ "#. #{escape_quotes(definition.comment)}\n" if definition.comment
96
90
  end
97
91
 
98
92
  def format_key(key)
@@ -1,6 +1,8 @@
1
1
  module Twine
2
2
  module Formatters
3
3
  class Flash < Abstract
4
+ include Twine::Placeholders
5
+
4
6
  def format_name
5
7
  'flash'
6
8
  end
@@ -9,16 +11,18 @@ module Twine
9
11
  '.properties'
10
12
  end
11
13
 
12
- def can_handle_directory?(path)
13
- return false
14
- end
15
-
16
14
  def default_file_name
17
- return 'resources.properties'
15
+ 'resources.properties'
18
16
  end
19
17
 
20
18
  def determine_language_given_path(path)
21
- return
19
+ # match two-letter language code, optionally followed by a two letter region code
20
+ path.split(File::SEPARATOR).reverse.find { |segment| segment =~ /^([a-z]{2}(-[a-z]{2})?)$/i }
21
+ end
22
+
23
+ def set_translation_for_key(key, lang, value)
24
+ value = convert_placeholders_from_flash_to_twine(value)
25
+ super(key, lang, value)
22
26
  end
23
27
 
24
28
  def read(io, lang)
@@ -28,23 +32,17 @@ module Twine
28
32
  if match
29
33
  key = match[1]
30
34
  value = match[2].strip
31
- value.gsub!(/\{[0-9]\}/, '%@')
35
+
32
36
  set_translation_for_key(key, lang, value)
33
- if last_comment
34
- set_comment_for_key(key, last_comment)
35
- end
37
+ set_comment_for_key(key, last_comment) if last_comment
36
38
  end
37
39
 
38
40
  match = /# *(.*)/.match(line)
39
- if match
40
- last_comment = match[1]
41
- else
42
- last_comment = nil
43
- end
41
+ last_comment = match ? match[1] : nil
44
42
  end
45
43
  end
46
44
 
47
- def format_sections(strings, lang)
45
+ def format_sections(twine_file, lang)
48
46
  super + "\n"
49
47
  end
50
48
 
@@ -56,8 +54,8 @@ module Twine
56
54
  "## #{section.name} ##\n"
57
55
  end
58
56
 
59
- def format_comment(row, lang)
60
- "# #{row.comment}\n" if row.comment
57
+ def format_comment(definition, lang)
58
+ "# #{definition.comment}\n" if definition.comment
61
59
  end
62
60
 
63
61
  def key_value_pattern
@@ -65,8 +63,7 @@ module Twine
65
63
  end
66
64
 
67
65
  def format_value(value)
68
- placeHolderNumber = -1
69
- value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
66
+ convert_placeholders_from_twine_to_flash(value)
70
67
  end
71
68
  end
72
69
  end