twine 0.9.1 → 0.10.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 (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