Mercurial > babel > old > mirror
annotate 0.9.x/babel/messages/catalog.py @ 509:cd2dec0823c9 stable
Python 2.3 compatibility: backporting r456 and r457 to 0.9 branch (see #233)
author | fschwarz |
---|---|
date | Fri, 04 Mar 2011 13:14:03 +0000 |
parents | 4adedf7d0f04 |
children |
rev | line source |
---|---|
263 | 1 # -*- coding: utf-8 -*- |
2 # | |
3 # Copyright (C) 2007 Edgewall Software | |
4 # All rights reserved. | |
5 # | |
6 # This software is licensed as described in the file COPYING, which | |
7 # you should have received as part of this distribution. The terms | |
8 # are also available at http://babel.edgewall.org/wiki/License. | |
9 # | |
10 # This software consists of voluntary contributions made by many | |
11 # individuals. For the exact contribution history, see the revision | |
12 # history and logs, available at http://babel.edgewall.org/log/. | |
13 | |
14 """Data structures for message catalogs.""" | |
15 | |
16 from cgi import parse_header | |
17 from datetime import datetime | |
18 from difflib import get_close_matches | |
19 from email import message_from_string | |
371 | 20 from copy import copy |
263 | 21 import re |
22 import time | |
23 | |
24 from babel import __version__ as VERSION | |
25 from babel.core import Locale | |
26 from babel.dates import format_datetime | |
381 | 27 from babel.messages.plurals import get_plural |
509
cd2dec0823c9
Python 2.3 compatibility: backporting r456 and r457 to 0.9 branch (see #233)
fschwarz
parents:
481
diff
changeset
|
28 from babel.util import odict, distinct, set, LOCALTZ, UTC, FixedOffsetTimezone |
263 | 29 |
30 __all__ = ['Message', 'Catalog', 'TranslationError'] | |
31 __docformat__ = 'restructuredtext en' | |
32 | |
371 | 33 |
34 PYTHON_FORMAT = re.compile(r'''(?x) | |
35 \% | |
36 (?:\(([\w]*)\))? | |
37 ( | |
38 [-#0\ +]?(?:\*|[\d]+)? | |
39 (?:\.(?:\*|[\d]+))? | |
40 [hlL]? | |
41 ) | |
42 ([diouxXeEfFgGcrs%]) | |
43 ''') | |
263 | 44 |
45 | |
46 class Message(object): | |
47 """Representation of a single message in a catalog.""" | |
48 | |
49 def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(), | |
50 user_comments=(), previous_id=(), lineno=None): | |
51 """Create the message object. | |
52 | |
53 :param id: the message ID, or a ``(singular, plural)`` tuple for | |
54 pluralizable messages | |
55 :param string: the translated message string, or a | |
56 ``(singular, plural)`` tuple for pluralizable messages | |
57 :param locations: a sequence of ``(filenname, lineno)`` tuples | |
58 :param flags: a set or sequence of flags | |
59 :param auto_comments: a sequence of automatic comments for the message | |
60 :param user_comments: a sequence of user comments for the message | |
61 :param previous_id: the previous message ID, or a ``(singular, plural)`` | |
62 tuple for pluralizable messages | |
63 :param lineno: the line number on which the msgid line was found in the | |
64 PO file, if any | |
65 """ | |
66 self.id = id #: The message ID | |
67 if not string and self.pluralizable: | |
68 string = (u'', u'') | |
69 self.string = string #: The message translation | |
70 self.locations = list(distinct(locations)) | |
71 self.flags = set(flags) | |
72 if id and self.python_format: | |
73 self.flags.add('python-format') | |
74 else: | |
75 self.flags.discard('python-format') | |
76 self.auto_comments = list(distinct(auto_comments)) | |
77 self.user_comments = list(distinct(user_comments)) | |
78 if isinstance(previous_id, basestring): | |
79 self.previous_id = [previous_id] | |
80 else: | |
81 self.previous_id = list(previous_id) | |
82 self.lineno = lineno | |
83 | |
84 def __repr__(self): | |
85 return '<%s %r (flags: %r)>' % (type(self).__name__, self.id, | |
86 list(self.flags)) | |
87 | |
88 def __cmp__(self, obj): | |
89 """Compare Messages, taking into account plural ids""" | |
90 if isinstance(obj, Message): | |
91 plural = self.pluralizable | |
92 obj_plural = obj.pluralizable | |
93 if plural and obj_plural: | |
94 return cmp(self.id[0], obj.id[0]) | |
95 elif plural: | |
96 return cmp(self.id[0], obj.id) | |
97 elif obj_plural: | |
98 return cmp(self.id, obj.id[0]) | |
99 return cmp(self.id, obj.id) | |
100 | |
316 | 101 def clone(self): |
371 | 102 return Message(*map(copy, (self.id, self.string, self.locations, |
103 self.flags, self.auto_comments, | |
104 self.user_comments, self.previous_id, | |
105 self.lineno))) | |
106 | |
107 def check(self, catalog=None): | |
108 """Run various validation checks on the message. Some validations | |
109 are only performed if the catalog is provided. This method returns | |
110 a sequence of `TranslationError` objects. | |
111 | |
112 :rtype: ``iterator`` | |
113 :param catalog: A catalog instance that is passed to the checkers | |
114 :see: `Catalog.check` for a way to perform checks for all messages | |
115 in a catalog. | |
116 """ | |
117 from babel.messages.checkers import checkers | |
118 errors = [] | |
119 for checker in checkers: | |
120 try: | |
121 checker(catalog, self) | |
122 except TranslationError, e: | |
123 errors.append(e) | |
124 return errors | |
316 | 125 |
263 | 126 def fuzzy(self): |
127 return 'fuzzy' in self.flags | |
128 fuzzy = property(fuzzy, doc="""\ | |
129 Whether the translation is fuzzy. | |
130 | |
131 >>> Message('foo').fuzzy | |
132 False | |
133 >>> msg = Message('foo', 'foo', flags=['fuzzy']) | |
134 >>> msg.fuzzy | |
135 True | |
136 >>> msg | |
137 <Message 'foo' (flags: ['fuzzy'])> | |
138 | |
139 :type: `bool` | |
140 """) | |
141 | |
142 def pluralizable(self): | |
143 return isinstance(self.id, (list, tuple)) | |
144 pluralizable = property(pluralizable, doc="""\ | |
145 Whether the message is plurizable. | |
146 | |
147 >>> Message('foo').pluralizable | |
148 False | |
149 >>> Message(('foo', 'bar')).pluralizable | |
150 True | |
151 | |
152 :type: `bool` | |
153 """) | |
154 | |
155 def python_format(self): | |
156 ids = self.id | |
157 if not isinstance(ids, (list, tuple)): | |
158 ids = [ids] | |
159 return bool(filter(None, [PYTHON_FORMAT.search(id) for id in ids])) | |
160 python_format = property(python_format, doc="""\ | |
161 Whether the message contains Python-style parameters. | |
162 | |
163 >>> Message('foo %(name)s bar').python_format | |
164 True | |
165 >>> Message(('foo %(name)s', 'foo %(name)s')).python_format | |
166 True | |
167 | |
168 :type: `bool` | |
169 """) | |
170 | |
171 | |
172 class TranslationError(Exception): | |
173 """Exception thrown by translation checkers when invalid message | |
174 translations are encountered.""" | |
175 | |
176 | |
177 DEFAULT_HEADER = u"""\ | |
178 # Translations template for PROJECT. | |
179 # Copyright (C) YEAR ORGANIZATION | |
180 # This file is distributed under the same license as the PROJECT project. | |
181 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | |
182 #""" | |
183 | |
184 | |
185 class Catalog(object): | |
186 """Representation of a message catalog.""" | |
187 | |
188 def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, | |
189 project=None, version=None, copyright_holder=None, | |
190 msgid_bugs_address=None, creation_date=None, | |
191 revision_date=None, last_translator=None, language_team=None, | |
192 charset='utf-8', fuzzy=True): | |
193 """Initialize the catalog object. | |
194 | |
195 :param locale: the locale identifier or `Locale` object, or `None` | |
196 if the catalog is not bound to a locale (which basically | |
197 means it's a template) | |
198 :param domain: the message domain | |
199 :param header_comment: the header comment as string, or `None` for the | |
200 default header | |
201 :param project: the project's name | |
202 :param version: the project's version | |
203 :param copyright_holder: the copyright holder of the catalog | |
204 :param msgid_bugs_address: the email address or URL to submit bug | |
205 reports to | |
206 :param creation_date: the date the catalog was created | |
207 :param revision_date: the date the catalog was revised | |
208 :param last_translator: the name and email of the last translator | |
209 :param language_team: the name and email of the language team | |
210 :param charset: the encoding to use in the output | |
211 :param fuzzy: the fuzzy bit on the catalog header | |
212 """ | |
213 self.domain = domain #: The message domain | |
214 if locale: | |
215 locale = Locale.parse(locale) | |
216 self.locale = locale #: The locale or `None` | |
217 self._header_comment = header_comment | |
218 self._messages = odict() | |
219 | |
220 self.project = project or 'PROJECT' #: The project name | |
221 self.version = version or 'VERSION' #: The project version | |
222 self.copyright_holder = copyright_holder or 'ORGANIZATION' | |
223 self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS' | |
224 | |
225 self.last_translator = last_translator or 'FULL NAME <EMAIL@ADDRESS>' | |
226 """Name and email address of the last translator.""" | |
227 self.language_team = language_team or 'LANGUAGE <LL@li.org>' | |
228 """Name and email address of the language team.""" | |
229 | |
230 self.charset = charset or 'utf-8' | |
231 | |
232 if creation_date is None: | |
233 creation_date = datetime.now(LOCALTZ) | |
234 elif isinstance(creation_date, datetime) and not creation_date.tzinfo: | |
235 creation_date = creation_date.replace(tzinfo=LOCALTZ) | |
236 self.creation_date = creation_date #: Creation date of the template | |
237 if revision_date is None: | |
238 revision_date = datetime.now(LOCALTZ) | |
239 elif isinstance(revision_date, datetime) and not revision_date.tzinfo: | |
240 revision_date = revision_date.replace(tzinfo=LOCALTZ) | |
241 self.revision_date = revision_date #: Last revision date of the catalog | |
242 self.fuzzy = fuzzy #: Catalog header fuzzy bit (`True` or `False`) | |
243 | |
244 self.obsolete = odict() #: Dictionary of obsolete messages | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
245 self._num_plurals = None |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
246 self._plural_expr = None |
263 | 247 |
248 def _get_header_comment(self): | |
249 comment = self._header_comment | |
250 comment = comment.replace('PROJECT', self.project) \ | |
251 .replace('VERSION', self.version) \ | |
252 .replace('YEAR', self.revision_date.strftime('%Y')) \ | |
253 .replace('ORGANIZATION', self.copyright_holder) | |
254 if self.locale: | |
255 comment = comment.replace('Translations template', '%s translations' | |
256 % self.locale.english_name) | |
257 return comment | |
258 | |
259 def _set_header_comment(self, string): | |
260 self._header_comment = string | |
261 | |
262 header_comment = property(_get_header_comment, _set_header_comment, doc="""\ | |
263 The header comment for the catalog. | |
264 | |
265 >>> catalog = Catalog(project='Foobar', version='1.0', | |
266 ... copyright_holder='Foo Company') | |
316 | 267 >>> print catalog.header_comment #doctest: +ELLIPSIS |
263 | 268 # Translations template for Foobar. |
316 | 269 # Copyright (C) ... Foo Company |
263 | 270 # This file is distributed under the same license as the Foobar project. |
316 | 271 # FIRST AUTHOR <EMAIL@ADDRESS>, .... |
263 | 272 # |
273 | |
274 The header can also be set from a string. Any known upper-case variables | |
275 will be replaced when the header is retrieved again: | |
276 | |
277 >>> catalog = Catalog(project='Foobar', version='1.0', | |
278 ... copyright_holder='Foo Company') | |
279 >>> catalog.header_comment = '''\\ | |
280 ... # The POT for my really cool PROJECT project. | |
281 ... # Copyright (C) 1990-2003 ORGANIZATION | |
282 ... # This file is distributed under the same license as the PROJECT | |
283 ... # project. | |
284 ... #''' | |
285 >>> print catalog.header_comment | |
286 # The POT for my really cool Foobar project. | |
287 # Copyright (C) 1990-2003 Foo Company | |
288 # This file is distributed under the same license as the Foobar | |
289 # project. | |
290 # | |
291 | |
292 :type: `unicode` | |
293 """) | |
294 | |
295 def _get_mime_headers(self): | |
296 headers = [] | |
297 headers.append(('Project-Id-Version', | |
298 '%s %s' % (self.project, self.version))) | |
299 headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address)) | |
300 headers.append(('POT-Creation-Date', | |
301 format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', | |
302 locale='en'))) | |
303 if self.locale is None: | |
304 headers.append(('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE')) | |
305 headers.append(('Last-Translator', 'FULL NAME <EMAIL@ADDRESS>')) | |
306 headers.append(('Language-Team', 'LANGUAGE <LL@li.org>')) | |
307 else: | |
308 headers.append(('PO-Revision-Date', | |
309 format_datetime(self.revision_date, | |
310 'yyyy-MM-dd HH:mmZ', locale='en'))) | |
311 headers.append(('Last-Translator', self.last_translator)) | |
312 headers.append(('Language-Team', | |
313 self.language_team.replace('LANGUAGE', | |
314 str(self.locale)))) | |
315 headers.append(('Plural-Forms', self.plural_forms)) | |
316 headers.append(('MIME-Version', '1.0')) | |
317 headers.append(('Content-Type', | |
318 'text/plain; charset=%s' % self.charset)) | |
319 headers.append(('Content-Transfer-Encoding', '8bit')) | |
320 headers.append(('Generated-By', 'Babel %s\n' % VERSION)) | |
321 return headers | |
322 | |
323 def _set_mime_headers(self, headers): | |
324 for name, value in headers: | |
295 | 325 if name.lower() == 'content-type': |
263 | 326 mimetype, params = parse_header(value) |
327 if 'charset' in params: | |
328 self.charset = params['charset'].lower() | |
329 break | |
330 for name, value in headers: | |
331 name = name.lower().decode(self.charset) | |
332 value = value.decode(self.charset) | |
333 if name == 'project-id-version': | |
334 parts = value.split(' ') | |
335 self.project = u' '.join(parts[:-1]) | |
336 self.version = parts[-1] | |
337 elif name == 'report-msgid-bugs-to': | |
338 self.msgid_bugs_address = value | |
339 elif name == 'last-translator': | |
340 self.last_translator = value | |
341 elif name == 'language-team': | |
342 self.language_team = value | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
343 elif name == 'plural-forms': |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
344 _, params = parse_header(' ;' + value) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
345 self._num_plurals = int(params.get('nplurals', 2)) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
346 self._plural_expr = params.get('plural', '(n != 1)') |
263 | 347 elif name == 'pot-creation-date': |
348 # FIXME: this should use dates.parse_datetime as soon as that | |
349 # is ready | |
481 | 350 value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1) |
351 | |
263 | 352 tt = time.strptime(value, '%Y-%m-%d %H:%M') |
353 ts = time.mktime(tt) | |
481 | 354 |
355 # Separate the offset into a sign component, hours, and minutes | |
356 plus_minus_s, rest = tzoffset[0], tzoffset[1:] | |
357 hours_offset_s, mins_offset_s = rest[:2], rest[2:] | |
358 | |
359 # Make them all integers | |
360 plus_minus = int(plus_minus_s + '1') | |
361 hours_offset = int(hours_offset_s) | |
362 mins_offset = int(mins_offset_s) | |
363 | |
364 # Calculate net offset | |
365 net_mins_offset = hours_offset * 60 | |
366 net_mins_offset += mins_offset | |
367 net_mins_offset *= plus_minus | |
368 | |
369 # Create an offset object | |
370 tzoffset = FixedOffsetTimezone(net_mins_offset) | |
371 | |
372 # Store the offset in a datetime object | |
263 | 373 dt = datetime.fromtimestamp(ts) |
374 self.creation_date = dt.replace(tzinfo=tzoffset) | |
477 | 375 elif name == 'po-revision-date': |
376 # Keep the value if it's not the default one | |
377 if 'YEAR' not in value: | |
378 # FIXME: this should use dates.parse_datetime as soon as | |
379 # that is ready | |
481 | 380 value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1) |
477 | 381 tt = time.strptime(value, '%Y-%m-%d %H:%M') |
382 ts = time.mktime(tt) | |
481 | 383 |
384 # Separate the offset into a sign component, hours, and | |
385 # minutes | |
386 plus_minus_s, rest = tzoffset[0], tzoffset[1:] | |
387 hours_offset_s, mins_offset_s = rest[:2], rest[2:] | |
388 | |
389 # Make them all integers | |
390 plus_minus = int(plus_minus_s + '1') | |
391 hours_offset = int(hours_offset_s) | |
392 mins_offset = int(mins_offset_s) | |
393 | |
394 # Calculate net offset | |
395 net_mins_offset = hours_offset * 60 | |
396 net_mins_offset += mins_offset | |
397 net_mins_offset *= plus_minus | |
398 | |
399 # Create an offset object | |
400 tzoffset = FixedOffsetTimezone(net_mins_offset) | |
401 | |
402 # Store the offset in a datetime object | |
477 | 403 dt = datetime.fromtimestamp(ts) |
404 self.revision_date = dt.replace(tzinfo=tzoffset) | |
263 | 405 |
406 mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ | |
407 The MIME headers of the catalog, used for the special ``msgid ""`` entry. | |
408 | |
409 The behavior of this property changes slightly depending on whether a locale | |
410 is set or not, the latter indicating that the catalog is actually a template | |
411 for actual translations. | |
412 | |
413 Here's an example of the output for such a catalog template: | |
414 | |
415 >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC) | |
416 >>> catalog = Catalog(project='Foobar', version='1.0', | |
417 ... creation_date=created) | |
418 >>> for name, value in catalog.mime_headers: | |
419 ... print '%s: %s' % (name, value) | |
420 Project-Id-Version: Foobar 1.0 | |
421 Report-Msgid-Bugs-To: EMAIL@ADDRESS | |
422 POT-Creation-Date: 1990-04-01 15:30+0000 | |
423 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE | |
424 Last-Translator: FULL NAME <EMAIL@ADDRESS> | |
425 Language-Team: LANGUAGE <LL@li.org> | |
426 MIME-Version: 1.0 | |
427 Content-Type: text/plain; charset=utf-8 | |
428 Content-Transfer-Encoding: 8bit | |
429 Generated-By: Babel ... | |
430 | |
431 And here's an example of the output when the locale is set: | |
432 | |
433 >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC) | |
434 >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', | |
435 ... creation_date=created, revision_date=revised, | |
436 ... last_translator='John Doe <jd@example.com>', | |
437 ... language_team='de_DE <de@example.com>') | |
438 >>> for name, value in catalog.mime_headers: | |
439 ... print '%s: %s' % (name, value) | |
440 Project-Id-Version: Foobar 1.0 | |
441 Report-Msgid-Bugs-To: EMAIL@ADDRESS | |
442 POT-Creation-Date: 1990-04-01 15:30+0000 | |
443 PO-Revision-Date: 1990-08-03 12:00+0000 | |
444 Last-Translator: John Doe <jd@example.com> | |
445 Language-Team: de_DE <de@example.com> | |
446 Plural-Forms: nplurals=2; plural=(n != 1) | |
447 MIME-Version: 1.0 | |
448 Content-Type: text/plain; charset=utf-8 | |
449 Content-Transfer-Encoding: 8bit | |
450 Generated-By: Babel ... | |
451 | |
452 :type: `list` | |
453 """) | |
454 | |
455 def num_plurals(self): | |
381 | 456 if self._num_plurals is None: |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
457 num = 2 |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
458 if self.locale: |
381 | 459 num = get_plural(self.locale)[0] |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
460 self._num_plurals = num |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
461 return self._num_plurals |
263 | 462 num_plurals = property(num_plurals, doc="""\ |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
463 The number of plurals used by the catalog or locale. |
263 | 464 |
465 >>> Catalog(locale='en').num_plurals | |
466 2 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
467 >>> Catalog(locale='ga').num_plurals |
263 | 468 3 |
469 | |
470 :type: `int` | |
471 """) | |
472 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
473 def plural_expr(self): |
381 | 474 if self._plural_expr is None: |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
475 expr = '(n != 1)' |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
476 if self.locale: |
381 | 477 expr = get_plural(self.locale)[1] |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
478 self._plural_expr = expr |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
479 return self._plural_expr |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
480 plural_expr = property(plural_expr, doc="""\ |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
481 The plural expression used by the catalog or locale. |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
482 |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
483 >>> Catalog(locale='en').plural_expr |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
484 '(n != 1)' |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
485 >>> Catalog(locale='ga').plural_expr |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
486 '(n==1 ? 0 : n==2 ? 1 : 2)' |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
487 |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
488 :type: `basestring` |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
489 """) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
490 |
263 | 491 def plural_forms(self): |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
492 return 'nplurals=%s; plural=%s' % (self.num_plurals, self.plural_expr) |
263 | 493 plural_forms = property(plural_forms, doc="""\ |
494 Return the plural forms declaration for the locale. | |
495 | |
496 >>> Catalog(locale='en').plural_forms | |
497 'nplurals=2; plural=(n != 1)' | |
498 >>> Catalog(locale='pt_BR').plural_forms | |
499 'nplurals=2; plural=(n > 1)' | |
500 | |
501 :type: `str` | |
502 """) | |
503 | |
504 def __contains__(self, id): | |
505 """Return whether the catalog has a message with the specified ID.""" | |
506 return self._key_for(id) in self._messages | |
507 | |
508 def __len__(self): | |
509 """The number of messages in the catalog. | |
510 | |
511 This does not include the special ``msgid ""`` entry. | |
512 """ | |
513 return len(self._messages) | |
514 | |
515 def __iter__(self): | |
516 """Iterates through all the entries in the catalog, in the order they | |
517 were added, yielding a `Message` object for every entry. | |
518 | |
519 :rtype: ``iterator`` | |
520 """ | |
521 buf = [] | |
522 for name, value in self.mime_headers: | |
523 buf.append('%s: %s' % (name, value)) | |
524 flags = set() | |
525 if self.fuzzy: | |
526 flags |= set(['fuzzy']) | |
527 yield Message(u'', '\n'.join(buf), flags=flags) | |
528 for key in self._messages: | |
529 yield self._messages[key] | |
530 | |
531 def __repr__(self): | |
532 locale = '' | |
533 if self.locale: | |
534 locale = ' %s' % self.locale | |
535 return '<%s %r%s>' % (type(self).__name__, self.domain, locale) | |
536 | |
537 def __delitem__(self, id): | |
538 """Delete the message with the specified ID.""" | |
539 key = self._key_for(id) | |
540 if key in self._messages: | |
541 del self._messages[key] | |
542 | |
543 def __getitem__(self, id): | |
544 """Return the message with the specified ID. | |
545 | |
546 :param id: the message ID | |
547 :return: the message with the specified ID, or `None` if no such message | |
548 is in the catalog | |
549 :rtype: `Message` | |
550 """ | |
551 return self._messages.get(self._key_for(id)) | |
552 | |
553 def __setitem__(self, id, message): | |
554 """Add or update the message with the specified ID. | |
555 | |
556 >>> catalog = Catalog() | |
557 >>> catalog[u'foo'] = Message(u'foo') | |
558 >>> catalog[u'foo'] | |
559 <Message u'foo' (flags: [])> | |
560 | |
561 If a message with that ID is already in the catalog, it is updated | |
562 to include the locations and flags of the new message. | |
563 | |
564 >>> catalog = Catalog() | |
565 >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)]) | |
566 >>> catalog[u'foo'].locations | |
567 [('main.py', 1)] | |
568 >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)]) | |
569 >>> catalog[u'foo'].locations | |
570 [('main.py', 1), ('utils.py', 5)] | |
571 | |
572 :param id: the message ID | |
573 :param message: the `Message` object | |
574 """ | |
575 assert isinstance(message, Message), 'expected a Message object' | |
576 key = self._key_for(id) | |
577 current = self._messages.get(key) | |
578 if current: | |
579 if message.pluralizable and not current.pluralizable: | |
580 # The new message adds pluralization | |
581 current.id = message.id | |
582 current.string = message.string | |
583 current.locations = list(distinct(current.locations + | |
584 message.locations)) | |
585 current.auto_comments = list(distinct(current.auto_comments + | |
586 message.auto_comments)) | |
587 current.user_comments = list(distinct(current.user_comments + | |
588 message.user_comments)) | |
589 current.flags |= message.flags | |
590 message = current | |
591 elif id == '': | |
592 # special treatment for the header message | |
593 headers = message_from_string(message.string.encode(self.charset)) | |
594 self.mime_headers = headers.items() | |
595 self.header_comment = '\n'.join(['# %s' % comment for comment | |
596 in message.user_comments]) | |
597 self.fuzzy = message.fuzzy | |
598 else: | |
599 if isinstance(id, (list, tuple)): | |
280 | 600 assert isinstance(message.string, (list, tuple)), \ |
601 'Expected sequence but got %s' % type(message.string) | |
263 | 602 self._messages[key] = message |
603 | |
604 def add(self, id, string=None, locations=(), flags=(), auto_comments=(), | |
605 user_comments=(), previous_id=(), lineno=None): | |
606 """Add or update the message with the specified ID. | |
607 | |
608 >>> catalog = Catalog() | |
609 >>> catalog.add(u'foo') | |
610 >>> catalog[u'foo'] | |
611 <Message u'foo' (flags: [])> | |
612 | |
613 This method simply constructs a `Message` object with the given | |
614 arguments and invokes `__setitem__` with that object. | |
615 | |
616 :param id: the message ID, or a ``(singular, plural)`` tuple for | |
617 pluralizable messages | |
618 :param string: the translated message string, or a | |
619 ``(singular, plural)`` tuple for pluralizable messages | |
620 :param locations: a sequence of ``(filenname, lineno)`` tuples | |
621 :param flags: a set or sequence of flags | |
622 :param auto_comments: a sequence of automatic comments | |
623 :param user_comments: a sequence of user comments | |
624 :param previous_id: the previous message ID, or a ``(singular, plural)`` | |
625 tuple for pluralizable messages | |
626 :param lineno: the line number on which the msgid line was found in the | |
627 PO file, if any | |
628 """ | |
629 self[id] = Message(id, string, list(locations), flags, auto_comments, | |
630 user_comments, previous_id, lineno=lineno) | |
631 | |
632 def check(self): | |
633 """Run various validation checks on the translations in the catalog. | |
634 | |
635 For every message which fails validation, this method yield a | |
636 ``(message, errors)`` tuple, where ``message`` is the `Message` object | |
637 and ``errors`` is a sequence of `TranslationError` objects. | |
638 | |
639 :rtype: ``iterator`` | |
640 """ | |
371 | 641 for message in self._messages.values(): |
642 errors = message.check(catalog=self) | |
643 if errors: | |
644 yield message, errors | |
263 | 645 |
646 def update(self, template, no_fuzzy_matching=False): | |
647 """Update the catalog based on the given template catalog. | |
648 | |
649 >>> from babel.messages import Catalog | |
650 >>> template = Catalog() | |
651 >>> template.add('green', locations=[('main.py', 99)]) | |
652 >>> template.add('blue', locations=[('main.py', 100)]) | |
653 >>> template.add(('salad', 'salads'), locations=[('util.py', 42)]) | |
654 >>> catalog = Catalog(locale='de_DE') | |
655 >>> catalog.add('blue', u'blau', locations=[('main.py', 98)]) | |
656 >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)]) | |
657 >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'), | |
658 ... locations=[('util.py', 38)]) | |
659 | |
660 >>> catalog.update(template) | |
661 >>> len(catalog) | |
662 3 | |
663 | |
664 >>> msg1 = catalog['green'] | |
665 >>> msg1.string | |
666 >>> msg1.locations | |
667 [('main.py', 99)] | |
668 | |
669 >>> msg2 = catalog['blue'] | |
670 >>> msg2.string | |
671 u'blau' | |
672 >>> msg2.locations | |
673 [('main.py', 100)] | |
674 | |
675 >>> msg3 = catalog['salad'] | |
676 >>> msg3.string | |
677 (u'Salat', u'Salate') | |
678 >>> msg3.locations | |
679 [('util.py', 42)] | |
680 | |
681 Messages that are in the catalog but not in the template are removed | |
682 from the main collection, but can still be accessed via the `obsolete` | |
683 member: | |
684 | |
685 >>> 'head' in catalog | |
686 False | |
687 >>> catalog.obsolete.values() | |
688 [<Message 'head' (flags: [])>] | |
689 | |
690 :param template: the reference catalog, usually read from a POT file | |
691 :param no_fuzzy_matching: whether to use fuzzy matching of message IDs | |
692 """ | |
693 messages = self._messages | |
316 | 694 remaining = messages.copy() |
263 | 695 self._messages = odict() |
696 | |
316 | 697 # Prepare for fuzzy matching |
698 fuzzy_candidates = [] | |
699 if not no_fuzzy_matching: | |
700 fuzzy_candidates = [ | |
701 self._key_for(msgid) for msgid in messages | |
702 if msgid and messages[msgid].string | |
703 ] | |
704 fuzzy_matches = set() | |
705 | |
280 | 706 def _merge(message, oldkey, newkey): |
316 | 707 message = message.clone() |
280 | 708 fuzzy = False |
709 if oldkey != newkey: | |
710 fuzzy = True | |
316 | 711 fuzzy_matches.add(oldkey) |
712 oldmsg = messages.get(oldkey) | |
280 | 713 if isinstance(oldmsg.id, basestring): |
714 message.previous_id = [oldmsg.id] | |
715 else: | |
716 message.previous_id = list(oldmsg.id) | |
316 | 717 else: |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
316
diff
changeset
|
718 oldmsg = remaining.pop(oldkey, None) |
280 | 719 message.string = oldmsg.string |
720 if isinstance(message.id, (list, tuple)): | |
721 if not isinstance(message.string, (list, tuple)): | |
722 fuzzy = True | |
723 message.string = tuple( | |
724 [message.string] + ([u''] * (len(message.id) - 1)) | |
725 ) | |
454 | 726 elif len(message.string) != self.num_plurals: |
280 | 727 fuzzy = True |
728 message.string = tuple(message.string[:len(oldmsg.string)]) | |
729 elif isinstance(message.string, (list, tuple)): | |
730 fuzzy = True | |
731 message.string = message.string[0] | |
732 message.flags |= oldmsg.flags | |
733 if fuzzy: | |
734 message.flags |= set([u'fuzzy']) | |
735 self[message.id] = message | |
736 | |
263 | 737 for message in template: |
738 if message.id: | |
739 key = self._key_for(message.id) | |
740 if key in messages: | |
280 | 741 _merge(message, key, key) |
263 | 742 else: |
743 if no_fuzzy_matching is False: | |
744 # do some fuzzy matching with difflib | |
745 matches = get_close_matches(key.lower().strip(), | |
316 | 746 fuzzy_candidates, 1) |
263 | 747 if matches: |
280 | 748 _merge(message, matches[0], key) |
263 | 749 continue |
750 | |
751 self[message.id] = message | |
752 | |
316 | 753 self.obsolete = odict() |
754 for msgid in remaining: | |
755 if no_fuzzy_matching or msgid not in fuzzy_matches: | |
756 self.obsolete[msgid] = remaining[msgid] | |
474 | 757 # Make updated catalog's POT-Creation-Date equal to the template |
758 # used to update the catalog | |
759 self.creation_date = template.creation_date | |
263 | 760 |
761 def _key_for(self, id): | |
762 """The key for a message is just the singular ID even for pluralizable | |
763 messages. | |
764 """ | |
765 key = id | |
766 if isinstance(key, (list, tuple)): | |
767 key = id[0] | |
768 return key |