twine 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +12 -0
- data/lib/twine.rb +21 -0
- data/lib/twine/cli.rb +68 -91
- data/lib/twine/formatters/abstract.rb +133 -82
- data/lib/twine/formatters/android.rb +49 -67
- data/lib/twine/formatters/apple.rb +33 -47
- data/lib/twine/formatters/django.rb +38 -48
- data/lib/twine/formatters/flash.rb +25 -40
- data/lib/twine/formatters/gettext.rb +37 -44
- data/lib/twine/formatters/jquery.rb +31 -33
- data/lib/twine/formatters/tizen.rb +38 -55
- data/lib/twine/output_processor.rb +57 -0
- data/lib/twine/placeholders.rb +54 -0
- data/lib/twine/runner.rb +78 -115
- data/lib/twine/stringsfile.rb +83 -60
- data/lib/twine/version.rb +1 -1
- data/test/command_test_case.rb +12 -0
- data/test/fixtures/consume_loc_drop.zip +0 -0
- data/test/fixtures/formatter_android.xml +15 -0
- data/test/fixtures/formatter_apple.strings +20 -0
- data/test/fixtures/formatter_django.po +28 -0
- data/test/fixtures/formatter_flash.properties +15 -0
- data/test/fixtures/formatter_gettext.po +26 -0
- data/test/fixtures/formatter_jquery.json +7 -0
- data/test/fixtures/formatter_tizen.xml +15 -0
- data/test/fixtures/gettext_multiline.po +10 -0
- data/test/fixtures/twine_accent_values.txt +13 -0
- data/test/test_abstract_formatter.rb +152 -0
- data/test/test_cli.rb +288 -0
- data/test/test_consume_loc_drop.rb +27 -0
- data/test/test_consume_string_file.rb +53 -0
- data/test/test_formatters.rb +236 -0
- data/test/test_generate_all_string_files.rb +44 -0
- data/test/test_generate_loc_drop.rb +44 -0
- data/test/test_generate_string_file.rb +51 -0
- data/test/test_output_processor.rb +85 -0
- data/test/test_placeholders.rb +86 -0
- data/test/test_strings_file.rb +58 -0
- data/test/test_strings_row.rb +47 -0
- data/test/test_validate_strings_file.rb +55 -0
- data/test/twine_file_dsl.rb +46 -0
- data/test/twine_test_case.rb +44 -0
- metadata +80 -37
- data/test/fixtures/en-1.json +0 -5
- data/test/fixtures/en-1.po +0 -16
- data/test/fixtures/en-1.strings +0 -10
- data/test/fixtures/en-2.po +0 -23
- data/test/fixtures/en-3.xml +0 -8
- data/test/fixtures/fr-1.xml +0 -10
- data/test/fixtures/strings-1.txt +0 -17
- data/test/fixtures/strings-2.txt +0 -5
- data/test/fixtures/strings-3.txt +0 -5
- data/test/fixtures/test-json-line-breaks/consumed.txt +0 -5
- data/test/fixtures/test-json-line-breaks/generated.json +0 -3
- data/test/fixtures/test-json-line-breaks/line-breaks.json +0 -3
- data/test/fixtures/test-json-line-breaks/line-breaks.txt +0 -4
- data/test/fixtures/test-output-1.txt +0 -12
- data/test/fixtures/test-output-10.txt +0 -9
- data/test/fixtures/test-output-11.txt +0 -9
- data/test/fixtures/test-output-12.txt +0 -12
- data/test/fixtures/test-output-2.txt +0 -12
- data/test/fixtures/test-output-3.txt +0 -18
- data/test/fixtures/test-output-4.txt +0 -21
- data/test/fixtures/test-output-5.txt +0 -4
- data/test/fixtures/test-output-6.txt +0 -10
- data/test/fixtures/test-output-7.txt +0 -16
- data/test/fixtures/test-output-8.txt +0 -9
- data/test/fixtures/test-output-9.txt +0 -21
- data/test/twine_test.rb +0 -134
@@ -5,6 +5,8 @@ require 'rexml/document'
|
|
5
5
|
module Twine
|
6
6
|
module Formatters
|
7
7
|
class Android < Abstract
|
8
|
+
include Twine::Placeholders
|
9
|
+
|
8
10
|
FORMAT_NAME = 'android'
|
9
11
|
EXTENSION = '.xml'
|
10
12
|
DEFAULT_FILE_NAME = 'strings.xml'
|
@@ -17,9 +19,6 @@ module Twine
|
|
17
19
|
'nb' => 'no'
|
18
20
|
# TODO: spanish
|
19
21
|
]
|
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
22
|
|
24
23
|
def self.can_handle_directory?(path)
|
25
24
|
Dir.entries(path).any? { |item| /^values.*$/.match(item) }
|
@@ -48,6 +47,16 @@ module Twine
|
|
48
47
|
return
|
49
48
|
end
|
50
49
|
|
50
|
+
def set_translation_for_key(key, lang, value)
|
51
|
+
value = CGI.unescapeHTML(value)
|
52
|
+
value.gsub!('\\\'', '\'')
|
53
|
+
value.gsub!('\\"', '"')
|
54
|
+
value = convert_placeholders_from_android_to_twine(value)
|
55
|
+
value.gsub!('\@', '@')
|
56
|
+
value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
|
57
|
+
super(key, lang, value)
|
58
|
+
end
|
59
|
+
|
51
60
|
def read_file(path, lang)
|
52
61
|
resources_regex = /<resources(?:[^>]*)>(.*)<\/resources>/m
|
53
62
|
key_regex = /<string name="(\w+)">/
|
@@ -65,16 +74,8 @@ module Twine
|
|
65
74
|
if key_match
|
66
75
|
key = key_match[1]
|
67
76
|
value_match = value_regex.match(line)
|
68
|
-
|
69
|
-
|
70
|
-
value = CGI.unescapeHTML(value)
|
71
|
-
value.gsub!('\\\'', '\'')
|
72
|
-
value.gsub!('\\"', '"')
|
73
|
-
value = iosify_substitutions(value)
|
74
|
-
value.gsub!(/(\\u0020)*|(\\u0020)*\z/) { |spaces| ' ' * (spaces.length / 6) }
|
75
|
-
else
|
76
|
-
value = ""
|
77
|
-
end
|
77
|
+
value = value_match ? value_match[1] : ""
|
78
|
+
|
78
79
|
set_translation_for_key(key, lang, value)
|
79
80
|
if comment and comment.length > 0 and !comment.start_with?("SECTION:")
|
80
81
|
set_comment_for_key(key, comment)
|
@@ -91,65 +92,46 @@ module Twine
|
|
91
92
|
end
|
92
93
|
end
|
93
94
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
default_lang = DEFAULT_LANG_CODES[lang]
|
98
|
-
end
|
99
|
-
File.open(path, 'w:UTF-8') do |f|
|
100
|
-
f.puts "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
|
101
|
-
f.write '<resources>'
|
102
|
-
@strings.sections.each do |section|
|
103
|
-
printed_section = false
|
104
|
-
section.rows.each do |row|
|
105
|
-
if row.matches_tags?(@options[:tags], @options[:untagged])
|
106
|
-
if !printed_section
|
107
|
-
f.puts ''
|
108
|
-
if section.name && section.name.length > 0
|
109
|
-
section_name = section.name.gsub('--', '—')
|
110
|
-
f.puts "\t<!-- SECTION: #{section_name} -->"
|
111
|
-
end
|
112
|
-
printed_section = true
|
113
|
-
end
|
95
|
+
def format_header(lang)
|
96
|
+
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Android Strings File -->\n<!-- Generated by Twine #{Twine::VERSION} -->\n<!-- Language: #{lang} -->"
|
97
|
+
end
|
114
98
|
|
115
|
-
|
99
|
+
def format_sections(strings, lang)
|
100
|
+
result = '<resources>'
|
101
|
+
|
102
|
+
result += super + "\n"
|
116
103
|
|
117
|
-
|
118
|
-
|
119
|
-
value = row.translated_string_for_lang(@strings.language_codes[0])
|
120
|
-
end
|
104
|
+
result += '</resources>'
|
105
|
+
end
|
121
106
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
# Android enforces the following rules on the values
|
126
|
-
# 1) apostrophes and quotes must be escaped with a backslash
|
127
|
-
value.gsub!('\'', '\\\\\'')
|
128
|
-
value.gsub!('"', '\\\\"')
|
129
|
-
# 2) HTML escape the string
|
130
|
-
value = CGI.escapeHTML(value)
|
131
|
-
# 3) fix substitutions (e.g. %s/%@)
|
132
|
-
value = androidify_substitutions(value)
|
133
|
-
# 4) replace beginning and end spaces with \0020. Otherwise Android strips them.
|
134
|
-
value.gsub!(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
|
135
|
-
|
136
|
-
comment = row.comment
|
137
|
-
if comment
|
138
|
-
comment = comment.gsub('--', '—')
|
139
|
-
end
|
140
|
-
|
141
|
-
if comment && comment.length > 0
|
142
|
-
f.puts "\t<!-- #{comment} -->\n"
|
143
|
-
end
|
144
|
-
f.puts "\t<string name=\"#{key}\">#{value}</string>"
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
107
|
+
def format_section_header(section)
|
108
|
+
"\t<!-- SECTION: #{section.name} -->"
|
109
|
+
end
|
149
110
|
|
150
|
-
|
151
|
-
|
111
|
+
def format_comment(row, lang)
|
112
|
+
"\t<!-- #{row.comment.gsub('--', '—')} -->\n" if row.comment
|
152
113
|
end
|
114
|
+
|
115
|
+
def key_value_pattern
|
116
|
+
"\t<string name=\"%{key}\">%{value}</string>"
|
117
|
+
end
|
118
|
+
|
119
|
+
def format_value(value)
|
120
|
+
# Android enforces the following rules on the values
|
121
|
+
# 1) apostrophes and quotes must be escaped with a backslash
|
122
|
+
value = escape_quotes(value)
|
123
|
+
value.gsub!("'", "\\\\'")
|
124
|
+
# 2) HTML escape the string
|
125
|
+
value = CGI.escapeHTML(value)
|
126
|
+
# 3) convert placeholders (e.g. %@ -> %s)
|
127
|
+
value = convert_placeholders_from_twine_to_android(value)
|
128
|
+
# 4) escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
|
129
|
+
resource_identifier_regex = /@(?!([a-z\.]+:)?[a-z+]+\/[a-zA-Z_]+)/ # @[<package_name>:]<resource_type>/<resource_name>
|
130
|
+
value.gsub!(resource_identifier_regex, '\@')
|
131
|
+
# 5) replace beginning and end spaces with \0020. Otherwise Android strips them.
|
132
|
+
value.gsub(/\A *| *\z/) { |spaces| '\u0020' * spaces.length }
|
133
|
+
end
|
134
|
+
|
153
135
|
end
|
154
136
|
end
|
155
137
|
end
|
@@ -27,6 +27,10 @@ module Twine
|
|
27
27
|
return
|
28
28
|
end
|
29
29
|
|
30
|
+
def output_path_for_language(lang)
|
31
|
+
"#{lang}.lproj"
|
32
|
+
end
|
33
|
+
|
30
34
|
def read_file(path, lang)
|
31
35
|
encoding = Twine::Encoding.encoding_for_path(path)
|
32
36
|
sep = nil
|
@@ -64,63 +68,45 @@ module Twine
|
|
64
68
|
key.gsub!('\\"', '"')
|
65
69
|
value = match[2]
|
66
70
|
value.gsub!('\\"', '"')
|
67
|
-
value = iosify_substitutions(value)
|
68
71
|
set_translation_for_key(key, lang, value)
|
69
72
|
if last_comment
|
70
73
|
set_comment_for_key(key, last_comment)
|
71
74
|
end
|
72
75
|
end
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
end
|
76
|
+
|
77
|
+
match = /\/\* (.*) \*\//.match(line)
|
78
|
+
if match
|
79
|
+
last_comment = match[1]
|
80
|
+
else
|
81
|
+
last_comment = nil
|
80
82
|
end
|
83
|
+
|
81
84
|
end
|
82
85
|
end
|
83
86
|
end
|
84
87
|
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
value = value.gsub('"', '\\\\"')
|
108
|
-
|
109
|
-
comment = row.comment
|
110
|
-
if comment
|
111
|
-
comment = comment.gsub('*/', '* /')
|
112
|
-
end
|
113
|
-
|
114
|
-
if comment && comment.length > 0
|
115
|
-
f.print "/* #{comment} */\n"
|
116
|
-
end
|
117
|
-
|
118
|
-
f.print "\"#{key}\" = \"#{value}\";\n"
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
88
|
+
def format_header(lang)
|
89
|
+
"/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
|
90
|
+
end
|
91
|
+
|
92
|
+
def format_section_header(section)
|
93
|
+
"/********** #{section.name} **********/\n"
|
94
|
+
end
|
95
|
+
|
96
|
+
def key_value_pattern
|
97
|
+
"\"%{key}\" = \"%{value}\";\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
def format_comment(row, lang)
|
101
|
+
"/* #{row.comment.gsub('*/', '* /')} */\n" if row.comment
|
102
|
+
end
|
103
|
+
|
104
|
+
def format_key(key)
|
105
|
+
escape_quotes(key)
|
106
|
+
end
|
107
|
+
|
108
|
+
def format_value(value)
|
109
|
+
escape_quotes(value)
|
124
110
|
end
|
125
111
|
end
|
126
112
|
end
|
@@ -26,7 +26,7 @@ module Twine
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def read_file(path, lang)
|
29
|
-
comment_regex =
|
29
|
+
comment_regex = /#\. *"?(.*)"?$/
|
30
30
|
key_regex = /msgid *"(.*)"$/
|
31
31
|
value_regex = /msgstr *"(.*)"$/m
|
32
32
|
|
@@ -60,14 +60,12 @@ module Twine
|
|
60
60
|
line = Iconv.iconv('UTF-8', encoding, line).join
|
61
61
|
end
|
62
62
|
end
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
end
|
68
|
-
else
|
69
|
-
comment = nil
|
63
|
+
|
64
|
+
comment_match = comment_regex.match(line)
|
65
|
+
if comment_match
|
66
|
+
comment = comment_match[1]
|
70
67
|
end
|
68
|
+
|
71
69
|
key_match = key_regex.match(line)
|
72
70
|
if key_match
|
73
71
|
key = key_match[1].gsub('\\"', '"')
|
@@ -83,6 +81,8 @@ module Twine
|
|
83
81
|
if comment and comment.length > 0 and !comment.start_with?("--------- ")
|
84
82
|
set_comment_for_key(key, comment)
|
85
83
|
end
|
84
|
+
key = nil
|
85
|
+
value = nil
|
86
86
|
comment = nil
|
87
87
|
end
|
88
88
|
|
@@ -90,53 +90,43 @@ module Twine
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
-
def
|
94
|
-
default_lang =
|
95
|
-
|
96
|
-
|
97
|
-
f.puts "##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n "
|
98
|
-
@strings.sections.each do |section|
|
99
|
-
printed_section = false
|
100
|
-
section.rows.each do |row|
|
101
|
-
if row.matches_tags?(@options[:tags], @options[:untagged])
|
102
|
-
f.puts ''
|
103
|
-
if !printed_section
|
104
|
-
if section.name && section.name.length > 0
|
105
|
-
f.print "#--------- #{section.name} ---------#\n\n"
|
106
|
-
end
|
107
|
-
printed_section = true
|
108
|
-
end
|
109
|
-
|
110
|
-
basetrans = row.translated_string_for_lang(default_lang)
|
93
|
+
def format_file(strings, lang)
|
94
|
+
@default_lang = strings.language_codes[0]
|
95
|
+
super
|
96
|
+
end
|
111
97
|
|
112
|
-
|
113
|
-
|
98
|
+
def format_header(lang)
|
99
|
+
"##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\n"
|
100
|
+
end
|
114
101
|
|
115
|
-
|
116
|
-
|
117
|
-
|
102
|
+
def format_section_header(section)
|
103
|
+
"#--------- #{section.name} ---------#\n"
|
104
|
+
end
|
118
105
|
|
119
|
-
|
106
|
+
def row_pattern
|
107
|
+
"%{comment}%{base_translation}%{key_value}"
|
108
|
+
end
|
120
109
|
|
121
|
-
|
122
|
-
|
123
|
-
|
110
|
+
def format_base_translation(row, lang)
|
111
|
+
base_translation = row.translations[@default_lang]
|
112
|
+
"# base translation: \"#{base_translation}\"\n" if base_translation
|
113
|
+
end
|
124
114
|
|
125
|
-
|
126
|
-
|
127
|
-
|
115
|
+
def key_value_pattern
|
116
|
+
"msgid \"%{key}\"\n" +
|
117
|
+
"msgstr \"%{value}\"\n"
|
118
|
+
end
|
128
119
|
|
129
|
-
|
130
|
-
|
131
|
-
|
120
|
+
def format_comment(row, lang)
|
121
|
+
"#. #{escape_quotes(row.comment)}\n" if row.comment
|
122
|
+
end
|
132
123
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
end
|
124
|
+
def format_key(key)
|
125
|
+
escape_quotes(key)
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_value(value)
|
129
|
+
escape_quotes(value)
|
140
130
|
end
|
141
131
|
end
|
142
132
|
end
|
@@ -51,59 +51,44 @@ module Twine
|
|
51
51
|
match = /((?:[^"\\]|\\.)+)\s*=\s*((?:[^"\\]|\\.)*)/.match(line)
|
52
52
|
if match
|
53
53
|
key = match[1]
|
54
|
-
value = match[2]
|
54
|
+
value = match[2].strip
|
55
55
|
value.gsub!(/\{[0-9]\}/, '%@')
|
56
56
|
set_translation_for_key(key, lang, value)
|
57
57
|
if last_comment
|
58
58
|
set_comment_for_key(key, last_comment)
|
59
59
|
end
|
60
60
|
end
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
end
|
61
|
+
|
62
|
+
match = /# *(.*)/.match(line)
|
63
|
+
if match
|
64
|
+
last_comment = match[1]
|
65
|
+
else
|
66
|
+
last_comment = nil
|
68
67
|
end
|
68
|
+
|
69
69
|
end
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
File.open(path, "w:#{encoding}") do |f|
|
77
|
-
f.puts "## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}\n"
|
78
|
-
@strings.sections.each do |section|
|
79
|
-
printed_section = false
|
80
|
-
section.rows.each do |row|
|
81
|
-
if row.matches_tags?(@options[:tags], @options[:untagged])
|
82
|
-
f.puts ''
|
83
|
-
if !printed_section
|
84
|
-
if section.name && section.name.length > 0
|
85
|
-
f.print "## #{section.name} ##\n\n"
|
86
|
-
end
|
87
|
-
printed_section = true
|
88
|
-
end
|
73
|
+
def format_header(lang)
|
74
|
+
"## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}"
|
75
|
+
end
|
89
76
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
placeHolderNumber = -1
|
94
|
-
value = value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
|
95
|
-
|
96
|
-
comment = row.comment
|
97
|
-
if comment && comment.length > 0
|
98
|
-
f.print "# #{comment}\n"
|
99
|
-
end
|
77
|
+
def format_section_header(section)
|
78
|
+
"## #{section.name} ##\n"
|
79
|
+
end
|
100
80
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
81
|
+
def format_comment(row, lang)
|
82
|
+
"# #{row.comment}\n" if row.comment
|
83
|
+
end
|
84
|
+
|
85
|
+
def key_value_pattern
|
86
|
+
"%{key}=%{value}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def format_value(value)
|
90
|
+
placeHolderNumber = -1
|
91
|
+
value.gsub(/%[d@]/) { placeHolderNumber += 1; '{%d}' % placeHolderNumber }
|
107
92
|
end
|
108
93
|
end
|
109
94
|
end
|