Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(67)

Delta Between Two Patch Sets: translate/storage/xliff.py

Issue 65: xliff2po & po2xliff should handle context SVN Base: https://translate.svn.sourceforge.net/svnroot/translate/src/trunk/
Left Patch Set: Update (minor changes) Created 1 year, 4 months ago
Right Patch Set: A more complete patch, supporting CPO and pypo Created 1 year, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: View regular side by side diff
Right: View regular side by side diff
LEFTRIGHT
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 # 3 #
4 # Copyright 2005-2007 Zuza Software Foundation 4 # Copyright 2005-2007 Zuza Software Foundation
5 # 5 #
6 # This file is part of translate. 6 # This file is part of translate.
7 # 7 #
8 # translate is free software; you can redistribute it and/or modify 8 # translate is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by 9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or 10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version. 11 # (at your option) any later version.
12 # 12 #
13 # translate is distributed in the hope that it will be useful, 13 # translate is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details. 16 # GNU General Public License for more details.
17 # 17 #
18 # You should have received a copy of the GNU General Public License 18 # You should have received a copy of the GNU General Public License
19 # along with translate; if not, write to the Free Software 19 # along with translate; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 # 21 #
22 22
23 """Module for handling XLIFF files for translation. 23 """Module for handling XLIFF files for translation.
24 24
25 The official recommendation is to use the extention .xlf for XLIFF files. 25 The official recommendation is to use the extention .xlf for XLIFF files.
26 """ 26 """
27 27
28 from translate.storage import base 28 from translate.storage import base
29 from translate.storage import lisa 29 from translate.storage import lisa
30 from lxml import etree 30 from lxml import etree
31 31
32 # TODO: handle translation types 32 # TODO: handle translation types
33 33
34 class xliffunit(lisa.LISAunit): 34 class xliffunit(lisa.LISAunit):
35 """A single term in the xliff file.""" 35 """A single term in the xliff file."""
36 36
37 rootNode = "trans-unit" 37 rootNode = "trans-unit"
38 languageNode = "source" 38 languageNode = "source"
39 textNode = "" 39 textNode = ""
40 namespace = 'urn:oasis:names:tc:xliff:document:1.1' 40 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
41 41
42 #TODO: id and all the trans-unit level stuff 42 #TODO: id and all the trans-unit level stuff
43 43
44 def createlanguageNode(self, lang, text, purpose): 44 def createlanguageNode(self, lang, text, purpose):
45 """Returns an xml Element setup with given parameters.""" 45 """Returns an xml Element setup with given parameters."""
46 46
47 #TODO: for now we do source, but we have to test if it is target, perhap s 47 #TODO: for now we do source, but we have to test if it is target, perhap s
48 # with parameter. Alternatively, we can use lang, if supplied, since an xliff 48 # with parameter. Alternatively, we can use lang, if supplied, since an xliff
49 #file has to conform to the bilingual nature promised by the header. 49 #file has to conform to the bilingual nature promised by the header.
50 assert purpose 50 assert purpose
(...skipping 169 matching lines...) Show 10 above Show 10 below
220 super(xliffunit, self).settarget(text, lang, append) 220 super(xliffunit, self).settarget(text, lang, append)
221 if text: 221 if text:
222 self.marktranslated() 222 self.marktranslated()
223 223
224 # This code is commented while this will almost always return false. 224 # This code is commented while this will almost always return false.
225 # This way pocount, etc. works well. 225 # This way pocount, etc. works well.
226 # def istranslated(self): 226 # def istranslated(self):
227 # targetnode = self.getlanguageNode(lang=None, index=1) 227 # targetnode = self.getlanguageNode(lang=None, index=1)
228 # return not targetnode is None and \ 228 # return not targetnode is None and \
229 # (targetnode.get("state") == "translated") 229 # (targetnode.get("state") == "translated")
230 230
231 def istranslatable(self): 231 def istranslatable(self):
232 value = self.xmlelement.get("translate") 232 value = self.xmlelement.get("translate")
233 if value and value.lower() == 'no': 233 if value and value.lower() == 'no':
234 return False 234 return False
235 return True 235 return True
236 236
237 def marktranslated(self): 237 def marktranslated(self):
238 targetnode = self.getlanguageNode(lang=None, index=1) 238 targetnode = self.getlanguageNode(lang=None, index=1)
239 if targetnode is None: 239 if targetnode is None:
240 return 240 return
241 if self.isfuzzy() and "state-qualifier" in targetnode.attrib: 241 if self.isfuzzy() and "state-qualifier" in targetnode.attrib:
242 #TODO: consider 242 #TODO: consider
243 del targetnode.attrib["state-qualifier"] 243 del targetnode.attrib["state-qualifier"]
244 targetnode.set("state", "translated") 244 targetnode.set("state", "translated")
245 245
246 def setid(self, id): 246 def setid(self, id):
247 self.xmlelement.set("id", id) 247 self.xmlelement.set("id", id)
248 248
249 def getid(self): 249 def getid(self):
250 return self.xmlelement.get("id") or "" 250 return self.xmlelement.get("id") or ""
251 251
252 def addlocation(self, location): 252 def addlocation(self, location):
253 self.setid(location) 253 self.setid(location)
254 254
255 def getlocations(self): 255 def getlocations(self):
256 return [self.getid()] 256 return [self.getid()]
257 257
258 def createcontextgroup(self, name, contexts=None, purpose=None): 258 def createcontextgroup(self, name, contexts=None, purpose=None):
259 """Add the context group to the trans-unit with contexts a list with 259 """Add the context group to the trans-unit with contexts a list with
260 (type, text) tuples describing each context.""" 260 (type, text) tuples describing each context."""
261 assert contexts 261 assert contexts
262 group = etree.Element(self.namespaced("context-group")) 262 group = etree.Element(self.namespaced("context-group"))
263 # context-group tags must appear at the start within <group> 263 # context-group tags must appear at the start within <group>
264 # tags. Otherwise it must be appended to the end of a group 264 # tags. Otherwise it must be appended to the end of a group
265 # of tags. 265 # of tags.
266 if self.xmlelement.tag == self.namespaced("group"): 266 if self.xmlelement.tag == self.namespaced("group"):
267 self.xmlelement.insert(0, group) 267 self.xmlelement.insert(0, group)
268 else: 268 else:
269 self.xmlelement.append(group) 269 self.xmlelement.append(group)
270 if name:
271 # append a kind of a random 'id' to the name attribute
272 name += '-' + str(id(name))
273 group.set("name", name) 270 group.set("name", name)
274 if purpose: 271 if purpose:
275 group.set("purpose", purpose) 272 group.set("purpose", purpose)
276 for type, text in contexts: 273 for type, text in contexts:
277 if isinstance(text, str): 274 if isinstance(text, str):
278 text = text.decode("utf-8") 275 text = text.decode("utf-8")
279 context = etree.SubElement(group, self.namespaced("context")) 276 context = etree.SubElement(group, self.namespaced("context"))
280 context.text = text 277 context.text = text
281 context.set("context-type", type) 278 context.set("context-type", type)
282 279
283 def getcontextgroups(self, name): 280 def getcontextgroups(self, name):
284 """Returns the contexts in the context groups with the specified name""" 281 """Returns the contexts in the context groups with the specified name"""
285 groups = [] 282 groups = []
286 grouptags = self.xmlelement.findall(".//%s" % self.namespaced("context-g roup"))
287 for group in grouptags:
288 if group.get("name").startswith(name):
289 contexts = group.findall(".//%s" % self.namespaced("context"))
290 pairs = []
291 for context in contexts:
292 pairs.append((context.get("context-type"), lisa.getText(cont ext)))
293 groups.append(pairs) #not extend 283 groups.append(pairs) #not extend
294 return groups 284 return groups
295 285
296 def delcontextgroup(self, name):
297 """Removes all the context groups with the specified name"""
298 #XXX: I really not sure about this behavior, maybe we should remove
299 # only the first group with the given name
300 grouptags = self.xmlelement.findall(
301 ".//%s" % self.namespaced("context-group"))
302 for group in grouptags:
303 if group.get("name").startswith(name):
304 self.xmlelement.remove(group)
305 # break
306
307 def getrestype(self): 286 def getrestype(self):
308 """returns the restype attribute in the trans-unit tag""" 287 """returns the restype attribute in the trans-unit tag"""
309 return self.xmlelement.get("restype") 288 return self.xmlelement.get("restype")
310
311 def merge(self, otherunit, overwrite=False, comments=True):
312 #TODO: consider other attributes like "approved"
313 super(xliffunit, self).merge(otherunit, overwrite, comments)
314 if self.target:
315 self.marktranslated()
316 if otherunit.isfuzzy():
317 self.markfuzzy()
318
319 def correctorigin(self, node, origin):
320 """Check against node tag's origin (e.g note or alt-trans)"""
321 if origin == None:
322 return True
323 elif origin in node.get("from", ""):
324 return True
325 elif origin in node.get("origin", ""):
326 return True
327 else:
328 return False
329
330 class xlifffile(lisa.LISAfile):
331 """Class representing a XLIFF file store."""
332 UnitClass = xliffunit
333 Name = "XLIFF file"
334 Mimetypes = ["application/x-xliff", "application/x-xliff+xml"]
335 Extensions = ["xlf", "xliff"]
336 rootNode = "xliff"
337 bodyNode = "body"
338 XMLskeleton = '''<?xml version="1.0" ?>
339 <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
340 <file original='NoName' source-language='en' datatype='plaintext'>
341 <body>
342 </body>
343 </file>
344 </xliff>'''
345 namespace = 'urn:oasis:names:tc:xliff:document:1.1'
346
347 def __init__(self, *args, **kwargs):
348 lisa.LISAfile.__init__(self, *args, **kwargs)
349 self._filename = "NoName"
350 self._messagenum = 0
351
352 # Allow the inputfile to override defaults for source and target languag e.
353 filenode = self.document.find('.//%s' % self.namespaced('file'))
354 sourcelanguage = filenode.get('source-language')
355 if sourcelanguage:
356 self.setsourcelanguage(sourcelanguage)
357 targetlanguage = filenode.get('target-language')
358 if targetlanguage:
359 self.settargetlanguage(targetlanguage)
360
361 def addheader(self):
362 """Initialise the file header."""
363 filenode = self.document.find(self.namespaced("file"))
364 filenode.set("source-language", self.sourcelanguage)
365 if self.targetlanguage:
366 filenode.set("target-language", self.targetlanguage)
367
368 def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
369 """creates a filenode with the given filename. All parameters are needed
370 for XLIFF compliance."""
371 self.removedefaultfile()
372 if sourcelanguage is None:
373 sourcelanguage = self.sourcelanguage
374 if targetlanguage is None:
375 targetlanguage = self.targetlanguage
376 filenode = etree.Element(self.namespaced("file"))
377 filenode.set("original", filename)
378 filenode.set("source-language", sourcelanguage)
379 if targetlanguage:
380 filenode.set("target-language", targetlanguage)
381 filenode.set("datatype", datatype)
382 bodyNode = etree.SubElement(filenode, self.namespaced(self.bodyNode))
383 return filenode
384
385 def getfilename(self, filenode):
386 """returns the name of the given file"""
387 return filenode.get("original")
388
389 def getfilenames(self):
390 """returns all filenames in this XLIFF file"""
391 filenodes = self.document.findall(self.namespaced("file"))
392 filenames = [self.getfilename(filenode) for filenode in filenodes]
393 filenames = filter(None, filenames)
394 if len(filenames) == 1 and filenames[0] == '':
395 filenames = []
396 return filenames
397
398 def getfilenode(self, filename):
399 """finds the filenode with the given name"""
400 filenodes = self.document.findall(self.namespaced("file"))
401 for filenode in filenodes:
402 if self.getfilename(filenode) == filename:
403 return filenode
404 return None
405
406 def getdatatype(self, filename=None):
407 """Returns the datatype of the stored file. If no filename is given,
408 the datatype of the first file is given."""
409 if filename:
410 node = self.getfilenode(filename)
411 if not node is None:
412 return node.get("datatype")
413 else:
414 filenames = self.getfilenames()
415 if len(filenames) > 0 and filenames[0] != "NoName":
416 return self.getdatatype(filenames[0])
417 return ""
418
419 def removedefaultfile(self):
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.Unit Class.rootNode)):
427 self.document.getroot().remove(filenode)
428 break
429
430 def getheadernode(self, filenode, createifmissing=False):
431 """finds the header node for the given filenode"""
432 # TODO: Deprecated?
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 a sked). 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("tran s-unit")))
476 #TODO: was 0 based before - consider
477 # messagenum = len(self.units)
478 #TODO: we want to number them consecutively inside a body/file tag
479 #instead of globally in the whole XLIFF file, but using len(self.units)
480 #will be much faster
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
493 def __str__(self):
494 self.removedefaultfile()
495 return super(xlifffile, self).__str__()
496
497 def parsestring(cls, storestring):
498 """Parses the string to return the correct file object"""
499 xliff = super(xlifffile, cls).parsestring(storestring)
500 if xliff.units:
501 header = xliff.units[0]
502 if ("gettext-domain-header" in (header.getrestype() or "") \
503 or xliff.getdatatype() == "po") \
504 and cls.__name__.lower() != "poxlifffile":
505 import poxliff
506 xliff = poxliff.PoXliffFile.parsestring(storestring)
507 return xliff
508 parsestring = classmethod(parsestring)
509
LEFTRIGHT

Powered by Google App Engine
This is Rietveld r159