From 45732397c673ad61618677c96565877b9e607771 Mon Sep 17 00:00:00 2001 From: Alex Zolotarev Date: Mon, 13 Aug 2012 22:14:53 +0300 Subject: [PATCH] Updated string resources generation tool --- tools/twine/README.md | 21 ++- tools/twine/lib/twine/formatters/abstract.rb | 85 ++++++++++++ tools/twine/lib/twine/formatters/android.rb | 135 +++++-------------- tools/twine/lib/twine/formatters/apple.rb | 25 ++-- tools/twine/lib/twine/stringsfile.rb | 4 +- tools/twine/lib/twine/version.rb | 2 +- tools/twine/test/fixtures/fr-1.xml | 1 + tools/twine/test/fixtures/strings-1.txt | 1 + tools/twine/test/fixtures/strings-2.txt | 5 + tools/twine/test/fixtures/test-output-1.txt | 1 + tools/twine/test/fixtures/test-output-2.txt | 1 + tools/twine/test/fixtures/test-output-3.txt | 1 + tools/twine/test/fixtures/test-output-4.txt | 1 + tools/twine/test/fixtures/test-output-5.txt | 2 +- tools/twine/test/fixtures/test-output-6.txt | 10 ++ tools/twine/test/twine_test.rb | 8 ++ 16 files changed, 186 insertions(+), 117 deletions(-) create mode 100644 tools/twine/test/fixtures/strings-2.txt create mode 100644 tools/twine/test/fixtures/test-output-6.txt diff --git a/tools/twine/README.md b/tools/twine/README.md index fc0027c1d4..036c1ddb80 100644 --- a/tools/twine/README.md +++ b/tools/twine/README.md @@ -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 diff --git a/tools/twine/lib/twine/formatters/abstract.rb b/tools/twine/lib/twine/formatters/abstract.rb index 8cd6c4591f..0eb73e8bbc 100644 --- a/tools/twine/lib/twine/formatters/abstract.rb +++ b/tools/twine/lib/twine/formatters/abstract.rb @@ -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 diff --git a/tools/twine/lib/twine/formatters/android.rb b/tools/twine/lib/twine/formatters/android.rb index a79f5545ef..b0f0125bb3 100644 --- a/tools/twine/lib/twine/formatters/android.rb +++ b/tools/twine/lib/twine/formatters/android.rb @@ -49,19 +49,44 @@ module Twine end def read_file(path, lang) + resources_regex = /(.*)<\/resources>/m + key_regex = // + comment_regex = // + value_regex = /(.*)<\/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 '' 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 diff --git a/tools/twine/lib/twine/formatters/apple.rb b/tools/twine/lib/twine/formatters/apple.rb index 70cb10db78..b5b9bd3b62 100644 --- a/tools/twine/lib/twine/formatters/apple.rb +++ b/tools/twine/lib/twine/formatters/apple.rb @@ -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 diff --git a/tools/twine/lib/twine/stringsfile.rb b/tools/twine/lib/twine/stringsfile.rb index f9316858b8..6be485c28e 100644 --- a/tools/twine/lib/twine/stringsfile.rb +++ b/tools/twine/lib/twine/stringsfile.rb @@ -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('') diff --git a/tools/twine/lib/twine/version.rb b/tools/twine/lib/twine/version.rb index 199cb1a640..fe9ab016be 100644 --- a/tools/twine/lib/twine/version.rb +++ b/tools/twine/lib/twine/version.rb @@ -1,3 +1,3 @@ module Twine - VERSION = '0.2.2' + VERSION = '0.3.1' end diff --git a/tools/twine/test/fixtures/fr-1.xml b/tools/twine/test/fixtures/fr-1.xml index 78cd8f34d4..b57f5bc7bb 100644 --- a/tools/twine/test/fixtures/fr-1.xml +++ b/tools/twine/test/fixtures/fr-1.xml @@ -3,6 +3,7 @@ + key1-french key2-french key3-french diff --git a/tools/twine/test/fixtures/strings-1.txt b/tools/twine/test/fixtures/strings-1.txt index ec5da6065c..85017efd56 100644 --- a/tools/twine/test/fixtures/strings-1.txt +++ b/tools/twine/test/fixtures/strings-1.txt @@ -2,6 +2,7 @@ [key1] en = key1-english tags = tag1 + comment = This is a comment es = key1-spanish fr = key1-french [key2] diff --git a/tools/twine/test/fixtures/strings-2.txt b/tools/twine/test/fixtures/strings-2.txt new file mode 100644 index 0000000000..fcfff0d958 --- /dev/null +++ b/tools/twine/test/fixtures/strings-2.txt @@ -0,0 +1,5 @@ +[[My Strings]] + [key with space ] + en = `string with space ` + tags = tag1 + comment = String ends with space diff --git a/tools/twine/test/fixtures/test-output-1.txt b/tools/twine/test/fixtures/test-output-1.txt index bc051c3973..2c2a810ef6 100644 --- a/tools/twine/test/fixtures/test-output-1.txt +++ b/tools/twine/test/fixtures/test-output-1.txt @@ -4,6 +4,7 @@ + key1-french key2-french key3-english diff --git a/tools/twine/test/fixtures/test-output-2.txt b/tools/twine/test/fixtures/test-output-2.txt index c530a59b52..8f211e80bd 100644 --- a/tools/twine/test/fixtures/test-output-2.txt +++ b/tools/twine/test/fixtures/test-output-2.txt @@ -6,6 +6,7 @@ /********** My Strings **********/ +/* This is a comment */ "key1" = "key1-english"; "key3" = "key3-english"; diff --git a/tools/twine/test/fixtures/test-output-3.txt b/tools/twine/test/fixtures/test-output-3.txt index 1f26afe869..8db9d000c4 100644 --- a/tools/twine/test/fixtures/test-output-3.txt +++ b/tools/twine/test/fixtures/test-output-3.txt @@ -2,6 +2,7 @@ [key1] en = key1-english tags = tag1 + comment = This is a comment es = key1-spanish fr = key1-french [key2] diff --git a/tools/twine/test/fixtures/test-output-4.txt b/tools/twine/test/fixtures/test-output-4.txt index 98f81f295b..435fad7cc3 100644 --- a/tools/twine/test/fixtures/test-output-4.txt +++ b/tools/twine/test/fixtures/test-output-4.txt @@ -6,6 +6,7 @@ [key1] en = key1-english tags = tag1 + comment = This is a comment es = key1-spanish fr = key1-french [key2] diff --git a/tools/twine/test/fixtures/test-output-5.txt b/tools/twine/test/fixtures/test-output-5.txt index 02aa52d773..be867e2857 100644 --- a/tools/twine/test/fixtures/test-output-5.txt +++ b/tools/twine/test/fixtures/test-output-5.txt @@ -6,6 +6,6 @@ { /* My Strings */ -"key1":"key1-english", +"key1":"key1-english", /* This is a comment */ "key3":"key3-english" } diff --git a/tools/twine/test/fixtures/test-output-6.txt b/tools/twine/test/fixtures/test-output-6.txt new file mode 100644 index 0000000000..d622fc8b60 --- /dev/null +++ b/tools/twine/test/fixtures/test-output-6.txt @@ -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 "; diff --git a/tools/twine/test/twine_test.rb b/tools/twine/test/twine_test.rb index f4bced703e..9c5ac3469d 100644 --- a/tools/twine/test/twine_test.rb +++ b/tools/twine/test/twine_test.rb @@ -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')