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 = {
65 "new": S_UNTRANSLATED + 1,
66 "needs-translation": S_NEEDS_TRANSLATION,
67 "needs-adaptation": S_NEEDS_TRANSLATION + 1,
68 "needs-l10n": S_NEEDS_TRANSLATION + 2,
69 "needs-review-translation": S_NEEDS_REVIEW,
70 "needs-review-adaptation": S_NEEDS_REVIEW + 1,
71 "needs-review-l10n": S_NEEDS_REVIEW + 2,
72 "translated": S_TRANSLATED,
73 "signed-off": S_SIGNED_OFF,
74 "final": S_FINAL,
75 }
76
77 statemap_r = dict((i[1], i[0]) for i in statemap.iteritems())
78
79 STATE = {
80 S_UNTRANSLATED: (state.EMPTY, state.NEEDS_WORK),
81 S_NEEDS_TRANSLATION: (state.NEEDS_WORK, state.NEEDS_REVIEW),
82 S_NEEDS_REVIEW: (state.NEEDS_REVIEW, state.UNREVIEWED),
83 S_TRANSLATED: (state.UNREVIEWED, state.FINAL),
84 S_SIGNED_OFF: (state.FINAL, state.MAX),
85 }
86
87 - def __init__(self, source, empty=False, **kwargs):
88 """Override the constructor to set xml:space="preserve"."""
89 super(xliffunit, self).__init__(source, empty, **kwargs)
90 if empty:
91 return
92 lisa.setXMLspace(self.xmlelement, "preserve")
93
95 """Returns an xml Element setup with given parameters."""
96
97
98
99
100 assert purpose
101 langset = etree.Element(self.namespaced(purpose))
102
103
104
105
106 langset.text = text
107 return langset
108
124
126 sourcelanguageNode = self.get_source_dom()
127 if sourcelanguageNode is None:
128 sourcelanguageNode = self.createlanguageNode(sourcelang, u'', "source")
129 self.set_source_dom(sourcelanguageNode)
130
131
132 for i in range(len(sourcelanguageNode)):
133 del sourcelanguageNode[0]
134 sourcelanguageNode.text = None
135
136 strelem_to_xml(sourcelanguageNode, value[0])
137
144 rich_source = property(get_rich_source, set_rich_source)
145
164
171 rich_target = property(get_rich_target, set_rich_target)
172
173 - def addalttrans(self, txt, origin=None, lang=None, sourcetxt=None, matchquality=None):
174 """Adds an alt-trans tag and alt-trans components to the unit.
175
176 @type txt: String
177 @param txt: Alternative translation of the source text.
178 """
179
180
181
182 if isinstance(txt, str):
183 txt = txt.decode("utf-8")
184 alttrans = etree.SubElement(self.xmlelement, self.namespaced("alt-trans"))
185 lisa.setXMLspace(alttrans, "preserve")
186 if sourcetxt:
187 if isinstance(sourcetxt, str):
188 sourcetxt = sourcetxt.decode("utf-8")
189 altsource = etree.SubElement(alttrans, self.namespaced("source"))
190 altsource.text = sourcetxt
191 alttarget = etree.SubElement(alttrans, self.namespaced("target"))
192 alttarget.text = txt
193 if matchquality:
194 alttrans.set("match-quality", matchquality)
195 if origin:
196 alttrans.set("origin", origin)
197 if lang:
198 lisa.setXMLlang(alttrans, lang)
199
226
228 """Removes the supplied alternative from the list of alt-trans tags"""
229 self.xmlelement.remove(alternative.xmlelement)
230
231 - def addnote(self, text, origin=None, position="append"):
232 """Add a note specifically in a "note" tag"""
233 if position != "append":
234 self.removenotes(origin=origin)
235
236 if text:
237 text = text.strip()
238 if not text:
239 return
240 if isinstance(text, str):
241 text = text.decode("utf-8")
242 note = etree.SubElement(self.xmlelement, self.namespaced("note"))
243 note.text = text
244 if origin:
245 note.set("from", origin)
246
248 """Private method that returns the text from notes matching 'origin' or all notes."""
249 notenodes = self.xmlelement.iterdescendants(self.namespaced("note"))
250
251
252
253 initial_list = [lisa.getText(note, getXMLspace(self.xmlelement, self._default_xml_space)) for note in notenodes if self.correctorigin(note, origin)]
254
255
256 dictset = {}
257 notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
258
259 return notelist
260
263
265 """Remove all the translator notes."""
266 notes = self.xmlelement.iterdescendants(self.namespaced("note"))
267 for note in notes:
268 if self.correctorigin(note, origin=origin):
269 self.xmlelement.remove(note)
270
271 - def adderror(self, errorname, errortext):
272 """Adds an error message to this unit."""
273
274 text = errorname
275 if errortext:
276 text += ': ' + errortext
277 self.addnote(text, origin="pofilter")
278
280 """Get all error messages."""
281
282 notelist = self.getnotelist(origin="pofilter")
283 errordict = {}
284 for note in notelist:
285 errorname, errortext = note.split(': ')
286 errordict[errorname] = errortext
287 return errordict
288
310
327
329 """States whether this unit is approved."""
330 return self.xmlelement.get("approved") == "yes"
331
333 """Mark this unit as approved."""
334 if value:
335 self.xmlelement.set("approved", "yes")
336 elif self.isapproved():
337 self.xmlelement.set("approved", "no")
338
342
352
359
370
371 - def settarget(self, text, lang='xx', append=False):
376
377
378
379
380
381
382
383
385 value = self.xmlelement.get("translate")
386 if value and value.lower() == 'no':
387 return False
388 return True
389
394
398
411
414
416 id_attr = unicode(self.xmlelement.get("id") or u"")
417 if id_attr:
418 return [id_attr]
419 return []
420
421 - def createcontextgroup(self, name, contexts=None, purpose=None):
422 """Add the context group to the trans-unit with contexts a list with
423 (type, text) tuples describing each context."""
424 assert contexts
425 group = etree.Element(self.namespaced("context-group"))
426
427
428
429 if self.xmlelement.tag == self.namespaced("group"):
430 self.xmlelement.insert(0, group)
431 else:
432 self.xmlelement.append(group)
433 group.set("name", name)
434 if purpose:
435 group.set("purpose", purpose)
436 for type, text in contexts:
437 if isinstance(text, str):
438 text = text.decode("utf-8")
439 context = etree.SubElement(group, self.namespaced("context"))
440 context.text = text
441 context.set("context-type", type)
442
443 - def getcontextgroups(self, name):
444 """Returns the contexts in the context groups with the specified name"""
445 groups = []
446 grouptags = self.xmlelement.iterdescendants(self.namespaced("context-group"))
447
448 for group in grouptags:
449 if group.get("name") == name:
450 contexts = group.iterdescendants(self.namespaced("context"))
451 pairs = []
452 for context in contexts:
453 pairs.append((context.get("context-type"), lisa.getText(context, getXMLspace(self.xmlelement, self._default_xml_space))))
454 groups.append(pairs)
455 return groups
456
458 """returns the restype attribute in the trans-unit tag"""
459 return self.xmlelement.get("restype")
460
461 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
472
474 """Check against node tag's origin (e.g note or alt-trans)"""
475 if origin == None:
476 return True
477 elif origin in node.get("from", ""):
478 return True
479 elif origin in node.get("origin", ""):
480 return True
481 else:
482 return False
483
485 """Override L{TranslationUnit.multistring_to_rich} which is used by the
486 C{rich_source} and C{rich_target} properties."""
487 strings = mstr
488 if isinstance(mstr, multistring):
489 strings = mstr.strings
490 elif isinstance(mstr, basestring):
491 strings = [mstr]
492
493 return [xml_to_strelem(s) for s in strings]
494 multistring_to_rich = classmethod(multistring_to_rich)
495
497 """Override L{TranslationUnit.rich_to_multistring} which is used by the
498 C{rich_source} and C{rich_target} properties."""
499 return multistring([unicode(elem) for elem in elem_list])
500 rich_to_multistring = classmethod(rich_to_multistring)
501
502
504 """Class representing a XLIFF file store."""
505 UnitClass = xliffunit
506 Name = _("XLIFF Translation File")
507 Mimetypes = ["application/x-xliff", "application/x-xliff+xml"]
508 Extensions = ["xlf", "xliff", "sdlxliff"]
509 rootNode = "xliff"
510 bodyNode = "body"
511 XMLskeleton = '''<?xml version="1.0" ?>
512 <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
513 <file original='NoName' source-language='en' datatype='plaintext'>
514 <body>
515 </body>
516 </file>
517 </xliff>'''
518 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
519 suggestions_in_format = True
520 """xliff units have alttrans tags which can be used to store suggestions"""
521
523 self._filename = None
524 lisa.LISAfile.__init__(self, *args, **kwargs)
525 self._messagenum = 0
526
527 - def initbody(self):
528 self.namespace = self.document.getroot().nsmap.get(None, None)
529
530 if self._filename:
531 filenode = self.getfilenode(self._filename, createifmissing=True)
532 else:
533 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
534 self.body = self.getbodynode(filenode, createifmissing=True)
535
537 """Initialise the file header."""
538 pass
539
540 - def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
565
567 """returns the name of the given file"""
568 return filenode.get("original")
569
571 """set the name of the given file"""
572 return filenode.set("original", filename)
573
575 """returns all filenames in this XLIFF file"""
576 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
577 filenames = [self.getfilename(filenode) for filenode in filenodes]
578 filenames = filter(None, filenames)
579 if len(filenames) == 1 and filenames[0] == '':
580 filenames = []
581 return filenames
582
583 - def getfilenode(self, filename, createifmissing=False):
584 """finds the filenode with the given name"""
585 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
586 for filenode in filenodes:
587 if self.getfilename(filenode) == filename:
588 return filenode
589 if createifmissing:
590 filenode = self.createfilenode(filename)
591 return filenode
592 return None
593
594 - def getids(self, filename=None):
595 if not filename:
596 return super(xlifffile, self).getids()
597
598 self.id_index = {}
599 prefix = filename + ID_SEPARATOR
600 units = (unit for unit in self.units if unit.getid().startswith(prefix))
601 for index, unit in enumerate(units):
602 self.id_index[unit.getid()[len(prefix):]] = unit
603 return self.id_index.keys()
604
606 if not language:
607 return
608 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
609 filenode.set("source-language", language)
610
612 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
613 return filenode.get("source-language")
614 sourcelanguage = property(getsourcelanguage, setsourcelanguage)
615
617 if not language:
618 return
619 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
620 filenode.set("target-language", language)
621
623 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
624 return filenode.get("target-language")
625 targetlanguage = property(gettargetlanguage, settargetlanguage)
626
628 """Returns the datatype of the stored file. If no filename is given,
629 the datatype of the first file is given."""
630 if filename:
631 node = self.getfilenode(filename)
632 if not node is None:
633 return node.get("datatype")
634 else:
635 filenames = self.getfilenames()
636 if len(filenames) > 0 and filenames[0] != "NoName":
637 return self.getdatatype(filenames[0])
638 return ""
639
641 """Returns the date attribute for the file. If no filename is given,
642 the date of the first file is given. If the date attribute is not
643 specified, None is returned."""
644 if filename:
645 node = self.getfilenode(filename)
646 if not node is None:
647 return node.get("date")
648 else:
649 filenames = self.getfilenames()
650 if len(filenames) > 0 and filenames[0] != "NoName":
651 return self.getdate(filenames[0])
652 return None
653
655 """We want to remove the default file-tag as soon as possible if we
656 know if still present and empty."""
657 filenodes = list(self.document.getroot().iterchildren(self.namespaced("file")))
658 if len(filenodes) > 1:
659 for filenode in filenodes:
660 if filenode.get("original") == "NoName" and \
661 not list(filenode.iterdescendants(self.namespaced(self.UnitClass.rootNode))):
662 self.document.getroot().remove(filenode)
663 break
664
666 """finds the header node for the given filenode"""
667
668 headernode = filenode.iterchildren(self.namespaced("header"))
669 try:
670 return headernode.next()
671 except StopIteration:
672 pass
673 if not createifmissing:
674 return None
675 headernode = etree.SubElement(filenode, self.namespaced("header"))
676 return headernode
677
678 - def getbodynode(self, filenode, createifmissing=False):
679 """finds the body node for the given filenode"""
680 bodynode = filenode.iterchildren(self.namespaced("body"))
681 try:
682 return bodynode.next()
683 except StopIteration:
684 pass
685 if not createifmissing:
686 return None
687 bodynode = etree.SubElement(filenode, self.namespaced("body"))
688 return bodynode
689
690 - def addsourceunit(self, source, filename="NoName", createifmissing=False):
691 """adds the given trans-unit to the last used body node if the
692 filename has changed it uses the slow method instead (will
693 create the nodes required if asked). Returns success"""
694 if self._filename != filename:
695 if not self.switchfile(filename, createifmissing):
696 return None
697 unit = super(xlifffile, self).addsourceunit(source)
698 self._messagenum += 1
699 unit.setid("%d" % self._messagenum)
700 return unit
701
702 - def switchfile(self, filename, createifmissing=False):
703 """adds the given trans-unit (will create the nodes required if asked). Returns success"""
704 self._filename = filename
705 filenode = self.getfilenode(filename)
706 if filenode is None:
707 if not createifmissing:
708 return False
709 filenode = self.createfilenode(filename)
710 self.document.getroot().append(filenode)
711
712 self.body = self.getbodynode(filenode, createifmissing=createifmissing)
713 if self.body is None:
714 return False
715 self._messagenum = len(list(self.body.iterdescendants(self.namespaced("trans-unit"))))
716
717
718
719
720
721 return True
722
723 - def creategroup(self, filename="NoName", createifmissing=False, restype=None):
724 """adds a group tag into the specified file"""
725 if self._filename != filename:
726 if not self.switchfile(filename, createifmissing):
727 return None
728 group = etree.SubElement(self.body, self.namespaced("group"))
729 if restype:
730 group.set("restype", restype)
731 return group
732
736
748 parsestring = classmethod(parsestring)
749