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
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 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 split:
210 splitlist1 = []
211 splitlist2 = []
212 for item in list1:
213 splitlist1.extend(item.split())
214 for item in list2:
215 splitlist2.extend(item.split())
216 list1.extend([item for item in splitlist2 if not item in splitlist1])
217 else:
218
219 if list1 != list2:
220 for item in list2:
221
222 if item not in list1 or len(item) < 5:
223 list1.append(item)
224
225 if not isinstance(otherpo, pounit):
226 super(pounit, self).merge(otherpo, overwrite, comments)
227 return
228 if comments:
229 mergelists(self.othercomments, otherpo.othercomments)
230 mergelists(self.typecomments, otherpo.typecomments)
231 if not authoritative:
232
233
234 mergelists(self.automaticcomments, otherpo.automaticcomments)
235
236 mergelists(self.sourcecomments, otherpo.sourcecomments, split=True)
237 if not self.istranslated() or overwrite:
238
239 if pocommon.extract_msgid_comment(otherpo.target):
240 otherpo.target = otherpo.target.replace('_: ' + otherpo._extract_msgidcomments()+ '\n', '')
241 self.target = otherpo.target
242 if self.source != otherpo.source or self.getcontext() != otherpo.getcontext():
243 self.markfuzzy()
244 else:
245 self.markfuzzy(otherpo.isfuzzy())
246 elif not otherpo.istranslated():
247 if self.source != otherpo.source:
248 self.markfuzzy()
249 else:
250 if self.target != otherpo.target:
251 self.markfuzzy()
252
254
255 return not self.getid() and len(self.target) > 0
256
263
268
277
287
290
293
296
299
302
304 """Makes this unit obsolete"""
305 self.obsolete = True
306 self.sourcecomments = []
307 self.automaticcomments = []
308
310 """Makes an obsolete unit normal"""
311 self.obsolete = False
312
317
321
323 """convert to a string. double check that unicode is handled somehow here"""
324 _cpo_unit = cpo.pounit.buildfromunit(self)
325 return str(_cpo_unit)
326
328 """Get a list of locations from sourcecomments in the PO unit
329
330 rtype: List
331 return: A list of the locations with '#: ' stripped
332
333 """
334
335 return self.sourcecomments
336
338 """Add a location to sourcecomments in the PO unit
339
340 @param location: Text location e.g. 'file.c:23' does not include #:
341 @type location: String
342 """
343 self.sourcecomments.extend(location.split())
344
355
356 - def getcontext(self):
357 """Get the message context."""
358 return self._msgctxt + self.msgidcomment
359
374
407 buildfromunit = classmethod(buildfromunit)
408
409 -class pofile(pocommon.pofile):
410 """A .po file containing various units"""
411 UnitClass = pounit
412
414 """Deprecated: changes the encoding on the file."""
415
416
417
418 raise DeprecationWarning
419
420 self._encoding = encodingToUse(newencoding)
421 if not self.units:
422 return
423 header = self.header()
424 if not header or header.isblank():
425 return
426 charsetline = None
427 headerstr = header.target
428 for line in headerstr.split("\n"):
429 if not ":" in line:
430 continue
431 key, value = line.strip().split(":", 1)
432 if key.strip() != "Content-Type":
433 continue
434 charsetline = line
435 if charsetline is None:
436 headerstr += "Content-Type: text/plain; charset=%s" % self._encoding
437 else:
438 charset = re.search("charset=([^ ]*)", charsetline)
439 if charset is None:
440 newcharsetline = charsetline
441 if not newcharsetline.strip().endswith(";"):
442 newcharsetline += ";"
443 newcharsetline += " charset=%s" % self._encoding
444 else:
445 charset = charset.group(1)
446 newcharsetline = charsetline.replace("charset=%s" % charset, "charset=%s" % self._encoding, 1)
447 headerstr = headerstr.replace(charsetline, newcharsetline, 1)
448 header.target = headerstr
449
451 """Builds up this store from the internal cpo store.
452
453 A user must ensure that self._cpo_store already exists, and that it is
454 deleted afterwards."""
455 for unit in self._cpo_store.units:
456 self.addunit(self.UnitClass.buildfromunit(unit))
457 self._encoding = self._cpo_store._encoding
458
460 """Builds the internal cpo store from the data in self.
461
462 A user must ensure that self._cpo_store does not exist, and should
463 delete it after using it."""
464 self._cpo_store = cpo.pofile()
465 for unit in self.units:
466 if not unit.isblank():
467 self._cpo_store.addunit(cpo.pofile.UnitClass.buildfromunit(unit))
468 if not self._cpo_store.header():
469
470 self._cpo_store.makeheader(charset="utf-8", encoding="8bit")
471
472
474 """Parses the given file or file source string."""
475 try:
476 if hasattr(input, 'name'):
477 self.filename = input.name
478 elif not getattr(self, 'filename', ''):
479 self.filename = ''
480 tmp_header_added = False
481
482
483
484 self.units = []
485 self._cpo_store = cpo.pofile(input)
486 self._build_self_from_cpo()
487 del self._cpo_store
488 if tmp_header_added:
489 self.units = self.units[1:]
490 except Exception, e:
491 raise base.ParseError(e)
492
494 """Make sure each msgid is unique ; merge comments etc from duplicates into original"""
495
496
497 id_dict = {}
498 uniqueunits = []
499
500
501 markedpos = []
502 def addcomment(thepo):
503 thepo.msgidcomment = " ".join(thepo.getlocations())
504 markedpos.append(thepo)
505 for thepo in self.units:
506 id = thepo.getid()
507 if thepo.isheader() and not thepo.getlocations():
508
509 uniqueunits.append(thepo)
510 elif id in id_dict:
511 if duplicatestyle == "merge":
512 if id:
513 id_dict[id].merge(thepo)
514 else:
515 addcomment(thepo)
516 uniqueunits.append(thepo)
517 elif duplicatestyle == "msgctxt":
518 origpo = id_dict[id]
519 if origpo not in markedpos:
520 origpo._msgctxt += " ".join(origpo.getlocations())
521 markedpos.append(thepo)
522 thepo._msgctxt += " ".join(thepo.getlocations())
523 uniqueunits.append(thepo)
524 else:
525 if not id:
526 if duplicatestyle == "merge":
527 addcomment(thepo)
528 else:
529 thepo._msgctxt += u" ".join(thepo.getlocations())
530 id_dict[id] = thepo
531 uniqueunits.append(thepo)
532 self.units = uniqueunits
533
535 """Convert to a string. double check that unicode is handled somehow here"""
536 self._cpo_store = cpo.pofile(encoding=self._encoding)
537 self._build_cpo_from_self()
538 output = str(self._cpo_store)
539 del self._cpo_store
540 return output
541