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
@@ -60,50 +60,43 @@ module Twine
60
60
  end
61
61
  end
62
62
 
63
- def write_file(path, lang)
64
- default_lang = @strings.language_codes[0]
65
- encoding = @options[:output_encoding] || 'UTF-8'
66
- File.open(path, "w:#{encoding}") do |f|
67
- f.puts "msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\\n\"\n\"X-Generator: Twine #{Twine::VERSION}\\n\"\n\n"
68
- @strings.sections.each do |section|
69
- printed_section = false
70
- section.rows.each do |row|
71
- if row.matches_tags?(@options[:tags], @options[:untagged])
72
- if !printed_section
73
- f.puts ''
74
- if section.name && section.name.length > 0
75
- section_name = section.name.gsub('--', '—')
76
- f.puts "# SECTION: #{section_name}"
77
- end
78
- printed_section = true
79
- end
80
-
81
- basetrans = row.translated_string_for_lang(default_lang)
82
-
83
- if basetrans
84
- key = row.key
85
- key = key.gsub('"', '\\\\"')
86
-
87
- comment = row.comment
88
- if comment
89
- comment = comment.gsub('"', '\\\\"')
90
- end
91
-
92
- if comment && comment.length > 0
93
- f.print "#. \"#{comment}\"\n"
94
- end
95
-
96
- f.print "msgctxt \"#{key}\"\nmsgid \"#{basetrans}\"\n"
97
- value = row.translated_string_for_lang(lang)
98
- if value
99
- value = value.gsub('"', '\\\\"')
100
- end
101
- f.print "msgstr \"#{value}\"\n\n"
102
- end
103
- end
104
- end
105
- end
106
- end
63
+ def format_file(strings, lang)
64
+ @default_lang = strings.language_codes[0]
65
+ super
66
+ end
67
+
68
+ def format_header(lang)
69
+ "msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\\n\"\n\"X-Generator: Twine #{Twine::VERSION}\\n\"\n"
70
+ end
71
+
72
+ def format_section_header(section)
73
+ "# SECTION: #{section.name}"
74
+ end
75
+
76
+ def row_pattern
77
+ "%{comment}%{key}%{base_translation}%{value}"
78
+ end
79
+
80
+ def format_row(row, lang)
81
+ return nil unless row.translated_string_for_lang(@default_lang)
82
+
83
+ super
84
+ end
85
+
86
+ def format_comment(row, lang)
87
+ "#. \"#{escape_quotes(row.comment)}\"\n" if row.comment
88
+ end
89
+
90
+ def format_key(row, lang)
91
+ "msgctxt \"#{row.key.dup}\"\n"
92
+ end
93
+
94
+ def format_base_translation(row, lang)
95
+ "msgid \"#{row.translations[@default_lang]}\"\n"
96
+ end
97
+
98
+ def format_value(row, lang)
99
+ "msgstr \"#{row.translated_string_for_lang(lang)}\"\n"
107
100
  end
108
101
  end
109
102
  end
@@ -35,52 +35,50 @@ module Twine
35
35
  open(path) do |io|
36
36
  json = JSON.load(io)
37
37
  json.each do |key, value|
38
- value.gsub!("\n","\\n")
39
38
  set_translation_for_key(key, lang, value)
40
39
  end
41
40
  end
42
41
  end
43
42
 
44
- def write_file(path, lang)
45
- begin
46
- require "json"
47
- rescue LoadError
48
- raise Twine::Error.new "You must run 'gem install json' in order to read or write jquery-localize files."
49
- end
43
+ def format_file(strings, lang)
44
+ "{\n#{super}\n}"
45
+ end
50
46
 
51
- printed_string = false
52
- default_lang = @strings.language_codes[0]
53
- encoding = @options[:output_encoding] || 'UTF-8'
54
- File.open(path, "w:#{encoding}") do |f|
55
- f.print "{"
47
+ def format_sections(strings, lang)
48
+ sections = strings.sections.map { |section| format_section(section, lang) }
49
+ sections.join(",\n\n")
50
+ end
51
+
52
+ def format_section_header(section)
53
+ end
56
54
 
57
- @strings.sections.each_with_index do |section, si|
58
- printed_section = false
59
- section.rows.each_with_index do |row, ri|
60
- if row.matches_tags?(@options[:tags], @options[:untagged])
61
- if printed_string
62
- f.print ",\n"
63
- end
55
+ def format_section(section, lang)
56
+ rows = section.rows.dup
64
57
 
65
- if !printed_section
66
- f.print "\n"
67
- printed_section = true
68
- end
58
+ rows.map! { |row| format_row(row, lang) }
59
+ rows.compact! # remove nil entries
60
+ rows.join(",\n")
61
+ end
69
62
 
70
- key = row.key
71
- key = key.gsub('"', '\\\\"')
63
+ def key_value_pattern
64
+ "\"%{key}\":\"%{value}\""
65
+ end
72
66
 
73
- value = row.translated_string_for_lang(lang, default_lang)
74
- value = value.gsub('"', '\\\\"')
67
+ def format_key(key)
68
+ escape_quotes(key)
69
+ end
75
70
 
76
- f.print "\"#{key}\":\"#{value}\""
77
- printed_string = true
78
- end
79
- end
80
- end
81
- f.puts "\n}"
71
+ def format_value(value)
72
+ escape_quotes(value)
73
+ end
82
74
 
75
+ def write_file(path, lang)
76
+ begin
77
+ require "json"
78
+ rescue LoadError
79
+ raise Twine::Error.new "You must run 'gem install json' in order to read or write jquery-localize files."
83
80
  end
81
+ super
84
82
  end
85
83
  end
86
84
  end
@@ -5,6 +5,8 @@ require 'rexml/document'
5
5
  module Twine
6
6
  module Formatters
7
7
  class Tizen < Abstract
8
+ include Twine::Placeholders
9
+
8
10
  FORMAT_NAME = 'tizen'
9
11
  EXTENSION = '.xml'
10
12
  DEFAULT_FILE_NAME = 'strings.xml'
@@ -90,7 +92,7 @@ module Twine
90
92
  value = CGI.unescapeHTML(value)
91
93
  value.gsub!('\\\'', '\'')
92
94
  value.gsub!('\\"', '"')
93
- value = iosify_substitutions(value)
95
+ value = convert_placeholders_from_android_to_twine(value)
94
96
  value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
95
97
  else
96
98
  value = ""
@@ -111,64 +113,45 @@ module Twine
111
113
  end
112
114
  end
113
115
 
114
- def write_file(path, lang)
115
- default_lang = nil
116
- if DEFAULT_LANG_CODES.has_key?(lang)
117
- default_lang = DEFAULT_LANG_CODES[lang]
118
- end
119
- File.open(path, 'w:UTF-8') do |f|
120
- f.puts "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Tizen Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
121
- f.write '<string_table Bversion="2.0.0.201311071819" Dversion="20120315">'
122
- @strings.sections.each do |section|
123
- printed_section = false
124
- section.rows.each do |row|
125
- if row.matches_tags?(@options[:tags], @options[:untagged])
126
- if !printed_section
127
- f.puts ''
128
- if section.name && section.name.length > 0
129
- section_name = section.name.gsub('--', '—')
130
- f.puts "\t<!-- SECTION: #{section_name} -->"
131
- end
132
- printed_section = true
133
- end
116
+ def format_header(lang)
117
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Tizen Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
118
+ end
134
119
 
135
- key = row.key
120
+ def format_sections(strings, lang)
121
+ result = '<string_table Bversion="2.0.0.201311071819" Dversion="20120315">'
122
+
123
+ result += super + "\n"
136
124
 
137
- value = row.translated_string_for_lang(lang, default_lang)
138
- if !value && @options[:include_untranslated]
139
- value = row.translated_string_for_lang(@strings.language_codes[0])
140
- end
125
+ result += '</string_table>'
126
+ end
141
127
 
142
- if value # if values is nil, there was no appropriate translation, so let Tizen handle the defaulting
143
- value = String.new(value) # use a copy to prevent modifying the original
144
-
145
- # Tizen enforces the following rules on the values
146
- # 1) apostrophes and quotes must be escaped with a backslash
147
- value.gsub!('\'', '\\\\\'')
148
- value.gsub!('"', '\\\\"')
149
- # 2) HTML escape the string
150
- value = CGI.escapeHTML(value)
151
- # 3) fix substitutions (e.g. %s/%@)
152
- value = androidify_substitutions(value)
153
- # 4) replace beginning and end spaces with \0020. Otherwise Tizen strips them.
154
- value.gsub!(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
155
-
156
- comment = row.comment
157
- if comment
158
- comment = comment.gsub('--', '—')
159
- end
160
-
161
- if comment && comment.length > 0
162
- f.puts "\t<!-- #{comment} -->\n"
163
- end
164
- f.puts "\t<text id=\"IDS_#{key.upcase}\">#{value}</text>"
165
- end
166
- end
167
- end
168
- end
128
+ def format_section_header(section)
129
+ "\t<!-- SECTION: #{section.name} -->"
130
+ end
169
131
 
170
- f.puts '</string_table>'
171
- end
132
+ def format_comment(row, lang)
133
+ "\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
134
+ end
135
+
136
+ def key_value_pattern
137
+ "\t<text id=\"IDS_%{key}\">%{value}</text>"
138
+ end
139
+
140
+ def format_key(key)
141
+ key.upcase
142
+ end
143
+
144
+ def format_value(value)
145
+ value = escape_quotes(value)
146
+ # Tizen enforces the following rules on the values
147
+ # 1) apostrophes and quotes must be escaped with a backslash
148
+ value.gsub!("'", "\\\\'")
149
+ # 2) HTML escape the string
150
+ value = CGI.escapeHTML(value)
151
+ # 3) fix substitutions (e.g. %s/%@)
152
+ value = convert_placeholders_from_twine_to_android(value)
153
+ # 4) replace beginning and end spaces with \0020. Otherwise Tizen strips them.
154
+ value.gsub(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
172
155
  end
173
156
  end
174
157
  end
@@ -0,0 +1,57 @@
1
+ module Twine
2
+ module Processors
3
+
4
+ class OutputProcessor
5
+ def initialize(strings, options)
6
+ @strings = strings
7
+ @options = options
8
+ end
9
+
10
+ def default_language
11
+ @options[:developer_language] || @strings.language_codes[0]
12
+ end
13
+
14
+ def fallback_languages(language)
15
+ fallback_mapping = {
16
+ 'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
17
+ }
18
+
19
+ [fallback_mapping[language], default_language].flatten.compact
20
+ end
21
+
22
+ def process(language)
23
+ result = StringsFile.new
24
+
25
+ result.language_codes.concat @strings.language_codes
26
+ @strings.sections.each do |section|
27
+ new_section = StringsSection.new section.name
28
+
29
+ section.rows.each do |row|
30
+ next unless row.matches_tags?(@options[:tags], @options[:untagged])
31
+
32
+ value = row.translated_string_for_lang(language)
33
+
34
+ next if value && @options[:include] == 'untranslated'
35
+
36
+ if value.nil? && @options[:include] != 'translated'
37
+ value = row.translated_string_for_lang(fallback_languages(language))
38
+ end
39
+
40
+ next unless value
41
+
42
+ new_row = row.dup
43
+ new_row.translations[language] = value
44
+
45
+ new_section.rows << new_row
46
+ result.strings_map[new_row.key] = new_row
47
+ end
48
+
49
+ result.sections << new_section
50
+ end
51
+
52
+ return result
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ module Twine
2
+ module Placeholders
3
+ extend self
4
+
5
+ PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+ 0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t)?'
6
+ PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH = '(\d+\$)?' + PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH
7
+
8
+ # http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
9
+ # http://stackoverflow.com/questions/4414389/android-xml-percent-symbol
10
+ # https://github.com/mobiata/twine/pull/106
11
+ def convert_placeholders_from_twine_to_android(input)
12
+ placeholder_types = '[diufFeEgGxXoscpaA]'
13
+
14
+ # %@ -> %s
15
+ value = input.gsub(/(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})@/, '\1s')
16
+
17
+ placeholder_syntax = PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH + placeholder_types
18
+ placeholder_regex = /%#{placeholder_syntax}/
19
+
20
+ number_of_placeholders = value.scan(placeholder_regex).size
21
+
22
+ return value if number_of_placeholders == 0
23
+
24
+ # got placeholders -> need to double single percent signs
25
+ # % -> %% (but %% -> %%, %d -> %d)
26
+ single_percent_regex = /([^%])(%)(?!(%|#{placeholder_syntax}))/
27
+ value.gsub! single_percent_regex, '\1%%'
28
+
29
+ return value if number_of_placeholders < 2
30
+
31
+ # number placeholders
32
+ non_numbered_placeholder_regex = /%(#{PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH}#{placeholder_types})/
33
+
34
+ number_of_non_numbered_placeholders = value.scan(non_numbered_placeholder_regex).size
35
+
36
+ return value if number_of_non_numbered_placeholders == 0
37
+
38
+ raise Twine::Error.new("The value \"#{input}\" contains numbered and non-numbered placeholders") if number_of_placeholders != number_of_non_numbered_placeholders
39
+
40
+ # %d -> %$1d
41
+ index = 0
42
+ value.gsub!(non_numbered_placeholder_regex) { "%#{index += 1}$#{$1}" }
43
+
44
+ value
45
+ end
46
+
47
+ def convert_placeholders_from_android_to_twine(input)
48
+ placeholder_regex = /(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})s/
49
+
50
+ # %s -> %@
51
+ input.gsub(placeholder_regex, '\1@')
52
+ end
53
+ end
54
+ end
@@ -1,30 +1,38 @@
1
1
  require 'tmpdir'
2
+ require 'fileutils'
2
3
 
3
4
  Twine::Plugin.new # Initialize plugins first in Runner.
4
5
 
5
6
  module Twine
6
- VALID_COMMANDS = ['generate-string-file', 'generate-all-string-files', 'consume-string-file', 'consume-all-string-files', 'generate-loc-drop', 'consume-loc-drop', 'validate-strings-file']
7
-
8
7
  class Runner
9
- def initialize(args)
10
- @options = {}
11
- @args = args
12
- end
13
-
14
8
  def self.run(args)
15
- new(args).run
16
- end
9
+ options = CLI.parse(args)
10
+
11
+ strings = StringsFile.new
12
+ strings.read options[:strings_file]
13
+ runner = new(options, strings)
17
14
 
18
- def run
19
- # Parse all CLI arguments.
20
- CLI::parse_args(@args, @options)
21
- read_strings_data
22
- execute_command
15
+ case options[:command]
16
+ when 'generate-string-file'
17
+ runner.generate_string_file
18
+ when 'generate-all-string-files'
19
+ runner.generate_all_string_files
20
+ when 'consume-string-file'
21
+ runner.consume_string_file
22
+ when 'consume-all-string-files'
23
+ runner.consume_all_string_files
24
+ when 'generate-loc-drop'
25
+ runner.generate_loc_drop
26
+ when 'consume-loc-drop'
27
+ runner.consume_loc_drop
28
+ when 'validate-strings-file'
29
+ runner.validate_strings_file
30
+ end
23
31
  end
24
32
 
25
- def read_strings_data
26
- @strings = StringsFile.new
27
- @strings.read @options[:strings_file]
33
+ def initialize(options = {}, strings = StringsFile.new)
34
+ @options = options
35
+ @strings = strings
28
36
  end
29
37
 
30
38
  def write_strings_data(path)
@@ -34,44 +42,24 @@ module Twine
34
42
  @strings.write(path)
35
43
  end
36
44
 
37
- def execute_command
38
- case @options[:command]
39
- when 'generate-string-file'
40
- generate_string_file
41
- when 'generate-all-string-files'
42
- generate_all_string_files
43
- when 'consume-string-file'
44
- consume_string_file
45
- when 'consume-all-string-files'
46
- consume_all_string_files
47
- when 'generate-loc-drop'
48
- generate_loc_drop
49
- when 'consume-loc-drop'
50
- consume_loc_drop
51
- when 'validate-strings-file'
52
- validate_strings_file
53
- end
54
- end
55
-
56
45
  def generate_string_file
57
46
  lang = nil
58
- if @options[:languages]
59
- lang = @options[:languages][0]
60
- end
47
+ lang = @options[:languages][0] if @options[:languages]
61
48
 
62
49
  read_write_string_file(@options[:output_path], false, lang)
63
50
  end
64
51
 
65
52
  def generate_all_string_files
66
53
  if !File.directory?(@options[:output_path])
67
- raise Twine::Error.new("Directory does not exist: #{@options[:output_path]}")
54
+ if @options[:create_folders]
55
+ FileUtils.mkdir_p(@options[:output_path])
56
+ else
57
+ raise Twine::Error.new("Directory does not exist: #{@options[:output_path]}")
58
+ end
68
59
  end
69
60
 
70
- format = @options[:format]
71
- if !format
72
- format = determine_format_given_directory(@options[:output_path])
73
- end
74
- if !format
61
+ format = @options[:format] || determine_format_given_directory(@options[:output_path])
62
+ unless format
75
63
  raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}"
76
64
  end
77
65
 
@@ -101,7 +89,7 @@ module Twine
101
89
  begin
102
90
  read_write_string_file(item, true, nil)
103
91
  rescue Twine::Error => e
104
- STDERR.puts "#{e.message}"
92
+ Twine::stderr.puts "#{e.message}"
105
93
  end
106
94
  end
107
95
  end
@@ -115,23 +103,15 @@ module Twine
115
103
  raise Twine::Error.new("File does not exist: #{path}")
116
104
  end
117
105
 
118
- format = @options[:format]
119
- if !format
120
- format = determine_format_given_path(path)
121
- end
122
- if !format
106
+ format = @options[:format] || determine_format_given_path(path)
107
+ unless format
123
108
  raise Twine::Error.new "Unable to determine format of #{path}"
124
109
  end
125
110
 
126
111
  formatter = formatter_for_format(format)
127
112
 
128
- if !lang
129
- lang = determine_language_given_path(path)
130
- end
131
- if !lang
132
- lang = formatter.determine_language_given_path(path)
133
- end
134
- if !lang
113
+ lang = lang || determine_language_given_path(path) || formatter.determine_language_given_path(path)
114
+ unless lang
135
115
  raise Twine::Error.new "Unable to determine language for #{path}"
136
116
  end
137
117
 
@@ -147,18 +127,14 @@ module Twine
147
127
  end
148
128
 
149
129
  def generate_loc_drop
150
- begin
151
- require 'zip/zip'
152
- rescue LoadError
153
- raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization drops."
154
- end
130
+ require_rubyzip
155
131
 
156
132
  if File.file?(@options[:output_path])
157
133
  File.delete(@options[:output_path])
158
134
  end
159
135
 
160
136
  Dir.mktmpdir do |dir|
161
- Zip::ZipFile.open(@options[:output_path], Zip::ZipFile::CREATE) do |zipfile|
137
+ Zip::File.open(@options[:output_path], Zip::File::CREATE) do |zipfile|
162
138
  zipfile.mkdir('Locales')
163
139
 
164
140
  formatter = formatter_for_format(@options[:format])
@@ -176,18 +152,14 @@ module Twine
176
152
  end
177
153
 
178
154
  def consume_loc_drop
155
+ require_rubyzip
156
+
179
157
  if !File.file?(@options[:input_path])
180
158
  raise Twine::Error.new("File does not exist: #{@options[:input_path]}")
181
159
  end
182
160
 
183
- begin
184
- require 'zip/zip'
185
- rescue LoadError
186
- raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization drops."
187
- end
188
-
189
161
  Dir.mktmpdir do |dir|
190
- Zip::ZipFile.open(@options[:input_path]) do |zipfile|
162
+ Zip::File.open(@options[:input_path]) do |zipfile|
191
163
  zipfile.each do |entry|
192
164
  if !entry.name.end_with?'/' and !File.basename(entry.name).start_with?'.'
193
165
  real_path = File.join(dir, entry.name)
@@ -196,7 +168,7 @@ module Twine
196
168
  begin
197
169
  read_write_string_file(real_path, true, nil)
198
170
  rescue Twine::Error => e
199
- STDERR.puts "#{e.message}"
171
+ Twine::stderr.puts "#{e.message}"
200
172
  end
201
173
  end
202
174
  end
@@ -212,81 +184,72 @@ module Twine
212
184
  all_keys = Set.new
213
185
  duplicate_keys = Set.new
214
186
  keys_without_tags = Set.new
215
- errors = []
187
+ invalid_keys = Set.new
188
+ valid_key_regex = /^[A-Za-z0-9_]+$/
216
189
 
217
190
  @strings.sections.each do |section|
218
191
  section.rows.each do |row|
219
192
  total_strings += 1
220
193
 
221
- if all_keys.include? row.key
222
- duplicate_keys.add(row.key)
223
- else
224
- all_keys.add(row.key)
225
- end
194
+ duplicate_keys.add(row.key) if all_keys.include? row.key
195
+ all_keys.add(row.key)
226
196
 
227
- if row.tags == nil || row.tags.length == 0
228
- keys_without_tags.add(row.key)
229
- end
197
+ keys_without_tags.add(row.key) if row.tags == nil or row.tags.length == 0
198
+
199
+ invalid_keys << row.key unless row.key =~ valid_key_regex
230
200
  end
231
201
  end
232
202
 
233
- if duplicate_keys.length > 0
234
- error_body = duplicate_keys.to_a.join("\n ")
235
- errors << "Found duplicate string key(s):\n #{error_body}"
203
+ errors = []
204
+ join_keys = lambda { |set| set.map { |k| " " + k }.join("\n") }
205
+
206
+ unless duplicate_keys.empty?
207
+ errors << "Found duplicate string key(s):\n#{join_keys.call(duplicate_keys)}"
236
208
  end
237
209
 
238
210
  if keys_without_tags.length == total_strings
239
211
  errors << "None of your strings have tags."
240
212
  elsif keys_without_tags.length > 0
241
- error_body = keys_without_tags.to_a.join("\n ")
242
- errors << "Found strings(s) without tags:\n #{error_body}"
213
+ errors << "Found strings without tags:\n#{join_keys.call(keys_without_tags)}"
243
214
  end
244
215
 
245
- if errors.length > 0
246
- raise Twine::Error.new errors.join("\n\n")
216
+ unless invalid_keys.empty?
217
+ errors << "Found key(s) with invalid characters:\n#{join_keys.call(invalid_keys)}"
247
218
  end
248
219
 
249
- puts "#{@options[:strings_file]} is valid."
220
+ raise Twine::Error.new errors.join("\n\n") unless errors.empty?
221
+
222
+ Twine::stdout.puts "#{@options[:strings_file]} is valid."
250
223
  end
251
224
 
252
225
  def determine_language_given_path(path)
253
226
  code = File.basename(path, File.extname(path))
254
- if !@strings.language_codes.include? code
255
- code = nil
256
- end
257
-
258
- code
227
+ return code if @strings.language_codes.include? code
259
228
  end
260
229
 
261
230
  def determine_format_given_path(path)
262
- ext = File.extname(path)
263
- Formatters.formatters.each do |formatter|
264
- if formatter::EXTENSION == ext
265
- return formatter::FORMAT_NAME
266
- end
267
- end
268
-
269
- return
231
+ formatter = Formatters.formatters.find { |f| f::EXTENSION == File.extname(path) }
232
+ return formatter::FORMAT_NAME if formatter
270
233
  end
271
234
 
272
235
  def determine_format_given_directory(directory)
273
- Formatters.formatters.each do |formatter|
274
- if formatter.can_handle_directory?(directory)
275
- return formatter::FORMAT_NAME
276
- end
277
- end
278
-
279
- return
236
+ formatter = Formatters.formatters.find { |f| f.can_handle_directory?(directory) }
237
+ return formatter::FORMAT_NAME if formatter
280
238
  end
281
239
 
282
240
  def formatter_for_format(format)
283
- Formatters.formatters.each do |formatter|
284
- if formatter::FORMAT_NAME == format
285
- return formatter.new(@strings, @options)
286
- end
287
- end
241
+ formatter = Formatters.formatters.find { |f| f::FORMAT_NAME == format }
242
+ return formatter.new(@strings, @options) if formatter
243
+ end
244
+
245
+ private
288
246
 
289
- return
247
+ def require_rubyzip
248
+ begin
249
+ require 'zip'
250
+ rescue LoadError
251
+ raise Twine::Error.new "You must run 'gem install rubyzip' in order to create or consume localization drops."
252
+ end
290
253
  end
291
254
  end
292
255
  end