| LEFT | RIGHT |
| 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 | |
| LEFT | RIGHT |