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 lisa.LISAfile.__init__(self, *args, **kwargs)
429 self._filename = "NoName"
430 self._messagenum = 0
431
432
433 - def initbody(self):
434 super(xlifffile, self).initbody()
435 filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
436 sourcelanguage = filenode.get('source-language')
437 if sourcelanguage:
438 self.setsourcelanguage(sourcelanguage)
439 targetlanguage = filenode.get('target-language')
440 if targetlanguage:
441 self.settargetlanguage(targetlanguage)
442
444 """Initialise the file header."""
445 filenode = self.document.getroot().iterchildren(self.namespaced("file")).next()
446 filenode.set("source-language", self.sourcelanguage)
447 if self.targetlanguage:
448 filenode.set("target-language", self.targetlanguage)
449
450 - def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
451 """creates a filenode with the given filename. All parameters are needed
452 for XLIFF compliance."""
453 self.removedefaultfile()
454 if sourcelanguage is None:
455 sourcelanguage = self.sourcelanguage
456 if targetlanguage is None:
457 targetlanguage = self.targetlanguage
458 filenode = etree.Element(self.namespaced("file"))
459 filenode.set("original", filename)
460 filenode.set("source-language", sourcelanguage)
461 if targetlanguage:
462 filenode.set("target-language", targetlanguage)
463 filenode.set("datatype", datatype)
464 bodyNode = etree.SubElement(filenode, self.namespaced(self.bodyNode))
465 return filenode
466
468 """returns the name of the given file"""
469 return filenode.get("original")
470
472 """set the name of the given file"""
473 return filenode.set("original", filename)
474
476 """returns all filenames in this XLIFF file"""
477 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
478 filenames = [self.getfilename(filenode) for filenode in filenodes]
479 filenames = filter(None, filenames)
480 if len(filenames) == 1 and filenames[0] == '':
481 filenames = []
482 return filenames
483
485 """finds the filenode with the given name"""
486 filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
487 for filenode in filenodes:
488 if self.getfilename(filenode) == filename:
489 return filenode
490 return None
491
493 """Returns the datatype of the stored file. If no filename is given,
494 the datatype of the first file is given."""
495 if filename:
496 node = self.getfilenode(filename)
497 if not node is None:
498 return node.get("datatype")
499 else:
500 filenames = self.getfilenames()
501 if len(filenames) > 0 and filenames[0] != "NoName":
502 return self.getdatatype(filenames[0])
503 return ""
504
506 """Returns the date attribute for the file. If no filename is given,
507 the date of the first file is given. If the date attribute is not
508 specified, None is returned."""
509 if filename:
510 node = self.getfilenode(filename)
511 if not node is None:
512 return node.get("date")
513 else:
514 filenames = self.getfilenames()
515 if len(filenames) > 0 and filenames[0] != "NoName":
516 return self.getdate(filenames[0])
517 return None
518
520 """We want to remove the default file-tag as soon as possible if we
521 know if still present and empty."""
522 filenodes = list(self.document.getroot().iterchildren(self.namespaced("file")))
523 if len(filenodes) > 1:
524 for filenode in filenodes:
525 if filenode.get("original") == "NoName" and \
526 not list(filenode.iterdescendants(self.namespaced(self.UnitClass.rootNode))):
527 self.document.getroot().remove(filenode)
528 break
529
531 """finds the header node for the given filenode"""
532
533 headernode = filenode.iterchildren(self.namespaced("header"))
534 try:
535 return headernode.next()
536 except StopIteration:
537 pass
538 if not createifmissing:
539 return None
540 headernode = etree.SubElement(filenode, self.namespaced("header"))
541 return headernode
542
543 - def getbodynode(self, filenode, createifmissing=False):
544 """finds the body node for the given filenode"""
545 bodynode = filenode.iterchildren(self.namespaced("body"))
546 try:
547 return bodynode.next()
548 except StopIteration:
549 pass
550 if not createifmissing:
551 return None
552 bodynode = etree.SubElement(filenode, self.namespaced("body"))
553 return bodynode
554
555 - def addsourceunit(self, source, filename="NoName", createifmissing=False):
556 """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"""
557 if self._filename != filename:
558 if not self.switchfile(filename, createifmissing):
559 return None
560 unit = super(xlifffile, self).addsourceunit(source)
561 self._messagenum += 1
562 unit.setid("%d" % self._messagenum)
563 return unit
564
565 - def switchfile(self, filename, createifmissing=False):
566 """adds the given trans-unit (will create the nodes required if asked). Returns success"""
567 self._filename = filename
568 filenode = self.getfilenode(filename)
569 if filenode is None:
570 if not createifmissing:
571 return False
572 filenode = self.createfilenode(filename)
573 self.document.getroot().append(filenode)
574
575 self.body = self.getbodynode(filenode, createifmissing=createifmissing)
576 if self.body is None:
577 return False
578 self._messagenum = len(list(self.body.iterdescendants(self.namespaced("trans-unit"))))
579
580
581
582
583
584 return True
585
586 - def creategroup(self, filename="NoName", createifmissing=False, restype=None):
587 """adds a group tag into the specified file"""
588 if self._filename != filename:
589 if not self.switchfile(filename, createifmissing):
590 return None
591 group = etree.SubElement(self.body, self.namespaced("group"))
592 if restype:
593 group.set("restype", restype)
594 return group
595
599
611 parsestring = classmethod(parsestring)
612