Skip to content

Instantly share code, notes, and snippets.

@dsample
Last active December 17, 2015 13:19
Show Gist options
  • Save dsample/5616235 to your computer and use it in GitHub Desktop.
Save dsample/5616235 to your computer and use it in GitHub Desktop.
A converter from Android's strings.xml to gettext PO format for use with online translation services like PO Editor and Launchpad. Store it in a directory within your Android project. It will output a strings_<locale>.po file. You can store 'context' information (to help translators) in a YAML file with the string names as keys and context descr…
#!/usr/bin/ruby
require 'nokogiri'
require 'fileutils'
require 'yaml'
require 'optparse'
@PROJECT_ID_VERSION = 'ttrscoreboard-android'
@REPORT_MSGID_BUGS_TO = '' # Where to report (A URL or email address)
@EXCLUDE_FIELDS = [ "about_translators" ]
@CONTEXTS_FILE = 'contexts.yml'
########################################################################
# Reference language information
# (the strings stored in values/strings.xml
########################################################################
# Additional help: http://www.gnu.org/software/gettext/manual/gettext.html#index-header-entry-of-a-PO-file-265
# Language code
# Can be any of the following:
# "ll" An ISO 639 two-letter language code in lowercase
# "ll_CC" the above with the addition of an ISO 3166 two-letter country code in uppercase
# "ll_CC@variant" the above with the addition of the variant designator (eg. latin/cyrillic)
@LANGUAGE = 'en-GB'
# Content type (for the reference language)
@CONTENT_TYPE = 'UTF-8'
########################################################################
@options = {}
OptionParser.new do |opts|
opts.banner = "Usage: ruby android_xml_to_po.rb [options]"
opts.on("-t", "--translation LANGUAGE",
"Create a PO file for the given translation") do |value|
@options[:translation] = value
@translation_path = File.join('..', 'res', "values-#{value}", 'strings.xml')
abort "Cannot find strings file for values-#{value}" unless File.exists?(@translation_path)
@LANGUAGE = value
end
end.parse!
# translator-comments
#. extracted-comments
#: reference
#, flag
#| msgid previous-untranslated-string
#msgid "untranslated string"
#msgstr "translated string"
def header_entry
header_entry = Array.new
header_entry.push "Project-Id-Version: #{@PROJECT_ID_VERSION}"
header_entry.push "Language: #{@LANGUAGE}"
header_entry.push "Report-Msgid-Bugs-To: #{@REPORT_MSGID_BUGS_TO}"
header_entry.push "POT-Creation-Date: #{Time.now.strftime('%F %H:%M%z')}"
header_entry.push "MIME-Version: 1.0"
header_entry.push "Content-Type: text/plain; charset=#{@CONTENT_TYPE}"
header_entry.push "Content-Transfer-Encoding: 8bit"
header_entry.push "X-Generator: android_xml_to_po.rb"
%|msgid ""\n| +
%|msgstr #{header_entry.map!{|x| %|"#{x}\\n"|}.insert(0,%|""|).join("\n")}|
end
def entry(field_name, reference_string, translated_string, context=nil)
entry = Array.new
#white-space
entry.push ""
entry.push ""
# translator-comments
#"# #{context}" unless context.nil?
#. extracted-comments
#: reference
entry.push "#: #{field_name}" unless field_name.nil?
#, flag
#| msgid previous-untranslated-string
#msgid "untranslated string"
#msgctxt "Context for the translation"
entry.push %|msgctxt "#{clean_string(context)}"| unless context.nil?
entry.push %|msgid "#{clean_string(reference_string)}"|
entry.push %|msgstr "#{clean_string(translated_string)}"|
#msgstr "translated string"
return entry.join("\n")
end
def clean_string(string)
#
return string.gsub(/\n/, '\n').gsub(/\\n\s*/, '\n').gsub(%q|\'|, "'")
end
def get_strings(strings_path)
if File.exists?(strings_path)
doc = Nokogiri.XML File.read(strings_path)
strings = Hash.new
doc.xpath('/resources/string').each do |string|
strings[string.attributes['name'].to_s] = string.content.to_s unless @EXCLUDE_FIELDS.include?(string.attributes['name'].to_s)
end
return strings
end
abort "File not found: #{strings_path}"
end
def get_contexts
if File.exists? @CONTEXTS_FILE
YAML.load_file(@CONTEXTS_FILE)
else
Array.new
end
end
def output_po
f = header_entry
reference_strings_path = File.join('..', 'res', 'values', 'strings.xml')
strings = get_strings reference_strings_path
translated_strings = get_strings @translation_path if @options[:translation]
contexts = get_contexts
strings.each do |field,str|
if @options[:translation]
if translated_strings[field]
translated_string = translated_strings[field]
else
translated_string = str
end
else
translated_string = str
end
if contexts[field].nil?
f << entry(field,str,translated_string)
else
f << entry(field,str,translated_string,contexts[field])
end
end
return f
end
def write_po
File.open("strings_#{@LANGUAGE}.po", 'w') { |file| file.write(output_po) }
end
def print_po
puts output_po
end
write_po
@dsample
Copy link
Author

dsample commented May 22, 2013

Changes

  • Now has a command-line argument -t to set the translation language code to use for the msgstr strings (ie. create a PO file which has translated strings rather than just reference strings).
  • The script now outputs valid PO files (previously they were invalid if single quotes existed in a string)
  • The script now supports multi-line strings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment