Add LocNote class to better track localization note info
[itstool:itstool.git] / itstool.in
1 #!/usr/bin/python -s
2 #
3 # Copyright (c) 2010-2011 Shaun McCance <shaunm@gnome.org>
4 #
5 # ITS Tool program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
9 #
10 # ITS Tool is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
13 # for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with ITS Tool; if not, write to the Free Software Foundation, 59 Temple
17 # Place, Suite 330, Boston, MA  0211-1307  USA.
18 #
19
20 VERSION="@VERSION@"
21 DATADIR="@DATADIR@"
22
23 import gettext
24 import hashlib
25 import libxml2
26 import optparse
27 import os
28 import os.path
29 import re
30 import sys
31 import time
32
33 NS_ITS = 'http://www.w3.org/2005/11/its'
34 NS_ITST = 'http://itstool.org/extensions/'
35 NS_BLANK = 'http://itstool.org/extensions/blank/'
36 NS_XLINK = 'http://www.w3.org/1999/xlink'
37 NS_XML = 'http://www.w3.org/XML/1998/namespace'
38
39 class NoneTranslations:
40     def gettext(self, message):
41         return None
42
43     def lgettext(self, message):
44         return None
45
46     def ngettext(self, msgid1, msgid2, n):
47         return None
48
49     def lngettext(self, msgid1, msgid2, n):
50         return None
51
52     def ugettext(self, message):
53         return None
54
55     def ungettext(self, msgid1, msgid2, n):
56         return None
57
58
59 class MessageList (object):
60     def __init__ (self):
61         self._messages = []
62         self._by_node = {}
63         self._has_credits = False
64
65     def add_message (self, message, node):
66         self._messages.append (message)
67         if node is not None:
68             self._by_node[node] = message
69
70     def add_credits(self):
71         if self._has_credits:
72             return
73         msg = Message()
74         msg.set_context('_')
75         msg.add_text('translator-credits')
76         msg.add_comment(Comment('Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2'))
77         self._messages.append(msg)
78         self._has_credits = True
79
80     def get_message_by_node (self, node):
81         return self._by_node.get(node, None)
82
83     def get_nodes_with_messages (self):
84         return self._by_node.keys()
85
86     def output (self, out):
87         msgs = []
88         msgdict = {}
89         for msg in self._messages:
90             key = (msg.get_context(), msg.get_string())
91             if msgdict.has_key(key):
92                 for source in msg.get_sources():
93                     msgdict[key].add_source(source)
94                 for marker in msg.get_markers():
95                     msgdict[key].add_marker(marker)
96                 for comment in msg.get_comments():
97                     msgdict[key].add_comment(comment)
98                 for idvalue in msg.get_id_values():
99                     msgdict[key].add_id_value(idvalue)
100                 if msg.get_preserve_space():
101                     msgdict[key].set_preserve_space()
102                 if msg.get_locale_filter() is not None:
103                     locale = msgdict[key].get_locale_filter()
104                     if locale is not None:
105                         msgdict[key].set_locale_filter('%s, %s' % (locale, msg.get_locale_filter()))
106                     else:
107                         msgdict[key].set_locale_filter(msg.get_locale_filter())
108                 
109             else:
110                 msgs.append(msg)
111                 msgdict[key] = msg
112         out.write('msgid ""\n')
113         out.write('msgstr ""\n')
114         out.write('"Project-Id-Version: PACKAGE VERSION\\n"\n')
115         out.write('"POT-Creation-Date: %s\\n"\n' % time.strftime("%Y-%m-%d %H:%M%z"))
116         out.write('"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n')
117         out.write('"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"\n')
118         out.write('"Language-Team: LANGUAGE <LL@li.org>\\n"\n')
119         out.write('"MIME-Version: 1.0\\n"\n')
120         out.write('"Content-Type: text/plain; charset=UTF-8\\n"\n')
121         out.write('"Content-Transfer-Encoding: 8bit\\n"\n')
122         out.write('\n')
123         for msg in msgs:
124             out.write(msg.format().encode('utf-8'))
125             out.write('\n')
126
127
128 class Comment (object):
129     def __init__ (self, text):
130         self._text = str(text)
131         assert(text is not None)
132         self._markers = []
133
134     def add_marker (self, marker):
135         self._markers.append(marker)
136
137     def get_markers (self):
138         return self._markers
139
140     def get_text (self):
141         return self._text
142
143     def format (self):
144         ret = u''
145         markers = {}
146         for marker in self._markers:
147             if not markers.has_key(marker):
148                 ret += '#. (itstool) comment: ' + marker + '\n'
149                 markers[marker] = marker
150         if '\n' in self._text:
151             doadd = False
152             for line in self._text.split('\n'):
153                 if line != '':
154                     doadd = True
155                 if not doadd:
156                     continue
157                 ret += u'#. %s\n' % line
158         else:
159             text = self._text
160             while len(text) > 72:
161                 j = text.rfind(' ', 0, 72)
162                 if j == -1:
163                     j = text.find(' ')
164                 if j == -1:
165                     break
166                 ret += u'#. %s\n' % text[:j]
167                 text = text[j+1:]
168             ret += '#. %s\n' % text
169         return ret
170
171
172 class Message (object):
173     def __init__ (self):
174         self._message = []
175         self._empty = True
176         self._ctxt = None
177         self._placeholders = []
178         self._sources = []
179         self._markers = []
180         self._id_values = []
181         self._locale_filter = None
182         self._comments = []
183         self._preserve = False
184
185     def __repr__(self):
186         if self._empty:
187             return "Empty message"
188         return self.get_string()
189
190     class Placeholder (object):
191         def __init__ (self, node):
192             self.node = node
193             self.name = unicode(node.name, 'utf-8')
194
195     def escape (self, text):
196         return text.replace('\\','\\\\').replace('"', "\\\"").replace("\n","\\n").replace("\t","\\t")
197
198     def add_text (self, text):
199         if len(self._message) == 0 or not(isinstance(self._message[-1], basestring)):
200             self._message.append('')
201         if not isinstance(text, unicode):
202             text = unicode(text, 'utf-8')
203         self._message[-1] += text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
204         if re.sub('\s+', ' ', text).strip() != '':
205             self._empty = False
206
207     def add_placeholder (self, node):
208         holder = Message.Placeholder(node)
209         self._placeholders.append(holder)
210         self._message.append(holder)
211
212     def get_placeholder (self, name):
213         placeholder = 1
214         for holder in self._placeholders:
215             holdername = u'%s-%i' % (holder.name, placeholder)
216             if holdername == unicode(name, 'utf-8'):
217                 return holder
218             placeholder += 1
219
220     def add_start_tag (self, node):
221         if len(self._message) == 0 or not(isinstance(self._message[-1], basestring)):
222             self._message.append('')
223         if node.ns() is not None and node.ns().name is not None:
224             self._message[-1] += ('<%s:%s' % (unicode(node.ns().name, 'utf-8'), unicode(node.name, 'utf-8')))
225         else:
226             self._message[-1] += ('<%s' % unicode(node.name, 'utf-8'))
227         for prop in xml_attr_iter(node):
228             name = prop.name
229             if prop.ns() is not None:
230                 name = prop.ns().name + ':' + name
231             atval = prop.content
232             if not isinstance(atval, unicode):
233                 atval = unicode(atval, 'utf-8')
234             atval = atval.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
235             self._message += " %s=\"%s\"" % (name, atval)
236         if node.children is not None:
237             self._message[-1] += '>'
238         else:
239             self._message[-1] += '/>'
240
241     def add_end_tag (self, node):
242         if node.children is not None:
243             if len(self._message) == 0 or not(isinstance(self._message[-1], basestring)):
244                 self._message.append('')
245             if node.ns() is not None and node.ns().name is not None:
246                 self._message[-1] += ('</%s:%s>' % (unicode(node.ns().name, 'utf-8'), unicode(node.name, 'utf-8')))
247             else:
248                 self._message[-1] += ('</%s>' % unicode(node.name, 'utf-8'))
249
250     def is_empty (self):
251         return self._empty
252
253     def get_context (self):
254         return self._ctxt
255
256     def set_context (self, ctxt):
257         self._ctxt = ctxt
258
259     def add_source (self, source):
260         if not isinstance(source, unicode):
261             source = unicode(source, 'utf-8')
262         self._sources.append(source)
263
264     def get_sources (self):
265         return self._sources
266
267     def add_marker (self, marker):
268         if not isinstance(marker, unicode):
269             marker = unicode(marker, 'utf-8')
270         self._markers.append(marker)
271
272     def get_markers (self):
273         return self._markers
274
275     def add_id_value(self, id_value):
276         self._id_values.append(id_value)
277
278     def get_id_values(self):
279         return self._id_values
280
281     def add_comment (self, comment):
282         if comment is not None:
283             self._comments.append(comment)
284
285     def get_comments (self):
286         return self._comments
287
288     def get_string (self):
289         message = u''
290         placeholder = 1
291         for msg in self._message:
292             if isinstance(msg, basestring):
293                 message += msg
294             elif isinstance(msg, Message.Placeholder):
295                 message += u'<_:%s-%i/>' % (msg.name, placeholder)
296                 placeholder += 1
297         if not self._preserve:
298             message = re.sub('\s+', ' ', message).strip()
299         return message
300
301     def get_preserve_space (self):
302         return self._preserve
303
304     def set_preserve_space (self, preserve=True):
305         self._preserve = preserve
306
307     def get_locale_filter(self):
308         return self._locale_filter
309
310     def set_locale_filter(self, locale):
311         self._locale_filter = locale
312
313     def format (self):
314         ret = u''
315         markers = {}
316         for marker in self._markers:
317             if not markers.has_key(marker):
318                 ret += '#. (itstool) path: ' + marker + '\n'
319                 markers[marker] = marker
320         for idvalue in self._id_values:
321             ret += '#. (itstool) id: ' + idvalue + '\n'
322         if self._locale_filter is not None:
323             ret += '#. (itstool) locale filter: ' + self._locale_filter + '\n'
324         comments = []
325         commentsdict = {}
326         for comment in self._comments:
327             key = comment.get_text()
328             if commentsdict.has_key(key):
329                 for marker in comment.get_markers():
330                     commentsdict[key].add_marker(marker)
331             else:
332                 comments.append(comment)
333                 commentsdict[key] = comment
334         for i in range(len(comments)):
335             if i != 0:
336                 ret += '#.\n'
337             ret += comments[i].format()
338         for source in self._sources:
339             ret += u'#: %s\n' % source
340         if self._preserve:
341             ret += u'#, no-wrap\n'
342         if self._ctxt is not None:
343             ret += u'msgctxt "%s"\n' % self._ctxt
344         message = self.get_string()
345         if self._preserve:
346             ret += u'msgid ""\n'
347             lines = message.split('\n')
348             for line, no in zip(lines, range(len(lines))):
349                 if no == len(lines) - 1:
350                     ret += u'"%s"\n' % self.escape(line)
351                 else:
352                     ret += u'"%s\\n"\n' % self.escape(line)
353         else:
354             ret += u'msgid "%s"\n' % self.escape(message)
355         ret += u'msgstr ""\n'
356         return ret
357
358
359 def xml_child_iter (node):
360     child = node.children
361     while child is not None:
362         yield child
363         child = child.next
364
365 def xml_attr_iter (node):
366     attr = node.get_properties()
367     while attr is not None:
368         yield attr
369         attr = attr.next
370
371 def xml_is_ns_name (node, ns, name):
372     if node.type != 'element':
373         return False
374     return node.name == name and node.ns() is not None and node.ns().content == ns
375
376 def xml_get_node_path(node):
377     # The built-in nodePath() method only does numeric indexes
378     # when necessary for disambiguation. For various reasons,
379     # we prefer always using indexes.
380     name = node.name
381     if node.ns() is not None and node.ns().name is not None:
382         name = node.ns().name + ':' + name
383     if node.type == 'attribute':
384         name = '@' + name
385     name = '/' + name
386     if node.type == 'element' and node.parent.type == 'element':
387         count = 1
388         prev = node.previousElementSibling()
389         while prev is not None:
390             if prev.name == node.name:
391                 if prev.ns() is None:
392                     if node.ns() is None:
393                         count += 1
394                 else:
395                     if node.ns() is not None:
396                         if prev.ns().name == node.ns().name:
397                             count += 1
398             prev = prev.previousElementSibling()
399         name = '%s[%i]' % (name, count)
400     if node.parent.type == 'element':
401         name = xml_get_node_path(node.parent) + name
402     return name
403
404 def xml_error_catcher(doc, error):
405     doc._xml_err += " %s" % error
406
407 def fix_node_ns (node, nsdefs):
408     childnsdefs = nsdefs.copy()
409     nsdef = node.nsDefs()
410     while nsdef is not None:
411         nextnsdef = nsdef.next
412         if nsdefs.has_key(nsdef.name) and nsdefs[nsdef.name] == nsdef.content:
413             node.removeNsDef(nsdef.content)
414         else:
415             childnsdefs[nsdef.name] = nsdef.content
416         nsdef = nextnsdef
417     for child in xml_child_iter(node):
418         if child.type == 'element':
419             fix_node_ns(child, childnsdefs)
420
421
422 class LocNote (object):
423     def __init__(self, locnote=None, locnoteref=None, locnotetype=None, space=False):
424         self.locnote = locnote
425         self.locnoteref = locnoteref
426         self.locnotetype = locnotetype
427         if self.locnotetype != 'alert':
428             self.locnotetype = 'description'
429         self._preserve_space=space
430
431     def __repr__(self):
432         if self.locnote is not None:
433             if self._preserve_space:
434                 return self.locnote
435             else:
436                 return re.sub('\s+', ' ', self.locnote).strip()
437         elif self.locnoteref is not None:
438             return '(itstool) link: ' + re.sub('\s+', ' ', self.locnoteref).strip()
439         return ''
440
441
442 class Document (object):
443     def __init__ (self, filename, messages):
444         self._xml_err = ''
445         libxml2.registerErrorHandler(xml_error_catcher, self)
446         try:
447             ctxt = libxml2.createFileParserCtxt(filename)
448         except:
449             sys.stderr.write('Error: cannot open XML file %s\n' % filename)
450             sys.exit(1)
451         ctxt.lineNumbers(1)
452         ctxt.replaceEntities(1)
453         ctxt.parseDocument()
454         self._filename = filename
455         self._doc = ctxt.doc()
456         self._localrules = []
457         def pre_process (node):
458             for child in xml_child_iter(node):
459                 if xml_is_ns_name(child, 'http://www.w3.org/2001/XInclude', 'include'):
460                     if child.nsProp('parse', None) == 'text':
461                         child.xincludeProcessTree()
462                 elif xml_is_ns_name(child, NS_ITS, 'rules'):
463                     if child.hasNsProp('href', NS_XLINK):
464                         href = child.nsProp('href', NS_XLINK)
465                         href = os.path.join(os.path.dirname(filename), href)
466                         hctxt = libxml2.createFileParserCtxt(href)
467                         hctxt.replaceEntities(1)
468                         hctxt.parseDocument()
469                         root = hctxt.doc().getRootElement()
470                         version = None
471                         if root.hasNsProp('version', None):
472                             version = root.nsProp('version', None)
473                         else:
474                             sys.stderr.write('Warning: ITS file %s missing version attribute\n' %
475                                              os.path.basename(href))
476                         if version is not None and version not in ('1.0', '2.0'):
477                             sys.stderr.write('Warning: Skipping ITS file %s with unknown version %s\n' %
478                                              (os.path.basename(href), root.nsProp('version', None)))
479                         else:
480                             self._localrules.append(root)
481                     version = None
482                     if child.hasNsProp('version', None):
483                         version = child.nsProp('version', None)
484                     else:
485                         root = child.doc.getRootElement()
486                         if root.hasNsProp('version', NS_ITS):
487                             version = root.nsProp('version', NS_ITS)
488                         else:
489                             sys.stderr.write('Warning: Local ITS rules missing version attribute\n')
490                     if version is not None and version not in ('1.0', '2.0'):
491                         sys.stderr.write('Warning: Skipping local ITS rules with unknown version %s\n' %
492                                          version)
493                     else:
494                         self._localrules.append(child)
495                 pre_process(child)
496         pre_process(self._doc)
497         try:
498             self._check_errors()
499         except libxml2.parserError as e:
500             sys.stderr.write('Error: Could not parse document:\n%s\n' % str(e))
501             sys.exit(1)
502         self._msgs = messages
503         self._its_translate_nodes = {}
504         self._its_within_text_nodes = {}
505         self._its_locale_filters = {}
506         self._its_id_values = {}
507         self._its_loc_notes = {}
508         self._its_preserve_space_nodes = {}
509         self._itst_drop_nodes = {}
510         self._itst_contexts = {}
511         self._its_lang = {}
512         self._itst_lang_attr = {}
513         self._itst_credits = None
514         self._its_externals = {}
515
516     def _check_errors(self):
517         if self._xml_err:
518             raise libxml2.parserError(self._xml_err)
519
520     def apply_its_rule(self, rule, xpath):
521         if rule.type != 'element':
522             return
523         if xml_is_ns_name(rule, NS_ITS, 'translateRule'):
524             if rule.nsProp('selector', None) is not None:
525                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
526                     self._its_translate_nodes[node] = rule.nsProp('translate', None)
527         elif xml_is_ns_name(rule, NS_ITS, 'withinTextRule'):
528             if rule.nsProp('selector', None) is not None:
529                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
530                     self._its_within_text_nodes[node] = rule.nsProp('withinText', None)
531         elif xml_is_ns_name(rule, NS_ITST, 'preserveSpaceRule'):
532             if rule.nsProp('selector', None) is not None:
533                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
534                     val = rule.nsProp('preserveSpace', None)
535                     if val == 'yes':
536                         self._its_preserve_space_nodes[node] = 'preserve'
537         elif xml_is_ns_name(rule, NS_ITS, 'preserveSpaceRule'):
538             if rule.nsProp('selector', None) is not None:
539                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
540                     self._its_preserve_space_nodes[node] = rule.nsProp('space', None)
541         elif xml_is_ns_name(rule, NS_ITS, 'localeFilterRule'):
542             if rule.nsProp('selector', None) is not None:
543                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
544                     self._its_locale_filters[node] = rule.nsProp('localeFilterList', None)
545         elif xml_is_ns_name(rule, NS_ITST, 'dropRule'):
546             if rule.nsProp('selector', None) is not None:
547                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
548                     self._itst_drop_nodes[node] = rule.nsProp('drop', None)
549         elif xml_is_ns_name(rule, NS_ITS, 'idValueRule'):
550             sel = rule.nsProp('selector', None)
551             idv = rule.nsProp('idValue', None)
552             if sel is not None and idv is not None:
553                 for node in self._try_xpath_eval(xpath, sel):
554                     try:
555                         oldnode = xpath.contextNode()
556                     except:
557                         oldnode = None
558                     xpath.setContextNode(node)
559                     idvalue = self._try_xpath_eval(xpath, idv)
560                     if isinstance(idvalue, basestring):
561                         self._its_id_values[node] = idvalue
562                     else:
563                         for val in idvalue:
564                             self._its_id_values[node] = val.content
565                             break
566                     xpath.setContextNode(oldnode)
567             pass
568         elif xml_is_ns_name(rule, NS_ITST, 'contextRule'):
569             if rule.nsProp('selector', None) is not None:
570                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
571                     if rule.hasNsProp('context', None):
572                         self._itst_contexts[node] = rule.nsProp('context', None)
573                     elif rule.hasNsProp('contextPointer', None):
574                         try:
575                             oldnode = xpath.contextNode()
576                         except:
577                             oldnode = None
578                         xpath.setContextNode(node)
579                         ctxt = self._try_xpath_eval(xpath, rule.nsProp('contextPointer', None))
580                         if isinstance(ctxt, basestring):
581                             self._itst_contexts[node] = ctxt
582                         else:
583                             for ctxt in ctxt:
584                                 self._itst_contexts[node] = ctxt.content
585                                 break
586                         xpath.setContextNode(oldnode)
587         elif xml_is_ns_name(rule, NS_ITS, 'locNoteRule'):
588             locnote = None
589             notetype = rule.nsProp('locNoteType', None)
590             for child in xml_child_iter(rule):
591                 if xml_is_ns_name(child, NS_ITS, 'locNote'):
592                     locnote = LocNote(locnote=child.content, locnotetype=notetype)
593                     break
594             if locnote is None:
595                 if rule.hasNsProp('locNoteRef', None):
596                     locnote = LocNote(locnoteref=rule.nsProp('locNoteRef', None), locnotetype=notetype)
597             if rule.nsProp('selector', None) is not None:
598                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
599                     if locnote is not None:
600                         self._its_loc_notes.setdefault(node, []).append(locnote)
601                     else:
602                         if rule.hasNsProp('locNotePointer', None):
603                             sel = rule.nsProp('locNotePointer', None)
604                             ref = False
605                         elif rule.hasNsProp('locNoteRefPointer', None):
606                             sel = rule.nsProp('locNoteRefPointer', None)
607                             ref = True
608                         else:
609                             continue
610                         try:
611                             oldnode = xpath.contextNode()
612                         except:
613                             oldnode = None
614                         xpath.setContextNode(node)
615                         note = self._try_xpath_eval(xpath, sel)
616                         if isinstance(note, basestring):
617                             if ref:
618                                 nodenote = LocNote(locnoteref=note, locnotetype=notetype)
619                             else:
620                                 nodenote = LocNote(locnote=note, locnotetype=notetype)
621                             self._its_loc_notes.setdefault(node, []).append(nodenote)
622                         else:
623                             for note in note:
624                                 if ref:
625                                     nodenote = LocNote(locnoteref=note.content, locnotetype=notetype)
626                                 else:
627                                     nodenote = LocNote(locnote=note.content, locnotetype=notetype,
628                                                       space=self.get_preserve_space(note))
629                                 self._its_loc_notes.setdefault(node, []).append(nodenote)
630                                 break
631                         xpath.setContextNode(oldnode)
632         elif xml_is_ns_name(rule, NS_ITS, 'langRule'):
633             if rule.nsProp('selector', None) is not None and rule.nsProp('langPointer', None) is not None:
634                 for node in self._try_xpath_eval(xpath, rule.nsProp('selector', None)):
635                     try:
636                         oldnode = xpath.contextNode()
637                     except:
638                         oldnode = None
639                     xpath.setContextNode(node)
640                     res = self._try_xpath_eval(xpath, rule.nsProp('langPointer', None))
641                     if len(res) > 0:
642                         self._its_lang[node] = res[0].content
643                     # We need to construct language attributes, not just read
644                     # language information. Technically, langPointer could be
645                     # any XPath expression. But if it looks like an attribute
646                     # accessor, just use the attribute name.
647                     if rule.nsProp('langPointer', None)[0] == '@':
648                         self._itst_lang_attr[node] = rule.nsProp('langPointer', None)[1:]
649                     xpath.setContextNode(oldnode)
650         elif xml_is_ns_name(rule, NS_ITST, 'credits'):
651             if rule.nsProp('appendTo', None) is not None:
652                 for node in self._try_xpath_eval(xpath, rule.nsProp('appendTo', None)):
653                     self._itst_credits = (node, rule)
654                     break
655         elif (xml_is_ns_name(rule, NS_ITS, 'externalResourceRefRule') or
656               xml_is_ns_name(rule, NS_ITST, 'externalRefRule')):
657             sel = rule.nsProp('selector', None)
658             if xml_is_ns_name(rule, NS_ITS, 'externalResourceRefRule'):
659                 ptr = rule.nsProp('externalResourceRefPointer', None)
660             else:
661                 ptr = rule.nsProp('refPointer', None)
662             if sel is not None and ptr is not None:
663                 for node in self._try_xpath_eval(xpath, sel):
664                     try:
665                         oldnode = xpath.contextNode()
666                     except:
667                         oldnode = None
668                     xpath.setContextNode(node)
669                     res = self._try_xpath_eval(xpath, ptr)
670                     if len(res) > 0:
671                         self._its_externals[node] = res[0].content
672                     xpath.setContextNode(oldnode)
673
674     def apply_its_rules (self, builtins):
675         if builtins:
676             dirs = []
677             ddir = os.getenv('XDG_DATA_HOME', '')
678             if ddir == '':
679                 ddir = os.path.join(os.path.expanduser('~'), '.local', 'share')
680             dirs.append(ddir)
681             ddir = os.getenv('XDG_DATA_DIRS', '')
682             if ddir == '':
683                 if DATADIR not in ('/usr/local/share', '/usr/share'):
684                     ddir += DATADIR + ':'
685                 ddir += '/usr/local/share:/usr/share'
686             dirs.extend(ddir.split(':'))
687             ddone = {}
688             for ddir in dirs:
689                 itsdir = os.path.join(ddir, 'itstool', 'its')
690                 if not os.path.exists(itsdir):
691                     continue
692                 for dfile in os.listdir(itsdir):
693                     if dfile.endswith('.its'):
694                         if not ddone.get(dfile, False):
695                             self.apply_its_file(os.path.join(itsdir, dfile))
696                             ddone[dfile] = True
697         self.apply_local_its_rules()
698
699     def apply_its_file (self, filename):
700         doc = libxml2.parseFile(filename)
701         root = doc.getRootElement()
702         if not xml_is_ns_name(root, NS_ITS, 'rules'):
703             return
704         version = None
705         if root.hasNsProp('version', None):
706             version = root.nsProp('version', None)
707         else:
708             sys.stderr.write('Warning: ITS file %s missing version attribute\n' %
709                              os.path.basename(filename))
710         if version is not None and version not in ('1.0', '2.0'):
711             sys.stderr.write('Warning: Skipping ITS file %s with unknown version %s\n' %
712                              (os.path.basename(filename), root.nsProp('version', None)))
713             return
714         matched = True
715         for match in xml_child_iter(root):
716             if xml_is_ns_name(match, NS_ITST, 'match'):
717                 matched = False
718                 xpath = self._doc.xpathNewContext()
719                 par = match
720                 nss = {}
721                 while par is not None:
722                     nsdef = par.nsDefs()
723                     while nsdef is not None:
724                         if nsdef.name is not None:
725                             if not nss.has_key(nsdef.name):
726                                 nss[nsdef.name] = nsdef.content
727                                 xpath.xpathRegisterNs(nsdef.name, nsdef.content)
728                         nsdef = nsdef.next
729                     par = par.parent
730                 if match.hasNsProp('selector', None):
731                     if len(self._try_xpath_eval(xpath, match.nsProp('selector', None))) > 0:
732                         matched = True
733                         break
734         if matched == False:
735             return
736         for rule in xml_child_iter(root):
737             xpath = self._doc.xpathNewContext()
738             par = match
739             nss = {}
740             while par is not None:
741                 nsdef = par.nsDefs()
742                 while nsdef is not None:
743                     if nsdef.name is not None:
744                         if not nss.has_key(nsdef.name):
745                             nss[nsdef.name] = nsdef.content
746                             xpath.xpathRegisterNs(nsdef.name, nsdef.content)
747                     nsdef = nsdef.next
748                 par = par.parent
749             self.apply_its_rule(rule, xpath)
750
751     def apply_local_its_rules (self):
752         for rules in self._localrules:
753             def reg_ns(xpath, node):
754                 if node.parent is not None:
755                     reg_ns(xpath, node.parent)
756                 nsdef = node.nsDefs()
757                 while nsdef is not None:
758                     if nsdef.name is not None:
759                         xpath.xpathRegisterNs(nsdef.name, nsdef.content)
760                     nsdef = nsdef.next
761             xpath = self._doc.xpathNewContext()
762             reg_ns(xpath, rules)
763             for rule in xml_child_iter(rules):
764                 if rule.type != 'element':
765                     continue
766                 if rule.nsDefs() is not None:
767                     rule_xpath = self._doc.xpathNewContent()
768                     reg_ns(rule_xpath, rule)
769                 else:
770                     rule_xpath = xpath
771                 self.apply_its_rule(rule, rule_xpath)
772
773     def _append_credits(self, parent, node, trdata):
774         if xml_is_ns_name(node, NS_ITST, 'for-each'):
775             select = node.nsProp('select', None)
776             if select == 'years':
777                 for year in trdata[2].split(','):
778                     for child in xml_child_iter(node):
779                         self._append_credits(parent, child, trdata + (year.strip(),))
780         elif xml_is_ns_name(node, NS_ITST, 'value-of'):
781             select = node.nsProp('select', None)
782             val = None
783             if select == 'name':
784                 val = trdata[0]
785             elif select == 'email':
786                 val = trdata[1]
787             elif select == 'years':
788                 val = trdata[2]
789             elif select == 'year' and len(trdata) == 4:
790                 val = trdata[3]
791             if val is not None:
792                 val = val.encode('utf-8')
793                 parent.addContent(val)
794         else:
795             newnode = node.copyNode(2)
796             parent.addChild(newnode)
797             for child in xml_child_iter(node):
798                 self._append_credits(newnode, child, trdata)
799
800     def merge_credits(self, translations, language, node):
801         if self._itst_credits is None:
802             return
803         # Dear Python, please implement pgettext.
804         # http://bugs.python.org/issue2504
805         # Sincerely, Shaun
806         trans = translations.ugettext('_\x04translator-credits')
807         if trans is None or trans == 'translator-credits':
808             return
809         regex = re.compile('(.*) \<(.*)\>, (.*)')
810         for credit in trans.split('\n'):
811             match = regex.match(credit)
812             if not match:
813                 continue
814             trdata = match.groups()
815             for node in xml_child_iter(self._itst_credits[1]):
816                 self._append_credits(self._itst_credits[0], node, trdata)
817
818     def join_translations(self, translations, node=None, strict=False):
819         is_root = False
820         if node is None:
821             is_root = True
822             self.generate_messages(comments=False)
823             node = self._doc.getRootElement()
824         if node is None or node.type != 'element':
825             return
826         if self.get_itst_drop(node) == 'yes':
827             prev = node.prev
828             node.unlinkNode()
829             node.freeNode()
830             if prev.isBlankNode():
831                 prev.unlinkNode()
832                 prev.freeNode()
833             return
834         msg = self._msgs.get_message_by_node(node)
835         if msg is None:
836             self.translate_attrs(node, node)
837             children = [child for child in xml_child_iter(node)]
838             for child in children:
839                 self.join_translations(translations, node=child, strict=strict)
840         else:
841             prevnode = None
842             if node.prev is not None and node.prev.type == 'text':
843                 prevtext = node.prev.content
844                 if re.sub('\s+', '', prevtext) == '':
845                     prevnode = node.prev
846             for lang in sorted(translations.keys(), reverse=True):
847                 locale = self.get_its_locale_filter(node)
848                 if not match_locale_list(locale, lang):
849                     continue
850                 newnode = self.get_translated(node, translations[lang], strict=strict, lang=lang)
851                 if newnode != node:
852                     newnode.setProp('xml:lang', lang)
853                     node.addNextSibling(newnode)
854                     if prevnode is not None:
855                         node.addNextSibling(prevnode.copyNode(0))
856         if is_root:
857             # Because of the way we create nodes and rewrite the document,
858             # we end up with lots of redundant namespace definitions. We
859             # kill them off in one fell swoop at the end.
860             fix_node_ns(node, {})
861             self._check_errors()
862
863     def merge_translations(self, translations, language, node=None, strict=False):
864         is_root = False
865         if node is None:
866             is_root = True
867             self.generate_messages(comments=False)
868             node = self._doc.getRootElement()
869         if node is None or node.type != 'element':
870             return
871         locale = self.get_its_locale_filter(node)
872         if locale != '*':
873             if not match_locale_list(locale, language):
874                 locale = ''
875         if self.get_itst_drop(node) == 'yes' or locale == '':
876             prev = node.prev
877             node.unlinkNode()
878             node.freeNode()
879             if prev.isBlankNode():
880                 prev.unlinkNode()
881                 prev.freeNode()
882             return
883         if is_root:
884             self.merge_credits(translations, language, node)
885         msg = self._msgs.get_message_by_node(node)
886         if msg is None:
887             self.translate_attrs(node, node)
888             children = [child for child in xml_child_iter(node)]
889             for child in children:
890                 self.merge_translations(translations, language, node=child, strict=strict)
891         else:
892             newnode = self.get_translated(node, translations, strict=strict, lang=language)
893             if newnode != node:
894                 self.translate_attrs(node, newnode)
895                 node.replaceNode(newnode)
896         if is_root:
897             # Apply language attributes to untranslated nodes. We don't do
898             # this before processing, because then these attributes would
899             # be copied into the new nodes. We apply the attribute without
900             # checking whether it was translated, because any that were will
901             # just be floating around, unattached to a document.
902             for lcnode in self._msgs.get_nodes_with_messages():
903                 attr = self._itst_lang_attr.get(lcnode)
904                 if attr is None:
905                     continue
906                 origlang = None
907                 lcpar = lcnode
908                 while lcpar is not None:
909                     origlang = self._its_lang.get(lcpar)
910                     if origlang is not None:
911                         break
912                     lcpar = lcpar.parent
913                 if origlang is not None:
914                     lcnode.setProp(attr, origlang)
915             # And then set the language attribute on the root node.
916             if language is not None:
917                 attr = self._itst_lang_attr.get(node)
918                 if attr is not None:
919                     node.setProp(attr, language)
920             # Because of the way we create nodes and rewrite the document,
921             # we end up with lots of redundant namespace definitions. We
922             # kill them off in one fell swoop at the end.
923             fix_node_ns(node, {})
924             self._check_errors()
925
926     def translate_attrs(self, oldnode, newnode):
927         trans_attrs = [attr for attr in xml_attr_iter(oldnode) if self._its_translate_nodes.get(attr, 'no') == 'yes']
928         for attr in trans_attrs:
929             newcontent = translations.ugettext(attr.get_content())
930             if newcontent:
931                 newnode.setProp(attr.name, translations.ugettext(attr.get_content()))
932
933     def get_translated (self, node, translations, strict=False, lang=None):
934         msg = self._msgs.get_message_by_node(node)
935         if msg is None:
936             return node
937         msgstr = msg.get_string()
938         # Dear Python, please implement pgettext.
939         # http://bugs.python.org/issue2504
940         # Sincerely, Shaun
941         if msg.get_context() is not None:
942             msgstr = msg.get_context() + '\x04' + msgstr
943         trans = translations.ugettext(msgstr)
944         if trans is None:
945             return node
946         nss = {}
947         def reg_ns(node, nss):
948             if node.parent is not None:
949                 reg_ns(node.parent, nss)
950             nsdef = node.nsDefs()
951             while nsdef is not None:
952                 nss[nsdef.name] = nsdef.content
953                 nsdef = nsdef.next
954         reg_ns(node, nss)
955         nss['_'] = NS_BLANK
956         blurb = '<' + node.name
957         for nsname in nss.keys():
958             if nsname is None:
959                 blurb += ' xmlns="%s"' % nss[nsname]
960             else:
961                 blurb += ' xmlns:%s="%s"' % (nsname, nss[nsname])
962         blurb += '>%s</%s>' % (trans.encode('utf-8'), node.name)
963         ctxt = libxml2.createDocParserCtxt(blurb)
964         ctxt.replaceEntities(0)
965         ctxt.parseDocument()
966         trnode = ctxt.doc().getRootElement()
967         try:
968             self._check_errors()
969         except libxml2.parserError as e:
970             if strict:
971                 raise
972             else:
973                 sys.stderr.write('Warning: Could not merge %stranslation for msgid:\n%s\n' % (
974                         (lang + ' ') if lang is not None else '',
975                         msgstr.encode('utf-8')))
976                 self._xml_err = ''
977                 return node
978         def scan_node(node):
979             children = [child for child in xml_child_iter(node)]
980             for child in children:
981                 if child.type != 'element':
982                     continue
983                 if child.ns() is not None and child.ns().content == NS_BLANK:
984                     ph_node = msg.get_placeholder(child.name).node
985                     if self.has_child_elements(ph_node):
986                         self.merge_translations(translations, None, ph_node, strict=strict)
987                         child.replaceNode(ph_node)
988                     else:
989                         repl = self.get_translated(ph_node, translations, strict=strict, lang=lang)
990                         child.replaceNode(repl)
991                 scan_node(child)
992         scan_node(trnode)
993         retnode = node.copyNode(2)
994         for child in xml_child_iter(trnode):
995             retnode.addChild(child.copyNode(1))
996         return retnode
997
998     def generate_messages(self, comments=True):
999         if self._itst_credits is not None:
1000             self._msgs.add_credits()
1001         for child in xml_child_iter(self._doc):
1002             if child.type == 'element':
1003                 self.generate_message(child, None, comments=comments)
1004                 break
1005
1006     def generate_message (self, node, msg, comments=True, path=None):
1007         if node.type in ('text', 'cdata') and msg is not None:
1008             msg.add_text(node.content)
1009             return
1010         if node.type != 'element':
1011             return
1012         if node.hasNsProp('drop', NS_ITST) and node.nsProp('drop', NS_ITST) == 'yes':
1013             return
1014         if self._itst_drop_nodes.get(node, 'no') == 'yes':
1015             return
1016         if self.get_its_locale_filter(node) == '':
1017             return
1018         if path is None:
1019             path = ''
1020         translate = self.get_its_translate(node)
1021         withinText = False
1022         if translate == 'no':
1023             if msg is not None:
1024                 msg.add_placeholder(node)
1025             is_unit = False
1026             msg = None
1027         else:
1028             is_unit = msg is None or self.is_translation_unit(node)
1029             if is_unit:
1030                 if msg is not None:
1031                     msg.add_placeholder(node)
1032                 msg = Message()
1033                 ctxt = None
1034                 if node.hasNsProp('context', NS_ITST):
1035                     ctxt = node.nsProp('context', NS_ITST)
1036                 if ctxt is None:
1037                     ctxt = self._itst_contexts.get(node)
1038                 if ctxt is not None:
1039                     msg.set_context(ctxt)
1040                 idvalue = self.get_its_id_value(node)
1041                 if idvalue is not None:
1042                     basename = os.path.basename(self._filename)
1043                     msg.add_id_value(basename + '#' + idvalue)
1044                 if self.get_preserve_space(node):
1045                     msg.set_preserve_space()
1046                 if self.get_its_locale_filter(node) != '*':
1047                     msg.set_locale_filter(self.get_its_locale_filter(node))
1048                 msg.add_source('%s:%i' % (self._doc.name, node.lineNo()))
1049                 msg.add_marker('%s/%s' % (node.parent.name, node.name))
1050             else:
1051                 withinText = True
1052                 msg.add_start_tag(node)
1053
1054         if not withinText:
1055             # Add msg for translatable node attributes
1056             for attr in xml_attr_iter(node):
1057                 if self._its_translate_nodes.get(attr, 'no') == 'yes':
1058                     attr_msg = Message()
1059                     attr_msg.add_source('%s:%i' % (self._doc.name, node.lineNo()))
1060                     attr_msg.add_marker('%s/%s@%s' % (node.parent.name, node.name, attr.name))
1061                     attr_msg.add_text(attr.content)
1062                     if comments:
1063                         for locnote in self.get_its_loc_notes(attr):
1064                             comment = Comment(locnote)
1065                             comment.add_marker ('%s/%s@%s' % (
1066                                     node.parent.name, node.name, attr.name))
1067                             attr_msg.add_comment(comment)
1068                     self._msgs.add_message(attr_msg, attr)
1069
1070         if comments and msg is not None:
1071             cnode = node
1072             while cnode is not None:
1073                 hasnote = False
1074                 for locnote in self.get_its_loc_notes(cnode):
1075                     comment = Comment(locnote)
1076                     if withinText:
1077                         comment.add_marker('.%s/%s' % (path, cnode.name))
1078                     msg.add_comment(comment)
1079                     hasnote = True
1080                 if hasnote or not is_unit:
1081                     break
1082                 cnode = cnode.parent
1083
1084         self.generate_external_resource_message(node)
1085         for attr in xml_attr_iter(node):
1086             self.generate_external_resource_message(attr)
1087             idvalue = self.get_its_id_value(attr)
1088             if idvalue is not None:
1089                 basename = os.path.basename(self._filename)
1090                 msg.add_id_value(basename + '#' + idvalue)
1091
1092         if withinText:
1093             path = path + '/' + node.name
1094         for child in xml_child_iter(node):
1095             self.generate_message(child, msg, comments=comments, path=path)
1096
1097         if translate:
1098             if is_unit and not msg.is_empty():
1099                 self._msgs.add_message(msg, node)
1100             elif msg is not None:
1101                 msg.add_end_tag(node)
1102
1103     def generate_external_resource_message(self, node):
1104         if not self._its_externals.has_key(node):
1105             return
1106         resref = self._its_externals[node]
1107         if node.type == 'element':
1108             translate = self.get_its_translate(node)
1109             marker = '%s/%s' % (node.parent.name, node.name)
1110         else:
1111             translate = self.get_its_translate(node.parent)
1112             marker = '%s/%s/@%s' % (node.parent.parent.name, node.parent.name, node.name)
1113         if translate == 'no':
1114             return
1115         msg = Message()
1116         try:
1117             fullfile = os.path.join(os.path.dirname(self._filename), resref)
1118             filefp = open(fullfile)
1119             filemd5 = hashlib.md5(filefp.read()).hexdigest()
1120             filefp.close()
1121         except:
1122             filemd5 = '__failed__'
1123         txt = "external ref='%s' md5='%s'" % (resref, filemd5)
1124         msg.set_context('_')
1125         msg.add_text(txt)
1126         msg.add_source('%s:%i' % (self._doc.name, node.lineNo()))
1127         msg.add_marker(marker)
1128         msg.add_comment(Comment('This is a reference to an external file such as an image or'
1129                                 ' video. When the file changes, the md5 hash will change to'
1130                                 ' let you know you need to update your localized copy. The'
1131                                 ' msgstr is not used at all. Set it to whatever you like'
1132                                 ' once you have updated your copy of the file.'))
1133         self._msgs.add_message(msg, None)
1134
1135     def is_translation_unit (self, node):
1136         return self.get_its_within_text(node) != 'yes'
1137
1138     def has_child_elements(self, node):
1139         return len([child for child in xml_child_iter(node) if child.type=='element'])
1140
1141     def get_preserve_space (self, node):
1142         if node.getSpacePreserve() == 1:
1143             return True
1144         else:
1145             while node.type == 'element':
1146                 if self._its_preserve_space_nodes.has_key(node):
1147                     return (self._its_preserve_space_nodes[node] == 'preserve')
1148                 node = node.parent
1149         return False
1150
1151     def get_its_translate(self, node):
1152         val = None
1153         if node.hasNsProp('translate', NS_ITS):
1154             val = node.nsProp('translate', NS_ITS)
1155         elif xml_is_ns_name(node, NS_ITS, 'span') and node.hasNsProp('translate', None):
1156             val = node.nsProp('translate', None)
1157         elif self._its_translate_nodes.has_key(node):
1158             val = self._its_translate_nodes[node]
1159         if val is not None:
1160             return val
1161         if node.type == 'attribute':
1162             return 'no'
1163         if node.parent.type == 'element':
1164             return self.get_its_translate(node.parent)
1165         return 'yes'
1166
1167     def get_its_within_text(self, node):
1168         if node.hasNsProp('withinText', NS_ITS):
1169             val = node.nsProp('withinText', NS_ITS)
1170         elif xml_is_ns_name(node, NS_ITS, 'span') and node.hasNsProp('withinText', None):
1171             val = node.nsProp('withinText', None)
1172         else:
1173             return self._its_within_text_nodes.get(node, 'no')
1174         if val in ('yes', 'nested'):
1175             return val
1176         return 'no'
1177
1178     def get_its_locale_filter(self, node):
1179         if node.hasNsProp('localeFilterList', NS_ITS):
1180             return node.nsProp('localeFilterList', NS_ITS)
1181         if xml_is_ns_name(node, NS_ITS, 'span') and node.hasNsProp('localeFilterList', None):
1182             return node.nsProp('localeFilterList', None)
1183         if self._its_locale_filters.has_key(node):
1184             return self._its_locale_filters[node]
1185         if node.parent.type == 'element':
1186             return self.get_its_locale_filter(node.parent)
1187         return '*'
1188
1189     def get_itst_drop(self, node):
1190         if node.hasNsProp('drop', NS_ITST) and node.nsProp('drop', NS_ITST) == 'yes':
1191             return 'yes'
1192         if self._itst_drop_nodes.get(node, 'no') == 'yes':
1193             return 'yes'
1194         return 'no'
1195
1196     def get_its_id_value(self, node):
1197         if node.hasNsProp('id', NS_XML):
1198             return node.nsProp('id', NS_XML)
1199         return self._its_id_values.get(node, None)
1200
1201     def get_its_loc_notes(self, node):
1202         ret = []
1203         if node.hasNsProp('locNote', NS_ITS) or node.hasNsProp('locNoteRef', NS_ITS) or node.hasNsProp('locNoteType', NS_ITS):
1204             notetype = node.nsProp('locNoteType', NS_ITS)
1205             if node.hasNsProp('locNote', NS_ITS):
1206                 ret.append(LocNote(locnote=node.nsProp('locNote', NS_ITS), locnotetype=notetype))
1207             elif node.hasNsProp('locNoteRef', NS_ITS):
1208                 ret.append(LocNote(locnoteref=node.nsProp('locNoteRef', NS_ITS), locnotetype=notetype))
1209         if xml_is_ns_name(node, NS_ITS, 'span'):
1210             if node.hasNsProp('locNote', None) or node.hasNsProp('locNoteRef', None) or node.hasNsProp('locNoteType', None):
1211                 notetype = node.nsProp('locNoteType', None)
1212                 if node.hasNsProp('locNote', None):
1213                     ret.append(LocNote(locnote=node.nsProp('locNote', None), locnotetype=notetype))
1214                 elif node.hasNsProp('locNoteRef', None):
1215                     ret.append(LocNote(locnoteref=node.nsProp('locNoteRef', None), locnotetype=notetype))
1216         for locnote in self._its_loc_notes.get(node, []):
1217             ret.append(locnote)
1218         return ret
1219
1220     def output_test_data(self, category, out, node=None):
1221         if node is None:
1222             node = self._doc.getRootElement()
1223         compval = ''
1224         if category == 'translate':
1225             compval = 'translate="%s"' % self.get_its_translate(node)
1226         elif category == 'withinText':
1227             if node.type != 'attribute':
1228                 compval = 'withinText="%s"' % self.get_its_within_text(node)
1229         elif category == 'localeFilterList':
1230             compval = 'localeFilterList="%s"' % self.get_its_locale_filter(node)
1231         elif category == 'locNote':
1232             val = self.get_its_loc_notes(node)
1233             if len(val) > 0:
1234                 if val[0].locnote is not None:
1235                     compval = 'locNote="%s"\tlocNoteType="%s"' % (str(val[0]), val[0].locnotetype)
1236                 elif val[0].locnoteref is not None:
1237                     compval = 'locNoteRef="%s"\tlocNoteType="%s"' % (val[0].locnoteref, val[0].locnotetype)
1238         elif category == 'externalResourceRef':
1239             val = self._its_externals.get(node, '')
1240             if val != '':
1241                 compval = 'externalResourceRef="%s"' % val
1242         elif category == 'idValue':
1243             val = self.get_its_id_value(node)
1244             if val is not None:
1245                 compval = 'idValue="%s"' % val
1246         elif category == 'preserveSpace':
1247             if self.get_preserve_space(node):
1248                 compval = 'space="preserve"'
1249             else:
1250                 compval = 'space="default"'
1251         else:
1252             sys.stderr.write('Error: Unrecognized category %s\n' % category)
1253             sys.exit(1)
1254         if compval != '':
1255             out.write('%s\t%s\r\n' % (xml_get_node_path(node), compval))
1256         else:
1257             out.write('%s\r\n' % (xml_get_node_path(node)))
1258         for attr in sorted(xml_attr_iter(node), lambda x, y: cmp(str(x), str(y))):
1259             self.output_test_data(category, out, attr)
1260         for child in xml_child_iter(node):
1261             if child.type == 'element':
1262                 self.output_test_data(category, out, child)
1263
1264     @staticmethod
1265     def _try_xpath_eval (xpath, expr):
1266         try:
1267             return xpath.xpathEval(expr)
1268         except:
1269             sys.stderr.write('Warning: Invalid XPath: %s\n' % expr)
1270             return []
1271
1272 def match_locale_list(extranges, locale):
1273     if extranges.strip() == '':
1274         return False
1275     for extrange in [extrange.strip() for extrange in extranges.split(',')]:
1276         if match_locale(extrange, locale):
1277             return True
1278     return False
1279
1280 def match_locale(extrange, locale):
1281     # Extended filtering for extended language ranges as
1282     # defined by RFC4647, part of BCP47.
1283     # http://tools.ietf.org/html/rfc4647#section-3.3.2
1284     rangelist = [x.lower() for x in extrange.split('-')]
1285     localelist = [x.lower() for x in locale.split('-')]
1286     if rangelist[0] not in ('*', localelist[0]):
1287         return False
1288     rangei = localei = 0
1289     while rangei < len(rangelist):
1290         if rangelist[rangei] == '*':
1291             rangei += 1
1292             continue
1293         if localei >= len(localelist):
1294             return False
1295         if rangelist[rangei] in ('*', localelist[localei]):
1296             rangei += 1
1297             localei += 1
1298             continue
1299         if len(localelist[localei]) == 1:
1300             return False
1301         localei += 1
1302     return True
1303
1304 _locale_pattern = re.compile('([a-zA-Z0-9-]+)(_[A-Za-z0-9]+)?(@[A-Za-z0-9]+)?(\.[A-Za-z0-9]+)?')
1305 def convert_locale (locale):
1306     # Automatically convert POSIX-style locales to BCP47
1307     match = _locale_pattern.match(locale)
1308     if match is None:
1309         return locale
1310     ret = match.group(1).lower()
1311     variant = match.group(3)
1312     if variant == '@cyrillic':
1313         ret += '-Cyrl'
1314         variant = None
1315     if variant == '@devanagari':
1316         ret += '-Deva'
1317         variant = None
1318     elif variant == '@latin':
1319         ret += '-Latn'
1320         variant = None
1321     elif variant == '@shaw':
1322         ret += '-Shaw'
1323         variant = None
1324     if match.group(2) is not None:
1325         ret += '-' + match.group(2)[1:].upper()
1326     if variant is not None and variant != '@euro':
1327         ret += '-' + variant[1:].lower()
1328     return ret
1329
1330
1331 if __name__ == '__main__':
1332     options = optparse.OptionParser()
1333     options.set_usage('\n  itstool [OPTIONS] [XMLFILES]\n  itstool -m <MOFILE> [OPTIONS] [XMLFILES]')
1334     options.add_option('-i', '--its',
1335                        action='append',
1336                        dest='itsfile',
1337                        metavar='ITS',
1338                        help='load the ITS rules in the file ITS (can specify multiple times)')
1339     options.add_option('-l', '--lang',
1340                        dest='lang',
1341                        default=None,
1342                        metavar='LANGUAGE',
1343                        help='explicitly set the language code for output file')
1344     options.add_option('-j', '--join',
1345                        dest='join',
1346                        metavar='FILE',
1347                        help='join multiple MO files with the XML file FILE and output XML file')
1348     options.add_option('-m', '--merge',
1349                        dest='merge',
1350                        metavar='FILE',
1351                        help='merge from a PO or MO file FILE and output XML files')
1352     options.add_option('-n', '--no-builtins',
1353                        action='store_true',
1354                        dest='nobuiltins',
1355                        default=False,
1356                        help='do not apply the built-in ITS rules')
1357     options.add_option('-o', '--output',
1358                        dest='output',
1359                        default=None,
1360                        metavar='OUT',
1361                        help='output PO files to file OUT or XML files in directory OUT')
1362     options.add_option('-s', '--strict',
1363                        action='store_true',
1364                        dest='strict',
1365                        default=False,
1366                        help='Exit with error when PO files contain broken XML')
1367     options.add_option('-t', '--test',
1368                        dest='test',
1369                        default=None,
1370                        metavar='CATEGORY',
1371                        help='generate conformance test output for CATEGORY')
1372     options.add_option('-v', '--version',
1373                        action='store_true',
1374                        dest='version',
1375                        default=False,
1376                        help='print itstool version and exit')
1377     (opts, args) = options.parse_args(sys.argv)
1378
1379     if opts.version:
1380         print('itstool %s' % VERSION)
1381         sys.exit(0)
1382
1383     if opts.merge is None and opts.join is None:
1384         messages = MessageList()
1385         for filename in args[1:]:
1386             doc = Document(filename, messages)
1387             doc.apply_its_rules(not(opts.nobuiltins))
1388             if opts.itsfile is not None:
1389                 for itsfile in opts.itsfile:
1390                     doc.apply_its_file(itsfile)
1391             if opts.test is None:
1392                 doc.generate_messages()
1393         if opts.output is None or opts.output == '-':
1394             out = sys.stdout
1395         else:
1396             try:
1397                 out = file(opts.output, 'w')
1398             except:
1399                 sys.stderr.write('Error: Cannot write to file %s\n' % opts.output)
1400                 sys.exit(1)
1401         if opts.test is not None:
1402             doc.output_test_data(opts.test, out)
1403         else:
1404             messages.output(out)
1405     elif opts.merge is not None:
1406         try:
1407             translations = gettext.GNUTranslations(open(opts.merge, 'rb'))
1408         except:
1409             sys.stderr.write('Error: cannot open mo file %s\n' % opts.merge)
1410             sys.exit(1)
1411         translations.add_fallback(NoneTranslations())
1412         if opts.lang is None:
1413             opts.lang = convert_locale(os.path.splitext(os.path.basename(opts.merge))[0])
1414         if opts.output is None:
1415             out = './'
1416         elif os.path.isdir(opts.output):
1417             out = opts.output
1418         elif len(args) == 2:
1419             if opts.output == '-':
1420                 out = sys.stdout
1421             else:
1422                 out = file(opts.output, 'w')
1423         else:
1424             sys.stderr.write('Error: Non-directory output for multiple files\n')
1425             sys.exit(1)
1426         for filename in args[1:]:
1427             messages = MessageList()
1428             doc = Document(filename, messages)
1429             doc.apply_its_rules(not(opts.nobuiltins))
1430             if opts.itsfile is not None:
1431                 for itsfile in opts.itsfile:
1432                     doc.apply_its_file(itsfile)
1433             try:
1434                 doc.merge_translations(translations, opts.lang, strict=opts.strict)
1435             except Exception as e:
1436                 sys.stderr.write('Error: Could not merge translations:\n%s\n' % str(e))
1437                 sys.exit(1)
1438             fout = out
1439             if isinstance(fout, basestring):
1440                 fout = file(os.path.join(fout, os.path.basename(filename)), 'w')
1441             fout.write(doc._doc.serialize('utf-8'))
1442     elif opts.join is not None:
1443         translations = {}
1444         for filename in args[1:]:
1445             try:
1446                 thistr = gettext.GNUTranslations(open(filename, 'rb'))
1447             except:
1448                 sys.stderr.write('Error: cannot open mo file %s\n' % filename)
1449                 sys.exit(1)
1450             thistr.add_fallback(NoneTranslations())
1451             lang = convert_locale(os.path.splitext(os.path.basename(filename))[0])
1452             translations[lang] = thistr
1453         if opts.output is None:
1454             out = sys.stdout
1455         elif os.path.isdir(opts.output):
1456             out = file(os.path.join(opts.output, os.path.basename(filename)), 'w')
1457         else:
1458             out = file(opts.output, 'w')
1459         messages = MessageList()
1460         doc = Document(opts.join, messages)
1461         doc.apply_its_rules(not(opts.nobuiltins))
1462         doc.join_translations(translations, strict=opts.strict)
1463         out.write(doc._doc.serialize('utf-8'))
1464         if False:
1465             if opts.itsfile is not None:
1466                 for itsfile in opts.itsfile:
1467                     doc.apply_its_file(itsfile)
1468             try:
1469                 doc.merge_translations(translations, opts.lang, strict=opts.strict)
1470             except Exception as e:
1471                 sys.stderr.write('Error: Could not merge translations:\n%s\n' % str(e))
1472                 sys.exit(1)
1473             fout = out
1474             if isinstance(fout, basestring):
1475                 fout = file(os.path.join(fout, os.path.basename(filename)), 'w')
1476             fout.write(doc._doc.serialize('utf-8'))