1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """Classes for the support of Gettext .po and .pot files.
22
23 This implementation assumes that cpo is working. This should not be used
24 directly, but can be used once cpo has been established to work."""
25
26
27
28
29
30
31 from translate.misc.multistring import multistring
32 from translate.lang import data
33 from translate.storage import pocommon, base, cpo, poparser
34 from translate.storage.pocommon import encodingToUse
35 import re
36 import copy
37 import cStringIO
38 import urllib
39
40 lsep = " "
41 """Seperator for #: entries"""
42
43 basic_header = r'''msgid ""
44 msgstr ""
45 "Content-Type: text/plain; charset=UTF-8\n"
46 "Content-Transfer-Encoding: 8bit\n"
47 '''
48
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 __shallow__ = ['_store']
65
66 - def __init__(self, source=None, encoding="UTF-8"):
73
82
85
98 source = property(getsource, setsource)
99
100
102 """Returns the unescaped msgstr"""
103 return self._target
104
106 """Sets the msgstr to the given (unescaped) value"""
107 self._rich_target = None
108
109
110 if self.hasplural():
111 if isinstance(target, multistring):
112 self._target = target
113 else:
114
115 self._target = multistring(target)
116 elif isinstance(target, (dict, list)):
117 if len(target) == 1:
118 self._target = target[0]
119 else:
120 raise ValueError("po msgid element has no plural but msgstr has %d elements (%s)" % (len(target), target))
121 else:
122 self._target = target
123 target = property(gettarget, settarget)
124
126 """Return comments based on origin value (programmer, developer, source code and translator)"""
127 if origin == None:
128 comments = u"\n".join(self.othercomments)
129 comments += u"\n".join(self.automaticcomments)
130 elif origin == "translator":
131 comments = u"\n".join(self.othercomments)
132 elif origin in ["programmer", "developer", "source code"]:
133 comments = u"\n".join(self.automaticcomments)
134 else:
135 raise ValueError("Comment type not valid")
136 return comments
137
138 - def addnote(self, text, origin=None, position="append"):
139 """This is modeled on the XLIFF method. See xliff.py::xliffunit.addnote"""
140
141 if not (text and text.strip()):
142 return
143 text = data.forceunicode(text)
144 commentlist = self.othercomments
145 autocomments = False
146 if origin in ["programmer", "developer", "source code"]:
147 autocomments = True
148 commentlist = self.automaticcomments
149 if text.endswith(u'\n'):
150 text = text[:-1]
151 newcomments = text.split(u"\n")
152 if position == "append":
153 newcomments = commentlist + newcomments
154 elif position == "prepend":
155 newcomments = newcomments + commentlist
156
157 if autocomments:
158 self.automaticcomments = newcomments
159 else:
160 self.othercomments = newcomments
161
163 """Remove all the translator's notes (other comments)"""
164 self.othercomments = []
165
167
168 new_unit = self.__class__()
169
170
171 shallow = set(self.__shallow__)
172
173 for key, value in self.__dict__.iteritems():
174 if key not in shallow:
175 setattr(new_unit, key, copy.deepcopy(value))
176
177 for key in set(shallow):
178 setattr(new_unit, key, getattr(self, key))
179
180
181 memo[id(self)] = self
182
183 return new_unit
184
186 return copy.deepcopy(self)
187
193
199
200 - def merge(self, otherpo, overwrite=False, comments=True, authoritative=False):
201 """Merges the otherpo (with the same msgid) into this one.
202
203 Overwrite non-blank self.msgstr only if overwrite is True
204 merge comments only if comments is True
205 """
206
207 def mergelists(list1, list2, split=False):
208
209 if unicode in [type(item) for item in list2] + [type(item) for item in list1]:
210 for position, item in enumerate(list1):
211 if isinstance(item, str):
212 list1[position] = item.decode("utf-8")
213 for position, item in enumerate(list2):
214 if isinstance(item, str):
215 list2[position] = item.decode("utf-8")
216
217
218 lineend = ""
219 if list2 and list2[0]:
220 for candidate in ["\n", "\r", "\n\r"]:
221 if list2[0].endswith(candidate):
222 lineend = candidate
223 if not lineend:
224 lineend = ""
225
226
227 if split:
228 splitlist1 = []
229 splitlist2 = []
230 for item in list1:
231 splitlist1.extend(item.split())
232 for item in list2:
233 splitlist2.extend(item.split())
234 list1.extend([item for item in splitlist2 if not item in splitlist1])
235 else:
236
237 if list1 != list2:
238 for item in list2:
239 item = item.rstrip(lineend)
240
241 if item not in list1 or len(item) < 5:
242 list1.append(item)
243
244 if not isinstance(otherpo, pounit):
245 super(pounit, self).merge(otherpo, overwrite, comments)
246 return
247 if comments:
248 mergelists(self.othercomments, otherpo.othercomments)
249 mergelists(self.typecomments, otherpo.typecomments)
250 if not authoritative:
251
252
253 mergelists(self.automaticcomments, otherpo.automaticcomments)
254
255 mergelists(self.sourcecomments, otherpo.sourcecomments, split=True)
256 if not self.istranslated() or overwrite:
257
258 if pocommon.extract_msgid_comment(otherpo.target):
259 otherpo.target = otherpo.target.replace('_: ' + otherpo._extract_msgidcomments()+ '\n', '')
260 self.target = otherpo.target
261 if self.source != otherpo.source or self.getcontext() != otherpo.getcontext():
262 self.markfuzzy()
263 else:
264 self.markfuzzy(otherpo.isfuzzy())
265 elif not otherpo.istranslated():
266 if self.source != otherpo.source:
267 self.markfuzzy()
268 else:
269 if self.target != otherpo.target:
270 self.markfuzzy()
271
273
274 return not self.getid() and len(self.target) > 0
275
282
287
296
306
309
312
315
317 """Makes this unit obsolete"""
318 self.sourcecomments = []
319 self.automaticcomments = []
320 super(pounit, self).makeobsolete()
321
326
330
332 """convert to a string. double check that unicode is handled somehow here"""
333 _cpo_unit = cpo.pounit.buildfromunit(self)
334 return str(_cpo_unit)
335
337 """Get a list of locations from sourcecomments in the PO unit
338
339 rtype: List
340 return: A list of the locations with '#: ' stripped
341
342 """
343
344 return [urllib.unquote_plus(loc) for loc in self.sourcecomments]
345
347 """Add a location to sourcecomments in the PO unit
348
349 @param location: Text location e.g. 'file.c:23' does not include #:
350 @type location: String
351 """
352 if location.find(" ") != -1:
353 location = urllib.quote_plus(location)
354 self.sourcecomments.extend(location.split())
355
366
367 - def getcontext(self):
368 """Get the message context."""
369 return self._msgctxt + self.msgidcomment
370
371 - def setcontext(self, context):
372 context = data.forceunicode(context or u"")
373 self._msgctxt = context
374
389
422 buildfromunit = classmethod(buildfromunit)
423
424 -class pofile(pocommon.pofile):
425 """A .po file containing various units"""
426 UnitClass = pounit
427
429 """Deprecated: changes the encoding on the file."""
430
431
432
433 raise DeprecationWarning
434
435 self._encoding = encodingToUse(newencoding)
436 if not self.units:
437 return
438 header = self.header()
439 if not header or header.isblank():
440 return
441 charsetline = None
442 headerstr = header.target
443 for line in headerstr.split("\n"):
444 if not ":" in line:
445 continue
446 key, value = line.strip().split(":", 1)
447 if key.strip() != "Content-Type":
448 continue
449 charsetline = line
450 if charsetline is None:
451 headerstr += "Content-Type: text/plain; charset=%s" % self._encoding
452 else:
453 charset = re.search("charset=([^ ]*)", charsetline)
454 if charset is None:
455 newcharsetline = charsetline
456 if not newcharsetline.strip().endswith(";"):
457 newcharsetline += ";"
458 newcharsetline += " charset=%s" % self._encoding
459 else:
460 charset = charset.group(1)
461 newcharsetline = charsetline.replace("charset=%s" % charset, "charset=%s" % self._encoding, 1)
462 headerstr = headerstr.replace(charsetline, newcharsetline, 1)
463 header.target = headerstr
464
466 """Builds up this store from the internal cpo store.
467
468 A user must ensure that self._cpo_store already exists, and that it is
469 deleted afterwards."""
470 for unit in self._cpo_store.units:
471 self.addunit(self.UnitClass.buildfromunit(unit))
472 self._encoding = self._cpo_store._encoding
473
475 """Builds the internal cpo store from the data in self.
476
477 A user must ensure that self._cpo_store does not exist, and should
478 delete it after using it."""
479 self._cpo_store = cpo.pofile(noheader=True)
480 for unit in self.units:
481 if not unit.isblank():
482 self._cpo_store.addunit(cpo.pofile.UnitClass.buildfromunit(unit))
483 if not self._cpo_store.header():
484
485 self._cpo_store.makeheader(charset="utf-8", encoding="8bit")
486
487
489 """Parses the given file or file source string."""
490 try:
491 if hasattr(input, 'name'):
492 self.filename = input.name
493 elif not getattr(self, 'filename', ''):
494 self.filename = ''
495 tmp_header_added = False
496
497
498
499 self.units = []
500 self._cpo_store = cpo.pofile(input, noheader=True)
501 self._build_self_from_cpo()
502 del self._cpo_store
503 if tmp_header_added:
504 self.units = self.units[1:]
505 except Exception, e:
506 raise base.ParseError(e)
507
509 """Make sure each msgid is unique ; merge comments etc from duplicates into original"""
510
511
512 id_dict = {}
513 uniqueunits = []
514
515
516 markedpos = []
517 def addcomment(thepo):
518 thepo.msgidcomment = " ".join(thepo.getlocations())
519 markedpos.append(thepo)
520 for thepo in self.units:
521 id = thepo.getid()
522 if thepo.isheader() and not thepo.getlocations():
523
524 uniqueunits.append(thepo)
525 elif id in id_dict:
526 if duplicatestyle == "merge":
527 if id:
528 id_dict[id].merge(thepo)
529 else:
530 addcomment(thepo)
531 uniqueunits.append(thepo)
532 elif duplicatestyle == "msgctxt":
533 origpo = id_dict[id]
534 if origpo not in markedpos:
535 origpo._msgctxt += " ".join(origpo.getlocations())
536 markedpos.append(thepo)
537 thepo._msgctxt += " ".join(thepo.getlocations())
538 uniqueunits.append(thepo)
539 else:
540 if not id:
541 if duplicatestyle == "merge":
542 addcomment(thepo)
543 else:
544 thepo._msgctxt += u" ".join(thepo.getlocations())
545 id_dict[id] = thepo
546 uniqueunits.append(thepo)
547 self.units = uniqueunits
548
550 """Convert to a string. double check that unicode is handled somehow here"""
551 self._cpo_store = cpo.pofile(encoding=self._encoding, noheader=True)
552 self._build_cpo_from_self()
553 output = str(self._cpo_store)
554 del self._cpo_store
555 return output
556