traduco 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ require 'traduco/formatters/abstract'
2
+ require 'traduco/formatters/android'
3
+ require 'traduco/formatters/apple'
4
+ require 'traduco/formatters/flash'
5
+ require 'traduco/formatters/gettext'
6
+ require 'traduco/formatters/jquery'
7
+
8
+ module Traduco
9
+ module Formatters
10
+ FORMATTERS = [Formatters::Apple, Formatters::Android, Formatters::Gettext, Formatters::JQuery, Formatters::Flash]
11
+ end
12
+ end
@@ -0,0 +1,164 @@
1
+ module Traduco
2
+ module Formatters
3
+ class Abstract
4
+ attr_accessor :strings
5
+ attr_accessor :options
6
+
7
+ def self.can_handle_directory?(path)
8
+ return false
9
+ end
10
+
11
+ def initialize(strings, options)
12
+ @strings = strings
13
+ @options = options
14
+ end
15
+
16
+ def iosify_substitutions(str)
17
+ # 1) use "@" instead of "s" for substituting strings
18
+ str.gsub!(/%([0-9\$]*)s/, '%\1@')
19
+
20
+ # 2) if substitutions are numbered, see if we can remove the numbering safely
21
+ expectedSub = 1
22
+ startFound = false
23
+ foundSub = 0
24
+ str.each_char do |c|
25
+ if startFound
26
+ if c == "%"
27
+ # this is a literal %, keep moving
28
+ startFound = false
29
+ elsif c.match(/\d/)
30
+ foundSub *= 10
31
+ foundSub += Integer(c)
32
+ elsif c == "$"
33
+ if expectedSub == foundSub
34
+ # okay to keep going
35
+ startFound = false
36
+ expectedSub += 1
37
+ else
38
+ # the numbering appears to be important (or non-existent), leave it alone
39
+ return str
40
+ end
41
+ end
42
+ elsif c == "%"
43
+ startFound = true
44
+ foundSub = 0
45
+ end
46
+ end
47
+
48
+ # if we got this far, then the numbering (if any) is in order left-to-right and safe to remove
49
+ if expectedSub > 1
50
+ str.gsub!(/%\d+\$(.)/, '%\1')
51
+ end
52
+
53
+ return str
54
+ end
55
+
56
+ def androidify_substitutions(str)
57
+ # 1) use "s" instead of "@" for substituting strings
58
+ str.gsub!(/%([0-9\$]*)@/, '%\1s')
59
+
60
+ # 1a) escape strings that begin with a lone "@"
61
+ str.sub!(/^@ /, '\\@ ')
62
+
63
+ # 2) if there is more than one substitution in a string, make sure they are numbered
64
+ substituteCount = 0
65
+ startFound = false
66
+ str.each_char do |c|
67
+ if startFound
68
+ if c == "%"
69
+ # ignore as this is a literal %
70
+ elsif c.match(/\d/)
71
+ # leave the string alone if it already has numbered substitutions
72
+ return str
73
+ else
74
+ substituteCount += 1
75
+ end
76
+ startFound = false
77
+ elsif c == "%"
78
+ startFound = true
79
+ end
80
+ end
81
+
82
+ if substituteCount > 1
83
+ currentSub = 1
84
+ startFound = false
85
+ newstr = ""
86
+ str.each_char do |c|
87
+ if startFound
88
+ if !(c == "%")
89
+ newstr = newstr + "#{currentSub}$"
90
+ currentSub += 1
91
+ end
92
+ startFound = false
93
+ elsif c == "%"
94
+ startFound = true
95
+ end
96
+ newstr = newstr + c
97
+ end
98
+ return newstr
99
+ else
100
+ return str
101
+ end
102
+ end
103
+
104
+ def set_translation_for_key(key, lang, value)
105
+ if @strings.strings_map.include?(key)
106
+ @strings.strings_map[key].translations[lang] = value
107
+ elsif @options[:consume_all]
108
+ STDERR.puts "Adding new string '#{key}' to strings data file."
109
+ arr = @strings.sections.select { |s| s.name == 'Uncategorized' }
110
+ current_section = arr ? arr[0] : nil
111
+ if !current_section
112
+ current_section = StringsSection.new('Uncategorized')
113
+ @strings.sections.insert(0, current_section)
114
+ end
115
+ current_row = StringsRow.new(key)
116
+ current_section.rows << current_row
117
+ @strings.strings_map[key] = current_row
118
+ @strings.strings_map[key].translations[lang] = value
119
+ else
120
+ STDERR.puts "Warning: '#{key}' not found in strings data file."
121
+ end
122
+ if !@strings.language_codes.include?(lang)
123
+ @strings.add_language_code(lang)
124
+ end
125
+ end
126
+
127
+ def set_comment_for_key(key, comment)
128
+ if @strings.strings_map.include?(key)
129
+ @strings.strings_map[key].comment = comment
130
+ end
131
+ end
132
+
133
+ def default_file_name
134
+ raise NotImplementedError.new("You must implement default_file_name in your formatter class.")
135
+ end
136
+
137
+ def determine_language_given_path(path)
138
+ raise NotImplementedError.new("You must implement determine_language_given_path in your formatter class.")
139
+ end
140
+
141
+ def read_file(path, lang)
142
+ raise NotImplementedError.new("You must implement read_file in your formatter class.")
143
+ end
144
+
145
+ def write_file(path, lang)
146
+ raise NotImplementedError.new("You must implement write_file in your formatter class.")
147
+ end
148
+
149
+ def write_all_files(path)
150
+ if !File.directory?(path)
151
+ raise Traduco::Error.new("Directory does not exist: #{path}")
152
+ end
153
+
154
+ file_name = @options[:file_name] || default_file_name
155
+ Dir.foreach(path) do |item|
156
+ lang = determine_language_given_path(item)
157
+ if lang
158
+ write_file(File.join(path, item, file_name), lang)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,152 @@
1
+ # encoding: utf-8
2
+ require 'cgi'
3
+ require 'rexml/document'
4
+
5
+ module Traduco
6
+ module Formatters
7
+ class Android < Abstract
8
+ FORMAT_NAME = 'android'
9
+ EXTENSION = '.xml'
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
+ ]
23
+
24
+ def self.can_handle_directory?(path)
25
+ Dir.entries(path).any? { |item| /^values.*$/.match(item) }
26
+ end
27
+
28
+ def default_file_name
29
+ return DEFAULT_FILE_NAME
30
+ end
31
+
32
+ def determine_language_given_path(path)
33
+ path_arr = path.split(File::SEPARATOR)
34
+ path_arr.each do |segment|
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
45
+ end
46
+ end
47
+
48
+ return
49
+ end
50
+
51
+ def read_file(path, lang)
52
+ resources_regex = /<resources>(.*)<\/resources>/m
53
+ key_regex = /<string name="(\w+)">/
54
+ comment_regex = /<!-- (.*) -->/
55
+ value_regex = /<string name="\w+">(.*)<\/string>/
56
+ key = nil
57
+ value = nil
58
+ comment = nil
59
+
60
+ File.open(path, 'r:UTF-8') do |f|
61
+ content_match = resources_regex.match(f.read)
62
+ if content_match
63
+ for line in content_match[1].split(/\r?\n/)
64
+ key_match = key_regex.match(line)
65
+ if key_match
66
+ key = key_match[1]
67
+ value_match = value_regex.match(line)
68
+ if value_match
69
+ value = value_match[1]
70
+ value = CGI.unescapeHTML(value)
71
+ value.gsub!('\\\'', '\'')
72
+ value.gsub!('\\"', '"')
73
+ value = iosify_substitutions(value)
74
+ else
75
+ value = ""
76
+ end
77
+ set_translation_for_key(key, lang, value)
78
+ if comment and comment.length > 0 and !comment.start_with?("SECTION:")
79
+ set_comment_for_key(key, comment)
80
+ end
81
+ comment = nil
82
+ end
83
+
84
+ comment_match = comment_regex.match(line)
85
+ if comment_match
86
+ comment = comment_match[1]
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def write_file(path, lang)
94
+ default_lang = nil
95
+ if DEFAULT_LANG_CODES.has_key?(lang)
96
+ default_lang = DEFAULT_LANG_CODES[lang]
97
+ end
98
+ File.open(path, 'w:UTF-8') do |f|
99
+ f.puts "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Traduco #{Traduco::VERSION} -->\n<!-- Language: #{lang} -->"
100
+ f.write '<resources>'
101
+ @strings.sections.each do |section|
102
+ printed_section = false
103
+ section.rows.each do |row|
104
+ if row.matches_tags?(@options[:tags], @options[:untagged])
105
+ if !printed_section
106
+ f.puts ''
107
+ if section.name && section.name.length > 0
108
+ section_name = section.name.gsub('--', '—')
109
+ f.puts "\t<!-- SECTION: #{section_name} -->"
110
+ end
111
+ printed_section = true
112
+ end
113
+
114
+ key = row.key
115
+
116
+ value = row.translated_string_for_lang(lang, default_lang)
117
+ if !value && @options[:include_untranslated]
118
+ value = row.translated_string_for_lang(@strings.language_codes[0])
119
+ end
120
+
121
+ if value # if values is nil, there was no appropriate translation, so let Android handle the defaulting
122
+ value = String.new(value) # use a copy to prevent modifying the original
123
+
124
+ # Android enforces the following rules on the values
125
+ # 1) apostrophes and quotes must be escaped with a backslash
126
+ value.gsub!('\'', '\\\\\'')
127
+ value.gsub!('"', '\\\\"')
128
+ # 2) HTML escape the string
129
+ value = CGI.escapeHTML(value)
130
+ # 3) fix substitutions (e.g. %s/%@)
131
+ value = androidify_substitutions(value)
132
+
133
+ comment = row.comment
134
+ if comment
135
+ comment = comment.gsub('--', '—')
136
+ end
137
+
138
+ if comment && comment.length > 0
139
+ f.puts "\t<!-- #{comment} -->\n"
140
+ end
141
+ f.puts "\t<string name=\"#{key}\">#{value}</string>"
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ f.puts '</resources>'
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,125 @@
1
+ module Traduco
2
+ module Formatters
3
+ class Apple < Abstract
4
+ FORMAT_NAME = 'apple'
5
+ EXTENSION = '.strings'
6
+ DEFAULT_FILE_NAME = 'Localizable.strings'
7
+
8
+ def self.can_handle_directory?(path)
9
+ Dir.entries(path).any? { |item| /^.+\.lproj$/.match(item) }
10
+ end
11
+
12
+ def default_file_name
13
+ return DEFAULT_FILE_NAME
14
+ end
15
+
16
+ def determine_language_given_path(path)
17
+ path_arr = path.split(File::SEPARATOR)
18
+ path_arr.each do |segment|
19
+ match = /^(.+)\.lproj$/.match(segment)
20
+ if match
21
+ return match[1]
22
+ end
23
+ end
24
+
25
+ return
26
+ end
27
+
28
+ def read_file(path, lang)
29
+ encoding = Traduco::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
+ last_comment = nil
50
+ while line = (sep) ? f.gets(sep) : f.gets
51
+ if encoding.index('UTF-16')
52
+ if line.respond_to? :encode!
53
+ line.encode!('UTF-8')
54
+ else
55
+ require 'iconv'
56
+ line = Iconv.iconv('UTF-8', encoding, line).join
57
+ end
58
+ end
59
+ match = /"((?:[^"\\]|\\.)+)"\s*=\s*"((?:[^"\\]|\\.)*)"/.match(line)
60
+ if match
61
+ key = match[1]
62
+ key.gsub!('\\"', '"')
63
+ value = match[2]
64
+ value.gsub!('\\"', '"')
65
+ value = iosify_substitutions(value)
66
+ set_translation_for_key(key, lang, value)
67
+ if last_comment
68
+ set_comment_for_key(key, last_comment)
69
+ end
70
+ end
71
+ if @options[:consume_comments]
72
+ match = /\/\* (.*) \*\//.match(line)
73
+ if match
74
+ last_comment = match[1]
75
+ else
76
+ last_comment = nil
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def write_file(path, lang)
84
+ default_lang = @strings.language_codes[0]
85
+ encoding = @options[:output_encoding] || 'UTF-8'
86
+ File.open(path, "w:#{encoding}") do |f|
87
+ f.puts "/**\n * Apple Strings File\n * Generated by Traduco #{Traduco::VERSION}\n * Language: #{lang}\n */"
88
+ @strings.sections.each do |section|
89
+ printed_section = false
90
+ section.rows.each do |row|
91
+ if row.matches_tags?(@options[:tags], @options[:untagged])
92
+ f.puts ''
93
+ if !printed_section
94
+ if section.name && section.name.length > 0
95
+ f.print "/********** #{section.name} **********/\n\n"
96
+ end
97
+ printed_section = true
98
+ end
99
+
100
+ key = row.key
101
+ key = key.gsub('"', '\\\\"')
102
+
103
+ value = row.translated_string_for_lang(lang, default_lang)
104
+ if value
105
+ value = value.gsub('"', '\\\\"')
106
+
107
+ comment = row.comment
108
+ if comment
109
+ comment = comment.gsub('*/', '* /')
110
+ end
111
+
112
+ if comment && comment.length > 0
113
+ f.print "/* #{comment} */\n"
114
+ end
115
+
116
+ f.print "\"#{key}\" = \"#{value}\";\n"
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end