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 import re
32 import copy
33 import cStringIO
34
35 from translate.lang import data
36 from translate.misc.multistring import multistring
37 from translate.storage import pocommon, base, cpo, poparser
38 from translate.storage.pocommon import encodingToUse
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
49
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 __shallow__ = ['_store']
66
67 - def __init__(self, source=None, encoding="UTF-8"):
74
83
86
99 source = property(getsource, setsource)
100
102 """Returns the unescaped msgstr"""
103 return self._target
104
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
318
320 """Makes this unit obsolete"""
321 self.sourcecomments = []
322 self.automaticcomments = []
323 super(pounit, self).makeobsolete()
324
329
333
335 """convert to a string. double check that unicode is handled somehow here"""
336 _cpo_unit = cpo.pounit.buildfromunit(self)
337 return str(_cpo_unit)
338
340 """Get a list of locations from sourcecomments in the PO unit
341
342 rtype: List
343 return: A list of the locations with '#: ' stripped
344
345 """
346
347 return [pocommon.unquote_plus(loc) for loc in self.sourcecomments]
348
350 """Add a location to sourcecomments in the PO unit
351
352 @param location: Text location e.g. 'file.c:23' does not include #:
353 @type location: String
354 """
355 if location.find(" ") != -1:
356 location = pocommon.quote_plus(location)
357 self.sourcecomments.extend(location.split())
358
369
370 - def getcontext(self):
371 """Get the message context."""
372 return self._msgctxt + self.msgidcomment
373
374 - def setcontext(self, context):
375 context = data.forceunicode(context or u"")
376 self._msgctxt = context
377
392
425 buildfromunit = classmethod(buildfromunit)
426
427
428 -class pofile(pocommon.pofile):
429 """A .po file containing various units"""
430 UnitClass = pounit
431
433 """Deprecated: changes the encoding on the file."""
434
435
436
437 raise DeprecationWarning
438
439 self._encoding = encodingToUse(newencoding)
440 if not self.units:
441 return
442 header = self.header()
443 if not header or header.isblank():
444 return
445 charsetline = None
446 headerstr = header.target
447 for line in headerstr.split("\n"):
448 if not ":" in line:
449 continue
450 key, value = line.strip().split(":", 1)
451 if key.strip() != "Content-Type":
452 continue
453 charsetline = line
454 if charsetline is None:
455 headerstr += "Content-Type: text/plain; charset=%s" % self._encoding
456 else:
457 charset = re.search("charset=([^ ]*)", charsetline)
458 if charset is None:
459 newcharsetline = charsetline
460 if not newcharsetline.strip().endswith(";"):
461 newcharsetline += ";"
462 newcharsetline += " charset=%s" % self._encoding
463 else:
464 charset = charset.group(1)
465 newcharsetline = charsetline.replace("charset=%s" % charset, "charset=%s" % self._encoding, 1)
466 headerstr = headerstr.replace(charsetline, newcharsetline, 1)
467 header.target = headerstr
468
470 """Builds up this store from the internal cpo store.
471
472 A user must ensure that self._cpo_store already exists, and that it is
473 deleted afterwards."""
474 for unit in self._cpo_store.units:
475 self.addunit(self.UnitClass.buildfromunit(unit))
476 self._encoding = self._cpo_store._encoding
477
479 """Builds the internal cpo store from the data in self.
480
481 A user must ensure that self._cpo_store does not exist, and should
482 delete it after using it."""
483 self._cpo_store = cpo.pofile(noheader=True)
484 for unit in self.units:
485 if not unit.isblank():
486 self._cpo_store.addunit(cpo.pofile.UnitClass.buildfromunit(unit, self._encoding))
487 if not self._cpo_store.header():
488
489 self._cpo_store.makeheader(charset=self._encoding, encoding="8bit")
490
492 """Parses the given file or file source string."""
493 try:
494 if hasattr(input, 'name'):
495 self.filename = input.name
496 elif not getattr(self, 'filename', ''):
497 self.filename = ''
498 tmp_header_added = False
499
500
501
502 self.units = []
503 self._cpo_store = cpo.pofile(input, noheader=True)
504 self._build_self_from_cpo()
505 del self._cpo_store
506 if tmp_header_added:
507 self.units = self.units[1:]
508 except Exception, e:
509 raise base.ParseError(e)
510
512 """Make sure each msgid is unique ; merge comments etc from duplicates into original"""
513
514
515 id_dict = {}
516 uniqueunits = []
517
518
519 markedpos = []
520
521 def addcomment(thepo):
522 thepo.msgidcomment = " ".join(thepo.getlocations())
523 markedpos.append(thepo)
524 for thepo in self.units:
525 id = thepo.getid()
526 if thepo.isheader() and not thepo.getlocations():
527
528 uniqueunits.append(thepo)
529 elif id in id_dict:
530 if duplicatestyle == "merge":
531 if id:
532 id_dict[id].merge(thepo)
533 else:
534 addcomment(thepo)
535 uniqueunits.append(thepo)
536 elif duplicatestyle == "msgctxt":
537 origpo = id_dict[id]
538 if origpo not in markedpos:
539 origpo._msgctxt += " ".join(origpo.getlocations())
540 markedpos.append(thepo)
541 thepo._msgctxt += " ".join(thepo.getlocations())
542 uniqueunits.append(thepo)
543 else:
544 if not id:
545 if duplicatestyle == "merge":
546 addcomment(thepo)
547 else:
548 thepo._msgctxt += u" ".join(thepo.getlocations())
549 id_dict[id] = thepo
550 uniqueunits.append(thepo)
551 self.units = uniqueunits
552
554 """Convert to a string. double check that unicode is handled somehow here"""
555 self._cpo_store = cpo.pofile(encoding=self._encoding, noheader=True)
556 try:
557 self._build_cpo_from_self()
558 except UnicodeEncodeError, e:
559 self._encoding = "utf-8"
560 self.updateheader(add=True, Content_Type="text/plain; charset=UTF-8")
561 self._build_cpo_from_self()
562 output = str(self._cpo_store)
563 del self._cpo_store
564 return output
565