1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """Module for handling XLIFF files for translation.
22
23 The official recommendation is to use the extention .xlf for XLIFF files.
24 """
25
26 from lxml import etree
27
28 from translate.misc.multistring import multistring
29 from translate.storage import base, lisa
30 from translate.storage.lisa import getXMLspace
31 from translate.storage.placeables.lisa import xml_to_strelem, strelem_to_xml
32 from translate.storage.workflow import StateEnum as state
33
34
35
36 ID_SEPARATOR = u"\04"
37
38
39
40
41
42 ID_SEPARATOR_SAFE = u"__%04__"
43
44
46 """A single term in the xliff file."""
47
48 rootNode = "trans-unit"
49 languageNode = "source"
50 textNode = ""
51 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
52
53 _default_xml_space = "default"
54
55
56
57 S_UNTRANSLATED = state.EMPTY
58 S_NEEDS_TRANSLATION = state.NEEDS_WORK
59 S_NEEDS_REVIEW = state.NEEDS_REVIEW
60 S_TRANSLATED = state.UNREVIEWED
61 S_SIGNED_OFF = state.FINAL
62 S_FINAL = state.MAX
63
64 statemap = {"new": S_UNTRANSLATED + 1,
65 "needs-translation": S_NEEDS_TRANSLATION,
66 "needs-adaptation": S_NEEDS_TRANSLATION + 1,
67 "needs-l10n": S_NEEDS_TRANSLATION + 2,
68 "needs-review-translation": S_NEEDS_REVIEW,
69 "needs-review-adaptation": S_NEEDS_REVIEW + 1,
70 "needs-review-l10n": S_NEEDS_REVIEW + 2,
71 "translated": S_TRANSLATED,
72 "signed-off": S_SIGNED_OFF,
73 "final": S_FINAL,
74 }
75
76 statemap_r = dict((i[1], i[0]) for i in statemap.iteritems())
77
78 STATE = {
79 S_UNTRANSLATED: (state.EMPTY, state.NEEDS_WORK),
80 S_NEEDS_TRANSLATION: (state.NEEDS_WORK, state.NEEDS_REVIEW),
81 S_NEEDS_REVIEW: (state.NEEDS_REVIEW, state.UNREVIEWED),
82 S_TRANSLATED: (state.UNREVIEWED, state.FINAL),
83 S_SIGNED_OFF: (state.FINAL, state.MAX),
84 }
85
86 - def __init__(self, source, empty=False, **kwargs):
87 """Override the constructor to set xml:space="preserve"."""
88 super(xliffunit, self).__init__(source, empty, **kwargs)
89 if empty:
90 return
91 lisa.setXMLspace(self.xmlelement, "preserve")
92
94 """Returns an xml Element setup with given parameters."""
95
96
97
98
99 assert purpose
100 langset = etree.Element(self.namespaced(purpose))
101
102
103
104
105 langset.text = text
106 return langset
107
123
125 sourcelanguageNode = self.get_source_dom()
126 if sourcelanguageNode is None:
127 sourcelanguageNode = self.createlanguageNode(sourcelang, u'', "source")
128 self.set_source_dom(sourcelanguageNode)
129
130
131 for i in range(len(sourcelanguageNode)):
132 del sourcelanguageNode[0]
133 sourcelanguageNode.text = None
134
135 strelem_to_xml(sourcelanguageNode, value[0])
136
143 rich_source = property(get_rich_source, set_rich_source)
144
161
166 rich_target = property(get_rich_target, set_rich_target)
167
168 - def addalttrans(self, txt, origin=None, lang=None, sourcetxt=None, matchquality=None):
169 """Adds an alt-trans tag and alt-trans components to the unit.
170
171 @type txt: String
172 @param txt: Alternative translation of the source text.
173 """
174
175
176
177 if isinstance(txt, str):
178 txt = txt.decode("utf-8")
179 alttrans = etree.SubElement(self.xmlelement, self.namespaced("alt-trans"))
180 lisa.setXMLspace(alttrans, "preserve")
181 if sourcetxt:
182 if isinstance(sourcetxt, str):
183 sourcetxt = sourcetxt.decode("utf-8")
184 altsource = etree.SubElement(alttrans, self.namespaced("source"))
185 altsource.text = sourcetxt
186 alttarget = etree.SubElement(alttrans, self.namespaced("target"))
187 alttarget.text = txt
188 if matchquality:
189 alttrans.set("match-quality", matchquality)
190 if origin:
191 alttrans.set("origin", origin)
192 if lang:
193 lisa.setXMLlang(alttrans, lang)
194
221
223 """Removes the supplied alternative from the list of alt-trans tags"""
224 self.xmlelement.remove(alternative.xmlelement)
225
226 - def addnote(self, text, origin=None, position="append"):
227 """Add a note specifically in a "note" tag"""
228 if position != "append":
229 self.removenotes(origin=origin)
230
231 if text:
232 text = text.strip()
233 if not text:
234 return
235 if isinstance(text, str):
236 text = text.decode("utf-8")
237 note = etree.SubElement(self.xmlelement, self.namespaced("note"))
238 note.text = text
239 if origin:
240 note.set("from", origin)
241
243 """Private method that returns the text from notes matching 'origin' or all notes."""
244 notenodes = self.xmlelement.iterdescendants(self.namespaced("note"))
245
246
247
248 initial_list = [lisa.getText(note, getXMLspace(self.xmlelement, self._default_xml_space)) for note in notenodes if self.correctorigin(note, origin)]
249
250
251 dictset = {}
252 notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
253
254 return notelist
255
258
260 """Remove all the translator notes."""
261 notes = self.xmlelement.iterdescendants(self.namespaced("note"))
262 for note in notes:
263 if self.correctorigin(note, origin=origin):
264 self.xmlelement.remove(note)
265
266 - def adderror(self, errorname, errortext):
267 """Adds an error message to this unit."""
268
269 text = errorname
270 if errortext:
271 text += ': ' + errortext
272 self.addnote(text, origin="pofilter")
273
275 """Get all error messages."""
276
277 notelist = self.getnotelist(origin="pofilter")
278 errordict = {}
279 for note in notelist:
280 errorname, errortext = note.split(': ')
281 errordict[errorname] = errortext
282 return errordict
283
307
324
326 """States whether this unit is approved."""
327 return self.xmlelement.get("approved") == "yes"
328
330 """Mark this unit as approved."""
331 if value:
332 self.xmlelement.set("approved", "yes")
333 elif self.isapproved():
334 self.xmlelement.set("approved", "no")
335
339
349
356
367
368 - def settarget(self, text, lang='xx', append=False):
373
374
375
376
377
378
379
380
382 value = self.xmlelement.get("translate")
383 if value and value.lower() == 'no':
384 return False
385 return True
386
391
395
408
411
413 id_attr = unicode(self.xmlelement.get("id") or u"")
414 if id_attr:
415 return [id_attr]
416 return []
417
418 - def createcontextgroup(self, name, contexts=None, purpose=None):
419 """Add the context group to the trans-unit with contexts a list with
420 (type, text) tuples describing each context."""
421 assert contexts
422 group = etree.Element(self.namespaced("context-group"))
423
424
425
426 if self.xmlelement.tag == self.namespaced("group"):
427 self.xmlelement.insert(0, group)
428 else:
429 self.xmlelement.append(group)
430 group.set("name", name)
431 if purpose:
432 group.set("purpose", purpose)
433 for type, text in contexts:
434 if isinstance(text, str):
435 text = text.decode("utf-8")
436 context = etree.SubElement(group, self.namespaced("context"))
437 context.text = text
438 context.set("context-type", type)
439
440 - def getcontextgroups(self, name):
441 """Returns the contexts in the context groups with the specified name"""
442 groups = []
443 grouptags = self.xmlelement.iterdescendants(self.namespaced("context-group"))
444
445 for group in grouptags:
446 if group.get("name") == name:
447 contexts = group.iterdescendants(self.namespaced("context"))
448 pairs = []
449 for context in contexts:
450 pairs.append((context.get("context-type"), lisa.getText(context, getXMLspace(self.xmlelement, self._default_xml_space))))
451 groups.append(pairs)
452 return groups
453
455 """returns the restype attribute in the trans-unit tag"""
456 return self.xmlelement.get("restype")
457
458 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
469
471 """Check against node tag's origin (e.g note or alt-trans)"""
472 if origin == None:
473 return True
474 elif origin in node.get("from", ""):
475 return True
476 elif origin in node.get("origin", ""):
477 return True
478 else:
479 return False
480
482 """Override L{TranslationUnit.multistring_to_rich} which is used by the
483 C{rich_source} and C{rich_target} properties."""
484 strings = mstr
485 if isinstance(mstr, multistring):
486 strings = mstr.strings
487 elif isinstance(mstr, basestring):
488 strings = [mstr]
489
490 return [xml_to_strelem(s) for s in strings]
491 multistring_to_rich = classmethod(multistring_to_rich)
492
494 """Override L{TranslationUnit.rich_to_multistring} which is used by the
495 C{rich_source} and C{rich_target} properties."""
496 return multistring([unicode(elem) for elem in elem_list])
497 rich_to_multistring = classmethod(rich_to_multistring)
498
499
501 """Class representing a XLIFF file store."""
502 UnitClass = xliffunit
503 Name = _("XLIFF Translation File")
504 Mimetypes = ["application/x-xliff", "application/x-xliff+xml"]
505 Extensions = ["xlf", "xliff", "sdlxliff"]
506 rootNode = "xliff"
507 bodyNode = "body"
508 XMLskeleton = '''<?xml version="1.0" ?>
509 <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
510 <file original='NoName' source-language='en' datatype='plaintext'>
511 <body>
512 </body>
513 </file>
514 </xliff>'''
515 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
516 suggestions_in_format = True
517 """xliff units have alttrans tags which can be used to store suggestions"""
518
520 self._filename = None
521 lisa.LISAfile.__init__(self, *args, **kwargs)
522 self._messagenum = 0
523
524 - def initbody(self):
525 self.namespace = self.document.getroot().nsmap.get(None, None)
526
527 if self._filename:
528 filenode = self.getfilenode(self._filename, createifmissing=True)
529 else:
530 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
531 self.body = self.getbodynode(filenode, createifmissing=True)
532
534 """Initialise the file header."""
535 pass
536
537 - def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
562
564 """returns the name of the given file"""
565 return filenode.get("original")
566
568 """set the name of the given file"""
569 return filenode.set("original", filename)
570
572 """returns all filenames in this XLIFF file"""
573 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
574 filenames = [self.getfilename(filenode) for filenode in filenodes]
575 filenames = filter(None, filenames)
576 if len(filenames) == 1 and filenames[0] == '':
577 filenames = []
578 return filenames
579
580 - def getfilenode(self, filename, createifmissing=False):
581 """finds the filenode with the given name"""
582 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
583 for filenode in filenodes:
584 if self.getfilename(filenode) == filename:
585 return filenode
586 if createifmissing:
587 filenode = self.createfilenode(filename)
588 return filenode
589 return None
590
591 - def getids(self, filename=None):
592 if not filename:
593 return super(xlifffile, self).getids()
594
595 self.id_index = {}
596 prefix = filename + ID_SEPARATOR
597 units = (unit for unit in self.units if unit.getid().startswith(prefix))
598 for index, unit in enumerate(units):
599 self.id_index[unit.getid()[len(prefix):]] = unit
600 return self.id_index.keys()
601
603 if not language:
604 return
605 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
606 filenode.set("source-language", language)
607
609 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
610 return filenode.get("source-language")
611 sourcelanguage = property(getsourcelanguage, setsourcelanguage)
612
614 if not language:
615 return
616 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
617 filenode.set("target-language", language)
618
620 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
621 return filenode.get("target-language")
622 targetlanguage = property(gettargetlanguage, settargetlanguage)
623
625 """Returns the datatype of the stored file. If no filename is given,
626 the datatype of the first file is given."""
627 if filename:
628 node = self.getfilenode(filename)
629 if not node is None:
630 return node.get("datatype")
631 else:
632 filenames = self.getfilenames()
633 if len(filenames) > 0 and filenames[0] != "NoName":
634 return self.getdatatype(filenames[0])
635 return ""
636
638 """Returns the date attribute for the file. If no filename is given,
639 the date of the first file is given. If the date attribute is not
640 specified, None is returned."""
641 if filename:
642 node = self.getfilenode(filename)
643 if not node is None:
644 return node.get("date")
645 else:
646 filenames = self.getfilenames()
647 if len(filenames) > 0 and filenames[0] != "NoName":
648 return self.getdate(filenames[0])
649 return None
650
652 """We want to remove the default file-tag as soon as possible if we
653 know if still present and empty."""
654 filenodes = list(self.document.getroot().iterchildren(self.namespaced("file")))
655 if len(filenodes) > 1:
656 for filenode in filenodes:
657 if filenode.get("original") == "NoName" and \
658 not list(filenode.iterdescendants(self.namespaced(self.UnitClass.rootNode))):
659 self.document.getroot().remove(filenode)
660 break
661
663 """finds the header node for the given filenode"""
664
665 headernode = filenode.iterchildren(self.namespaced("header"))
666 try:
667 return headernode.next()
668 except StopIteration:
669 pass
670 if not createifmissing:
671 return None
672 headernode = etree.SubElement(filenode, self.namespaced("header"))
673 return headernode
674
675 - def getbodynode(self, filenode, createifmissing=False):
676 """finds the body node for the given filenode"""
677 bodynode = filenode.iterchildren(self.namespaced("body"))
678 try:
679 return bodynode.next()
680 except StopIteration:
681 pass
682 if not createifmissing:
683 return None
684 bodynode = etree.SubElement(filenode, self.namespaced("body"))
685 return bodynode
686
687 - def addsourceunit(self, source, filename="NoName", createifmissing=False):
688 """adds the given trans-unit to the last used body node if the
689 filename has changed it uses the slow method instead (will
690 create the nodes required if asked). Returns success"""
691 if self._filename != filename:
692 if not self.switchfile(filename, createifmissing):
693 return None
694 unit = super(xlifffile, self).addsourceunit(source)
695 self._messagenum += 1
696 unit.setid("%d" % self._messagenum)
697 return unit
698
699 - def switchfile(self, filename, createifmissing=False):
700 """adds the given trans-unit (will create the nodes required if asked). Returns success"""
701 self._filename = filename
702 filenode = self.getfilenode(filename)
703 if filenode is None:
704 if not createifmissing:
705 return False
706 filenode = self.createfilenode(filename)
707 self.document.getroot().append(filenode)
708
709 self.body = self.getbodynode(filenode, createifmissing=createifmissing)
710 if self.body is None:
711 return False
712 self._messagenum = len(list(self.body.iterdescendants(self.namespaced("trans-unit"))))
713
714
715
716
717
718 return True
719
720 - def creategroup(self, filename="NoName", createifmissing=False, restype=None):
721 """adds a group tag into the specified file"""
722 if self._filename != filename:
723 if not self.switchfile(filename, createifmissing):
724 return None
725 group = etree.SubElement(self.body, self.namespaced("group"))
726 if restype:
727 group.set("restype", restype)
728 return group
729
733
745 parsestring = classmethod(parsestring)
746