Imported Upstream version 1.0.1 53/195153/1 upstream/1.0.1
authorDongHun Kwak <dh0128.kwak@samsung.com>
Tue, 11 Dec 2018 07:06:42 +0000 (16:06 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Tue, 11 Dec 2018 07:06:43 +0000 (16:06 +0900)
Change-Id: I366cbe592fc559cca7a757307eb69c1f1f0cc62f
Signed-off-by: DongHun Kwak <dh0128.kwak@samsung.com>
ChangeLog
Makefile.am
Makefile.in
NEWS
configure
configure.ac
itstool [changed mode: 0755->0644]
itstool.in [new file with mode: 0755]

index 9953df62a208a70c33af67ead15b70c6caae306f..4c034facef6588037a9c14dcf3465ae1314b1913 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,62 @@
+commit e9344508508112ecd177232ba49b5664e860bc9f
+Author: Shaun McCance <shaunm@gnome.org>
+Date:   Tue May 3 12:48:53 2011 -0400
+
+    itstool: Convert posixy locale strings to BCP47
+
+ itstool.in |   29 ++++++++++++++++++++++++++++-
+ 1 files changed, 28 insertions(+), 1 deletions(-)
+
+commit 81af5ab154bf68b1e267e1f4c1ecea760a19706f
+Author: Shaun McCance <shaunm@gnome.org>
+Date:   Sat Apr 30 14:57:48 2011 -0400
+
+    Fixed --help string for --version
+
+ itstool.in |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+commit 4aac006a9f0350f3dbd6588cdb4254e8b895ec78
+Author: Shaun McCance <shaunm@gnome.org>
+Date:   Sat Apr 30 14:56:07 2011 -0400
+
+    Added --version
+
+ .gitignore   |    1 +
+ Makefile.am  |    2 +-
+ configure.ac |    1 +
+ itstool      |  786 ---------------------------------------------------------
+ itstool.in   |  797 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ 5 files changed, 800 insertions(+), 787 deletions(-)
+
+commit 6d60e6e1d14bc6b62e0c43cb0781d562d23dd684
+Author: Shaun McCance <shaunm@gnome.org>
+Date:   Tue Apr 26 17:27:22 2011 -0400
+
+    Use #. for comments. Plain # is for notes written by translators
+
+ itstool |    6 +++---
+ 1 files changed, 3 insertions(+), 3 deletions(-)
+
+commit e091a4736f972b6de8a62dd934ea7a38b089078a
+Author: Shaun McCance <shaunm@gnome.org>
+Date:   Tue Apr 26 16:22:24 2011 -0400
+
+    Adding the PO header, at Claude's request
+
+ itstool |   12 ++++++++++++
+ 1 files changed, 12 insertions(+), 0 deletions(-)
+
+commit 65ecbc8856e91b991d812e08dfa10300504bfb1b
+Author: Shaun McCance <shaunm@gnome.org>
+Date:   Tue Apr 26 11:33:03 2011 -0400
+
+    Version 1.0.0
+
+ NEWS         |    3 +++
+ configure.ac |    2 +-
+ 2 files changed, 4 insertions(+), 1 deletions(-)
+
 commit f3d1ea105a2f99a3c5732d10d03011208cf7ed66
 Author: Shaun McCance <shaunm@gnome.org>
 Date:   Tue Apr 26 11:22:51 2011 -0400
index cc18f19d8d32be0f5006f5590df6ff47fb1078b8..14d037ea8e1df5efc71bca39685c18aeb8dde2e9 100644 (file)
@@ -2,7 +2,7 @@ SUBDIRS = its
 
 bin_SCRIPTS = itstool
 
-EXTRA_DIST = ChangeLog COPYING.GPL3 $(bin_SCRIPTS)
+EXTRA_DIST = ChangeLog COPYING.GPL3 $(bin_SCRIPTS) itstool.in
 
 ChangeLog:
        @if test -f $(top_srcdir)/.git/HEAD; then \
index 638be4ad1f5d3e1d5cfb982dfbfb1dc247cd973e..8da4ea7b21d7f51a01ff2f48dde98f1376951d33 100644 (file)
@@ -34,8 +34,9 @@ PRE_UNINSTALL = :
 POST_UNINSTALL = :
 subdir = .
 DIST_COMMON = README $(am__configure_deps) $(srcdir)/Makefile.am \
-       $(srcdir)/Makefile.in $(top_srcdir)/configure AUTHORS COPYING \
-       ChangeLog INSTALL NEWS install-sh missing
+       $(srcdir)/Makefile.in $(srcdir)/itstool.in \
+       $(top_srcdir)/configure AUTHORS COPYING ChangeLog INSTALL NEWS \
+       install-sh missing
 ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
 am__aclocal_m4_deps = $(top_srcdir)/configure.ac
 am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
@@ -43,7 +44,7 @@ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
 am__CONFIG_DISTCLEAN_FILES = config.status config.cache config.log \
  configure.lineno config.status.lineno
 mkinstalldirs = $(install_sh) -d
-CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_FILES = itstool
 CONFIG_CLEAN_VPATH_FILES =
 am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
 am__vpath_adj = case $$p in \
@@ -195,7 +196,7 @@ top_builddir = @top_builddir@
 top_srcdir = @top_srcdir@
 SUBDIRS = its
 bin_SCRIPTS = itstool
-EXTRA_DIST = ChangeLog COPYING.GPL3 $(bin_SCRIPTS)
+EXTRA_DIST = ChangeLog COPYING.GPL3 $(bin_SCRIPTS) itstool.in
 all: all-recursive
 
 .SUFFIXES:
@@ -233,6 +234,8 @@ $(top_srcdir)/configure:  $(am__configure_deps)
 $(ACLOCAL_M4):  $(am__aclocal_m4_deps)
        $(am__cd) $(srcdir) && $(ACLOCAL) $(ACLOCAL_AMFLAGS)
 $(am__aclocal_m4_deps):
+itstool: $(top_builddir)/config.status $(srcdir)/itstool.in
+       cd $(top_builddir) && $(SHELL) ./config.status $@
 install-binSCRIPTS: $(bin_SCRIPTS)
        @$(NORMAL_INSTALL)
        test -z "$(bindir)" || $(MKDIR_P) "$(DESTDIR)$(bindir)"
diff --git a/NEWS b/NEWS
index 8dab63d191f2231577f1cbf148a3b06d19891fe8..0806b07cfaa6494aa6bb776dcb97576c9810d815 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,10 @@
+1.0.1
+=====
+* Convert POSIX-style locales to BCP47
+* Use #. instead of plain # for comments
+* Added PO header to output
+* Added --version
+
 1.0.0
 =====
 * Initial release
index 5cbff18a65c4d2938e844f826f9d0993b5da4bb7..59f81eedf0713b09b964cfd271128a1c9a7d3789 100755 (executable)
--- a/configure
+++ b/configure
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.66 for itstool 1.0.0.
+# Generated by GNU Autoconf 2.66 for itstool 1.0.1.
 #
 #
 # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1998, 1999, 2000, 2001,
@@ -548,8 +548,8 @@ MAKEFLAGS=
 # Identity of this package.
 PACKAGE_NAME='itstool'
 PACKAGE_TARNAME='itstool'
-PACKAGE_VERSION='1.0.0'
-PACKAGE_STRING='itstool 1.0.0'
+PACKAGE_VERSION='1.0.1'
+PACKAGE_STRING='itstool 1.0.1'
 PACKAGE_BUGREPORT=''
 PACKAGE_URL=''
 
@@ -1164,7 +1164,7 @@ if test "$ac_init_help" = "long"; then
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures itstool 1.0.0 to adapt to many kinds of systems.
+\`configure' configures itstool 1.0.1 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1230,7 +1230,7 @@ fi
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of itstool 1.0.0:";;
+     short | recursive ) echo "Configuration of itstool 1.0.1:";;
    esac
   cat <<\_ACEOF
 
@@ -1297,7 +1297,7 @@ fi
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-itstool configure 1.0.0
+itstool configure 1.0.1
 generated by GNU Autoconf 2.66
 
 Copyright (C) 2010 Free Software Foundation, Inc.
@@ -1314,7 +1314,7 @@ cat >config.log <<_ACEOF
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by itstool $as_me 1.0.0, which was
+It was created by itstool $as_me 1.0.1, which was
 generated by GNU Autoconf 2.66.  Invocation command line was
 
   $ $0 $@
@@ -2129,7 +2129,7 @@ fi
 
 # Define the identity of the package.
  PACKAGE='itstool'
- VERSION='1.0.0'
+ VERSION='1.0.1'
 
 
 cat >>confdefs.h <<_ACEOF
@@ -2170,7 +2170,7 @@ am__tar='${AMTAR} chof - "$$tardir"'; am__untar='${AMTAR} xf -'
 
 
 
-ac_config_files="$ac_config_files Makefile its/Makefile"
+ac_config_files="$ac_config_files Makefile itstool its/Makefile"
 
 
 cat >confcache <<\_ACEOF
@@ -2716,7 +2716,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by itstool $as_me 1.0.0, which was
+This file was extended by itstool $as_me 1.0.1, which was
 generated by GNU Autoconf 2.66.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -2769,7 +2769,7 @@ _ACEOF
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
 ac_cs_version="\\
-itstool config.status 1.0.0
+itstool config.status 1.0.1
 configured by $0, generated by GNU Autoconf 2.66,
   with options \\"\$ac_cs_config\\"
 
@@ -2877,6 +2877,7 @@ for ac_config_target in $ac_config_targets
 do
   case $ac_config_target in
     "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;;
+    "itstool") CONFIG_FILES="$CONFIG_FILES itstool" ;;
     "its/Makefile") CONFIG_FILES="$CONFIG_FILES its/Makefile" ;;
 
   *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;;
index 40aa580a67ab680bb1e085ad5e01c49cbaacd7d7..623317d90ab0dee0193668cb7c95aa39c6fdc98a 100644 (file)
@@ -1,8 +1,9 @@
-AC_INIT([itstool], [1.0.0], [])
+AC_INIT([itstool], [1.0.1], [])
 AM_INIT_AUTOMAKE([1.9 no-dist-gzip dist-bzip2])
 
 AC_CONFIG_FILES([
 Makefile
+itstool
 its/Makefile
 ])
 
diff --git a/itstool b/itstool
old mode 100755 (executable)
new mode 100644 (file)
index 4c82fdf..80a2318
--- a/itstool
+++ b/itstool
@@ -1,5 +1,7 @@
 #!/usr/bin/env python
 
+VERSION="1.0.1"
+
 import gettext
 import hashlib
 import libxml2
@@ -8,6 +10,7 @@ import os
 import os.path
 import re
 import sys
+import time
 
 NS_ITS = 'http://www.w3.org/2005/11/its'
 NS_ITST = 'http://itstool.org/extensions/'
@@ -76,6 +79,17 @@ class MessageList (object):
             else:
                 msgs.append(msg)
                 msgdict[key] = msg
+        out.write('msgid ""\n')
+        out.write('msgstr ""\n')
+        out.write('"Project-Id-Version: PACKAGE VERSION\\n"\n')
+        out.write('"POT-Creation-Date: %s\\n"\n' % time.strftime("%Y-%m-%d %H:%M%z"))
+        out.write('"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n')
+        out.write('"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"\n')
+        out.write('"Language-Team: LANGUAGE <LL@li.org>\\n"\n')
+        out.write('"MIME-Version: 1.0\\n"\n')
+        out.write('"Content-Type: text/plain; charset=UTF-8\\n"\n')
+        out.write('"Content-Transfer-Encoding: 8bit\\n"\n')
+        out.write('\n')
         for msg in msgs:
             out.write(msg.format())
             out.write('\n')
@@ -187,7 +201,7 @@ class Message (object):
         ret = ''
         for i in range(len(self._comments)):
             if i != 0:
-                ret += '#\n'
+                ret += '#.\n'
             comment = self._comments[i]
             while len(comment) > 72:
                 j = comment.rfind(' ', 0, 72)
@@ -195,9 +209,9 @@ class Message (object):
                     j = comment.find(' ')
                 if j == -1:
                     break
-                ret += '# %s\n' % comment[:j]
+                ret += '#. %s\n' % comment[:j]
                 comment = comment[j+1:]
-            ret += '# %s\n' % comment
+            ret += '#. %s\n' % comment
         for source in self._sources:
             ret += '#: %s\n' % source
         if self._preserve:
@@ -697,6 +711,33 @@ class Document (object):
         return self._its_loc_notes.get(node, None)
 
 
+_locale_pattern = re.compile('([a-zA-Z0-9-]+)(_[A-Za-z0-9]+)?(@[A-Za-z0-9]+)?(\.[A-Za-z0-9]+)?')
+def convert_locale (locale):
+    # Automatically convert POSIX-style locales to BCP47
+    match = _locale_pattern.match(locale)
+    if match is None:
+        return locale
+    ret = match.group(1).lower()
+    variant = match.group(3)
+    if variant == '@cyrillic':
+        ret += '-Cyrl'
+        variant = None
+    if variant == '@devanagari':
+        ret += '-Deva'
+        variant = None
+    elif variant == '@latin':
+        ret += '-Latn'
+        variant = None
+    elif variant == '@shaw':
+        ret += '-Shaw'
+        variant = None
+    if match.group(2) is not None:
+        ret += '-' + match.group(2)[1:].upper()
+    if variant is not None and variant != '@euro':
+        ret += '-' + variant[1:].lower()
+    return ret
+
+
 if __name__ == '__main__':
     options = optparse.OptionParser()
     options.set_usage('\n  itstool [OPTIONS] [XMLFILES]\n  itstool -m <MOFILE> [OPTIONS] [XMLFILES]')
@@ -719,8 +760,17 @@ if __name__ == '__main__':
                        default=None,
                        metavar='OUT',
                        help='output PO files to file OUT or XML files in directory OUT')
+    options.add_option('-v', '--version',
+                       action='store_true',
+                       dest='version',
+                       default=False,
+                       help='print itstool version and exit')
     (opts, args) = options.parse_args(sys.argv)
 
+    if opts.version:
+        print 'itstool ' + VERSION
+        sys.exit(0)
+
     if opts.merge is None:
         messages = MessageList()
         for filename in args[1:]:
@@ -747,7 +797,7 @@ if __name__ == '__main__':
             sys.exit(1)
         translations.add_fallback(NoneTranslations())
         if opts.lang is None:
-            opts.lang = os.path.splitext(os.path.basename(opts.merge))[0]
+            opts.lang = convert_locale(os.path.splitext(os.path.basename(opts.merge))[0])
         if opts.output is None:
             out = './'
         elif os.path.isdir(opts.output):
diff --git a/itstool.in b/itstool.in
new file mode 100755 (executable)
index 0000000..445628e
--- /dev/null
@@ -0,0 +1,824 @@
+#!/usr/bin/env python
+
+VERSION="@VERSION@"
+
+import gettext
+import hashlib
+import libxml2
+import optparse
+import os
+import os.path
+import re
+import sys
+import time
+
+NS_ITS = 'http://www.w3.org/2005/11/its'
+NS_ITST = 'http://itstool.org/extensions/'
+NS_BLANK = 'http://itstool.org/extensions/blank/'
+NS_XLINK = 'http://www.w3.org/1999/xlink'
+
+class NoneTranslations:
+    def gettext(self, message):
+        return None
+
+    def lgettext(self, message):
+        return None
+
+    def ngettext(self, msgid1, msgid2, n):
+        return None
+
+    def lngettext(self, msgid1, msgid2, n):
+        return None
+
+    def ugettext(self, message):
+        return None
+
+    def ungettext(self, msgid1, msgid2, n):
+        return None
+
+
+class MessageList (object):
+    def __init__ (self):
+        self._messages = []
+        self._by_node = {}
+        self._has_credits = False
+
+    def add_message (self, message, node):
+        self._messages.append (message)
+        if node is not None:
+            self._by_node[node] = message
+
+    def add_credits(self):
+        if self._has_credits:
+            return
+        msg = Message()
+        msg.set_context('_')
+        msg.add_text('translator-credits')
+        msg.add_comment('Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2')
+        self._messages.append(msg)
+        self._has_credits = True
+
+    def get_message_by_node (self, node):
+        return self._by_node.get(node, None)
+
+    def get_nodes_with_messages (self):
+        return self._by_node.keys()
+
+    def output (self, out):
+        msgs = []
+        msgdict = {}
+        for msg in self._messages:
+            key = (msg.get_context(), msg.get_string())
+            if msgdict.has_key(key):
+                for source in msg.get_sources():
+                    msgdict[key].add_source(source)
+                for comment in msg.get_comments():
+                    msgdict[key].add_comment(comment)
+                if msg.get_preserve_space():
+                    msgdict[key].set_preserve_space()
+            else:
+                msgs.append(msg)
+                msgdict[key] = msg
+        out.write('msgid ""\n')
+        out.write('msgstr ""\n')
+        out.write('"Project-Id-Version: PACKAGE VERSION\\n"\n')
+        out.write('"POT-Creation-Date: %s\\n"\n' % time.strftime("%Y-%m-%d %H:%M%z"))
+        out.write('"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n')
+        out.write('"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"\n')
+        out.write('"Language-Team: LANGUAGE <LL@li.org>\\n"\n')
+        out.write('"MIME-Version: 1.0\\n"\n')
+        out.write('"Content-Type: text/plain; charset=UTF-8\\n"\n')
+        out.write('"Content-Transfer-Encoding: 8bit\\n"\n')
+        out.write('\n')
+        for msg in msgs:
+            out.write(msg.format())
+            out.write('\n')
+
+
+class Message (object):
+    def __init__ (self):
+        self._message = []
+        self._empty = True
+        self._ctxt = None
+        self._placeholders = []
+        self._sources = []
+        self._comments = []
+        self._preserve = False
+
+    class Placeholder (object):
+        def __init__ (self, node):
+            self.node = node
+            self.name = node.name
+
+    def escape (self, text):
+        return text.replace('\\','\\\\').replace('"', "\\\"").replace("\n","\\n").replace("\t","\\t")
+
+    def add_text (self, text):
+        if len(self._message) == 0 or not(isinstance(self._message[-1], basestring)):
+            self._message.append('')
+        self._message[-1] += text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
+        if re.sub('\s+', ' ', text).strip() != '':
+            self._empty = False
+
+    def add_placeholder (self, node):
+        holder = Message.Placeholder(node)
+        self._placeholders.append(holder)
+        self._message.append(holder)
+
+    def get_placeholder (self, name):
+        placeholder = 1
+        for holder in self._placeholders:
+            holdername = '%s-%i' % (holder.name, placeholder)
+            if holdername == name:
+                return holder
+            placeholder += 1
+
+    def add_start_tag (self, node):
+        if len(self._message) == 0 or not(isinstance(self._message[-1], basestring)):
+            self._message.append('')
+        self._message[-1] += ('<%s' % node.name)
+        if node.properties is not None:
+            for prop in node.properties:
+                if prop.type == 'attribute':
+                    name = prop.name
+                    if prop.ns() is not None:
+                        name = prop.ns().name + ':' + name
+                    atval = prop.content
+                    atval = atval.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
+                    self._message += " %s=\"%s\"" % (name, atval)
+        if node.children is not None:
+            self._message[-1] += '>'
+        else:
+            self._message[-1] += '/>'
+
+    def add_end_tag (self, node):
+        if node.children is not None:
+            if len(self._message) == 0 or not(isinstance(self._message[-1], basestring)):
+                self._message.append('')
+            self._message[-1] += ('</%s>' % node.name)
+
+    def is_empty (self):
+        return self._empty
+
+    def get_context (self):
+        return self._ctxt
+
+    def set_context (self, ctxt):
+        self._ctxt = ctxt
+
+    def add_source (self, source):
+        self._sources.append(source)
+
+    def get_sources (self):
+        return self._sources
+
+    def add_comment (self, comment):
+        self._comments.append(comment)
+
+    def get_comments (self):
+        return self._comments
+
+    def get_string (self):
+        message = ''
+        placeholder = 1
+        for msg in self._message:
+            if isinstance(msg, basestring):
+                message += msg
+            elif isinstance(msg, Message.Placeholder):
+                message += '<_:%s-%i/>' % (msg.name, placeholder)
+                placeholder += 1
+        if not self._preserve:
+            message = re.sub('\s+', ' ', message).strip()
+        return message
+
+    def get_preserve_space (self):
+        return self._preserve
+
+    def set_preserve_space (self, preserve=True):
+        self._preserve = preserve
+
+    def format (self):
+        ret = ''
+        for i in range(len(self._comments)):
+            if i != 0:
+                ret += '#.\n'
+            comment = self._comments[i]
+            while len(comment) > 72:
+                j = comment.rfind(' ', 0, 72)
+                if j == -1:
+                    j = comment.find(' ')
+                if j == -1:
+                    break
+                ret += '#. %s\n' % comment[:j]
+                comment = comment[j+1:]
+            ret += '#. %s\n' % comment
+        for source in self._sources:
+            ret += '#: %s\n' % source
+        if self._preserve:
+            ret += '#, no-wrap\n'
+        if self._ctxt is not None:
+            ret += 'msgctxt "%s"\n' % self._ctxt
+        message = self.get_string()
+        if self._preserve:
+            ret += 'msgid ""\n'
+            lines = message.split('\n')
+            for line, no in zip(lines, range(len(lines))):
+                if no == len(lines) - 1:
+                    ret += '"%s"\n' % self.escape(line)
+                else:
+                    ret += '"%s\\n"\n' % self.escape(line)
+        else:
+            ret += 'msgid "%s"\n' % self.escape(message)
+        ret += 'msgstr ""\n'
+        return ret
+
+
+def xml_child_iter (node):
+    child = node.children
+    while child is not None:
+        yield child
+        child = child.next
+
+def xml_is_ns_name (node, ns, name):
+    if node.type != 'element':
+        return False
+    return node.name == name and node.ns() is not None and node.ns().content == ns
+
+
+class Document (object):
+    def __init__ (self, filename, messages):
+        ctxt = libxml2.createFileParserCtxt(filename)
+        ctxt.lineNumbers(1)
+        ctxt.replaceEntities(1)
+        ctxt.parseDocument()
+        self._filename = filename
+        self._doc = ctxt.doc()
+        self._localrules = []
+        def pre_process (node):
+            for child in xml_child_iter(node):
+                if xml_is_ns_name(child, 'http://www.w3.org/2001/XInclude', 'include'):
+                    if child.prop('parse') == 'text':
+                        child.xincludeProcessTree()
+                elif xml_is_ns_name(child, NS_ITS, 'rules'):
+                    if child.hasNsProp('href', NS_XLINK):
+                        href = child.nsProp('href', NS_XLINK)
+                        href = os.path.join(os.path.dirname(filename), href)
+                        hctxt = libxml2.createFileParserCtxt(href)
+                        hctxt.replaceEntities(1)
+                        hctxt.parseDocument()
+                        self._localrules.append(hctxt.doc().getRootElement())
+                    else:
+                        self._localrules.append(child)
+                pre_process(child)
+        pre_process(self._doc)
+        self._msgs = messages
+        self._its_translate_nodes = {}
+        self._its_within_text_nodes = {}
+        self._its_loc_notes = {}
+        self._itst_preserve_space_nodes = {}
+        self._its_lang = {}
+        self._itst_lang_attr = {}
+        self._itst_credits = None
+        self._itst_externals = []
+
+    def apply_its_rule(self, rule, xpath):
+        if rule.type != 'element':
+            return
+        if xml_is_ns_name(rule, NS_ITS, 'translateRule'):
+            if rule.prop('selector') is not None:
+                for node in xpath.xpathEval(rule.prop('selector')):
+                    self._its_translate_nodes[node] = rule.prop('translate')
+        elif xml_is_ns_name(rule, NS_ITS, 'withinTextRule'):
+            if rule.prop('selector') is not None:
+                for node in xpath.xpathEval(rule.prop('selector')):
+                    self._its_within_text_nodes[node] = rule.prop('withinText')
+        elif xml_is_ns_name(rule, NS_ITST, 'preserveSpaceRule'):
+            if rule.prop('selector') is not None:
+                for node in xpath.xpathEval(rule.prop('selector')):
+                    self._itst_preserve_space_nodes[node] = rule.prop('preserveSpace')
+        elif xml_is_ns_name(rule, NS_ITS, 'locNoteRule'):
+            locnote = None
+            for child in xml_child_iter(rule):
+                if xml_is_ns_name(child, NS_ITS, 'locNote'):
+                    locnote = re.sub('\s+', ' ', child.content).strip()
+                    break
+            if locnote is None:
+                if rule.hasProp('locNoteRef'):
+                    locnote = 'SEE: ' + re.sub('\s+', ' ', rule.prop('locNoteRef')).strip()
+            if rule.prop('selector') is not None:
+                for node in xpath.xpathEval(rule.prop('selector')):
+                    if locnote is not None:
+                        self._its_loc_notes[node] = locnote
+                    else:
+                        if rule.hasProp('locNotePointer'):
+                            sel = rule.prop('locNotePointer')
+                            ref = False
+                        elif rule.hasProp('locNoteRefPointer'):
+                            sel = rule.prop('locNoteRefPointer')
+                            ref = True
+                        else:
+                            continue
+                        try:
+                            oldnode = xpath.contextNode()
+                        except:
+                            oldnode = None
+                        xpath.setContextNode(node)
+                        for note in xpath.xpathEval(sel):
+                            cont = re.sub('\s+', ' ', note.content).strip()
+                            if ref:
+                                cont = 'SEE: ' + cont
+                            self._its_loc_notes[node] = cont
+                            break
+                        xpath.setContextNode(oldnode)
+        elif xml_is_ns_name(rule, NS_ITS, 'langRule'):
+            if rule.prop('selector') is not None and rule.prop('langPointer') is not None:
+                for node in xpath.xpathEval(rule.prop('selector')):
+                    try:
+                        oldnode = xpath.contextNode()
+                    except:
+                        oldnode = None
+                    xpath.setContextNode(node)
+                    res = xpath.xpathEval(rule.prop('langPointer'))
+                    if len(res) > 0:
+                        self._its_lang[node] = res[0].content
+                    # We need to construct language attributes, not just read
+                    # language information. Technically, langPointer could be
+                    # any XPath expression. But if it looks like an attribute
+                    # accessor, just use the attribute name.
+                    if rule.prop('langPointer')[0] == '@':
+                        self._itst_lang_attr[node] = rule.prop('langPointer')[1:]
+                    xpath.setContextNode(oldnode)
+        elif xml_is_ns_name(rule, NS_ITST, 'credits'):
+            if rule.prop('appendTo') is not None:
+                for node in xpath.xpathEval(rule.prop('appendTo')):
+                    self._itst_credits = (node, rule)
+                    break
+        elif xml_is_ns_name(rule, NS_ITST, 'externalRefRule'):
+            if rule.prop('selector') is not None and rule.prop('refPointer') is not None:
+                for node in xpath.xpathEval(rule.prop('selector')):
+                    try:
+                        oldnode = xpath.contextNode()
+                    except:
+                        oldnode = None
+                    xpath.setContextNode(node)
+                    res = xpath.xpathEval(rule.prop('refPointer'))
+                    if len(res) > 0:
+                        self._itst_externals.append((node, res[0].content))
+                    xpath.setContextNode(oldnode)
+
+    def apply_its_rules (self):
+        dirs = []
+        ddir = os.getenv('XDG_DATA_HOME', '')
+        if ddir == '':
+            ddir = os.path.join(os.path.expanduser('~'), '.local', 'share')
+        dirs.append(ddir)
+        ddir = os.getenv('XDG_DATA_DIRS', '')
+        if ddir == '':
+            ddir = '/usr/local/share:/usr/share'
+        dirs.extend(ddir.split(':'))
+        ddone = {}
+        for ddir in dirs:
+            itsdir = os.path.join(ddir, 'itstool', 'its')
+            if not os.path.exists(itsdir):
+                continue
+            for dfile in os.listdir(itsdir):
+                if dfile.endswith('.its'):
+                    if not ddone.get(dfile, False):
+                        self.apply_its_file(os.path.join(itsdir, dfile))
+                        ddone[dfile] = True
+        self.apply_local_its_rules()
+
+    def apply_its_file (self, filename):
+        doc = libxml2.parseFile(filename)
+        root = doc.getRootElement()
+        if not xml_is_ns_name(root, NS_ITS, 'rules'):
+            return
+        matched = True
+        for match in xml_child_iter(root):
+            if xml_is_ns_name(match, NS_ITST, 'match'):
+                matched = False
+                xpath = self._doc.xpathNewContext()
+                par = match
+                nss = {}
+                while par is not None:
+                    nsdef = par.nsDefs()
+                    while nsdef is not None:
+                        if nsdef.name is not None:
+                            if not nss.has_key(nsdef.name):
+                                nss[nsdef.name] = nsdef.content
+                                xpath.xpathRegisterNs(nsdef.name, nsdef.content)
+                        nsdef = nsdef.next
+                    par = par.parent
+                if match.hasProp('selector'):
+                    if len(xpath.xpathEval(match.prop('selector'))) > 0:
+                        matched = True
+                        break
+        if matched == False:
+            return
+        for rule in xml_child_iter(root):
+            xpath = self._doc.xpathNewContext()
+            par = match
+            nss = {}
+            while par is not None:
+                nsdef = par.nsDefs()
+                while nsdef is not None:
+                    if nsdef.name is not None:
+                        if not nss.has_key(nsdef.name):
+                            nss[nsdef.name] = nsdef.content
+                            xpath.xpathRegisterNs(nsdef.name, nsdef.content)
+                    nsdef = nsdef.next
+                par = par.parent
+            self.apply_its_rule(rule, xpath)
+
+    def apply_local_its_rules (self):
+        for rules in self._localrules:
+            def reg_ns(xpath, node):
+                if node.parent is not None:
+                    reg_ns(xpath, node.parent)
+                nsdef = node.nsDefs()
+                while nsdef is not None:
+                    if nsdef.name is not None:
+                        xpath.xpathRegisterNs(nsdef.name, nsdef.content)
+                    nsdef = nsdef.next
+            xpath = self._doc.xpathNewContext()
+            reg_ns(xpath, rules)
+            for rule in xml_child_iter(rules):
+                if rule.nsDefs() is not None:
+                    rule_xpath = self._doc.xpathNewContent()
+                    reg_ns(rule_xpath, rule)
+                else:
+                    rule_xpath = xpath
+                self.apply_its_rule(rule, rule_xpath)
+
+    def _append_credits(self, parent, node, trdata):
+        if xml_is_ns_name(node, NS_ITST, 'for-each'):
+            select = node.prop('select')
+            if select == 'years':
+                for year in trdata[2].split(','):
+                    for child in xml_child_iter(node):
+                        self._append_credits(parent, child, trdata + (year.strip(),))
+        elif xml_is_ns_name(node, NS_ITST, 'value-of'):
+            select = node.prop('select')
+            val = None
+            if select == 'name':
+                val = trdata[0]
+            elif select == 'email':
+                val = trdata[1]
+            elif select == 'years':
+                val = trdata[2]
+            elif select == 'year' and len(trdata) == 4:
+                val = trdata[3]
+            if val is not None:
+                val = val.encode('utf-8')
+                parent.addContent(val)
+        else:
+            newnode = node.copyNode(2)
+            parent.addChild(newnode)
+            for child in xml_child_iter(node):
+                self._append_credits(newnode, child, trdata)
+
+    def merge_credits(self, translations, language, node):
+        if self._itst_credits is None:
+            return
+        # Dear Python, please implement pgettext.
+        # http://bugs.python.org/issue2504
+        # Sincerely, Shaun
+        trans = translations.ugettext('_\x04translator-credits')
+        if trans is None or trans == 'translator-credits':
+            return
+        regex = re.compile('(.*) \<(.*)\>, (.*)')
+        for credit in trans.split('\n'):
+            match = regex.match(credit)
+            if not match:
+                continue
+            trdata = match.groups()
+            for node in xml_child_iter(self._itst_credits[1]):
+                self._append_credits(self._itst_credits[0], node, trdata)
+
+    def merge_translations(self, translations, language, node=None):
+        is_root = False
+        if node is None:
+            is_root = True
+            self.generate_messages(comments=False)
+            node = self._doc.getRootElement()
+        if node is None or node.type != 'element':
+            return
+        if is_root:
+            self.merge_credits(translations, language, node)
+        msg = self._msgs.get_message_by_node(node)
+        if msg is None:
+            children = [child for child in xml_child_iter(node)]
+            for child in children:
+                self.merge_translations(translations, language, node=child)
+        else:
+            newnode = self.get_translated(node, translations)
+            if newnode != node:
+                node.replaceNode(newnode)
+        if is_root:
+            # Apply language attributes to untranslated nodes. We don't do
+            # this before processing, because then these attributes would
+            # be copied into the new nodes. We apply the attribute without
+            # checking whether it was translated, because any that were will
+            # just be floating around, unattached to a document.
+            for lcnode in self._msgs.get_nodes_with_messages():
+                attr = self._itst_lang_attr.get(lcnode)
+                if attr is None:
+                    continue
+                origlang = None
+                lcpar = lcnode
+                while lcpar is not None:
+                    origlang = self._its_lang.get(lcpar)
+                    if origlang is not None:
+                        break
+                    lcpar = lcpar.parent
+                if origlang is not None:
+                    lcnode.setProp(attr, origlang)
+            # And then set the language attribute on the root node.
+            if language is not None:
+                attr = self._itst_lang_attr.get(node)
+                if attr is not None:
+                    node.setProp(attr, language)
+            # Because of the way we create nodes and rewrite the document,
+            # we end up with lots of redundant namespace definitions. We
+            # kill them off in one fell swoop at the end.
+            def fix_node_ns (node, nsdefs):
+                childnsdefs = nsdefs.copy()
+                nsdef = node.nsDefs()
+                while nsdef is not None:
+                    nextnsdef = nsdef.next
+                    if nsdefs.has_key(nsdef.name) and nsdefs[nsdef.name] == nsdef.content:
+                        node.removeNsDef(nsdef.content)
+                    else:
+                        childnsdefs[nsdef.name] = nsdef.content
+                    nsdef = nextnsdef
+                for child in xml_child_iter(node):
+                    if child.type == 'element':
+                        fix_node_ns(child, childnsdefs)
+            fix_node_ns(node, {})
+
+    def get_translated (self, node, translations):
+        msg = self._msgs.get_message_by_node(node)
+        if msg is None:
+            return node
+        trans = translations.ugettext(msg.get_string())
+        if trans is None:
+            return node
+        nss = {}
+        def reg_ns(node, nss):
+            if node.parent is not None:
+                reg_ns(node.parent, nss)
+            nsdef = node.nsDefs()
+            while nsdef is not None:
+                nss[nsdef.name] = nsdef.content
+                nsdef = nsdef.next
+        reg_ns(node, nss)
+        nss['_'] = NS_BLANK
+        blurb = '<' + node.name
+        for nsname in nss.keys():
+            if nsname is None:
+                blurb += ' xmlns="%s"' % nss[nsname]
+            else:
+                blurb += ' xmlns:%s="%s"' % (nsname, nss[nsname])
+        blurb += '>%s</%s>' % (trans.encode('utf-8'), node.name)
+        ctxt = libxml2.createDocParserCtxt(blurb)
+        ctxt.replaceEntities(0)
+        ctxt.parseDocument()
+        trnode = ctxt.doc().getRootElement()
+        def scan_node(node):
+            for child in xml_child_iter(node):
+                if child.type != 'element':
+                    continue
+                if child.ns() is not None and child.ns().content == NS_BLANK:
+                    repl = self.get_translated(msg.get_placeholder(child.name).node, translations)
+                    child.replaceNode(repl)
+                scan_node(child)
+        scan_node(trnode)
+        retnode = node.copyNode(2)
+        for child in xml_child_iter(trnode):
+            retnode.addChild(child.copyNode(1))
+        return retnode
+
+    def generate_messages(self, comments=True):
+        if self._itst_credits is not None:
+            self._msgs.add_credits()
+        for ext in self._itst_externals:
+            msg = Message()
+            try:
+                fullfile = os.path.join(os.path.dirname(self._filename), ext[1])
+                filefp = open(fullfile)
+                filemd5 = hashlib.md5(filefp.read()).hexdigest()
+                filefp.close()
+            except:
+                filemd5 = '__failed__'
+            txt = "external ref='%s' md5='%s'" % (ext[1], filemd5)
+            msg.set_context('_')
+            msg.add_text(txt)
+            msg.add_source('%s:%i(%s)' % (self._doc.name, ext[0].lineNo(), ext[0].name))
+            msg.add_comment('This is a reference to an external file such as an image or'
+                            ' video. When the file changes, the md5 hash will change to'
+                            ' let you know you need to update your localized copy. The'
+                            ' msgstr is not used at all. Set it to whatever you like'
+                            ' once you have updated your copy of the file.')
+            self._msgs.add_message(msg, None)
+        self._in_translatable = True
+        for child in xml_child_iter(self._doc):
+            if child.type == 'element':
+                self.generate_message(child, None, comments=comments)
+                break
+
+    def generate_message (self, node, msg, comments=True):
+        if node.type in ('text', 'cdata') and msg is not None:
+            msg.add_text(node.content)
+            return
+        if node.type != 'element':
+            return
+        translate = self.get_its_translate(node)
+        if translate is None:
+            if self._in_translatable:
+                translate = 'yes'
+            else:
+                translate = 'no'
+        if translate == 'no':
+            if msg is not None:
+                msg.add_placeholder(node)
+            is_unit = False
+            msg = None
+        else:
+            is_unit = msg is None or self.is_translation_unit(node)
+            if is_unit:
+                if msg is not None:
+                    msg.add_placeholder(node)
+                msg = Message()
+                if self.get_preserve_space(node):
+                    msg.set_preserve_space()
+                msg.add_source('%s:%i(%s/%s)' % (self._doc.name, node.lineNo(), node.parent.name, node.name))
+            else:
+                msg.add_start_tag(node)
+
+        if comments and msg is not None:
+            comment = self.get_its_loc_note(node)
+            if comment is not None:
+                msg.add_comment(comment)
+
+        in_translatable = self._in_translatable
+        self._in_translatable = (translate == 'yes')
+        for child in xml_child_iter(node):
+            self.generate_message(child, msg, comments=comments)
+        self._in_translatable = in_translatable
+
+        if translate:
+            if is_unit and not msg.is_empty():
+                self._msgs.add_message(msg, node)
+            elif msg is not None:
+                msg.add_end_tag(node)
+
+    def is_translation_unit (self, node):
+        return self.get_its_within_text(node) != 'yes'
+
+    def get_preserve_space (self, node):
+        if node.getSpacePreserve() == 1:
+            return True
+        else:
+            while node.type == 'element':
+                if self._itst_preserve_space_nodes.has_key(node):
+                    return (self._itst_preserve_space_nodes[node] == 'yes')
+                node = node.parent
+        return False
+
+    def get_its_translate (self, node):
+        if node.hasNsProp('translate', NS_ITS):
+            return node.nsProp('translate', NS_ITS)
+        if xml_is_ns_name(node, NS_ITS, 'span'):
+            if node.hasProp('translate'):
+                return node.prop('translate')
+        if self._its_translate_nodes.has_key(node):
+            return self._its_translate_nodes[node]
+        return None
+
+    def get_its_within_text (self, node):
+        return self._its_within_text_nodes.get(node, 'no')
+
+    def get_its_loc_note (self, node):
+        if node.hasNsProp('locNote', NS_ITS):
+            return re.sub('\s+', ' ', node.nsProp('locNote', NS_ITS)).strip()
+        if node.hasNsProp('locNoteRef', NS_ITS):
+            return 'SEE: ' + re.sub('\s+', ' ', node.nsProp('locNoteRef', NS_ITS)).strip()
+        if xml_is_ns_name(node, NS_ITS, 'span'):
+            if node.hasProp('locNote'):
+                return re.sub('\s+', ' ', node.prop('locNote')).strip()
+            if node.hasProp('locNoteRef'):
+                return 'SEE: ' + re.sub('\s+', ' ', node.prop('locNoteRef')).strip()
+        return self._its_loc_notes.get(node, None)
+
+
+_locale_pattern = re.compile('([a-zA-Z0-9-]+)(_[A-Za-z0-9]+)?(@[A-Za-z0-9]+)?(\.[A-Za-z0-9]+)?')
+def convert_locale (locale):
+    # Automatically convert POSIX-style locales to BCP47
+    match = _locale_pattern.match(locale)
+    if match is None:
+        return locale
+    ret = match.group(1).lower()
+    variant = match.group(3)
+    if variant == '@cyrillic':
+        ret += '-Cyrl'
+        variant = None
+    if variant == '@devanagari':
+        ret += '-Deva'
+        variant = None
+    elif variant == '@latin':
+        ret += '-Latn'
+        variant = None
+    elif variant == '@shaw':
+        ret += '-Shaw'
+        variant = None
+    if match.group(2) is not None:
+        ret += '-' + match.group(2)[1:].upper()
+    if variant is not None and variant != '@euro':
+        ret += '-' + variant[1:].lower()
+    return ret
+
+
+if __name__ == '__main__':
+    options = optparse.OptionParser()
+    options.set_usage('\n  itstool [OPTIONS] [XMLFILES]\n  itstool -m <MOFILE> [OPTIONS] [XMLFILES]')
+    options.add_option('-i', '--its',
+                       action='append',
+                       dest='itsfile',
+                       metavar='ITS',
+                       help='load the ITS rules in the file ITS (can specify multiple times)')
+    options.add_option('-l', '--lang',
+                       dest='lang',
+                       default=None,
+                       metavar='LANGUAGE',
+                       help='explicitly set the language code for output file')
+    options.add_option('-m', '--merge',
+                       dest='merge',
+                       metavar='FILE',
+                       help='merge from a PO or MO file FILE and output XML files')
+    options.add_option('-o', '--output',
+                       dest='output',
+                       default=None,
+                       metavar='OUT',
+                       help='output PO files to file OUT or XML files in directory OUT')
+    options.add_option('-v', '--version',
+                       action='store_true',
+                       dest='version',
+                       default=False,
+                       help='print itstool version and exit')
+    (opts, args) = options.parse_args(sys.argv)
+
+    if opts.version:
+        print 'itstool ' + VERSION
+        sys.exit(0)
+
+    if opts.merge is None:
+        messages = MessageList()
+        for filename in args[1:]:
+            doc = Document(filename, messages)
+            doc.apply_its_rules()
+            if opts.itsfile is not None:
+                for itsfile in opts.itsfile:
+                    doc.apply_its_file(itsfile)
+            doc.generate_messages()
+        if opts.output is None or opts.output == '-':
+            out = sys.stdout
+        else:
+            try:
+                out = file(opts.output, 'w')
+            except:
+                sys.stderr.write('Error: Cannot write to file %s\n' % opts.output)
+                sys.exit(1)
+        messages.output(out)
+    else:
+        try:
+            translations = gettext.GNUTranslations(open(opts.merge, 'rb'))
+        except:
+            sys.stderr.write('Error: cannot open mo file %s\n' % opts.merge)
+            sys.exit(1)
+        translations.add_fallback(NoneTranslations())
+        if opts.lang is None:
+            opts.lang = convert_locale(os.path.splitext(os.path.basename(opts.merge))[0])
+        if opts.output is None:
+            out = './'
+        elif os.path.isdir(opts.output):
+            out = opts.output
+        elif len(args) == 2:
+            if opts.output == '-':
+                out = sys.stdout
+            else:
+                out = file(opts.output, 'w')
+        else:
+            sys.stderr.write('Error: Non-directory output for multiple files\n')
+            sys.exit(1)
+        for filename in args[1:]:
+            messages = MessageList()
+            doc = Document(filename, messages)
+            doc.apply_its_rules()
+            if opts.itsfile is not None:
+                for itsfile in opts.itsfile:
+                    doc.apply_its_file(itsfile)
+            doc.merge_translations(translations, opts.lang)
+            fout = out
+            if isinstance(fout, basestring):
+                fout = file(os.path.join(fout, os.path.basename(filename)), 'w')
+            fout.write(doc._doc.serialize('utf-8'))