1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """Module for handling Qt linguist (.ts) files.
22
23 This will eventually replace the older ts.py which only supports the older
24 format. While converters haven't been updated to use this module, we retain
25 both.
26
27 U{TS file format 4.3<http://doc.trolltech.com/4.3/linguist-ts-file-format.html>},
28 U{4.5<http://doc.trolltech.com/4.5/linguist-ts-file-format.html>},
29 U{Example<http://svn.ez.no/svn/ezcomponents/trunk/Translation/docs/linguist-format.txt>},
30 U{Plurals forms<http://www.koders.com/cpp/fidE7B7E83C54B9036EB7FA0F27BC56BCCFC4B9DF34.aspx#L200>}
31
32 U{Specification of the valid variable entries <http://doc.trolltech.com/4.3/qstring.html#arg>},
33 U{2 <http://doc.trolltech.com/4.3/qstring.html#arg-2>}
34 """
35
36 from lxml import etree
37
38 from translate.lang import data
39 from translate.misc.multistring import multistring
40 from translate.storage import base, lisa
41 from translate.storage.placeables import general
42 from translate.storage.workflow import StateEnum as state
43
44
45
46 NPLURALS = {
47 'jp': 1,
48 'en': 2,
49 'fr': 2,
50 'lv': 3,
51 'ga': 3,
52 'cs': 3,
53 'sk': 3,
54 'mk': 3,
55 'lt': 3,
56 'ru': 3,
57 'pl': 3,
58 'ro': 3,
59 'sl': 4,
60 'mt': 4,
61 'cy': 5,
62 'ar': 6,
63 }
64
65
67 """A single term in the xliff file."""
68
69 rootNode = "message"
70 languageNode = "source"
71 textNode = ""
72 namespace = ''
73 rich_parsers = general.parsers
74
75 S_OBSOLETE = state.OBSOLETE
76 S_UNTRANSLATED = state.EMPTY
77 S_FUZZY = state.NEEDS_WORK
78 S_TRANSLATED = state.UNREVIEWED
79
80 statemap = {
81 "obsolete": S_OBSOLETE,
82 "unfinished": S_FUZZY,
83 "": S_TRANSLATED,
84 None: S_TRANSLATED,
85 }
86 """This maps the unit "type" attribute to state."""
87
88 STATE = {
89 S_OBSOLETE: (state.OBSOLETE, state.EMPTY),
90 S_UNTRANSLATED: (state.EMPTY, state.NEEDS_WORK),
91 S_FUZZY: (state.NEEDS_WORK, state.UNREVIEWED),
92 S_TRANSLATED: (state.UNREVIEWED, state.MAX),
93 }
94
95 statemap_r = dict((i[1], i[0]) for i in statemap.iteritems())
96
98 """Returns an xml Element setup with given parameters."""
99
100 assert purpose
101 if purpose == "target":
102 purpose = "translation"
103 langset = etree.Element(self.namespaced(purpose))
104
105
106
107 langset.text = text
108 return langset
109
112
115
117 """We override this to get source and target nodes."""
118
119 def not_none(node):
120 return not node is None
121 return filter(not_none, [self._getsourcenode(), self._gettargetnode()])
122
130 source = property(getsource, lisa.LISAunit.setsource)
131 rich_source = property(base.TranslationUnit._get_rich_source, base.TranslationUnit._set_rich_source)
132
134
135
136
137
138
139
140 self._rich_target = None
141 if self.gettarget() == text:
142 return
143 strings = []
144 if isinstance(text, multistring):
145 strings = text.strings
146 elif isinstance(text, list):
147 strings = text
148 else:
149 strings = [text]
150 targetnode = self._gettargetnode()
151 type = targetnode.get("type")
152 targetnode.clear()
153 if type:
154 targetnode.set("type", type)
155 if self.hasplural() or len(strings) > 1:
156 self.xmlelement.set("numerus", "yes")
157 for string in strings:
158 numerus = etree.SubElement(targetnode, self.namespaced("numerusform"))
159 numerus.text = data.forceunicode(string) or u""
160
161 numerus.tail = u"\n "
162 else:
163 targetnode.text = data.forceunicode(text) or u""
164 targetnode.tail = u"\n "
165
167 targetnode = self._gettargetnode()
168 if targetnode is None:
169 etree.SubElement(self.xmlelement, self.namespaced("translation"))
170 return None
171 if self.hasplural():
172 numerus_nodes = targetnode.findall(self.namespaced("numerusform"))
173 return multistring([node.text or u"" for node in numerus_nodes])
174 else:
175 return data.forceunicode(targetnode.text) or u""
176 target = property(gettarget, settarget)
177 rich_target = property(base.TranslationUnit._get_rich_target, base.TranslationUnit._set_rich_target)
178
180 return self.xmlelement.get("numerus") == "yes"
181
182 - def addnote(self, text, origin=None, position="append"):
183 """Add a note specifically in a "comment" tag"""
184 if isinstance(text, str):
185 text = text.decode("utf-8")
186 current_notes = self.getnotes(origin)
187 self.removenotes(origin)
188 if origin in ["programmer", "developer", "source code"]:
189 note = etree.SubElement(self.xmlelement, self.namespaced("extracomment"))
190 else:
191 note = etree.SubElement(self.xmlelement, self.namespaced("translatorcomment"))
192 if position == "append":
193 note.text = "\n".join(filter(None, [current_notes, text.strip()]))
194 else:
195 note.text = text.strip()
196
198
199 comments = []
200 if origin in ["programmer", "developer", "source code", None]:
201 notenode = self.xmlelement.find(self.namespaced("comment"))
202 if notenode is not None and notenode.text is not None:
203 comments.append(notenode.text)
204 notenode = self.xmlelement.find(self.namespaced("extracomment"))
205 if notenode is not None and notenode.text is not None:
206 comments.append(notenode.text)
207 if origin in ["translator", None]:
208 notenode = self.xmlelement.find(self.namespaced("translatorcomment"))
209 if notenode is not None and notenode.text is not None:
210 comments.append(notenode.text)
211 return '\n'.join(comments)
212
214 """Remove all the translator notes."""
215 if origin in ["programmer", "developer", "source code", None]:
216 note = self.xmlelement.find(self.namespaced("comment"))
217 if not note is None:
218 self.xmlelement.remove(note)
219 note = self.xmlelement.find(self.namespaced("extracomment"))
220 if not note is None:
221 self.xmlelement.remove(note)
222 if origin in ["translator", None]:
223 note = self.xmlelement.find(self.namespaced("translatorcomment"))
224 if not note is None:
225 self.xmlelement.remove(note)
226
228 """Returns the type of this translation."""
229 targetnode = self._gettargetnode()
230 if targetnode is not None:
231 return targetnode.get("type")
232 return None
233
242
244 """States whether this unit needs to be reviewed"""
245 return self._gettype() == "unfinished"
246
249
255
257 if self.source is None:
258 return None
259 context_name = self.getcontext()
260
261
262 if context_name is not None:
263 return context_name + self.source
264 else:
265 return self.source
266
268
269
270
271
272
273 return bool(self.getid()) and not self.isobsolete()
274
275 - def getcontext(self):
276 parent = self.xmlelement.getparent()
277 if parent is None:
278 return None
279 context = parent.find("name")
280 if context is None:
281 return None
282 return context.text
283
285 if isinstance(location, str):
286 location = location.decode("utf-8")
287 newlocation = etree.SubElement(self.xmlelement, self.namespaced("location"))
288 try:
289 filename, line = location.split(':', 1)
290 except ValueError:
291 filename = location
292 line = None
293 newlocation.set("filename", filename)
294 if line is not None:
295 newlocation.set("line", line)
296
298 location_tags = self.xmlelement.iterfind(self.namespaced("location"))
299 locations = []
300 for location_tag in location_tags:
301 location = location_tag.get("filename")
302 line = location_tag.get("line")
303 if line:
304 if location:
305 location += ':' + line
306 else:
307 location = line
308 locations.append(location)
309 return locations
310
311 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
316
318 return self._gettype() == "obsolete"
319
330
340
341
343 """Class representing a XLIFF file store."""
344 UnitClass = tsunit
345 Name = _("Qt Linguist Translation File")
346 Mimetypes = ["application/x-linguist"]
347 Extensions = ["ts"]
348 rootNode = "TS"
349
350 bodyNode = "context"
351 XMLskeleton = '''<!DOCTYPE TS>
352 <TS>
353 </TS>
354 '''
355 namespace = ''
356
360
361 - def initbody(self):
362 """Initialises self.body."""
363 self.namespace = self.document.getroot().nsmap.get(None, None)
364 self.header = self.document.getroot()
365 if self._contextname:
366 self.body = self.getcontextnode(self._contextname)
367 else:
368 self.body = self.document.getroot()
369
371 """Get the source language for this .ts file.
372
373 The 'sourcelanguage' attribute was only added to the TS format in
374 Qt v4.5. We return 'en' if there is no sourcelanguage set.
375
376 We don't implement setsourcelanguage as users really shouldn't be
377 altering the source language in .ts files, it should be set correctly
378 by the extraction tools.
379
380 @return: ISO code e.g. af, fr, pt_BR
381 @rtype: String
382 """
383 lang = data.normalize_code(self.header.get('sourcelanguage', "en"))
384 if lang == 'en-us':
385 return 'en'
386 return lang
387
389 """Get the target language for this .ts file.
390
391 @return: ISO code e.g. af, fr, pt_BR
392 @rtype: String
393 """
394 return data.normalize_code(self.header.get('language'))
395
397 """Set the target language for this .ts file to L{targetlanguage}.
398
399 @param targetlanguage: ISO code e.g. af, fr, pt_BR
400 @type targetlanguage: String
401 """
402 if targetlanguage:
403 self.header.set('language', targetlanguage)
404
405 - def _createcontext(self, contextname, comment=None):
406 """Creates a context node with an optional comment"""
407 context = etree.SubElement(self.document.getroot(), self.namespaced(self.bodyNode))
408 name = etree.SubElement(context, self.namespaced("name"))
409 name.text = contextname
410 if comment:
411 comment_node = context.SubElement(context, "comment")
412 comment_node.text = comment
413 return context
414
415 - def _getcontextname(self, contextnode):
416 """Returns the name of the given context node."""
417 return contextnode.find(self.namespaced("name")).text
418
420 """Returns all contextnames in this TS file."""
421 contextnodes = self.document.findall(self.namespaced("context"))
422 contextnames = [self.getcontextname(contextnode) for contextnode in contextnodes]
423 return contextnames
424
425 - def _getcontextnode(self, contextname):
426 """Returns the context node with the given name."""
427 contextnodes = self.document.findall(self.namespaced("context"))
428 for contextnode in contextnodes:
429 if self._getcontextname(contextnode) == contextname:
430 return contextnode
431 return None
432
433 - def addunit(self, unit, new=True, contextname=None, createifmissing=True):
434 """Adds the given unit to the last used body node (current context).
435
436 If the contextname is specified, switch to that context (creating it
437 if allowed by createifmissing)."""
438 if contextname is None:
439 contextname = unit.getcontext()
440
441 if self._contextname != contextname:
442 if not self._switchcontext(contextname, createifmissing):
443 return None
444 super(tsfile, self).addunit(unit, new)
445
446 return unit
447
448 - def _switchcontext(self, contextname, createifmissing=False):
449 """Switch the current context to the one named contextname, optionally
450 creating it if it doesn't exist."""
451 self._contextname = contextname
452 contextnode = self._getcontextnode(contextname)
453 if contextnode is None:
454 if not createifmissing:
455 return False
456 contextnode = self._createcontext(contextname)
457
458 self.body = contextnode
459 if self.body is None:
460 return False
461 return True
462
469
471 """Converts to a string containing the file's XML.
472
473 We have to override this to ensure mimic the Qt convention:
474 - no XML decleration
475 - plain DOCTYPE that lxml seems to ignore
476 """
477
478
479
480
481 output = etree.tostring(self.document, pretty_print=True,
482 xml_declaration=False, encoding='utf-8')
483 if not "<!DOCTYPE TS>" in output[:30]:
484 output = "<!DOCTYPE TS>" + output
485 return output
486