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):
189 """Add a note specifically in a "note" tag"""
190 if isinstance(text, str):
191 text = text.decode("utf-8")
192 note = etree.SubElement(self.xmlelement, self.namespaced("note"))
193 note.text = text.strip()
194 if origin:
195 note.set("from", origin)
196
198 """Private method that returns the text from notes matching 'origin' or all notes."""
199 notenodes = self.xmlelement.iterdescendants(self.namespaced("note"))
200
201
202
203 initial_list = [lisa.getText(note, getXMLspace(self.xmlelement, self._default_xml_space)) for note in notenodes if self.correctorigin(note, origin)]
204
205
206 dictset = {}
207 notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
208
209 return notelist
210
213
215 """Remove all the translator notes."""
216 notes = self.xmlelement.iterdescendants(self.namespaced("note"))
217 for note in notes:
218 if self.correctorigin(note, origin=origin):
219 self.xmlelement.remove(note)
220
221 - def adderror(self, errorname, errortext):
222 """Adds an error message to this unit."""
223
224 text = errorname + ': ' + errortext
225 self.addnote(text, origin="pofilter")
226
228 """Get all error messages."""
229
230 notelist = self.getnotelist(origin="pofilter")
231 errordict = {}
232 for note in notelist:
233 errorname, errortext = note.split(': ')
234 errordict[errorname] = errortext
235 return errordict
236
238 """States whether this unit is approved."""
239 return self.xmlelement.get("approved") == "yes"
240
242 """Mark this unit as approved."""
243 if value:
244 self.xmlelement.set("approved", "yes")
245 elif self.isapproved():
246 self.xmlelement.set("approved", "no")
247
249 """States whether this unit needs to be reviewed"""
250 targetnode = self.getlanguageNode(lang=None, index=1)
251 return not targetnode is None and \
252 "needs-review" in targetnode.get("state", "")
253
255 """Marks the unit to indicate whether it needs review. Adds an optional explanation as a note."""
256 targetnode = self.getlanguageNode(lang=None, index=1)
257 if not targetnode is None:
258 if needsreview:
259 targetnode.set("state", "needs-review-translation")
260 if explanation:
261 self.addnote(explanation, origin="translator")
262 else:
263 del targetnode.attrib["state"]
264
271
273 if value:
274 self.markapproved(False)
275 else:
276 self.markapproved(True)
277 targetnode = self.getlanguageNode(lang=None, index=1)
278 if not targetnode is None:
279 if value:
280 targetnode.set("state", "needs-review-translation")
281 else:
282 for attribute in ["state", "state-qualifier"]:
283 if attribute in targetnode.attrib:
284 del targetnode.attrib[attribute]
285
286 - def settarget(self, text, lang='xx', append=False):
291
292
293
294
295
296
297
298
300 value = self.xmlelement.get("translate")
301 if value and value.lower() == 'no':
302 return False
303 return True
304
306 targetnode = self.getlanguageNode(lang=None, index=1)
307 if targetnode is None:
308 return
309 if self.isfuzzy() and "state-qualifier" in targetnode.attrib:
310
311 del targetnode.attrib["state-qualifier"]
312 targetnode.set("state", "translated")
313
315 self.xmlelement.set("id", id)
316
318 return self.xmlelement.get("id") or ""
319
322
324 return [self.getid()]
325
326 - def createcontextgroup(self, name, contexts=None, purpose=None):
327 """Add the context group to the trans-unit with contexts a list with
328 (type, text) tuples describing each context."""
329 assert contexts
330 group = etree.Element(self.namespaced("context-group"))
331
332
333
334 if self.xmlelement.tag == self.namespaced("group"):
335 self.xmlelement.insert(0, group)
336 else:
337 self.xmlelement.append(group)
338 group.set("name", name)
339 if purpose:
340 group.set("purpose", purpose)
341 for type, text in contexts:
342 if isinstance(text, str):
343 text = text.decode("utf-8")
344 context = etree.SubElement(group, self.namespaced("context"))
345 context.text = text
346 context.set("context-type", type)
347
348 - def getcontextgroups(self, name):
349 """Returns the contexts in the context groups with the specified name"""
350 groups = []
351 grouptags = self.xmlelement.iterdescendants(self.namespaced("context-group"))
352
353 for group in grouptags:
354 if group.get("name") == name:
355 contexts = group.iterdescendants(self.namespaced("context"))
356 pairs = []
357 for context in contexts:
358 pairs.append((context.get("context-type"), lisa.getText(context, getXMLspace(self.xmlelement, self._default_xml_space))))
359 groups.append(pairs)
360 return groups
361
363 """returns the restype attribute in the trans-unit tag"""
364 return self.xmlelement.get("restype")
365
366 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
377
379 """Check against node tag's origin (e.g note or alt-trans)"""
380 if origin == None:
381 return True
382 elif origin in node.get("from", ""):
383 return True
384 elif origin in node.get("origin", ""):
385 return True
386 else:
387 return False
388
390 """Override L{TranslationUnit.multistring_to_rich} which is used by the
391 C{rich_source} and C{rich_target} properties."""
392 strings = mstr
393 if isinstance(mstr, multistring):
394 strings = mstr.strings
395 elif isinstance(mstr, basestring):
396 strings = [mstr]
397
398 return [xml_to_strelem(s) for s in strings]
399 multistring_to_rich = classmethod(multistring_to_rich)
400
402 """Override L{TranslationUnit.rich_to_multistring} which is used by the
403 C{rich_source} and C{rich_target} properties."""
404 return multistring([unicode(elem) for elem in elem_list])
405 rich_to_multistring = classmethod(rich_to_multistring)
406
407
409 """Class representing a XLIFF file store."""
410 UnitClass = xliffunit
411 Name = _("XLIFF Translation File")
412 Mimetypes = ["application/x-xliff", "application/x-xliff+xml"]
413 Extensions = ["xlf", "xliff"]
414 rootNode = "xliff"
415 bodyNode = "body"
416 XMLskeleton = '''<?xml version="1.0" ?>
417 <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
418 <file original='NoName' source-language='en' datatype='plaintext'>
419 <body>
420 </body>
421 </file>
422 </xliff>'''
423 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
424 suggestions_in_format = True
425 """xliff units have alttrans tags which can be used to store suggestions"""
426
428 self._filename = None
429 lisa.LISAfile.__init__(self, *args, **kwargs)
430 self._messagenum = 0
431
432
433 - def initbody(self):
434 self.namespace = self.document.getroot().nsmap.get(None, None)
435
436 if self._filename:
437 self.body = self.getcontextnode(self._filename)
438 else:
439 self.body = self.document.getroot()
440
441 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
442 sourcelanguage = filenode.get('source-language')
443 if sourcelanguage:
444 self.setsourcelanguage(sourcelanguage)
445 targetlanguage = filenode.get('target-language')
446 if targetlanguage:
447 self.settargetlanguage(targetlanguage)
448
450 """Initialise the file header."""
451 filenode = self.document.getroot().iterchildren(self.namespaced("file")).next()
452 filenode.set("source-language", self.sourcelanguage)
453 if self.targetlanguage:
454 filenode.set("target-language", self.targetlanguage)
455
456 - def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
457 """creates a filenode with the given filename. All parameters are needed
458 for XLIFF compliance."""
459 self.removedefaultfile()
460 if sourcelanguage is None:
461 sourcelanguage = self.sourcelanguage
462 if targetlanguage is None:
463 targetlanguage = self.targetlanguage
464 filenode = etree.Element(self.namespaced("file"))
465 filenode.set("original", filename)
466 filenode.set("source-language", sourcelanguage)
467 if targetlanguage:
468 filenode.set("target-language", targetlanguage)
469 filenode.set("datatype", datatype)
470 bodyNode = etree.SubElement(filenode, self.namespaced(self.bodyNode))
471 return filenode
472
474 """returns the name of the given file"""
475 return filenode.get("original")
476
478 """set the name of the given file"""
479 return filenode.set("original", filename)
480
482 """returns all filenames in this XLIFF file"""
483 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
484 filenames = [self.getfilename(filenode) for filenode in filenodes]
485 filenames = filter(None, filenames)
486 if len(filenames) == 1 and filenames[0] == '':
487 filenames = []
488 return filenames
489
491 """finds the filenode with the given name"""
492 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
493 for filenode in filenodes:
494 if self.getfilename(filenode) == filename:
495 return filenode
496 return None
497
499 """Returns the datatype of the stored file. If no filename is given,
500 the datatype of the first file is given."""
501 if filename:
502 node = self.getfilenode(filename)
503 if not node is None:
504 return node.get("datatype")
505 else:
506 filenames = self.getfilenames()
507 if len(filenames) > 0 and filenames[0] != "NoName":
508 return self.getdatatype(filenames[0])
509 return ""
510
512 """Returns the date attribute for the file. If no filename is given,
513 the date of the first file is given. If the date attribute is not
514 specified, None is returned."""
515 if filename:
516 node = self.getfilenode(filename)
517 if not node is None:
518 return node.get("date")
519 else:
520 filenames = self.getfilenames()
521 if len(filenames) > 0 and filenames[0] != "NoName":
522 return self.getdate(filenames[0])
523 return None
524
526 """We want to remove the default file-tag as soon as possible if we
527 know if still present and empty."""
528 filenodes = list(self.document.getroot().iterchildren(self.namespaced("file")))
529 if len(filenodes) > 1:
530 for filenode in filenodes:
531 if filenode.get("original") == "NoName" and \
532 not list(filenode.iterdescendants(self.namespaced(self.UnitClass.rootNode))):
533 self.document.getroot().remove(filenode)
534 break
535
537 """finds the header node for the given filenode"""
538
539 headernode = filenode.iterchildren(self.namespaced("header"))
540 try:
541 return headernode.next()
542 except StopIteration:
543 pass
544 if not createifmissing:
545 return None
546 headernode = etree.SubElement(filenode, self.namespaced("header"))
547 return headernode
548
549 - def getbodynode(self, filenode, createifmissing=False):
550 """finds the body node for the given filenode"""
551 bodynode = filenode.iterchildren(self.namespaced("body"))
552 try:
553 return bodynode.next()
554 except StopIteration:
555 pass
556 if not createifmissing:
557 return None
558 bodynode = etree.SubElement(filenode, self.namespaced("body"))
559 return bodynode
560
561 - def addsourceunit(self, source, filename="NoName", createifmissing=False):
562 """adds the given trans-unit to the last used body node if the filename has changed it uses the slow method instead (will create the nodes required if asked). Returns success"""
563 if self._filename != filename:
564 if not self.switchfile(filename, createifmissing):
565 return None
566 unit = super(xlifffile, self).addsourceunit(source)
567 self._messagenum += 1
568 unit.setid("%d" % self._messagenum)
569 return unit
570
571 - def switchfile(self, filename, createifmissing=False):
572 """adds the given trans-unit (will create the nodes required if asked). Returns success"""
573 self._filename = filename
574 filenode = self.getfilenode(filename)
575 if filenode is None:
576 if not createifmissing:
577 return False
578 filenode = self.createfilenode(filename)
579 self.document.getroot().append(filenode)
580
581 self.body = self.getbodynode(filenode, createifmissing=createifmissing)
582 if self.body is None:
583 return False
584 self._messagenum = len(list(self.body.iterdescendants(self.namespaced("trans-unit"))))
585
586
587
588
589
590 return True
591
592 - def creategroup(self, filename="NoName", createifmissing=False, restype=None):
593 """adds a group tag into the specified file"""
594 if self._filename != filename:
595 if not self.switchfile(filename, createifmissing):
596 return None
597 group = etree.SubElement(self.body, self.namespaced("group"))
598 if restype:
599 group.set("restype", restype)
600 return group
601
605
617 parsestring = classmethod(parsestring)
618