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.misc.xml_helpers import *
30 from translate.storage import base, lisa
31 from translate.storage.lisa import getXMLspace
32 from translate.storage.placeables.lisa import xml_to_strelem, strelem_to_xml
33
34
35
37 """A single term in the xliff file."""
38
39 rootNode = "trans-unit"
40 languageNode = "source"
41 textNode = ""
42 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
43
44 _default_xml_space = "default"
45
46
47
48 - def __init__(self, source, empty=False, **kwargs):
49 """Override the constructor to set xml:space="preserve"."""
50 if empty:
51 return
52 super(xliffunit, self).__init__(source, empty, **kwargs)
53 lisa.setXMLspace(self.xmlelement, "preserve")
54
56 """Returns an xml Element setup with given parameters."""
57
58
59
60
61 assert purpose
62 langset = etree.Element(self.namespaced(purpose))
63
64
65
66
67 langset.text = text
68 return langset
69
85
87 sourcelanguageNode = self.get_source_dom()
88 if sourcelanguageNode is None:
89 sourcelanguageNode = self.createlanguageNode(sourcelang, u'', "source")
90 self.set_source_dom(sourcelanguageNode)
91
92
93 for i in range(len(sourcelanguageNode)):
94 del sourcelanguageNode[0]
95 sourcelanguageNode.text = None
96
97 strelem_to_xml(sourcelanguageNode, value[0])
98
105 rich_source = property(get_rich_source, set_rich_source)
106
123
128 rich_target = property(get_rich_target, set_rich_target)
129
130 - def addalttrans(self, txt, origin=None, lang=None, sourcetxt=None, matchquality=None):
131 """Adds an alt-trans tag and alt-trans components to the unit.
132
133 @type txt: String
134 @param txt: Alternative translation of the source text.
135 """
136
137
138
139 if isinstance(txt, str):
140 txt = txt.decode("utf-8")
141 alttrans = etree.SubElement(self.xmlelement, self.namespaced("alt-trans"))
142 lisa.setXMLspace(alttrans, "preserve")
143 if sourcetxt:
144 if isinstance(sourcetxt, str):
145 sourcetxt = sourcetxt.decode("utf-8")
146 altsource = etree.SubElement(alttrans, self.namespaced("source"))
147 altsource.text = sourcetxt
148 alttarget = etree.SubElement(alttrans, self.namespaced("target"))
149 alttarget.text = txt
150 if matchquality:
151 alttrans.set("match-quality", matchquality)
152 if origin:
153 alttrans.set("origin", origin)
154 if lang:
155 lisa.setXMLlang(alttrans, lang)
156
183
185 """Removes the supplied alternative from the list of alt-trans tags"""
186 self.xmlelement.remove(alternative.xmlelement)
187
188 - def addnote(self, text, origin=None, position="append"):
189 """Add a note specifically in a "note" tag"""
190 if text:
191 text = text.strip()
192 if not text:
193 return
194 if isinstance(text, str):
195 text = text.decode("utf-8")
196 note = etree.SubElement(self.xmlelement, self.namespaced("note"))
197 note.text = text
198 if origin:
199 note.set("from", origin)
200
202 """Private method that returns the text from notes matching 'origin' or all notes."""
203 notenodes = self.xmlelement.iterdescendants(self.namespaced("note"))
204
205
206
207 initial_list = [lisa.getText(note, getXMLspace(self.xmlelement, self._default_xml_space)) for note in notenodes if self.correctorigin(note, origin)]
208
209
210 dictset = {}
211 notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
212
213 return notelist
214
217
219 """Remove all the translator notes."""
220 notes = self.xmlelement.iterdescendants(self.namespaced("note"))
221 for note in notes:
222 if self.correctorigin(note, origin=origin):
223 self.xmlelement.remove(note)
224
225 - def adderror(self, errorname, errortext):
226 """Adds an error message to this unit."""
227
228 text = errorname + ': ' + errortext
229 self.addnote(text, origin="pofilter")
230
232 """Get all error messages."""
233
234 notelist = self.getnotelist(origin="pofilter")
235 errordict = {}
236 for note in notelist:
237 errorname, errortext = note.split(': ')
238 errordict[errorname] = errortext
239 return errordict
240
242 """States whether this unit is approved."""
243 return self.xmlelement.get("approved") == "yes"
244
246 """Mark this unit as approved."""
247 if value:
248 self.xmlelement.set("approved", "yes")
249 elif self.isapproved():
250 self.xmlelement.set("approved", "no")
251
253 """States whether this unit needs to be reviewed"""
254 targetnode = self.getlanguageNode(lang=None, index=1)
255 return not targetnode is None and \
256 "needs-review" in targetnode.get("state", "")
257
259 """Marks the unit to indicate whether it needs review. Adds an optional explanation as a note."""
260 targetnode = self.getlanguageNode(lang=None, index=1)
261 if not targetnode is None:
262 if needsreview:
263 targetnode.set("state", "needs-review-translation")
264 if explanation:
265 self.addnote(explanation, origin="translator")
266 else:
267 del targetnode.attrib["state"]
268
275
277 if value:
278 self.markapproved(False)
279 else:
280 self.markapproved(True)
281 targetnode = self.getlanguageNode(lang=None, index=1)
282 if not targetnode is None:
283 if value:
284 targetnode.set("state", "needs-review-translation")
285 else:
286 for attribute in ["state", "state-qualifier"]:
287 if attribute in targetnode.attrib:
288 del targetnode.attrib[attribute]
289
290 - def settarget(self, text, lang='xx', append=False):
295
296
297
298
299
300
301
302
304 value = self.xmlelement.get("translate")
305 if value and value.lower() == 'no':
306 return False
307 return True
308
310 targetnode = self.getlanguageNode(lang=None, index=1)
311 if targetnode is None:
312 return
313 if self.isfuzzy() and "state-qualifier" in targetnode.attrib:
314
315 del targetnode.attrib["state-qualifier"]
316 targetnode.set("state", "translated")
317
319 self.xmlelement.set("id", id)
320
322 return self.xmlelement.get("id") or ""
323
326
328 return [self.getid()]
329
330 - def createcontextgroup(self, name, contexts=None, purpose=None):
331 """Add the context group to the trans-unit with contexts a list with
332 (type, text) tuples describing each context."""
333 assert contexts
334 group = etree.Element(self.namespaced("context-group"))
335
336
337
338 if self.xmlelement.tag == self.namespaced("group"):
339 self.xmlelement.insert(0, group)
340 else:
341 self.xmlelement.append(group)
342 group.set("name", name)
343 if purpose:
344 group.set("purpose", purpose)
345 for type, text in contexts:
346 if isinstance(text, str):
347 text = text.decode("utf-8")
348 context = etree.SubElement(group, self.namespaced("context"))
349 context.text = text
350 context.set("context-type", type)
351
352 - def getcontextgroups(self, name):
353 """Returns the contexts in the context groups with the specified name"""
354 groups = []
355 grouptags = self.xmlelement.iterdescendants(self.namespaced("context-group"))
356
357 for group in grouptags:
358 if group.get("name") == name:
359 contexts = group.iterdescendants(self.namespaced("context"))
360 pairs = []
361 for context in contexts:
362 pairs.append((context.get("context-type"), lisa.getText(context, getXMLspace(self.xmlelement, self._default_xml_space))))
363 groups.append(pairs)
364 return groups
365
367 """returns the restype attribute in the trans-unit tag"""
368 return self.xmlelement.get("restype")
369
370 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
381
383 """Check against node tag's origin (e.g note or alt-trans)"""
384 if origin == None:
385 return True
386 elif origin in node.get("from", ""):
387 return True
388 elif origin in node.get("origin", ""):
389 return True
390 else:
391 return False
392
394 """Override L{TranslationUnit.multistring_to_rich} which is used by the
395 C{rich_source} and C{rich_target} properties."""
396 strings = mstr
397 if isinstance(mstr, multistring):
398 strings = mstr.strings
399 elif isinstance(mstr, basestring):
400 strings = [mstr]
401
402 return [xml_to_strelem(s) for s in strings]
403 multistring_to_rich = classmethod(multistring_to_rich)
404
406 """Override L{TranslationUnit.rich_to_multistring} which is used by the
407 C{rich_source} and C{rich_target} properties."""
408 return multistring([unicode(elem) for elem in elem_list])
409 rich_to_multistring = classmethod(rich_to_multistring)
410
411
413 """Class representing a XLIFF file store."""
414 UnitClass = xliffunit
415 Name = _("XLIFF Translation File")
416 Mimetypes = ["application/x-xliff", "application/x-xliff+xml"]
417 Extensions = ["xlf", "xliff"]
418 rootNode = "xliff"
419 bodyNode = "body"
420 XMLskeleton = '''<?xml version="1.0" ?>
421 <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
422 <file original='NoName' source-language='en' datatype='plaintext'>
423 <body>
424 </body>
425 </file>
426 </xliff>'''
427 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
428 suggestions_in_format = True
429 """xliff units have alttrans tags which can be used to store suggestions"""
430
432 self._filename = None
433 lisa.LISAfile.__init__(self, *args, **kwargs)
434 self._messagenum = 0
435
436 - def initbody(self):
437 self.namespace = self.document.getroot().nsmap.get(None, None)
438
439 if self._filename:
440 filenode = self.getfilenode(self._filename, createifmissing=True)
441 else:
442 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
443 self.body = self.getbodynode(filenode, createifmissing=True)
444
446 """Initialise the file header."""
447 pass
448
449 - def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
474
476 """returns the name of the given file"""
477 return filenode.get("original")
478
480 """set the name of the given file"""
481 return filenode.set("original", filename)
482
484 """returns all filenames in this XLIFF file"""
485 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
486 filenames = [self.getfilename(filenode) for filenode in filenodes]
487 filenames = filter(None, filenames)
488 if len(filenames) == 1 and filenames[0] == '':
489 filenames = []
490 return filenames
491
492 - def getfilenode(self, filename, createifmissing=False):
493 """finds the filenode with the given name"""
494 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
495 for filenode in filenodes:
496 if self.getfilename(filenode) == filename:
497 return filenode
498 if createifmissing:
499 filenode = self.createfilenode(filename)
500 return filenode
501 return None
502
504 if not language:
505 return
506 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
507 filenode.set("source-language", language)
508
510 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
511 return filenode.get("source-language")
512 sourcelanguage = property(getsourcelanguage, setsourcelanguage)
513
515 if not language:
516 return
517 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
518 filenode.set("target-language", language)
519
521 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
522 return filenode.get("target-language")
523 targetlanguage = property(gettargetlanguage, settargetlanguage)
524
526 """Returns the datatype of the stored file. If no filename is given,
527 the datatype of the first file is given."""
528 if filename:
529 node = self.getfilenode(filename)
530 if not node is None:
531 return node.get("datatype")
532 else:
533 filenames = self.getfilenames()
534 if len(filenames) > 0 and filenames[0] != "NoName":
535 return self.getdatatype(filenames[0])
536 return ""
537
539 """Returns the date attribute for the file. If no filename is given,
540 the date of the first file is given. If the date attribute is not
541 specified, None is returned."""
542 if filename:
543 node = self.getfilenode(filename)
544 if not node is None:
545 return node.get("date")
546 else:
547 filenames = self.getfilenames()
548 if len(filenames) > 0 and filenames[0] != "NoName":
549 return self.getdate(filenames[0])
550 return None
551
553 """We want to remove the default file-tag as soon as possible if we
554 know if still present and empty."""
555 filenodes = list(self.document.getroot().iterchildren(self.namespaced("file")))
556 if len(filenodes) > 1:
557 for filenode in filenodes:
558 if filenode.get("original") == "NoName" and \
559 not list(filenode.iterdescendants(self.namespaced(self.UnitClass.rootNode))):
560 self.document.getroot().remove(filenode)
561 break
562
564 """finds the header node for the given filenode"""
565
566 headernode = filenode.iterchildren(self.namespaced("header"))
567 try:
568 return headernode.next()
569 except StopIteration:
570 pass
571 if not createifmissing:
572 return None
573 headernode = etree.SubElement(filenode, self.namespaced("header"))
574 return headernode
575
576 - def getbodynode(self, filenode, createifmissing=False):
577 """finds the body node for the given filenode"""
578 bodynode = filenode.iterchildren(self.namespaced("body"))
579 try:
580 return bodynode.next()
581 except StopIteration:
582 pass
583 if not createifmissing:
584 return None
585 bodynode = etree.SubElement(filenode, self.namespaced("body"))
586 return bodynode
587
588 - def addsourceunit(self, source, filename="NoName", createifmissing=False):
589 """adds the given trans-unit to the last used body node if the
590 filename has changed it uses the slow method instead (will
591 create the nodes required if asked). Returns success"""
592 if self._filename != filename:
593 if not self.switchfile(filename, createifmissing):
594 return None
595 unit = super(xlifffile, self).addsourceunit(source)
596 self._messagenum += 1
597 unit.setid("%d" % self._messagenum)
598 return unit
599
600 - def switchfile(self, filename, createifmissing=False):
601 """adds the given trans-unit (will create the nodes required if asked). Returns success"""
602 self._filename = filename
603 filenode = self.getfilenode(filename)
604 if filenode is None:
605 if not createifmissing:
606 return False
607 filenode = self.createfilenode(filename)
608 self.document.getroot().append(filenode)
609
610 self.body = self.getbodynode(filenode, createifmissing=createifmissing)
611 if self.body is None:
612 return False
613 self._messagenum = len(list(self.body.iterdescendants(self.namespaced("trans-unit"))))
614
615
616
617
618
619 return True
620
621 - def creategroup(self, filename="NoName", createifmissing=False, restype=None):
622 """adds a group tag into the specified file"""
623 if self._filename != filename:
624 if not self.switchfile(filename, createifmissing):
625 return None
626 group = etree.SubElement(self.body, self.namespaced("group"))
627 if restype:
628 group.set("restype", restype)
629 return group
630
634
646 parsestring = classmethod(parsestring)
647