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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dd42325c94b784158be7dd018af67a02fb3099b0
4
- data.tar.gz: 7ee4dae663a07911d402a236fb1f40692b58cc43
3
+ metadata.gz: 46fb29dd5d1ec899c427901e72e40c081fa8c6c4
4
+ data.tar.gz: 3faf58c634c4d1903fda10a1ece3cace658dc761
5
5
  SHA512:
6
- metadata.gz: 54345a2371c8717af7b2285b7fee737b082695501da2e5ecaaee04f76762b6aa2a31eefc0c42c5a95d15ae06af38d1e8b31dfb34b6a20b56c86417044667255e
7
- data.tar.gz: 681b338e4b3f9efc9af0659c785b8fcaabb3878e6b3080b8fce7aca0856094e52f0414f25591c27c7d2688c82c45e671ddd6bbb2d25c59afdadb16858005d427
6
+ metadata.gz: 6ec4b6098a24520cba05e4bb2bd0ea16602c9b52382abfeb45e9bf0308a4b1c03287251160cdec49cb26f44ccd69ff96d17f5dd0c9d56b7612068435c750dfb2
7
+ data.tar.gz: b1a85e5a3fb56e20a790e7beb8649784f5ffab42f4e8b6db7132680a634275caad8ab698a09369d9b46821c873bf8a51c416f941337d1b101ad079f6bd6d2960
data/README.md CHANGED
@@ -27,6 +27,10 @@ Twine stores all of its strings in a single file. The format of this file is a s
27
27
 
28
28
  Each grouping section contains N string definitions. These string definitions start with the string key placed within a single pair of square brackets. This string definition then contains a number of key-value pairs, including a comment, a comma-separated list of tags (which are used by Twine to select a subset of strings), and all of the translations.
29
29
 
30
+ ### Placeholders
31
+
32
+ Twine supports [`printf` style placeholders](https://en.wikipedia.org/wiki/Printf_format_string) with one peculiarity: `@` is used for strings instead of `s`. This is because Twine started out as a tool for iOS and OS X projects.
33
+
30
34
  ### Tags
31
35
 
32
36
  Tags are used by Twine as a way to only work with a subset of your strings at any given point in time. Each string can be assigned zero or more tags which are separated by commas. Tags are optional, though highly recommended. You can get a list of all strings currently missing tags by executing the `validate-strings-file` command.
@@ -35,6 +39,10 @@ Tags are used by Twine as a way to only work with a subset of your strings at an
35
39
 
36
40
  Whitepace in this file is mostly ignored. If you absolutely need to put spaces at the beginning or end of your translated string, you can wrap the entire string in a pair of `` ` `` characters. If your actual string needs to start *and* end with a grave accent, you can wrap it in another pair of `` ` `` characters. See the example, below.
37
41
 
42
+ ### References
43
+
44
+ If you want a key to inherit the values of another key, you can use a reference. Any property not specified for a key will be taken from the reference.
45
+
38
46
  ### Example
39
47
 
40
48
  [[General]]
@@ -57,6 +65,9 @@ Whitepace in this file is mostly ignored. If you absolutely need to put spaces a
57
65
  en = The network is currently unavailable.
58
66
  tags = app1
59
67
  comment = An error describing when the device can not connect to the internet.
68
+ [dismiss_error]
69
+ ref = yes
70
+ en = Dismiss
60
71
 
61
72
  [[Escaping Example]]
62
73
  [list_item_separator]
@@ -216,6 +227,7 @@ Many thanks to all of the contributors to the Twine project, including:
216
227
  * [Kevin Wood](https://github.com/kwood)
217
228
  * [Mohammad Hejazi](https://github.com/MohammadHejazi)
218
229
  * [Robert Guo](http://www.robertguo.me/)
230
+ * [sebastianludwig](https://github.com/sebastianludwig)
219
231
  * [Sergey Pisarchik](https://github.com/SergeyPisarchik)
220
232
  * [Shai Shamir](https://github.com/pichirichi)
221
233
 
@@ -1,10 +1,31 @@
1
1
  module Twine
2
+ @@stdout = STDOUT
3
+ @@stderr = STDERR
4
+
5
+ def self.stdout
6
+ @@stdout
7
+ end
8
+
9
+ def self.stdout=(out)
10
+ @@stdout = out
11
+ end
12
+
13
+ def self.stderr
14
+ @@stderr
15
+ end
16
+
17
+ def self.stderr=(err)
18
+ @@stderr = err
19
+ end
20
+
2
21
  class Error < StandardError
3
22
  end
4
23
 
5
24
  require 'twine/plugin'
6
25
  require 'twine/cli'
7
26
  require 'twine/encoding'
27
+ require 'twine/output_processor'
28
+ require 'twine/placeholders'
8
29
  require 'twine/formatters'
9
30
  require 'twine/runner'
10
31
  require 'twine/stringsfile'
@@ -1,17 +1,19 @@
1
1
  require 'optparse'
2
2
 
3
3
  module Twine
4
- class CLI
5
- def initialize(args, options)
6
- @options = options
7
- @args = args
8
- end
9
-
10
- def self.parse_args(args, options)
11
- new(args, options).parse_args
12
- end
4
+ module CLI
5
+ NEEDED_COMMAND_ARGUMENTS = {
6
+ 'generate-string-file' => 3,
7
+ 'generate-all-string-files' => 3,
8
+ 'consume-string-file' => 3,
9
+ 'consume-all-string-files' => 3,
10
+ 'generate-loc-drop' => 3,
11
+ 'consume-loc-drop' => 3,
12
+ 'validate-strings-file' => 2
13
+ }
13
14
 
14
- def parse_args
15
+ def self.parse(args)
16
+ options = {}
15
17
  parser = OptionParser.new do |opts|
16
18
  opts.banner = 'Usage: twine COMMAND STRINGS_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]'
17
19
  opts.separator ''
@@ -27,7 +29,7 @@ module Twine
27
29
  opts.separator ''
28
30
  opts.separator 'consume-all-string-files -- Slurps all of the strings from a directory into the specified STRINGS_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.'
29
31
  opts.separator ''
30
- opts.separator 'generate-loc-drop -- Generates a zip archive of strings files in any format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-loc-drop command. This command assumes that --include-untranslated has been specified on the command line.'
32
+ opts.separator 'generate-loc-drop -- Generates a zip archive of strings files in any format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-loc-drop command.'
31
33
  opts.separator ''
32
34
  opts.separator 'consume-loc-drop -- Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-loc-drop command.'
33
35
  opts.separator ''
@@ -36,48 +38,56 @@ module Twine
36
38
  opts.separator 'General Options:'
37
39
  opts.separator ''
38
40
  opts.on('-l', '--lang LANGUAGES', Array, 'The language code(s) to use for the specified action.') do |langs|
39
- @options[:languages] = langs
41
+ options[:languages] = langs
40
42
  end
41
43
  opts.on('-t', '--tags TAGS', Array, 'The tag(s) to use for the specified action. Only strings with that tag will be processed. Do not specify any tags to match all strings in the strings data file.') do |tags|
42
- @options[:tags] = tags
44
+ options[:tags] = tags
43
45
  end
44
46
  opts.on('-u', '--untagged', 'If you have specified tags using the --tags flag, then only those tags will be selected. If you also want to select all strings that are untagged, then you can specify this option to do so.') do |u|
45
- @options[:untagged] = true
46
- end
47
- formats = []
48
- Formatters.formatters.each do |formatter|
49
- formats << formatter::FORMAT_NAME
47
+ options[:untagged] = true
50
48
  end
49
+ formats = Formatters.formatters.map { |f| f::FORMAT_NAME }
51
50
  opts.on('-f', '--format FORMAT', "The file format to read or write (#{formats.join(', ')}). Additional formatters can be placed in the formats/ directory.") do |format|
52
- lformat = format.downcase
53
- if !formats.include?(lformat)
54
- STDERR.puts "Invalid format: #{format}"
51
+ unless formats.include?(format.downcase)
52
+ raise Twine::Error.new "Invalid format: #{format}"
55
53
  end
56
- @options[:format] = lformat
54
+ options[:format] = format.downcase
57
55
  end
58
56
  opts.on('-a', '--consume-all', 'Normally, when consuming a string file, Twine will ignore any string keys that do not exist in your master file.') do |a|
59
- @options[:consume_all] = true
57
+ options[:consume_all] = true
58
+ end
59
+ opts.on('-i', '--include SET', "This flag will determine which strings are included when generating strings files. It's possible values:",
60
+ " all: All strings both translated and untranslated for the specified language are included. This is the default value.",
61
+ " translated: Only translated strings are included.",
62
+ " untranslated: Only untranslated strings are included.") do |set|
63
+ unless ['all', 'translated', 'untranslated'].include?(set.downcase)
64
+ raise Twine::Error.new "Invalid include flag: #{set}"
65
+ end
66
+ options[:include] = set.downcase
60
67
  end
61
- opts.on('-s', '--include-untranslated', 'This flag will cause any Android string files that are generated to include strings that have not yet been translated for the current language.') do |s|
62
- @options[:include_untranslated] = true
68
+ unless options[:include]
69
+ options[:include] = 'all'
63
70
  end
64
71
  opts.on('-o', '--output-file OUTPUT_FILE', 'Write the new strings database to this file instead of replacing the original file. This flag is only useful when running the consume-string-file or consume-loc-drop commands.') do |o|
65
- @options[:output_path] = o
72
+ options[:output_path] = o
66
73
  end
67
74
  opts.on('-n', '--file-name FILE_NAME', 'When running the generate-all-string-files command, this flag may be used to overwrite the default file name of the format.') do |n|
68
- @options[:file_name] = n
75
+ options[:file_name] = n
76
+ end
77
+ opts.on('-r', '--create-folders', "When running the generate-all-string-files command, this flag may be used to create output folders for all languages, if they don't exist yet. As a result all languages will be exported, not only the ones where an output folder already exists.") do |r|
78
+ options[:create_folders] = true
69
79
  end
70
- opts.on('-d', '--developer-language LANG', 'When writing the strings data file, set the specified language as the "developer language". In practice, this just means that this language will appear first in the strings data file.') do |d|
71
- @options[:developer_language] = d
80
+ opts.on('-d', '--developer-language LANG', 'When writing the strings data file, set the specified language as the "developer language". In practice, this just means that this language will appear first in the strings data file. When generating files this language will be used as default language and its translations will be used if a key is not localized for the output language.') do |d|
81
+ options[:developer_language] = d
72
82
  end
73
83
  opts.on('-c', '--consume-comments', 'Normally, when consuming a string file, Twine will ignore all comments in the file. With this flag set, any comments encountered will be read and parsed into the strings data file. This is especially useful when creating your first strings data file from an existing project.') do |c|
74
- @options[:consume_comments] = true
84
+ options[:consume_comments] = true
75
85
  end
76
- opts.on('-e', '--encoding ENCODING', 'Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate encoding for these files. For example, you could use this to write Apple .strings files in UTF-16. This flag currently only works with Apple .strings files and is currently only supported in Ruby 1.9.3 or greater.') do |e|
77
- if !"".respond_to?(:encode)
86
+ opts.on('-e', '--encoding ENCODING', 'Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate encoding for these files. For example, you could use this to write Apple .strings files in UTF-16. This flag is currently only supported in Ruby 1.9.3 or greater.') do |e|
87
+ unless "".respond_to? :encode
78
88
  raise Twine::Error.new "The --encoding flag is only supported on Ruby 1.9.3 or greater."
79
89
  end
80
- @options[:output_encoding] = e
90
+ options[:output_encoding] = e
81
91
  end
82
92
  opts.on('-h', '--help', 'Show this message.') do |h|
83
93
  puts opts.help
@@ -98,89 +108,56 @@ module Twine
98
108
  opts.separator '> twine consume-loc-drop strings.txt LocDrop5.zip'
99
109
  opts.separator '> twine validate-strings-file strings.txt'
100
110
  end
101
- parser.parse! @args
111
+ parser.parse! args
102
112
 
103
- if @args.length == 0
113
+ if args.length == 0
104
114
  puts parser.help
105
115
  exit
106
116
  end
107
117
 
108
- @options[:command] = @args[0]
109
-
110
- if !VALID_COMMANDS.include? @options[:command]
111
- raise Twine::Error.new "Invalid command: #{@options[:command]}"
118
+ number_of_needed_arguments = NEEDED_COMMAND_ARGUMENTS[args[0]]
119
+ unless number_of_needed_arguments
120
+ raise Twine::Error.new "Invalid command: #{args[0]}"
112
121
  end
122
+ options[:command] = args[0]
113
123
 
114
- if @args.length == 1
124
+ if args.length < 2
115
125
  raise Twine::Error.new 'You must specify your strings file.'
116
126
  end
127
+ options[:strings_file] = args[1]
117
128
 
118
- @options[:strings_file] = @args[1]
129
+ if args.length < number_of_needed_arguments
130
+ raise Twine::Error.new 'Not enough arguments.'
131
+ elsif args.length > number_of_needed_arguments
132
+ raise Twine::Error.new "Unknown argument: #{args[number_of_needed_arguments]}"
133
+ end
119
134
 
120
- case @options[:command]
135
+ case options[:command]
121
136
  when 'generate-string-file'
122
- if @args.length == 3
123
- @options[:output_path] = @args[2]
124
- elsif @args.length > 3
125
- raise Twine::Error.new "Unknown argument: #{@args[3]}"
126
- else
127
- raise Twine::Error.new 'Not enough arguments.'
128
- end
129
- if @options[:languages] and @options[:languages].length > 1
137
+ options[:output_path] = args[2]
138
+ if options[:languages] and options[:languages].length > 1
130
139
  raise Twine::Error.new 'Please only specify a single language for the generate-string-file command.'
131
140
  end
132
141
  when 'generate-all-string-files'
133
- if ARGV.length == 3
134
- @options[:output_path] = @args[2]
135
- elsif @args.length > 3
136
- raise Twine::Error.new "Unknown argument: #{@args[3]}"
137
- else
138
- raise Twine::Error.new 'Not enough arguments.'
139
- end
142
+ options[:output_path] = args[2]
140
143
  when 'consume-string-file'
141
- if @args.length == 3
142
- @options[:input_path] = @args[2]
143
- elsif @args.length > 3
144
- raise Twine::Error.new "Unknown argument: #{@args[3]}"
145
- else
146
- raise Twine::Error.new 'Not enough arguments.'
147
- end
148
- if @options[:languages] and @options[:languages].length > 1
144
+ options[:input_path] = args[2]
145
+ if options[:languages] and options[:languages].length > 1
149
146
  raise Twine::Error.new 'Please only specify a single language for the consume-string-file command.'
150
147
  end
151
148
  when 'consume-all-string-files'
152
- if @args.length == 3
153
- @options[:input_path] = @args[2]
154
- elsif @args.length > 3
155
- raise Twine::Error.new "Unknown argument: #{@args[3]}"
156
- else
157
- raise Twine::Error.new 'Not enough arguments.'
158
- end
149
+ options[:input_path] = args[2]
159
150
  when 'generate-loc-drop'
160
- @options[:include_untranslated] = true
161
- if @args.length == 3
162
- @options[:output_path] = @args[2]
163
- elsif @args.length > 3
164
- raise Twine::Error.new "Unknown argument: #{@args[3]}"
165
- else
166
- raise Twine::Error.new 'Not enough arguments.'
167
- end
168
- if !@options[:format]
151
+ options[:output_path] = args[2]
152
+ if !options[:format]
169
153
  raise Twine::Error.new 'You must specify a format.'
170
154
  end
171
155
  when 'consume-loc-drop'
172
- if @args.length == 3
173
- @options[:input_path] = @args[2]
174
- elsif @args.length > 3
175
- raise Twine::Error.new "Unknown argument: #{@args[3]}"
176
- else
177
- raise Twine::Error.new 'Not enough arguments.'
178
- end
156
+ options[:input_path] = args[2]
179
157
  when 'validate-strings-file'
180
- if @args.length > 2
181
- raise Twine::Error.new "Unknown argument: #{@args[2]}"
182
- end
183
158
  end
159
+
160
+ return options
184
161
  end
185
162
  end
186
163
  end
@@ -1,8 +1,10 @@
1
+ require 'fileutils'
2
+
1
3
  module Twine
2
4
  module Formatters
3
5
  class Abstract
4
- attr_accessor :strings
5
- attr_accessor :options
6
+ attr_reader :strings
7
+ attr_reader :options
6
8
 
7
9
  def self.can_handle_directory?(path)
8
10
  return false
@@ -11,70 +13,23 @@ module Twine
11
13
  def initialize(strings, options)
12
14
  @strings = strings
13
15
  @options = options
16
+ @output_processor = Processors::OutputProcessor.new @strings, @options
14
17
  end
15
18
 
16
- def iosify_substitutions(str)
17
- # use "@" instead of "s" for substituting strings
18
- str.gsub!(/%([0-9\$]*)s/, '%\1@')
19
- return str
20
- end
21
-
22
- def androidify_substitutions(str)
23
- # 1) use "s" instead of "@" for substituting strings
24
- str.gsub!(/%([0-9\$]*)@/, '%\1s')
25
-
26
- # 1a) escape strings that begin with a lone "@"
27
- str.sub!(/^@ /, '\\@ ')
28
-
29
- # 2) if there is more than one substitution in a string, make sure they are numbered
30
- substituteCount = 0
31
- startFound = false
32
- str.each_char do |c|
33
- if startFound
34
- if c == "%"
35
- # ignore as this is a literal %
36
- elsif c.match(/\d/)
37
- # leave the string alone if it already has numbered substitutions
38
- return str
39
- else
40
- substituteCount += 1
41
- end
42
- startFound = false
43
- elsif c == "%"
44
- startFound = true
45
- end
46
- end
47
-
48
- if substituteCount > 1
49
- currentSub = 1
50
- startFound = false
51
- newstr = ""
52
- str.each_char do |c|
53
- if startFound
54
- if !(c == "%")
55
- newstr = newstr + "#{currentSub}$"
56
- currentSub += 1
57
- end
58
- startFound = false
59
- elsif c == "%"
60
- startFound = true
61
- end
62
- newstr = newstr + c
63
- end
64
- return newstr
65
- else
66
- return str
67
- end
68
- end
69
-
70
19
  def set_translation_for_key(key, lang, value)
20
+ value = value.gsub("\n", "\\n")
21
+
71
22
  if @strings.strings_map.include?(key)
72
- @strings.strings_map[key].translations[lang] = value
23
+ row = @strings.strings_map[key]
24
+ reference = @strings.strings_map[row.reference_key] if row.reference_key
25
+
26
+ if !reference or value != reference.translations[lang]
27
+ row.translations[lang] = value
28
+ end
73
29
  elsif @options[:consume_all]
74
- STDERR.puts "Adding new string '#{key}' to strings data file."
75
- arr = @strings.sections.select { |s| s.name == 'Uncategorized' }
76
- current_section = arr ? arr[0] : nil
77
- if !current_section
30
+ Twine::stderr.puts "Adding new string '#{key}' to strings data file."
31
+ current_section = @strings.sections.find { |s| s.name == 'Uncategorized' }
32
+ unless current_section
78
33
  current_section = StringsSection.new('Uncategorized')
79
34
  @strings.sections.insert(0, current_section)
80
35
  end
@@ -82,13 +37,13 @@ module Twine
82
37
  current_section.rows << current_row
83
38
 
84
39
  if @options[:tags] && @options[:tags].length > 0
85
- current_row.tags = @options[:tags]
40
+ current_row.tags = @options[:tags]
86
41
  end
87
42
 
88
43
  @strings.strings_map[key] = current_row
89
44
  @strings.strings_map[key].translations[lang] = value
90
45
  else
91
- STDERR.puts "Warning: '#{key}' not found in strings data file."
46
+ Twine::stderr.puts "Warning: '#{key}' not found in strings data file."
92
47
  end
93
48
  if !@strings.language_codes.include?(lang)
94
49
  @strings.add_language_code(lang)
@@ -96,8 +51,16 @@ module Twine
96
51
  end
97
52
 
98
53
  def set_comment_for_key(key, comment)
54
+ return unless @options[:consume_comments]
55
+
99
56
  if @strings.strings_map.include?(key)
100
- @strings.strings_map[key].comment = comment
57
+ row = @strings.strings_map[key]
58
+
59
+ reference = @strings.strings_map[row.reference_key] if row.reference_key
60
+
61
+ if !reference or comment != reference.raw_comment
62
+ row.comment = comment
63
+ end
101
64
  end
102
65
  end
103
66
 
@@ -109,38 +72,126 @@ module Twine
109
72
  raise NotImplementedError.new("You must implement determine_language_given_path in your formatter class.")
110
73
  end
111
74
 
75
+ def output_path_for_language(lang)
76
+ lang
77
+ end
78
+
112
79
  def read_file(path, lang)
113
80
  raise NotImplementedError.new("You must implement read_file in your formatter class.")
114
81
  end
115
82
 
116
- def write_file(path, lang)
117
- raise NotImplementedError.new("You must implement write_file in your formatter class.")
83
+ def format_file(strings, lang)
84
+ header = format_header(lang)
85
+ result = ""
86
+ result += header + "\n" if header
87
+ result += format_sections(strings, lang)
118
88
  end
119
89
 
120
- def write_all_files(path)
121
- if !File.directory?(path)
122
- raise Twine::Error.new("Directory does not exist: #{path}")
90
+ def format_header(lang)
91
+ end
92
+
93
+ def format_sections(strings, lang)
94
+ sections = strings.sections.map { |section| format_section(section, lang) }
95
+ sections.join("\n")
96
+ end
97
+
98
+ def format_section_header(section)
99
+ end
100
+
101
+ def format_section(section, lang)
102
+ rows = section.rows.dup
103
+
104
+ result = ""
105
+ unless rows.empty?
106
+ if section.name && section.name.length > 0
107
+ section_header = format_section_header(section)
108
+ result += "\n#{section_header}" if section_header
109
+ end
123
110
  end
124
111
 
112
+ rows.map! { |row| format_row(row, lang) }
113
+ rows.compact! # remove nil entries
114
+ rows.map! { |row| "\n#{row}" } # prepend newline
115
+ result += rows.join
116
+ end
117
+
118
+ def row_pattern
119
+ "%{comment}%{key_value}"
120
+ end
121
+
122
+ def format_row(row, lang)
123
+ return nil unless row.translated_string_for_lang(lang)
124
+
125
+ result = row_pattern.scan(/%\{([a-z_]+)\}/).flatten
126
+ result.map! { |element| send("format_#{element}".to_sym, row, lang) }
127
+ result.flatten.join
128
+ end
129
+
130
+ def format_comment(row, lang)
131
+ end
132
+
133
+ def format_key_value(row, lang)
134
+ value = row.translated_string_for_lang(lang)
135
+ key_value_pattern % { key: format_key(row.key.dup), value: format_value(value.dup) }
136
+ end
137
+
138
+ def key_value_pattern
139
+ raise NotImplementedError.new("You must implement key_value_pattern in your formatter class.")
140
+ end
141
+
142
+ def format_key(key)
143
+ key
144
+ end
145
+
146
+ def format_value(value)
147
+ value
148
+ end
149
+
150
+ def escape_quotes(text)
151
+ text.gsub('"', '\\\\"')
152
+ end
153
+
154
+ def write_file(path, lang)
155
+ encoding = @options[:output_encoding] || 'UTF-8'
156
+
157
+ processed_strings = @output_processor.process(lang)
158
+
159
+ File.open(path, "w:#{encoding}") do |f|
160
+ f.puts format_file(processed_strings, lang)
161
+ end
162
+ end
163
+
164
+ def write_all_files(path)
125
165
  file_name = @options[:file_name] || default_file_name
126
- langs_written = []
127
- Dir.foreach(path) do |item|
128
- if item == "." or item == ".."
129
- next
166
+ if @options[:create_folders]
167
+ @strings.language_codes.each do |lang|
168
+ output_path = File.join(path, output_path_for_language(lang))
169
+
170
+ FileUtils.mkdir_p(output_path)
171
+
172
+ write_file(File.join(output_path, file_name), lang)
130
173
  end
131
- item = File.join(path, item)
132
- if File.directory?(item)
174
+ else
175
+ language_written = false
176
+ Dir.foreach(path) do |item|
177
+ next if item == "." or item == ".."
178
+
179
+ item = File.join(path, item)
180
+ next unless File.directory?(item)
181
+
133
182
  lang = determine_language_given_path(item)
134
- if lang
135
- write_file(File.join(item, file_name), lang)
136
- langs_written << lang
137
- end
183
+ next unless lang
184
+
185
+ write_file(File.join(item, file_name), lang)
186
+ language_written = true
187
+ end
188
+
189
+ if !language_written
190
+ raise Twine::Error.new("Failed to generate any files: No languages found at #{path}")
138
191
  end
139
- end
140
- if langs_written.empty?
141
- raise Twine::Error.new("Failed to generate any files: No languages found at #{path}")
142
192
  end
143
193
  end
194
+
144
195
  end
145
196
  end
146
197
  end