1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 """Module for handling XLIFF files for translation.
24
25 The official recommendation is to use the extention .xlf for XLIFF files.
26 """
27
28 from translate.storage import base
29 from translate.storage import lisa
30 from lxml import etree
31
32
33
35 """A single term in the xliff file."""
36
37 rootNode = "trans-unit"
38 languageNode = "source"
39 textNode = ""
40 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
41
42
43
45 """Returns an xml Element setup with given parameters."""
46
47
48
49
50 assert purpose
51 langset = etree.Element(self.namespaced(purpose))
52
53
54
55
56 langset.text = text
57 return langset
58
60 """We override this to get source and target nodes."""
61 sources = self.xmlelement.findall('.//%s' % self.namespaced(self.languageNode))
62 targets = self.xmlelement.findall('.//%s' % self.namespaced('target'))
63 sourcesl = len(sources)
64 targetsl = len(targets)
65 nodes = []
66 for pair in zip(sources, targets):
67 nodes.extend(list(pair))
68 if sourcesl > targetsl:
69 nodes.extend(sources[- (sourcesl - targetsl):])
70 return nodes
71
73 """Adds an alt-trans tag and alt-trans components to the unit.
74
75 @type txt: String
76 @param txt: Alternative translation of the source text.
77 """
78
79
80
81 if isinstance(txt, str):
82 txt = txt.decode("utf-8")
83 alttrans = etree.SubElement(self.xmlelement, self.namespaced("alt-trans"))
84 alttarget = etree.SubElement(alttrans, self.namespaced("target"))
85 alttarget.text = txt
86 if origin:
87 alttrans.set("origin", origin)
88 if lang:
89 lisa.setXMLlang(alttrans, lang)
90
92 """Returns <alt-trans> for the given origin as a list of units. No
93 origin means all alternatives."""
94 translist = []
95 for node in self.xmlelement.findall(".//%s" % self.namespaced("alt-trans")):
96 if self.correctorigin(node, origin):
97
98
99 newunit = base.TranslationUnit(self.source)
100
101
102 sourcenode = node.find(".//%s" % self.namespaced("source"))
103 if not sourcenode is None:
104 newunit.source = lisa.getText(sourcenode)
105
106
107 targetnode = node.find(".//%s" % self.namespaced("target"))
108 newunit.target = lisa.getText(targetnode)
109
110
111 newunit.xmlelement = node
112
113 translist.append(newunit)
114 return translist
115
117 """Removes the supplied alternative from the list of alt-trans tags"""
118 self.xmlelement.remove(alternative.xmlelement)
119
120 - def addnote(self, text, origin=None):
121 """Add a note specifically in a "note" tag"""
122 if isinstance(text, str):
123 text = text.decode("utf-8")
124 note = etree.SubElement(self.xmlelement, self.namespaced("note"))
125 note.text = text.strip()
126 if origin:
127 note.set("from", origin)
128
130 """Private method that returns the text from notes matching 'origin' or all notes."""
131 notenodes = self.xmlelement.findall(".//%s" % self.namespaced("note"))
132
133
134
135 initial_list = [lisa.getText(note) for note in notenodes if self.correctorigin(note, origin)]
136
137
138 dictset = {}
139 notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
140
141 return notelist
142
145
147 """Remove all the translator notes."""
148 notes = self.xmlelement.findall(".//%s" % self.namespaced("note"))
149 for note in notes:
150 if self.correctorigin(note, origin="translator"):
151 self.xmlelement.remove(note)
152
153 - def adderror(self, errorname, errortext):
154 """Adds an error message to this unit."""
155
156 text = errorname + ': ' + errortext
157 self.addnote(text, origin="pofilter")
158
160 """Get all error messages."""
161
162 notelist = self.getnotelist(origin="pofilter")
163 errordict = {}
164 for note in notelist:
165 errorname, errortext = note.split(': ')
166 errordict[errorname] = errortext
167 return errordict
168
170 """States whether this unit is approved."""
171 return self.xmlelement.get("approved") == "yes"
172
174 """Mark this unit as approved."""
175 if value:
176 self.xmlelement.set("approved", "yes")
177 elif self.isapproved():
178 self.xmlelement.set("approved", "no")
179
181 """States whether this unit needs to be reviewed"""
182 targetnode = self.getlanguageNode(lang=None, index=1)
183 return not targetnode is None and \
184 "needs-review" in targetnode.get("state", "")
185
187 """Marks the unit to indicate whether it needs review. Adds an optional explanation as a note."""
188 targetnode = self.getlanguageNode(lang=None, index=1)
189 if not targetnode is None:
190 if needsreview:
191 targetnode.set("state", "needs-review-translation")
192 if explanation:
193 self.addnote(explanation, origin="translator")
194 else:
195 del targetnode.attrib["state"]
196
203
205 if value:
206 self.markapproved(False)
207 else:
208 self.markapproved(True)
209 targetnode = self.getlanguageNode(lang=None, index=1)
210 if not targetnode is None:
211 if value:
212 targetnode.set("state", "needs-review-translation")
213 else:
214 for attribute in ["state", "state-qualifier"]:
215 if attribute in targetnode.attrib:
216 del targetnode.attrib[attribute]
217
218 - def settarget(self, text, lang='xx', append=False):
223
224
225
226
227
228
229
230
232 value = self.xmlelement.get("translate")
233 if value and value.lower() == 'no':
234 return False
235 return True
236
238 targetnode = self.getlanguageNode(lang=None, index=1)
239 if targetnode is None:
240 return
241 if self.isfuzzy() and "state-qualifier" in targetnode.attrib:
242
243 del targetnode.attrib["state-qualifier"]
244 targetnode.set("state", "translated")
245
247 self.xmlelement.set("id", id)
248
250 return self.xmlelement.get("id") or ""
251
254
256 return [self.getid()]
257
258 - def createcontextgroup(self, name, contexts=None, purpose=None):
259 """Add the context group to the trans-unit with contexts a list with
260 (type, text) tuples describing each context."""
261 assert contexts
262 group = etree.Element(self.namespaced("context-group"))
263
264
265
266 if self.xmlelement.tag == self.namespaced("group"):
267 self.xmlelement.insert(0, group)
268 else:
269 self.xmlelement.append(group)
270 group.set("name", name)
271 if purpose:
272 group.set("purpose", purpose)
273 for type, text in contexts:
274 if isinstance(text, str):
275 text = text.decode("utf-8")
276 context = etree.SubElement(group, self.namespaced("context"))
277 context.text = text
278 context.set("context-type", type)
279
280 - def getcontextgroups(self, name):
281 """Returns the contexts in the context groups with the specified name"""
282 groups = []
283 grouptags = self.xmlelement.findall(".//%s" % self.namespaced("context-group"))
284 for group in grouptags:
285 if group.get("name") == name:
286 contexts = group.findall(".//%s" % self.namespaced("context"))
287 pairs = []
288 for context in contexts:
289 pairs.append((context.get("context-type"), lisa.getText(context)))
290 groups.append(pairs)
291 return groups
292
294 """returns the restype attribute in the trans-unit tag"""
295 return self.xmlelement.get("restype")
296
297 - def merge(self, otherunit, overwrite=False, comments=True):
304
306 """Check against node tag's origin (e.g note or alt-trans)"""
307 if origin == None:
308 return True
309 elif origin in node.get("from", ""):
310 return True
311 elif origin in node.get("origin", ""):
312 return True
313 else:
314 return False
315
317 """Class representing a XLIFF file store."""
318 UnitClass = xliffunit
319 Name = "XLIFF file"
320 Mimetypes = ["application/x-xliff", "application/x-xliff+xml"]
321 Extensions = ["xlf", "xliff"]
322 rootNode = "xliff"
323 bodyNode = "body"
324 XMLskeleton = '''<?xml version="1.0" ?>
325 <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
326 <file original='NoName' source-language='en' datatype='plaintext'>
327 <body>
328 </body>
329 </file>
330 </xliff>'''
331 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
332
334 lisa.LISAfile.__init__(self, *args, **kwargs)
335 self._filename = "NoName"
336 self._messagenum = 0
337
338
339 filenode = self.document.find('.//%s' % self.namespaced('file'))
340 sourcelanguage = filenode.get('source-language')
341 if sourcelanguage:
342 self.setsourcelanguage(sourcelanguage)
343 targetlanguage = filenode.get('target-language')
344 if targetlanguage:
345 self.settargetlanguage(targetlanguage)
346
348 """Initialise the file header."""
349 filenode = self.document.find(self.namespaced("file"))
350 filenode.set("source-language", self.sourcelanguage)
351 if self.targetlanguage:
352 filenode.set("target-language", self.targetlanguage)
353
354 - def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
355 """creates a filenode with the given filename. All parameters are needed
356 for XLIFF compliance."""
357 self.removedefaultfile()
358 if sourcelanguage is None:
359 sourcelanguage = self.sourcelanguage
360 if targetlanguage is None:
361 targetlanguage = self.targetlanguage
362 filenode = etree.Element(self.namespaced("file"))
363 filenode.set("original", filename)
364 filenode.set("source-language", sourcelanguage)
365 if targetlanguage:
366 filenode.set("target-language", targetlanguage)
367 filenode.set("datatype", datatype)
368 bodyNode = etree.SubElement(filenode, self.namespaced(self.bodyNode))
369 return filenode
370
372 """returns the name of the given file"""
373 return filenode.get("original")
374
376 """returns all filenames in this XLIFF file"""
377 filenodes = self.document.findall(self.namespaced("file"))
378 filenames = [self.getfilename(filenode) for filenode in filenodes]
379 filenames = filter(None, filenames)
380 if len(filenames) == 1 and filenames[0] == '':
381 filenames = []
382 return filenames
383
385 """finds the filenode with the given name"""
386 filenodes = self.document.findall(self.namespaced("file"))
387 for filenode in filenodes:
388 if self.getfilename(filenode) == filename:
389 return filenode
390 return None
391
393 """Returns the datatype of the stored file. If no filename is given,
394 the datatype of the first file is given."""
395 if filename:
396 node = self.getfilenode(filename)
397 if not node is None:
398 return node.get("datatype")
399 else:
400 filenames = self.getfilenames()
401 if len(filenames) > 0 and filenames[0] != "NoName":
402 return self.getdatatype(filenames[0])
403 return ""
404
406 """Returns the date attribute for the file. If no filename is given,
407 the date of the first file is given. If the date attribute is not
408 specified, None is returned."""
409 if filename:
410 node = self.getfilenode(filename)
411 if not node is None:
412 return node.get("date")
413 else:
414 filenames = self.getfilenames()
415 if len(filenames) > 0 and filenames[0] != "NoName":
416 return self.getdate(filenames[0])
417 return None
418
420 """We want to remove the default file-tag as soon as possible if we
421 know if still present and empty."""
422 filenodes = self.document.findall(self.namespaced("file"))
423 if len(filenodes) > 1:
424 for filenode in filenodes:
425 if filenode.get("original") == "NoName" and \
426 not filenode.findall(".//%s" % self.namespaced(self.UnitClass.rootNode)):
427 self.document.getroot().remove(filenode)
428 break
429
431 """finds the header node for the given filenode"""
432
433 headernode = list(filenode.find(self.namespaced("header")))
434 if not headernode is None:
435 return headernode
436 if not createifmissing:
437 return None
438 headernode = etree.SubElement(filenode, self.namespaced("header"))
439 return headernode
440
441 - def getbodynode(self, filenode, createifmissing=False):
442 """finds the body node for the given filenode"""
443 bodynode = filenode.find(self.namespaced("body"))
444 if not bodynode is None:
445 return bodynode
446 if not createifmissing:
447 return None
448 bodynode = etree.SubElement(filenode, self.namespaced("body"))
449 return bodynode
450
451 - def addsourceunit(self, source, filename="NoName", createifmissing=False):
452 """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"""
453 if self._filename != filename:
454 if not self.switchfile(filename, createifmissing):
455 return None
456 unit = super(xlifffile, self).addsourceunit(source)
457 self._messagenum += 1
458 unit.setid("%d" % self._messagenum)
459 lisa.setXMLspace(unit.xmlelement, "preserve")
460 return unit
461
462 - def switchfile(self, filename, createifmissing=False):
463 """adds the given trans-unit (will create the nodes required if asked). Returns success"""
464 self._filename = filename
465 filenode = self.getfilenode(filename)
466 if filenode is None:
467 if not createifmissing:
468 return False
469 filenode = self.createfilenode(filename)
470 self.document.getroot().append(filenode)
471
472 self.body = self.getbodynode(filenode, createifmissing=createifmissing)
473 if self.body is None:
474 return False
475 self._messagenum = len(self.body.findall(".//%s" % self.namespaced("trans-unit")))
476
477
478
479
480
481 return True
482
483 - def creategroup(self, filename="NoName", createifmissing=False, restype=None):
484 """adds a group tag into the specified file"""
485 if self._filename != filename:
486 if not self.switchfile(filename, createifmissing):
487 return None
488 group = etree.SubElement(self.body, self.namespaced("group"))
489 if restype:
490 group.set("restype", restype)
491 return group
492
496
508 parsestring = classmethod(parsestring)
509