twine 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/twine CHANGED
@@ -4,5 +4,5 @@ begin
4
4
  Twine::Runner.run(ARGV)
5
5
  rescue Twine::Error => e
6
6
  STDERR.puts e.message
7
- abort
7
+ exit
8
8
  end
@@ -3,6 +3,7 @@ module Twine
3
3
  end
4
4
 
5
5
  require 'twine/cli'
6
+ require 'twine/encoding'
6
7
  require 'twine/formatters'
7
8
  require 'twine/runner'
8
9
  require 'twine/stringsfile'
@@ -25,7 +25,7 @@ module Twine
25
25
  opts.separator ''
26
26
  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.'
27
27
  opts.separator ''
28
- 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.'
28
+ 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 --all has been specified on the command line.'
29
29
  opts.separator ''
30
30
  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.'
31
31
  opts.separator ''
@@ -53,8 +53,14 @@ module Twine
53
53
  end
54
54
  @options[:format] = lformat
55
55
  end
56
- opts.on('-a', '--all', 'Normally, when consuming a string file, Twine will ignore any string keys that do not exist in your master file. This flag will force those missing strings to be added to your master file.') do |a|
57
- @options[:consume_all] = true
56
+ opts.on('-a', '--all', 'Normally, when consuming a string file, Twine will ignore any string keys that do not exist in your master file. This flag will also cause any Android string files that are generated to include strings that have not yet been translated for the current language.') do |a|
57
+ @options[:consume_generate_all] = true
58
+ end
59
+ 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|
60
+ if !"".respond_to?(:encode)
61
+ raise Twine::Error.new "The --encoding flag is only supported on Ruby 1.9.3 or greater."
62
+ end
63
+ @options[:output_encoding] = e
58
64
  end
59
65
  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|
60
66
  @options[:output_path] = o
@@ -128,6 +134,7 @@ module Twine
128
134
  raise Twine::Error.new 'Please only specify a single language for the consume-string-file command.'
129
135
  end
130
136
  when 'generate-loc-drop'
137
+ @options[:consume_generate_all] = true
131
138
  if @args.length == 3
132
139
  @options[:output_path] = @args[2]
133
140
  elsif @args.length > 3
@@ -0,0 +1,20 @@
1
+ module Twine
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
+
17
+ 'UTF-8'
18
+ end
19
+ end
20
+ end
@@ -16,7 +16,7 @@ module Twine
16
16
  def set_translation_for_key(key, lang, value)
17
17
  if @strings.strings_map.include?(key)
18
18
  @strings.strings_map[key].translations[lang] = value
19
- elsif @options[:consume_all]
19
+ elsif @options[:consume_generate_all]
20
20
  STDERR.puts "Adding new string '#{key}' to strings data file."
21
21
  arr = @strings.sections.select { |s| s.name == 'Uncategorized' }
22
22
  current_section = arr ? arr[0] : nil
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'cgi'
4
3
  require 'rexml/document'
5
4
 
6
5
  module Twine
@@ -9,9 +8,21 @@ module Twine
9
8
  FORMAT_NAME = 'android'
10
9
  EXTENSION = '.xml'
11
10
  DEFAULT_FILE_NAME = 'strings.xml'
11
+ LANG_CODES = Hash[
12
+ 'zh' => 'zh-Hans',
13
+ 'zh-rCN' => 'zh-Hans',
14
+ 'zh-rHK' => 'zh-Hant',
15
+ 'en-rGB' => 'en-UK',
16
+ 'in' => 'id',
17
+ 'nb' => 'no'
18
+ # TODO: spanish
19
+ ]
20
+ DEFAULT_LANG_CODES = Hash[
21
+ 'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
22
+ ]
12
23
 
13
24
  def self.can_handle_directory?(path)
14
- Dir.entries(path).any? { |item| /^values-.+$/.match(item) }
25
+ Dir.entries(path).any? { |item| /^values.*$/.match(item) }
15
26
  end
16
27
 
17
28
  def default_file_name
@@ -21,11 +32,16 @@ module Twine
21
32
  def determine_language_given_path(path)
22
33
  path_arr = path.split(File::SEPARATOR)
23
34
  path_arr.each do |segment|
24
- match = /^values-(.*)$/.match(segment)
25
- if match
26
- lang = match[1]
27
- lang.sub!('-r', '-')
28
- return lang
35
+ if segment == 'values'
36
+ return @strings.language_codes[0]
37
+ else
38
+ match = /^values-(.*)$/.match(segment)
39
+ if match
40
+ lang = match[1]
41
+ lang = LANG_CODES.fetch(lang, lang)
42
+ lang.sub!('-r', '-')
43
+ return lang
44
+ end
29
45
  end
30
46
  end
31
47
 
@@ -40,22 +56,28 @@ module Twine
40
56
  key = ele.attributes["name"]
41
57
  value = ele.text || ''
42
58
  value.gsub!('\\\'', '\'')
59
+ value.gsub!('\\"', '"')
43
60
  value.gsub!(/\n/, '')
44
- value.gsub!('%s', '%@')
61
+ value.gsub!(/%([0-9\$]*)s/, '%\1@')
62
+ value.gsub!('&lt;', '<')
63
+ value.gsub!('&amp;', '&')
45
64
  set_translation_for_key(key, lang, value)
46
65
  end
47
66
  end
48
67
  end
49
68
 
50
69
  def write_file(path, lang)
51
- default_lang = @strings.language_codes[0]
70
+ default_lang = nil
71
+ if DEFAULT_LANG_CODES.has_key?(lang)
72
+ default_lang = DEFAULT_LANG_CODES[lang]
73
+ end
52
74
  File.open(path, 'w:UTF-8') do |f|
53
75
  f.puts "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine -->\n<!-- Language: #{lang} -->"
54
76
  f.write '<resources>'
55
77
  @strings.sections.each do |section|
56
78
  printed_section = false
57
79
  section.rows.each do |row|
58
- if row.matches_tags?(options[:tags])
80
+ if row.matches_tags?(@options[:tags])
59
81
  if !printed_section
60
82
  f.puts ''
61
83
  if section.name && section.name.length > 0
@@ -66,22 +88,35 @@ module Twine
66
88
  end
67
89
 
68
90
  key = row.key
69
- key = CGI.escapeHTML(key)
70
91
 
71
92
  value = row.translated_string_for_lang(lang, default_lang)
72
- value.gsub!('\'', '\\\\\'')
73
- value.gsub!('%@', '%s')
74
- value = CGI.escapeHTML(value)
75
-
76
- comment = row.comment
77
- if comment
78
- comment = comment.gsub('--', '—')
93
+ if !value && @options[:consume_generate_all]
94
+ value = row.translated_string_for_lang(@strings.language_codes[0])
79
95
  end
80
96
 
81
- if comment && comment.length > 0
82
- f.puts "\t<!-- #{comment} -->\n"
97
+ if value # if values is nil, there was no appropriate translation, so let Android handle the defaulting
98
+ value = String.new(value) # use a copy to prevent modifying the original
99
+
100
+ # Android enforces the following rules on the values
101
+ # 1) apostrophes and quotes must be escaped with a backslash
102
+ value.gsub!('\'', '\\\\\'')
103
+ value.gsub!('"', '\\\\"')
104
+ # 2) ampersand and less-than must be in XML-escaped form
105
+ value.gsub!('&', '&amp;')
106
+ value.gsub!('<', '&lt;')
107
+ # 3) use "s" instead of "@" for substituting strings
108
+ value.gsub!(/%([0-9\$]*)@/, '%\1s')
109
+
110
+ comment = row.comment
111
+ if comment
112
+ comment = comment.gsub('--', '—')
113
+ end
114
+
115
+ if comment && comment.length > 0
116
+ f.puts "\t<!-- #{comment} -->\n"
117
+ end
118
+ f.puts "\t<string name=\"#{key}\">#{value}</string>"
83
119
  end
84
- f.puts "\t<string name=\"#{key}\">#{value}</string>"
85
120
  end
86
121
  end
87
122
  end
@@ -26,8 +26,35 @@ module Twine
26
26
  end
27
27
 
28
28
  def read_file(path, lang)
29
- File.open(path, 'r:UTF-8') do |f|
30
- while line = f.gets
29
+ encoding = Twine::Encoding.encoding_for_path(path)
30
+ sep = nil
31
+ if !encoding.respond_to?(:encode)
32
+ # This code is not necessary in 1.9.3 and does not work as it did in 1.8.7.
33
+ if encoding.end_with? 'LE'
34
+ sep = "\x0a\x00"
35
+ elsif encoding.end_with? 'BE'
36
+ sep = "\x00\x0a"
37
+ else
38
+ sep = "\n"
39
+ end
40
+ end
41
+
42
+ if encoding.index('UTF-16')
43
+ mode = "rb:#{encoding}"
44
+ else
45
+ mode = "r:#{encoding}"
46
+ end
47
+
48
+ File.open(path, mode) do |f|
49
+ while line = (sep) ? f.gets(sep) : f.gets
50
+ if encoding.index('UTF-16')
51
+ if line.respond_to? :encode!
52
+ line.encode!('UTF-8')
53
+ else
54
+ require 'iconv'
55
+ line = Iconv.iconv('UTF-8', encoding, line).join
56
+ end
57
+ end
31
58
  match = /"((?:[^"\\]|\\.)+)"\s*=\s*"((?:[^"\\]|\\.)*)"/.match(line)
32
59
  if match
33
60
  key = match[1]
@@ -42,12 +69,13 @@ module Twine
42
69
 
43
70
  def write_file(path, lang)
44
71
  default_lang = @strings.language_codes[0]
45
- File.open(path, 'w:UTF-8') do |f|
72
+ encoding = @options[:output_encoding] || 'UTF-8'
73
+ File.open(path, "w:#{encoding}") do |f|
46
74
  f.puts "/**\n * iOS Strings File\n * Generated by Twine\n * Language: #{lang}\n */"
47
75
  @strings.sections.each do |section|
48
76
  printed_section = false
49
77
  section.rows.each do |row|
50
- if row.matches_tags?(options[:tags])
78
+ if row.matches_tags?(@options[:tags])
51
79
  if !printed_section
52
80
  f.puts ''
53
81
  if section.name && section.name.length > 0
@@ -137,7 +137,7 @@ module Twine
137
137
  file_name = lang + formatter.class::EXTENSION
138
138
  real_path = File.join(dir, file_name)
139
139
  zip_path = File.join('Locales', file_name)
140
- formatter.write_file(real_path, lang, @options[:tags], @strings)
140
+ formatter.write_file(real_path, lang)
141
141
  zipfile.add(zip_path, real_path)
142
142
  end
143
143
  end
@@ -41,7 +41,18 @@ module Twine
41
41
  end
42
42
 
43
43
  def translated_string_for_lang(lang, default_lang=nil)
44
- @translations[lang] || @translations[default_lang]
44
+ if @translations[lang]
45
+ return @translations[lang]
46
+ elsif default_lang.respond_to?("each")
47
+ default_lang.each do |def_lang|
48
+ if @translations[def_lang]
49
+ return @translations[def_lang]
50
+ end
51
+ end
52
+ return nil
53
+ else
54
+ return @translations[default_lang]
55
+ end
45
56
  end
46
57
  end
47
58
 
@@ -94,7 +105,7 @@ module Twine
94
105
  parsed = true
95
106
  end
96
107
  else
97
- match = /^([^=]+)=(.+)$/.match(line)
108
+ match = /^([^=]+)=(.*)$/.match(line)
98
109
  if match
99
110
  key = match[1].strip
100
111
  value = match[2].strip
@@ -1,3 +1,3 @@
1
1
  module Twine
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -5,7 +5,7 @@ class TwineTest < Test::Unit::TestCase
5
5
  def test_generate_string_file_1
6
6
  Dir.mktmpdir do |dir|
7
7
  output_path = File.join(dir, 'fr.xml')
8
- Twine::Runner.run(%W(generate-string-file test/fixtures/strings-1.txt #{output_path}))
8
+ Twine::Runner.run(%W(generate-string-file test/fixtures/strings-1.txt #{output_path} --all))
9
9
  assert_equal(File.read('test/fixtures/test-output-1.txt'), File.read(output_path))
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-11 00:00:00.000000000 Z
12
+ date: 2012-02-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rubyzip
16
- requirement: &70180596581660 !ruby/object:Gem::Requirement
16
+ requirement: &70216088301260 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.9.5
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70180596581660
24
+ version_requirements: *70216088301260
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &70180596581180 !ruby/object:Gem::Requirement
27
+ requirement: &70216088300680 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: 0.9.2
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70180596581180
35
+ version_requirements: *70216088300680
36
36
  description: ! " Twine is a command line tool for managing your strings and their
37
37
  translations.\n \n It is geared toward Mac OS X, iOS, and Android developers.\n"
38
38
  email: twine@mobiata.com
@@ -45,6 +45,7 @@ files:
45
45
  - README.md
46
46
  - LICENSE
47
47
  - lib/twine/cli.rb
48
+ - lib/twine/encoding.rb
48
49
  - lib/twine/formatters/abstract.rb
49
50
  - lib/twine/formatters/android.rb
50
51
  - lib/twine/formatters/apple.rb