traduco 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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