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 import re
35 import copy
36 import cStringIO
37
38 lsep = " "
39 """Seperator for #: entries"""
40
41 basic_header = r'''msgid ""
42 msgstr ""
43 "Content-Type: text/plain; charset=UTF-8\n"
44 "Content-Transfer-Encoding: 8bit\n"
45 '''
46
48 """Tests whether the given encoding is known in the python runtime, or returns utf-8.
49 This function is used to ensure that a valid encoding is always used."""
50 if encoding == "CHARSET" or encoding == None:
51 return 'utf-8'
52 return encoding
53
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 __shallow__ = ['_store']
70
71 - def __init__(self, source=None, encoding="UTF-8"):
79
88
91
104 source = property(getsource, setsource)
105
106
108 """Returns the unescaped msgstr"""
109 return self._target
110
112 """Sets the msgstr to the given (unescaped) value"""
113 self._rich_target = None
114
115
116 if self.hasplural():
117 if isinstance(target, multistring):
118 self._target = target
119 else:
120
121 self._target = multistring(target)
122 elif isinstance(target, (dict, list)):
123 if len(target) == 1:
124 self._target = target[0]
125 else:
126 raise ValueError("po msgid element has no plural but msgstr has %d elements (%s)" % (len(target), target))
127 else:
128 self._target = target
129 target = property(gettarget, settarget)
130
132 """Return comments based on origin value (programmer, developer, source code and translator)"""
133 if origin == None:
134 comments = u"\n".join(self.othercomments)
135 comments += u"\n".join(self.automaticcomments)
136 elif origin == "translator":
137 comments = u"\n".join (self.othercomments)
138 elif origin in ["programmer", "developer", "source code"]:
139 comments = u"\n".join(self.automaticcomments)
140 else:
141 raise ValueError("Comment type not valid")
142 return comments
143
144 - def addnote(self, text, origin=None, position="append"):
145 """This is modeled on the XLIFF method. See xliff.py::xliffunit.addnote"""
146
147 if not (text and text.strip()):
148 return
149 text = data.forceunicode(text)
150 commentlist = self.othercomments
151 if origin in ["programmer", "developer", "source code"]:
152 autocomments = True
153 commentlist = self.automaticcomments
154 if text.endswith(u'\n'):
155 text = text[:-1]
156 text = text.split(u"\n")
157 if position == "append":
158 commentlist.extend(text)
159 else:
160 newcomments = text
161 newcomments.extend(commentlist)
162 if autocomments:
163 self.automaticcomments = newcomments
164 else:
165 self.othercomments = newcomments
166
168 """Remove all the translator's notes (other comments)"""
169 self.othercomments = []
170
172
173 new_unit = self.__class__()
174
175
176 shallow = set(self.__shallow__)
177
178 for key, value in self.__dict__.iteritems():
179 if key not in shallow:
180 setattr(new_unit, key, copy.deepcopy(value))
181
182 for key in set(shallow):
183 setattr(new_unit, key, getattr(self, key))
184
185
186 memo[id(self)] = self
187
188 return new_unit
189
191 return copy.deepcopy(self)
192
194 if self.hasplural():
195 len("".join([string.strip() for string in self.source.strings]))
196 else:
197 return len(self.source.strip())
198
200 if self.hasplural():
201 len("".join([string.strip() for string in self.target.strings]))
202 else:
203 return len(self.target.strip())
204
205 - def merge(self, otherpo, overwrite=False, comments=True, authoritative=False):
206 """Merges the otherpo (with the same msgid) into this one.
207
208 Overwrite non-blank self.msgstr only if overwrite is True
209 merge comments only if comments is True
210 """
211
212 def mergelists(list1, list2, split=False):
213
214 if split:
215 splitlist1 = []
216 splitlist2 = []
217 for item in list1:
218 splitlist1.extend(item.split())
219 for item in list2:
220 splitlist2.extend(item.split())
221 list1.extend([item for item in splitlist2 if not item in splitlist1])
222 else:
223
224 if list1 != list2:
225 for item in list2:
226
227 if item not in list1 or len(item) < 5:
228 list1.append(item)
229
230 if not isinstance(otherpo, pounit):
231 super(pounit, self).merge(otherpo, overwrite, comments)
232 return
233 if comments:
234 mergelists(self.othercomments, otherpo.othercomments)
235 mergelists(self.typecomments, otherpo.typecomments)
236 if not authoritative:
237
238
239 mergelists(self.automaticcomments, otherpo.automaticcomments)
240
241 mergelists(self.sourcecomments, otherpo.sourcecomments, split=True)
242 if not self.istranslated() or overwrite:
243
244 if pocommon.extract_msgid_comment(otherpo.target):
245 otherpo.target = otherpo.target.replace('_: ' + otherpo._extract_msgidcomments()+ '\n', '')
246 self.target = otherpo.target
247 if self.source != otherpo.source or self.getcontext() != otherpo.getcontext():
248 self.markfuzzy()
249 else:
250 self.markfuzzy(otherpo.isfuzzy())
251 elif not otherpo.istranslated():
252 if self.source != otherpo.source:
253 self.markfuzzy()
254 else:
255 if self.target != otherpo.target:
256 self.markfuzzy()
257
259
260 return not self.getid() and len(self.target) > 0
261
268
273
282
292
295
298
301
304
307
309 """Makes this unit obsolete"""
310 self.obsolete = True
311 self.sourcecomments = []
312 self.automaticcomments = []
313
315 """Makes an obsolete unit normal"""
316 self.obsolete = False
317
322
326
328 """convert to a string. double check that unicode is handled somehow here"""
329 _cpo_unit = cpo.pounit.buildfromunit(self)
330 return str(_cpo_unit)
331
333 """Get a list of locations from sourcecomments in the PO unit
334
335 rtype: List
336 return: A list of the locations with '#: ' stripped
337
338 """
339
340 return self.sourcecomments
341
343 """Add a location to sourcecomments in the PO unit
344
345 @param location: Text location e.g. 'file.c:23' does not include #:
346 @type location: String
347 """
348 self.sourcecomments.extend(location.split())
349
360
361 - def getcontext(self):
362 """Get the message context."""
363 return self._msgctxt + self.msgidcomment
364
379
412 buildfromunit = classmethod(buildfromunit)
413
414 -class pofile(pocommon.pofile):
415 """A .po file containing various units"""
416 UnitClass = pounit
417
419 """Construct a pofile, optionally reading in from inputfile.
420 encoding can be specified but otherwise will be read from the PO header"""
421 self.UnitClass = unitclass
422 pocommon.pofile.__init__(self, unitclass=unitclass)
423 self.units = []
424 self.filename = ''
425 self._encoding = encodingToUse(encoding)
426 if inputfile is not None:
427 self.parse(inputfile)
428
430 """Deprecated: changes the encoding on the file."""
431
432
433
434 raise DeprecationWarning
435
436 self._encoding = encodingToUse(newencoding)
437 if not self.units:
438 return
439 header = self.header()
440 if not header or header.isblank():
441 return
442 charsetline = None
443 headerstr = header.target
444 for line in headerstr.split("\n"):
445 if not ":" in line:
446 continue
447 key, value = line.strip().split(":", 1)
448 if key.strip() != "Content-Type":
449 continue
450 charsetline = line
451 if charsetline is None:
452 headerstr += "Content-Type: text/plain; charset=%s" % self._encoding
453 else:
454 charset = re.search("charset=([^ ]*)", charsetline)
455 if charset is None:
456 newcharsetline = charsetline
457 if not newcharsetline.strip().endswith(";"):
458 newcharsetline += ";"
459 newcharsetline += " charset=%s" % self._encoding
460 else:
461 charset = charset.group(1)
462 newcharsetline = charsetline.replace("charset=%s" % charset, "charset=%s" % self._encoding, 1)
463 headerstr = headerstr.replace(charsetline, newcharsetline, 1)
464 header.target = headerstr
465
467 """Builds up this store from the internal cpo store.
468
469 A user must ensure that self._cpo_store already exists, and that it is
470 deleted afterwards."""
471 for unit in self._cpo_store.units:
472 self.addunit(self.UnitClass.buildfromunit(unit))
473 self._encoding = self._cpo_store._encoding
474
476 """Builds the internal cpo store from the data in self.
477
478 A user must ensure that self._cpo_store does not exist, and should
479 delete it after using it."""
480 self._cpo_store = cpo.pofile()
481 for unit in self.units:
482 if not unit.isblank():
483 self._cpo_store.addunit(cpo.pofile.UnitClass.buildfromunit(unit))
484 if not self._cpo_store.header():
485
486 self._cpo_store.makeheader(charset="utf-8", encoding="8bit")
487
488
490 """Parses the given file or file source string."""
491 try:
492 if hasattr(input, 'name'):
493 self.filename = input.name
494 elif not getattr(self, 'filename', ''):
495 self.filename = ''
496 tmp_header_added = False
497
498
499
500 self._cpo_store = cpo.pofile(input)
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)
552 self._build_cpo_from_self()
553 output = str(self._cpo_store)
554 del self._cpo_store
555 return output
556