forked from organicmaps/organicmaps
Updated string resources generation tool
This commit is contained in:
parent
42bb390828
commit
45732397c6
16 changed files with 186 additions and 117 deletions
|
@ -1,6 +1,6 @@
|
|||
# Twine
|
||||
|
||||
Twine is a command line tool for managing your strings and their translations. These strings are all stored in a master text file and then Twine uses this file to import and export strings in a variety of file types, including iOS and Mac OS X `.strings` files as well as Android `.xml` files. This allows individuals and companies to easily share strings across multiple projects, as well as export strings in any format the user wants.
|
||||
Twine is a command line tool for managing your strings and their translations. These strings are all stored in a master text file and then Twine uses this file to import and export strings in a variety of file types, including iOS and Mac OS X `.strings` files, Android `.xml` files, and [jquery-localize][jquerylocalize] `.json` files. This allows individuals and companies to easily share strings across multiple projects, as well as export strings in any format the user wants.
|
||||
|
||||
## Install
|
||||
|
||||
|
@ -47,7 +47,7 @@ Whitepace in this file is mostly ignored. If you absolutely need to put spaces a
|
|||
en = No
|
||||
fr = Non
|
||||
ja = いいえ
|
||||
|
||||
|
||||
[[Errors]]
|
||||
[path_not_found_error]
|
||||
en = The file '%@' could not be found.
|
||||
|
@ -57,7 +57,7 @@ Whitepace in this file is mostly ignored. If you absolutely need to put spaces a
|
|||
en = The network is currently unavailable.
|
||||
tags = app1
|
||||
comment = An error describing when the device can not connect to the internet.
|
||||
|
||||
|
||||
[[Escaping Example]]
|
||||
[list_item_separator]
|
||||
en = `, `
|
||||
|
@ -68,10 +68,20 @@ Whitepace in this file is mostly ignored. If you absolutely need to put spaces a
|
|||
tags = myothertag
|
||||
comment = This string will evaluate to `%@`.
|
||||
|
||||
## Supported Output Formats
|
||||
|
||||
Twine currently supports the following formats for outputting strings:
|
||||
|
||||
* [iOS and OS X String Resources][applestrings] (format: apple)
|
||||
* [Android String Resources][androidstrings] (format: android)
|
||||
* [jquery-localize Language Files][jquerylocalize] (format: jquery)
|
||||
|
||||
If you would like to enable twine to create language files in another format, create an appropriate formatter in `lib/twine/formatters`.
|
||||
|
||||
## Usage
|
||||
|
||||
Usage: twine COMMAND STRINGS_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]
|
||||
|
||||
|
||||
### Commands
|
||||
|
||||
#### `generate-string-file`
|
||||
|
@ -149,3 +159,6 @@ Now, whenever you build your application, Xcode will automatically invoke Twine
|
|||
[rubyzip]: http://rubygems.org/gems/rubyzip
|
||||
[git]: http://git-scm.org/
|
||||
[INI]: http://en.wikipedia.org/wiki/INI_file
|
||||
[applestrings]: http://developer.apple.com/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html
|
||||
[androidstrings]: http://developer.android.com/guide/topics/resources/string-resource.html
|
||||
[jquerylocalize]: https://github.com/coderifous/jquery-localize
|
||||
|
|
|
@ -13,6 +13,91 @@ module Twine
|
|||
@options = options
|
||||
end
|
||||
|
||||
def iosify_substitutions(str)
|
||||
# 1) use "@" instead of "s" for substituting strings
|
||||
str.gsub!(/%([0-9\$]*)s/, '%\1@')
|
||||
|
||||
# 2) if substitutions are numbered, see if we can remove the numbering safely
|
||||
expectedSub = 1
|
||||
startFound = false
|
||||
foundSub = 0
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if c == "%"
|
||||
# this is a literal %, keep moving
|
||||
startFound = false
|
||||
elsif c.match(/\d/)
|
||||
foundSub *= 10
|
||||
foundSub += Integer(c)
|
||||
elsif c == "$"
|
||||
if expectedSub == foundSub
|
||||
# okay to keep going
|
||||
startFound = false
|
||||
expectedSub += 1
|
||||
else
|
||||
# the numbering appears to be important (or non-existent), leave it alone
|
||||
return str
|
||||
end
|
||||
end
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
foundSub = 0
|
||||
end
|
||||
end
|
||||
|
||||
# if we got this far, then the numbering (if any) is in order left-to-right and safe to remove
|
||||
if expectedSub > 1
|
||||
str.gsub!(/%\d+\$(.)/, '%\1')
|
||||
end
|
||||
|
||||
return str
|
||||
end
|
||||
|
||||
def androidify_substitutions(str)
|
||||
# 1) use "s" instead of "@" for substituting strings
|
||||
str.gsub!(/%([0-9\$]*)@/, '%\1s')
|
||||
|
||||
# 2) if there is more than one substitution in a string, make sure they are numbered
|
||||
substituteCount = 0
|
||||
startFound = false
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if c == "%"
|
||||
# ignore as this is a literal %
|
||||
elsif c.match(/\d/)
|
||||
# leave the string alone if it already has numbered substitutions
|
||||
return str
|
||||
else
|
||||
substituteCount += 1
|
||||
end
|
||||
startFound = false
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
end
|
||||
end
|
||||
|
||||
if substituteCount > 1
|
||||
currentSub = 1
|
||||
startFound = false
|
||||
newstr = ""
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if !(c == "%")
|
||||
newstr = newstr + "#{currentSub}$"
|
||||
currentSub += 1
|
||||
end
|
||||
startFound = false
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
end
|
||||
newstr = newstr + c
|
||||
end
|
||||
return newstr
|
||||
else
|
||||
return str
|
||||
end
|
||||
end
|
||||
|
||||
def set_translation_for_key(key, lang, value)
|
||||
if @strings.strings_map.include?(key)
|
||||
@strings.strings_map[key].translations[lang] = value
|
||||
|
|
|
@ -49,19 +49,44 @@ module Twine
|
|||
end
|
||||
|
||||
def read_file(path, lang)
|
||||
resources_regex = /<resources>(.*)<\/resources>/m
|
||||
key_regex = /<string name="(\w+)">/
|
||||
comment_regex = /<!-- (.*) -->/
|
||||
value_regex = /<string name="\w+">(.*)<\/string>/
|
||||
key = nil
|
||||
value = nil
|
||||
comment = nil
|
||||
|
||||
File.open(path, 'r:UTF-8') do |f|
|
||||
current_section = nil
|
||||
doc = REXML::Document.new(f)
|
||||
doc.elements.each('resources/string') do |ele|
|
||||
key = ele.attributes["name"]
|
||||
value = ele.text || ''
|
||||
value.gsub!('\\\'', '\'')
|
||||
value.gsub!('\\"', '"')
|
||||
value.gsub!(/\n/, '')
|
||||
value.gsub!('<', '<')
|
||||
value.gsub!('&', '&')
|
||||
value = iosify_substitutions(value)
|
||||
set_translation_for_key(key, lang, value)
|
||||
content_match = resources_regex.match(f.read)
|
||||
if content_match
|
||||
for line in content_match[1].split(/\r?\n/)
|
||||
key_match = key_regex.match(line)
|
||||
if key_match
|
||||
key = key_match[1]
|
||||
value_match = value_regex.match(line)
|
||||
if value_match
|
||||
value = value_match[1]
|
||||
value.gsub!('\\"', '"')
|
||||
value = iosify_substitutions(value)
|
||||
else
|
||||
value = ""
|
||||
end
|
||||
if @options[:tags]
|
||||
set_tags_for_key(key, @options[:tags])
|
||||
end
|
||||
set_translation_for_key(key, lang, value)
|
||||
if comment and comment.length > 0
|
||||
set_comment_for_key(key, comment)
|
||||
end
|
||||
comment = nil
|
||||
end
|
||||
|
||||
comment_match = comment_regex.match(line)
|
||||
if comment_match
|
||||
comment = comment_match[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -124,92 +149,6 @@ module Twine
|
|||
f.puts '</resources>'
|
||||
end
|
||||
end
|
||||
|
||||
def iosify_substitutions(str)
|
||||
# 1) use "@" instead of "s" for substituting strings
|
||||
str.gsub!(/%([0-9\$]*)s/, '%\1@')
|
||||
|
||||
# 2) if substitutions are numbered, see if we can remove the numbering safely
|
||||
expectedSub = 1
|
||||
startFound = false
|
||||
foundSub = 0
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if c == "%"
|
||||
# this is a literal %, keep moving
|
||||
startFound = false
|
||||
elsif c.match(/\d/)
|
||||
foundSub *= 10
|
||||
foundSub += Integer(c)
|
||||
elsif c == "$"
|
||||
if expectedSub == foundSub
|
||||
# okay to keep going
|
||||
startFound = false
|
||||
expectedSub += 1
|
||||
else
|
||||
# the numbering appears to be important (or non-existent), leave it alone
|
||||
return str
|
||||
end
|
||||
end
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
foundSub = 0
|
||||
end
|
||||
end
|
||||
|
||||
# if we got this far, then the numbering (if any) is in order left-to-right and safe to remove
|
||||
if expectedSub > 1
|
||||
str.gsub!(/%\d+\$(.)/, '%\1')
|
||||
end
|
||||
|
||||
return str
|
||||
end
|
||||
|
||||
def androidify_substitutions(str)
|
||||
# 1) use "s" instead of "@" for substituting strings
|
||||
str.gsub!(/%([0-9\$]*)@/, '%\1s')
|
||||
|
||||
# 2) if there is more than one substitution in a string, make sure they are numbered
|
||||
substituteCount = 0
|
||||
startFound = false
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if c == "%"
|
||||
# ignore as this is a literal %
|
||||
elsif c.match(/\d/)
|
||||
# leave the string alone if it already has numbered substitutions
|
||||
return str
|
||||
else
|
||||
substituteCount += 1
|
||||
end
|
||||
startFound = false
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
end
|
||||
end
|
||||
|
||||
if substituteCount > 1
|
||||
currentSub = 1
|
||||
startFound = false
|
||||
newstr = ""
|
||||
str.each_char do |c|
|
||||
if startFound
|
||||
if !(c == "%")
|
||||
newstr = newstr + "#{currentSub}$"
|
||||
currentSub += 1
|
||||
end
|
||||
startFound = false
|
||||
elsif c == "%"
|
||||
startFound = true
|
||||
end
|
||||
newstr = newstr + c
|
||||
end
|
||||
return newstr
|
||||
else
|
||||
return str
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,6 +62,7 @@ module Twine
|
|||
key.gsub!('\\"', '"')
|
||||
value = match[2]
|
||||
value.gsub!('\\"', '"')
|
||||
value = iosify_substitutions(value)
|
||||
set_translation_for_key(key, lang, value)
|
||||
if last_comment
|
||||
set_comment_for_key(key, last_comment)
|
||||
|
@ -100,19 +101,21 @@ module Twine
|
|||
key = key.gsub('"', '\\\\"')
|
||||
|
||||
value = row.translated_string_for_lang(lang, default_lang)
|
||||
value = value.gsub('"', '\\\\"')
|
||||
if value
|
||||
value = value.gsub('"', '\\\\"')
|
||||
|
||||
comment = row.comment
|
||||
if comment
|
||||
comment = comment.gsub('*/', '* /')
|
||||
comment = row.comment
|
||||
if comment
|
||||
comment = comment.gsub('*/', '* /')
|
||||
end
|
||||
|
||||
if comment && comment.length > 0
|
||||
f.print "/* #{comment} */\n"
|
||||
end
|
||||
|
||||
f.print "\"#{key}\" = \"#{value}\";\n"
|
||||
end
|
||||
|
||||
if comment && comment.length > 0
|
||||
f.print "/* #{comment} */\n"
|
||||
end
|
||||
|
||||
f.print "\"#{key}\" = \"#{value}\";\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -88,14 +88,14 @@ module Twine
|
|||
if line.length > 4 && line[0, 2] == '[['
|
||||
match = /^\[\[(.+)\]\]$/.match(line)
|
||||
if match
|
||||
current_section = StringsSection.new(match[1].strip)
|
||||
current_section = StringsSection.new(match[1])
|
||||
@sections << current_section
|
||||
parsed = true
|
||||
end
|
||||
elsif line.length > 2 && line[0, 1] == '['
|
||||
match = /^\[(.+)\]$/.match(line)
|
||||
if match
|
||||
current_row = StringsRow.new(match[1].strip)
|
||||
current_row = StringsRow.new(match[1])
|
||||
@strings_map[current_row.key] = current_row
|
||||
if !current_section
|
||||
current_section = StringsSection.new('')
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module Twine
|
||||
VERSION = '0.2.2'
|
||||
VERSION = '0.3.1'
|
||||
end
|
||||
|
|
1
tools/twine/test/fixtures/fr-1.xml
vendored
1
tools/twine/test/fixtures/fr-1.xml
vendored
|
@ -3,6 +3,7 @@
|
|||
<!-- Generated by Twine -->
|
||||
<!-- Language: fr -->
|
||||
<resources>
|
||||
<!-- This is a comment -->
|
||||
<string name="key1">key1-french</string>
|
||||
<string name="key2">key2-french</string>
|
||||
<string name="key3">key3-french</string>
|
||||
|
|
1
tools/twine/test/fixtures/strings-1.txt
vendored
1
tools/twine/test/fixtures/strings-1.txt
vendored
|
@ -2,6 +2,7 @@
|
|||
[key1]
|
||||
en = key1-english
|
||||
tags = tag1
|
||||
comment = This is a comment
|
||||
es = key1-spanish
|
||||
fr = key1-french
|
||||
[key2]
|
||||
|
|
5
tools/twine/test/fixtures/strings-2.txt
vendored
Normal file
5
tools/twine/test/fixtures/strings-2.txt
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
[[My Strings]]
|
||||
[key with space ]
|
||||
en = `string with space `
|
||||
tags = tag1
|
||||
comment = String ends with space
|
1
tools/twine/test/fixtures/test-output-1.txt
vendored
1
tools/twine/test/fixtures/test-output-1.txt
vendored
|
@ -4,6 +4,7 @@
|
|||
<!-- Language: fr -->
|
||||
<resources>
|
||||
<!-- My Strings -->
|
||||
<!-- This is a comment -->
|
||||
<string name="key1">key1-french</string>
|
||||
<string name="key2">key2-french</string>
|
||||
<string name="key3">key3-english</string>
|
||||
|
|
1
tools/twine/test/fixtures/test-output-2.txt
vendored
1
tools/twine/test/fixtures/test-output-2.txt
vendored
|
@ -6,6 +6,7 @@
|
|||
|
||||
/********** My Strings **********/
|
||||
|
||||
/* This is a comment */
|
||||
"key1" = "key1-english";
|
||||
|
||||
"key3" = "key3-english";
|
||||
|
|
1
tools/twine/test/fixtures/test-output-3.txt
vendored
1
tools/twine/test/fixtures/test-output-3.txt
vendored
|
@ -2,6 +2,7 @@
|
|||
[key1]
|
||||
en = key1-english
|
||||
tags = tag1
|
||||
comment = This is a comment
|
||||
es = key1-spanish
|
||||
fr = key1-french
|
||||
[key2]
|
||||
|
|
1
tools/twine/test/fixtures/test-output-4.txt
vendored
1
tools/twine/test/fixtures/test-output-4.txt
vendored
|
@ -6,6 +6,7 @@
|
|||
[key1]
|
||||
en = key1-english
|
||||
tags = tag1
|
||||
comment = This is a comment
|
||||
es = key1-spanish
|
||||
fr = key1-french
|
||||
[key2]
|
||||
|
|
2
tools/twine/test/fixtures/test-output-5.txt
vendored
2
tools/twine/test/fixtures/test-output-5.txt
vendored
|
@ -6,6 +6,6 @@
|
|||
{
|
||||
|
||||
/* My Strings */
|
||||
"key1":"key1-english",
|
||||
"key1":"key1-english", /* This is a comment */
|
||||
"key3":"key3-english"
|
||||
}
|
||||
|
|
10
tools/twine/test/fixtures/test-output-6.txt
vendored
Normal file
10
tools/twine/test/fixtures/test-output-6.txt
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* Apple Strings File
|
||||
* Generated by Twine <%= Twine::VERSION %>
|
||||
* Language: en
|
||||
*/
|
||||
|
||||
/********** My Strings **********/
|
||||
|
||||
/* String ends with space */
|
||||
"key with space " = "string with space ";
|
|
@ -28,6 +28,14 @@ class TwineTest < Test::Unit::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_generate_string_file_4
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'en.strings')
|
||||
Twine::Runner.run(%W(generate-string-file test/fixtures/strings-2.txt #{output_path} -t tag1))
|
||||
assert_equal(ERB.new(File.read('test/fixtures/test-output-6.txt')).result, File.read(output_path))
|
||||
end
|
||||
end
|
||||
|
||||
def test_consume_string_file_1
|
||||
Dir.mktmpdir do |dir|
|
||||
output_path = File.join(dir, 'strings.txt')
|
||||
|
|
Loading…
Add table
Reference in a new issue