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 the appropriate "*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("extracomment"))
202 if notenode is not None and notenode.text is not None:
203 comments.append(notenode.text)
204 if origin in ["translator", None]:
205 notenode = self.xmlelement.find(self.namespaced("translatorcomment"))
206 if notenode is not None and notenode.text is not None:
207 comments.append(notenode.text)
208 return '\n'.join(comments)
209
211 """Remove all the translator notes."""
212 if origin in ["programmer", "developer", "source code", None]:
213 note = self.xmlelement.find(self.namespaced("extracomment"))
214 if not note is None:
215 self.xmlelement.remove(note)
216 if origin in ["translator", None]:
217 note = self.xmlelement.find(self.namespaced("translatorcomment"))
218 if not note is None:
219 self.xmlelement.remove(note)
220
222 """Returns the type of this translation."""
223 targetnode = self._gettargetnode()
224 if targetnode is not None:
225 return targetnode.get("type")
226 return None
227
236
238 """States whether this unit needs to be reviewed"""
239 return self._gettype() == "unfinished"
240
243
249
251 if self.source is None:
252 return None
253 context_name = self.getcontext()
254
255
256 if context_name is not None:
257 return context_name + self.source
258 else:
259 return self.source
260
262
263
264
265
266
267 return bool(self.getid()) and not self.isobsolete()
268
269 - def getcontextname(self):
270 parent = self.xmlelement.getparent()
271 if parent is None:
272 return None
273 context = parent.find("name")
274 if context is None:
275 return None
276 return context.text
277
278 - def getcontext(self):
279 contexts = [self.getcontextname()]
280 commentnode = self.xmlelement.find(self.namespaced("comment"))
281 if commentnode is not None and commentnode.text is not None:
282 contexts.append(commentnode.text)
283 contexts = filter(None, contexts)
284 return '\n'.join(contexts)
285
287 if isinstance(location, str):
288 location = location.decode("utf-8")
289 newlocation = etree.SubElement(self.xmlelement, self.namespaced("location"))
290 try:
291 filename, line = location.split(':', 1)
292 except ValueError:
293 filename = location
294 line = None
295 newlocation.set("filename", filename)
296 if line is not None:
297 newlocation.set("line", line)
298
300 location_tags = self.xmlelement.iterfind(self.namespaced("location"))
301 locations = []
302 for location_tag in location_tags:
303 location = location_tag.get("filename")
304 line = location_tag.get("line")
305 if line:
306 if location:
307 location += ':' + line
308 else:
309 location = line
310 locations.append(location)
311 return locations
312
313 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
318
320 return self._gettype() == "obsolete"
321
332
342
343
345 """Class representing a XLIFF file store."""
346 UnitClass = tsunit
347 Name = _("Qt Linguist Translation File")
348 Mimetypes = ["application/x-linguist"]
349 Extensions = ["ts"]
350 rootNode = "TS"
351
352 bodyNode = "context"
353 XMLskeleton = '''<!DOCTYPE TS>
354 <TS>
355 </TS>
356 '''
357 namespace = ''
358
362
363 - def initbody(self):
364 """Initialises self.body."""
365 self.namespace = self.document.getroot().nsmap.get(None, None)
366 self.header = self.document.getroot()
367 if self._contextname:
368 self.body = self._getcontextnode(self._contextname)
369 else:
370 self.body = self.document.getroot()
371
373 """Get the source language for this .ts file.
374
375 The 'sourcelanguage' attribute was only added to the TS format in
376 Qt v4.5. We return 'en' if there is no sourcelanguage set.
377
378 We don't implement setsourcelanguage as users really shouldn't be
379 altering the source language in .ts files, it should be set correctly
380 by the extraction tools.
381
382 @return: ISO code e.g. af, fr, pt_BR
383 @rtype: String
384 """
385 lang = data.normalize_code(self.header.get('sourcelanguage', "en"))
386 if lang == 'en-us':
387 return 'en'
388 return lang
389
391 """Get the target language for this .ts file.
392
393 @return: ISO code e.g. af, fr, pt_BR
394 @rtype: String
395 """
396 return data.normalize_code(self.header.get('language'))
397
399 """Set the target language for this .ts file to L{targetlanguage}.
400
401 @param targetlanguage: ISO code e.g. af, fr, pt_BR
402 @type targetlanguage: String
403 """
404 if targetlanguage:
405 self.header.set('language', targetlanguage)
406
407 - def _createcontext(self, contextname, comment=None):
408 """Creates a context node with an optional comment"""
409 context = etree.SubElement(self.document.getroot(), self.namespaced(self.bodyNode))
410 name = etree.SubElement(context, self.namespaced("name"))
411 name.text = contextname
412 if comment:
413 comment_node = context.SubElement(context, "comment")
414 comment_node.text = comment
415 return context
416
417 - def _getcontextname(self, contextnode):
418 """Returns the name of the given context node."""
419 return contextnode.find(self.namespaced("name")).text
420
422 """Returns all contextnames in this TS file."""
423 contextnodes = self.document.findall(self.namespaced("context"))
424 contextnames = [self.getcontextname(contextnode) for contextnode in contextnodes]
425 return contextnames
426
427 - def _getcontextnode(self, contextname):
428 """Returns the context node with the given name."""
429 contextnodes = self.document.findall(self.namespaced("context"))
430 for contextnode in contextnodes:
431 if self._getcontextname(contextnode) == contextname:
432 return contextnode
433 return None
434
435 - def addunit(self, unit, new=True, contextname=None, createifmissing=True):
436 """Adds the given unit to the last used body node (current context).
437
438 If the contextname is specified, switch to that context (creating it
439 if allowed by createifmissing)."""
440 if contextname is None:
441 contextname = unit.getcontextname()
442
443 if self._contextname != contextname:
444 if not self._switchcontext(contextname, createifmissing):
445 return None
446 super(tsfile, self).addunit(unit, new)
447
448 return unit
449
450 - def _switchcontext(self, contextname, createifmissing=False):
451 """Switch the current context to the one named contextname, optionally
452 creating it if it doesn't exist."""
453 self._contextname = contextname
454 contextnode = self._getcontextnode(contextname)
455 if contextnode is None:
456 if not createifmissing:
457 return False
458 contextnode = self._createcontext(contextname)
459
460 self.body = contextnode
461 if self.body is None:
462 return False
463 return True
464
471
473 """Converts to a string containing the file's XML.
474
475 We have to override this to ensure mimic the Qt convention:
476 - no XML decleration
477 - plain DOCTYPE that lxml seems to ignore
478 """
479
480
481
482
483 output = etree.tostring(self.document, pretty_print=True,
484 xml_declaration=False, encoding='utf-8')
485 if not "<!DOCTYPE TS>" in output[:30]:
486 output = "<!DOCTYPE TS>" + output
487 return output
488