twine 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ebc7f372d107cb4c3b9a12da92b792c510209d59
4
- data.tar.gz: 72ccda8290612adfc0f7ac75f86756bc1d945333
3
+ metadata.gz: 09e04e3949a2a98ab2483771f9def4c71942edbc
4
+ data.tar.gz: eaf82148761e31028a7918faaab00b721f41c661
5
5
  SHA512:
6
- metadata.gz: 44bf1d87f0b3e88e70cc8962dd708a35866a74710e95706ae5e4061c02995b00afb6175acb1d5219605ec391b60ceeddf01edb83bd253d73e14681847cd8aac6
7
- data.tar.gz: ea504d997033b778bb9f458fb33ef1f75bfb4d57c6da1771685f092c3fa1b2362f8bdf82d2a85cbcaade440c42617f21e130c7baab7767f5522f27843f78ca5f
6
+ metadata.gz: e55fbf7a14149928f4ebfaa20f44ceef593152a7233d73b2e29ccbb0b88e5a6fd25086bfc9af04a7721513b9297487f8302b4947d3afdac5cc0e614648a203e2
7
+ data.tar.gz: b722d1aa76cb00b135ae13b1081bc14f488c9bc3da42527046e19096edf796f65df250e086f7c923af43bea72cd9a5154372f90af213570f5c44001634601a58
data/README.md CHANGED
@@ -100,7 +100,7 @@ If you would like to enable twine to create language files in another format, cr
100
100
 
101
101
  #### `generate-string-file`
102
102
 
103
- This command creates an Apple or Android strings file from the master strings data file.
103
+ This command creates an Apple or Android strings file from the master strings data file. If the output file would not contain any translations, twine will exit with an error.
104
104
 
105
105
  $ twine generate-string-file /path/to/strings.txt values-ja.xml --tags common,app1
106
106
  $ twine generate-string-file /path/to/strings.txt Localizable.strings --lang ja --tags mytag
@@ -108,7 +108,7 @@ This command creates an Apple or Android strings file from the master strings da
108
108
 
109
109
  #### `generate-all-string-files`
110
110
 
111
- This command is a convenient way to call `generate-string-file` multiple times. It uses standard Mac OS X, iOS, and Android conventions to figure out exactly which files to create given a parent directory. For example, if you point it to a parent directory containing `en.lproj`, `fr.lproj`, and `ja.lproj` subdirectories, Twine will create a `Localizable.strings` file of the appropriate language in each of them. This is often the command you will want to execute during the build phase of your project.
111
+ This command is a convenient way to call `generate-string-file` multiple times. It uses standard Mac OS X, iOS, and Android conventions to figure out exactly which files to create given a parent directory. For example, if you point it to a parent directory containing `en.lproj`, `fr.lproj`, and `ja.lproj` subdirectories, Twine will create a `Localizable.strings` file of the appropriate language in each of them. However, files that would not contain any translations will not be created; instead warnings will be logged to `stderr`. This is often the command you will want to execute during the build phase of your project.
112
112
 
113
113
  $ twine generate-all-string-files /path/to/strings.txt /path/to/project/locales/directory --tags common,app1
114
114
 
@@ -128,7 +128,7 @@ This command reads in a folder containing many `.strings` or `.xml` files. These
128
128
 
129
129
  #### `generate-loc-drop`
130
130
 
131
- This command is a convenient way to generate a zip file containing files created by the `generate-string-file` command. It is often used for creating a single zip containing a large number of strings in all languages which you can then hand off to your translation team.
131
+ This command is a convenient way to generate a zip file containing files created by the `generate-string-file` command. If a file would not contain any translated strings, it is skipped and a warning is logged to `stderr`. This command can be used to create a single zip containing a large number of strings in all languages which you can then hand off to your translation team.
132
132
 
133
133
  $ twine generate-loc-drop /path/to/strings.txt LocDrop1.zip
134
134
  $ twine generate-loc-drop /path/to/strings.txt LocDrop2.zip --lang en,fr,ja,ko --tags common,app1
data/lib/twine/cli.rb CHANGED
@@ -13,93 +13,103 @@ module Twine
13
13
  }
14
14
 
15
15
  def self.parse(args)
16
- options = {}
16
+ options = { include: :all }
17
+
17
18
  parser = OptionParser.new do |opts|
18
19
  opts.banner = 'Usage: twine COMMAND STRINGS_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]'
19
20
  opts.separator ''
20
- opts.separator 'The purpose of this script is to convert back and forth between multiple data formats, allowing us to treat our strings (and translations) as data stored in a text file. We can then use the data file to create drops for the localization team, consume similar drops returned by the localization team, and create formatted string files to ship with your products. Twine currently supports iOS, OS X, Android, gettext, and jquery-localize string files.'
21
+ opts.separator 'The purpose of this script is to convert back and forth between multiple data formats, allowing us to treat our strings (and translations) as data stored in a text file. We can then use the data file to create drops for the localization team, consume similar drops returned by the localization team, and create formatted string files to ship with your products.'
21
22
  opts.separator ''
22
23
  opts.separator 'Commands:'
23
24
  opts.separator ''
24
- opts.separator 'generate-string-file -- Generates a string file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.'
25
+ opts.separator '- generate-string-file'
26
+ opts.separator ' Generates a string file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.'
25
27
  opts.separator ''
26
- opts.separator 'generate-all-string-files -- Generates all the string files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent strings.'
28
+ opts.separator '- generate-all-string-files'
29
+ opts.separator ' Generates all the string files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent strings.'
27
30
  opts.separator ''
28
- opts.separator 'consume-string-file -- Slurps all of the strings from a translated strings file 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.'
31
+ opts.separator '- consume-string-file'
32
+ opts.separator ' Slurps all of the strings from a translated strings file 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
33
  opts.separator ''
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.'
34
+ opts.separator '- consume-all-string-files'
35
+ opts.separator ' 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.'
31
36
  opts.separator ''
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.'
37
+ opts.separator '- generate-loc-drop'
38
+ opts.separator ' 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.'
33
39
  opts.separator ''
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.'
40
+ opts.separator '- consume-loc-drop'
41
+ opts.separator ' Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-loc-drop command.'
35
42
  opts.separator ''
36
- opts.separator 'validate-strings-file -- Validates that the given strings file is parseable, contains no duplicates, and that every string has a tag. Exits with a non-zero exit code if those criteria are not met.'
43
+ opts.separator '- validate-strings-file'
44
+ opts.separator ' Validates that the given strings file is parseable, contains no duplicates, and that every string has a tag. Exits with a non-zero exit code if those criteria are not met.'
37
45
  opts.separator ''
38
46
  opts.separator 'General Options:'
39
47
  opts.separator ''
40
- opts.on('-l', '--lang LANGUAGES', Array, 'The language code(s) to use for the specified action.') do |langs|
41
- options[:languages] = langs
48
+ opts.on('-l', '--lang LANGUAGES', Array, 'The language code(s) to use for the specified action.') do |l|
49
+ options[:languages] = l
42
50
  end
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|
44
- options[:tags] = tags
51
+ opts.on('-t', '--tags TAG1,TAG2,TAG3', Array, 'The tag(s) to use for the specified action. Only strings with that tag will be processed. Omit this option to match',
52
+ ' all strings in the strings data file.') do |t|
53
+ options[:tags] = t
45
54
  end
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|
47
- options[:untagged] = true
55
+ opts.on('-u', '--[no-]untagged', 'If you have specified tags using the --tags flag, then only those tags will be selected. If you also want to select',
56
+ ' all strings that are untagged, then you can specify this option to do so.') do |u|
57
+ options[:untagged] = u
48
58
  end
49
- formats = Formatters.formatters.map(&:format_name)
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|
51
- unless formats.include?(format.downcase)
52
- raise Twine::Error.new "Invalid format: #{format}"
53
- end
54
- options[:format] = format.downcase
59
+ formats = Formatters.formatters.map(&:format_name).map(&:downcase)
60
+ opts.on('-f', '--format FORMAT', formats, "The file format to read or write: (#{formats.join(', ')}).",
61
+ " Additional formatters can be placed in the formats/ directory.") do |f|
62
+ options[:format] = f
55
63
  end
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|
64
+ opts.on('-a', '--[no-]consume-all', 'Normally, when consuming a string file, Twine will ignore any string keys that do not exist in your master file.') do |a|
57
65
  options[:consume_all] = true
58
66
  end
59
- opts.on('-i', '--include SET', "This flag will determine which strings are included when generating strings files. It's possible values:",
67
+ opts.on('-i', '--include SET', [:all, :translated, :untranslated],
68
+ "This flag will determine which strings are included when generating strings files. It's possible values:",
60
69
  " all: All strings both translated and untranslated for the specified language are included. This is the default value.",
61
70
  " 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
67
- end
68
- unless options[:include]
69
- options[:include] = 'all'
71
+ " untranslated: Only untranslated strings are included.") do |i|
72
+ options[:include] = i
70
73
  end
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|
74
+ 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',
75
+ ' running the consume-string-file or consume-loc-drop commands.') do |o|
72
76
  options[:output_path] = o
73
77
  end
74
78
  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|
75
79
  options[:file_name] = n
76
80
  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
81
+ opts.on('-r', '--[no-]create-folders', "When running the generate-all-string-files command, this flag may be used to create output folders for all languages,",
82
+ " if they don't exist yet. As a result all languages will be exported, not only the ones where an output folder already",
83
+ " exists.") do |r|
84
+ options[:create_folders] = r
79
85
  end
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|
86
+ opts.on('-d', '--developer-language LANG', 'When writing the strings data file, set the specified language as the "developer language". In practice, this just',
87
+ ' means that this language will appear first in the strings data file. When generating files this language will be',
88
+ ' used as default language and its translations will be used if a key is not localized for the output language.') do |d|
81
89
  options[:developer_language] = d
82
90
  end
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|
84
- options[:consume_comments] = true
91
+ opts.on('-c', '--[no-]consume-comments', 'Normally, when consuming a string file, Twine will ignore all comments in the file. With this flag set, any comments',
92
+ ' encountered will be read and parsed into the strings data file. This is especially useful when creating your first',
93
+ ' strings data file from an existing project.') do |c|
94
+ options[:consume_comments] = c
85
95
  end
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
88
- raise Twine::Error.new "The --encoding flag is only supported on Ruby 1.9.3 or greater."
89
- end
96
+ 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',
97
+ ' files. For example, you could use this to write Apple .strings files in UTF-16. When reading files, Twine does its best',
98
+ " to determine the encoding automatically. However, if the files are UTF-16 without BOM, you need to specify if it's",
99
+ ' UTF-16LE or UTF16-BE.') do |e|
90
100
  options[:output_encoding] = e
91
101
  end
92
- opts.on('--validate', 'Validate the strings file before formatting it') do
93
- options[:validate] = true
102
+ opts.on('--[no-]validate', 'Validate the strings file before formatting it.') do |validate|
103
+ options[:validate] = validate
94
104
  end
95
- opts.on('-p', '--pedantic', 'When validating a strings file, perform additional checks that go beyond pure validity (like presence of tags)') do
96
- options[:pedantic] = true
105
+ opts.on('-p', '--[no-]pedantic', 'When validating a strings file, perform additional checks that go beyond pure validity (like presence of tags).') do |p|
106
+ options[:pedantic] = p
97
107
  end
98
108
  opts.on('-h', '--help', 'Show this message.') do |h|
99
109
  puts opts.help
100
110
  exit
101
111
  end
102
- opts.on('--version', 'Print the version number and exit.') do |x|
112
+ opts.on('--version', 'Print the version number and exit.') do
103
113
  puts "Twine version #{Twine::VERSION}"
104
114
  exit
105
115
  end
@@ -114,11 +124,16 @@ module Twine
114
124
  opts.separator '> twine consume-loc-drop strings.txt LocDrop5.zip'
115
125
  opts.separator '> twine validate-strings-file strings.txt'
116
126
  end
117
- parser.parse! args
127
+ begin
128
+ parser.parse! args
129
+ rescue OptionParser::ParseError => e
130
+ Twine::stderr.puts e.message
131
+ exit false
132
+ end
118
133
 
119
134
  if args.length == 0
120
135
  puts parser.help
121
- exit
136
+ exit false
122
137
  end
123
138
 
124
139
  number_of_needed_arguments = NEEDED_COMMAND_ARGUMENTS[args[0]]
@@ -1,20 +1,22 @@
1
1
  module Twine
2
2
  module Encoding
3
- def self.encoding_for_path path
4
- File.open(path, 'rb') do |f|
5
- begin
6
- a = f.readbyte
7
- b = f.readbyte
8
- if (a == 0xfe && b == 0xff)
9
- return 'UTF-16BE'
10
- elsif (a == 0xff && b == 0xfe)
11
- return 'UTF-16LE'
12
- end
13
- rescue EOFError
14
- end
15
- end
16
3
 
17
- 'UTF-8'
4
+ def self.bom(path)
5
+ first_bytes = IO.binread(path, 2)
6
+ return nil unless first_bytes
7
+ first_bytes = first_bytes.codepoints.map.to_a
8
+ return 'UTF-16BE' if first_bytes == [0xFE, 0xFF]
9
+ return 'UTF-16LE' if first_bytes == [0xFF, 0xFE]
10
+ rescue EOFError
11
+ return nil
12
+ end
13
+
14
+ def self.has_bom?(path)
15
+ !bom(path).nil?
16
+ end
17
+
18
+ def self.encoding_for_path(path)
19
+ bom(path) || 'UTF-8'
18
20
  end
19
21
  end
20
22
  end
@@ -83,15 +83,20 @@ module Twine
83
83
  lang
84
84
  end
85
85
 
86
- def read_file(path, lang)
87
- raise NotImplementedError.new("You must implement read_file in your formatter class.")
86
+ def read(io, lang)
87
+ raise NotImplementedError.new("You must implement read in your formatter class.")
88
88
  end
89
89
 
90
- def format_file(strings, lang)
90
+ def format_file(lang)
91
+ output_processor = Processors::OutputProcessor.new(@strings, @options)
92
+ processed_strings = output_processor.process(lang)
93
+
94
+ return nil if processed_strings.strings_map.empty?
95
+
91
96
  header = format_header(lang)
92
97
  result = ""
93
98
  result += header + "\n" if header
94
- result += format_sections(strings, lang)
99
+ result += format_sections(processed_strings, lang)
95
100
  end
96
101
 
97
102
  def format_header(lang)
@@ -153,50 +158,6 @@ module Twine
153
158
  def escape_quotes(text)
154
159
  text.gsub('"', '\\\\"')
155
160
  end
156
-
157
- def write_file(path, lang)
158
- output_processor = Processors::OutputProcessor.new(@strings, @options)
159
- processed_strings = output_processor.process(lang)
160
-
161
- encoding = @options[:output_encoding] || 'UTF-8'
162
- File.open(path, "w:#{encoding}") do |f|
163
- f.puts format_file(processed_strings, lang)
164
- end
165
- end
166
-
167
- def write_all_files(path)
168
- file_name = @options[:file_name] || default_file_name
169
- if @options[:create_folders]
170
- @strings.language_codes.each do |lang|
171
- output_path = File.join(path, output_path_for_language(lang))
172
-
173
- FileUtils.mkdir_p(output_path)
174
-
175
- file_path = File.join(output_path, file_name)
176
- write_file(file_path, lang)
177
- end
178
- else
179
- language_written = false
180
- Dir.foreach(path) do |item|
181
- next if item == "." or item == ".."
182
-
183
- item = File.join(path, item)
184
- next unless File.directory?(item)
185
-
186
- lang = determine_language_given_path(item)
187
- next unless lang
188
-
189
- file_path = File.join(item, file_name)
190
- write_file(file_path, lang)
191
- language_written = true
192
- end
193
-
194
- if !language_written
195
- raise Twine::Error.new("Failed to generate any files: No languages found at #{path}")
196
- end
197
- end
198
- end
199
-
200
161
  end
201
162
  end
202
163
  end
@@ -7,11 +7,11 @@ module Twine
7
7
  class Android < Abstract
8
8
  include Twine::Placeholders
9
9
 
10
- LANG_CODES = Hash[
11
- 'zh' => 'zh-Hans',
10
+ LANG_MAPPINGS = Hash[
12
11
  'zh-rCN' => 'zh-Hans',
13
12
  'zh-rHK' => 'zh-Hant',
14
13
  'en-rGB' => 'en-UK',
14
+ 'zh' => 'zh-Hans',
15
15
  'in' => 'id',
16
16
  'nb' => 'no'
17
17
  # TODO: spanish
@@ -39,10 +39,12 @@ module Twine
39
39
  if segment == 'values'
40
40
  return @strings.language_codes[0]
41
41
  else
42
- match = /^values-(.*)$/.match(segment)
42
+ # 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
+ # see http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources
44
+ match = /^values-([a-z]{2}(-r[a-z]{2})?)$/i.match(segment)
43
45
  if match
44
46
  lang = match[1]
45
- lang = LANG_CODES.fetch(lang, lang)
47
+ lang = LANG_MAPPINGS.fetch(lang, lang)
46
48
  lang.sub!('-r', '-')
47
49
  return lang
48
50
  end
@@ -52,6 +54,10 @@ module Twine
52
54
  return
53
55
  end
54
56
 
57
+ def output_path_for_language(lang)
58
+ "values-" + (LANG_MAPPINGS.key(lang) || lang)
59
+ end
60
+
55
61
  def set_translation_for_key(key, lang, value)
56
62
  value = CGI.unescapeHTML(value)
57
63
  value.gsub!('\\\'', '\'')
@@ -62,7 +68,7 @@ module Twine
62
68
  super(key, lang, value)
63
69
  end
64
70
 
65
- def read_file(path, lang)
71
+ def read(io, lang)
66
72
  resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
67
73
  key_regex = /<string name="(\w+)">/
68
74
  comment_regex = /<!-- (.*) -->/
@@ -71,27 +77,25 @@ module Twine
71
77
  value = nil
72
78
  comment = nil
73
79
 
74
- File.open(path, 'r:UTF-8') do |f|
75
- content_match = resources_regex.match(f.read)
76
- if content_match
77
- for line in content_match[1].split(/\r?\n/)
78
- key_match = key_regex.match(line)
79
- if key_match
80
- key = key_match[1]
81
- value_match = value_regex.match(line)
82
- value = value_match ? value_match[1] : ""
83
-
84
- set_translation_for_key(key, lang, value)
85
- if comment and comment.length > 0 and !comment.start_with?("SECTION:")
86
- set_comment_for_key(key, comment)
87
- end
88
- comment = nil
89
- end
90
-
91
- comment_match = comment_regex.match(line)
92
- if comment_match
93
- comment = comment_match[1]
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)
94
92
  end
93
+ comment = nil
94
+ end
95
+
96
+ comment_match = comment_regex.match(line)
97
+ if comment_match
98
+ comment = comment_match[1]
95
99
  end
96
100
  end
97
101
  end
@@ -106,7 +110,7 @@ module Twine
106
110
 
107
111
  result += super + "\n"
108
112
 
109
- result += '</resources>'
113
+ result += "</resources>\n"
110
114
  end
111
115
 
112
116
  def format_section_header(section)
@@ -35,56 +35,28 @@ module Twine
35
35
  "#{lang}.lproj"
36
36
  end
37
37
 
38
- def read_file(path, lang)
39
- encoding = Twine::Encoding.encoding_for_path(path)
40
- sep = nil
41
- if !encoding.respond_to?(:encode)
42
- # This code is not necessary in 1.9.3 and does not work as it did in 1.8.7.
43
- if encoding.end_with? 'LE'
44
- sep = "\x0a\x00"
45
- elsif encoding.end_with? 'BE'
46
- sep = "\x00\x0a"
47
- else
48
- sep = "\n"
49
- end
50
- end
51
-
52
- if encoding.index('UTF-16')
53
- mode = "rb:#{encoding}"
54
- else
55
- mode = "r:#{encoding}"
56
- end
57
-
58
- File.open(path, mode) do |f|
59
- last_comment = nil
60
- while line = (sep) ? f.gets(sep) : f.gets
61
- if encoding.index('UTF-16')
62
- if line.respond_to? :encode!
63
- line.encode!('UTF-8')
64
- else
65
- require 'iconv'
66
- line = Iconv.iconv('UTF-8', encoding, line).join
67
- end
68
- end
69
- match = /"((?:[^"\\]|\\.)+)"\s*=\s*"((?:[^"\\]|\\.)*)"/.match(line)
70
- if match
71
- key = match[1]
72
- key.gsub!('\\"', '"')
73
- value = match[2]
74
- value.gsub!('\\"', '"')
75
- set_translation_for_key(key, lang, value)
76
- if last_comment
77
- set_comment_for_key(key, last_comment)
78
- end
79
- end
80
-
81
- match = /\/\* (.*) \*\//.match(line)
82
- if match
83
- last_comment = match[1]
84
- else
85
- last_comment = nil
38
+ def read(io, lang)
39
+ last_comment = nil
40
+ while line = io.gets
41
+ # matches a `key = "value"` line, where key may be quoted or unquoted. The former may also contain escaped characters
42
+ match = /^\s*((?:"(?:[^"\\]|\\.)+")|(?:[^"\s=]+))\s*=\s*"((?:[^"\\]|\\.)*)"/.match(line)
43
+ if match
44
+ key = match[1]
45
+ key = key[1..-2] if key[0] == '"' and key[-1] == '"'
46
+ key.gsub!('\\"', '"')
47
+ value = match[2]
48
+ value.gsub!('\\"', '"')
49
+ set_translation_for_key(key, lang, value)
50
+ if last_comment
51
+ set_comment_for_key(key, last_comment)
86
52
  end
53
+ end
87
54
 
55
+ match = /\/\* (.*) \*\//.match(line)
56
+ if match
57
+ last_comment = match[1]
58
+ else
59
+ last_comment = nil
88
60
  end
89
61
  end
90
62
  end