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
34 from translate.storage.pocommon import encodingToUse
35 import re
36 import copy
37 import cStringIO
38
39 lsep = " "
40 """Seperator for #: entries"""
41
42 basic_header = r'''msgid ""
43 msgstr ""
44 "Content-Type: text/plain; charset=UTF-8\n"
45 "Content-Transfer-Encoding: 8bit\n"
46 '''
47
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 __shallow__ = ['_store']
64
65 - 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 if origin in ["programmer", "developer", "source code"]:
146 autocomments = True
147 commentlist = self.automaticcomments
148 if text.endswith(u'\n'):
149 text = text[:-1]
150 text = text.split(u"\n")
151 if position == "append":
152 commentlist.extend(text)
153 else:
154 newcomments = text
155 newcomments.extend(commentlist)
156 if autocomments:
157 self.automaticcomments = newcomments
158 else:
159 self.othercomments = newcomments
160
162 """Remove all the translator's notes (other comments)"""
163 self.othercomments = []
164
166
167 new_unit = self.__class__()
168
169
170 shallow = set(self.__shallow__)
171
172 for key, value in self.__dict__.iteritems():
173 if key not in shallow:
174 setattr(new_unit, key, copy.deepcopy(value))
175
176 for key in set(shallow):
177 setattr(new_unit, key, getattr(self, key))
178
179
180 memo[id(self)] = self
181
182 return new_unit
183
185 return copy.deepcopy(self)
186
192
198
199 - def merge(self, otherpo, overwrite=False, comments=True, authoritative=False):
200 """Merges the otherpo (with the same msgid) into this one.
201
202 Overwrite non-blank self.msgstr only if overwrite is True
203 merge comments only if comments is True
204 """
205
206 def mergelists(list1, list2, split=False):
207
208 if split:
209 splitlist1 = []
210 splitlist2 = []
211 for item in list1:
212 splitlist1.extend(item.split())
213 for item in list2:
214 splitlist2.extend(item.split())
215 list1.extend([item for item in splitlist2 if not item in splitlist1])
216 else:
217
218 if list1 != list2:
219 for item in list2:
220
221 if item not in list1 or len(item) < 5:
222 list1.append(item)
223
224 if not isinstance(otherpo, pounit):
225 super(pounit, self).merge(otherpo, overwrite, comments)
226 return
227 if comments:
228 mergelists(self.othercomments, otherpo.othercomments)
229 mergelists(self.typecomments, otherpo.typecomments)
230 if not authoritative:
231
232
233 mergelists(self.automaticcomments, otherpo.automaticcomments)
234
235 mergelists(self.sourcecomments, otherpo.sourcecomments, split=True)
236 if not self.istranslated() or overwrite:
237
238 if pocommon.extract_msgid_comment(otherpo.target):
239 otherpo.target = otherpo.target.replace('_: ' + otherpo._extract_msgidcomments()+ '\n', '')
240 self.target = otherpo.target
241 if self.source != otherpo.source or self.getcontext() != otherpo.getcontext():
242 self.markfuzzy()
243 else:
244 self.markfuzzy(otherpo.isfuzzy())
245 elif not otherpo.istranslated():
246 if self.source != otherpo.source:
247 self.markfuzzy()
248 else:
249 if self.target != otherpo.target:
250 self.markfuzzy()
251
253
254 return not self.getid() and len(self.target) > 0
255
262
267
276
286
289
292
295
298
301
303 """Makes this unit obsolete"""
304 self.obsolete = True
305 self.sourcecomments = []
306 self.automaticcomments = []
307
309 """Makes an obsolete unit normal"""
310 self.obsolete = False
311
316
320
322 """convert to a string. double check that unicode is handled somehow here"""
323 _cpo_unit = cpo.pounit.buildfromunit(self)
324 return str(_cpo_unit)
325
327 """Get a list of locations from sourcecomments in the PO unit
328
329 rtype: List
330 return: A list of the locations with '#: ' stripped
331
332 """
333
334 return self.sourcecomments
335
337 """Add a location to sourcecomments in the PO unit
338
339 @param location: Text location e.g. 'file.c:23' does not include #:
340 @type location: String
341 """
342 self.sourcecomments.extend(location.split())
343
354
355 - def getcontext(self):
356 """Get the message context."""
357 return self._msgctxt + self.msgidcomment
358
373
406 buildfromunit = classmethod(buildfromunit)
407
408 -class pofile(pocommon.pofile):
409 """A .po file containing various units"""
410 UnitClass = pounit
411
413 """Deprecated: changes the encoding on the file."""
414
415
416
417 raise DeprecationWarning
418
419 self._encoding = encodingToUse(newencoding)
420 if not self.units:
421 return
422 header = self.header()
423 if not header or header.isblank():
424 return
425 charsetline = None
426 headerstr = header.target
427 for line in headerstr.split("\n"):
428 if not ":" in line:
429 continue
430 key, value = line.strip().split(":", 1)
431 if key.strip() != "Content-Type":
432 continue
433 charsetline = line
434 if charsetline is None:
435 headerstr += "Content-Type: text/plain; charset=%s" % self._encoding
436 else:
437 charset = re.search("charset=([^ ]*)", charsetline)
438 if charset is None:
439 newcharsetline = charsetline
440 if not newcharsetline.strip().endswith(";"):
441 newcharsetline += ";"
442 newcharsetline += " charset=%s" % self._encoding
443 else:
444 charset = charset.group(1)
445 newcharsetline = charsetline.replace("charset=%s" % charset, "charset=%s" % self._encoding, 1)
446 headerstr = headerstr.replace(charsetline, newcharsetline, 1)
447 header.target = headerstr
448
450 """Builds up this store from the internal cpo store.
451
452 A user must ensure that self._cpo_store already exists, and that it is
453 deleted afterwards."""
454 for unit in self._cpo_store.units:
455 self.addunit(self.UnitClass.buildfromunit(unit))
456 self._encoding = self._cpo_store._encoding
457
459 """Builds the internal cpo store from the data in self.
460
461 A user must ensure that self._cpo_store does not exist, and should
462 delete it after using it."""
463 self._cpo_store = cpo.pofile()
464 for unit in self.units:
465 if not unit.isblank():
466 self._cpo_store.addunit(cpo.pofile.UnitClass.buildfromunit(unit))
467 if not self._cpo_store.header():
468
469 self._cpo_store.makeheader(charset="utf-8", encoding="8bit")
470
471
473 """Parses the given file or file source string."""
474 try:
475 if hasattr(input, 'name'):
476 self.filename = input.name
477 elif not getattr(self, 'filename', ''):
478 self.filename = ''
479 tmp_header_added = False
480
481
482
483 self.units = []
484 self._cpo_store = cpo.pofile(input)
485 self._build_self_from_cpo()
486 del self._cpo_store
487 if tmp_header_added:
488 self.units = self.units[1:]
489 except Exception, e:
490 raise base.ParseError(e)
491
493 """Make sure each msgid is unique ; merge comments etc from duplicates into original"""
494
495
496 id_dict = {}
497 uniqueunits = []
498
499
500 markedpos = []
501 def addcomment(thepo):
502 thepo.msgidcomment = " ".join(thepo.getlocations())
503 markedpos.append(thepo)
504 for thepo in self.units:
505 id = thepo.getid()
506 if thepo.isheader() and not thepo.getlocations():
507
508 uniqueunits.append(thepo)
509 elif id in id_dict:
510 if duplicatestyle == "merge":
511 if id:
512 id_dict[id].merge(thepo)
513 else:
514 addcomment(thepo)
515 uniqueunits.append(thepo)
516 elif duplicatestyle == "msgctxt":
517 origpo = id_dict[id]
518 if origpo not in markedpos:
519 origpo._msgctxt += " ".join(origpo.getlocations())
520 markedpos.append(thepo)
521 thepo._msgctxt += " ".join(thepo.getlocations())
522 uniqueunits.append(thepo)
523 else:
524 if not id:
525 if duplicatestyle == "merge":
526 addcomment(thepo)
527 else:
528 thepo._msgctxt += u" ".join(thepo.getlocations())
529 id_dict[id] = thepo
530 uniqueunits.append(thepo)
531 self.units = uniqueunits
532
534 """Convert to a string. double check that unicode is handled somehow here"""
535 self._cpo_store = cpo.pofile(encoding=self._encoding)
536 self._build_cpo_from_self()
537 output = str(self._cpo_store)
538 del self._cpo_store
539 return output
540