~ubuntu-branches/ubuntu/trusty/python-babel/trusty

« back to all changes in this revision

Viewing changes to babel/messages/pofile.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2013-10-28 10:11:31 UTC
  • mfrom: (4.1.2 sid)
  • Revision ID: package-import@ubuntu.com-20131028101131-zwbmm8sc29iemmlr
Tags: 1.3-2ubuntu1
* Merge from Debian unstable.  Remaining changes:
  - debian/rules: Run the testsuite during builds.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
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
 
"""Reading and writing of files in the ``gettext`` PO (portable object)
15
 
format.
16
 
 
17
 
:see: `The Format of PO Files
18
 
       <http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_
19
 
"""
20
 
 
21
 
from datetime import date, datetime
 
2
"""
 
3
    babel.messages.pofile
 
4
    ~~~~~~~~~~~~~~~~~~~~~
 
5
 
 
6
    Reading and writing of files in the ``gettext`` PO (portable object)
 
7
    format.
 
8
 
 
9
    :copyright: (c) 2013 by the Babel Team.
 
10
    :license: BSD, see LICENSE for more details.
 
11
"""
 
12
 
22
13
import os
23
14
import re
24
15
 
25
 
from babel import __version__ as VERSION
26
16
from babel.messages.catalog import Catalog, Message
27
 
from babel.util import set, wraptext, LOCALTZ
 
17
from babel.util import wraptext
 
18
from babel._compat import text_type
28
19
 
29
 
__all__ = ['read_po', 'write_po']
30
 
__docformat__ = 'restructuredtext en'
31
20
 
32
21
def unescape(string):
33
22
    r"""Reverse `escape` the given string.
38
27
    <BLANKLINE>
39
28
 
40
29
    :param string: the string to unescape
41
 
    :return: the unescaped string
42
 
    :rtype: `str` or `unicode`
43
30
    """
44
 
    return string[1:-1].replace('\\\\', '\\') \
45
 
                       .replace('\\t', '\t') \
46
 
                       .replace('\\r', '\r') \
47
 
                       .replace('\\n', '\n') \
48
 
                       .replace('\\"', '\"')
 
31
    def replace_escapes(match):
 
32
        m = match.group(1)
 
33
        if m == 'n':
 
34
            return '\n'
 
35
        elif m == 't':
 
36
            return '\t'
 
37
        elif m == 'r':
 
38
            return '\r'
 
39
        # m is \ or "
 
40
        return m
 
41
    return re.compile(r'\\([\\trn"])').sub(replace_escapes, string[1:-1])
 
42
 
49
43
 
50
44
def denormalize(string):
51
45
    r"""Reverse the normalization done by the `normalize` function.
67
61
    <BLANKLINE>
68
62
 
69
63
    :param string: the string to denormalize
70
 
    :return: the denormalized string
71
 
    :rtype: `unicode` or `str`
72
64
    """
73
 
    if string.startswith('""'):
74
 
        lines = []
75
 
        for line in string.splitlines()[1:]:
76
 
            lines.append(unescape(line))
 
65
    if '\n' in string:
 
66
        escaped_lines = string.splitlines()
 
67
        if string.startswith('""'):
 
68
            escaped_lines = escaped_lines[1:]
 
69
        lines = map(unescape, escaped_lines)
77
70
        return ''.join(lines)
78
71
    else:
79
72
        return unescape(string)
80
73
 
81
 
def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False):
 
74
 
 
75
def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=None):
82
76
    """Read messages from a ``gettext`` PO (portable object) file from the given
83
77
    file-like object and return a `Catalog`.
84
78
 
 
79
    >>> from datetime import datetime
85
80
    >>> from StringIO import StringIO
86
81
    >>> buf = StringIO('''
87
82
    ... #: main.py:1
88
83
    ... #, fuzzy, python-format
89
84
    ... msgid "foo %(name)s"
90
 
    ... msgstr ""
 
85
    ... msgstr "quux %(name)s"
91
86
    ...
92
87
    ... # A user comment
93
88
    ... #. An auto comment
94
89
    ... #: main.py:3
95
90
    ... msgid "bar"
96
91
    ... msgid_plural "baz"
97
 
    ... msgstr[0] ""
98
 
    ... msgstr[1] ""
 
92
    ... msgstr[0] "bar"
 
93
    ... msgstr[1] "baaz"
99
94
    ... ''')
100
95
    >>> catalog = read_po(buf)
101
96
    >>> catalog.revision_date = datetime(2007, 04, 01)
105
100
    ...         print (message.id, message.string)
106
101
    ...         print ' ', (message.locations, message.flags)
107
102
    ...         print ' ', (message.user_comments, message.auto_comments)
108
 
    (u'foo %(name)s', '')
 
103
    (u'foo %(name)s', u'quux %(name)s')
109
104
      ([(u'main.py', 1)], set([u'fuzzy', u'python-format']))
110
105
      ([], [])
111
 
    ((u'bar', u'baz'), ('', ''))
 
106
    ((u'bar', u'baz'), (u'bar', u'baaz'))
112
107
      ([(u'main.py', 3)], set([]))
113
108
      ([u'A user comment'], [u'An auto comment'])
114
109
 
 
110
    .. versionadded:: 1.0
 
111
       Added support for explicit charset argument.
 
112
 
115
113
    :param fileobj: the file-like object to read the PO file from
116
114
    :param locale: the locale identifier or `Locale` object, or `None`
117
115
                   if the catalog is not bound to a locale (which basically
118
116
                   means it's a template)
119
117
    :param domain: the message domain
120
118
    :param ignore_obsolete: whether to ignore obsolete messages in the input
121
 
    :return: an iterator over ``(message, translation, location)`` tuples
122
 
    :rtype: ``iterator``
 
119
    :param charset: the character set of the catalog.
123
120
    """
124
 
    catalog = Catalog(locale=locale, domain=domain)
 
121
    catalog = Catalog(locale=locale, domain=domain, charset=charset)
125
122
 
126
123
    counter = [0]
127
124
    offset = [0]
132
129
    user_comments = []
133
130
    auto_comments = []
134
131
    obsolete = [False]
 
132
    context = []
135
133
    in_msgid = [False]
136
134
    in_msgstr = [False]
 
135
    in_msgctxt = [False]
137
136
 
138
137
    def _add_message():
139
138
        translations.sort()
151
150
            string = tuple([denormalize(t[1]) for t in string])
152
151
        else:
153
152
            string = denormalize(translations[0][1])
 
153
        if context:
 
154
            msgctxt = denormalize('\n'.join(context))
 
155
        else:
 
156
            msgctxt = None
154
157
        message = Message(msgid, string, list(locations), set(flags),
155
 
                          auto_comments, user_comments, lineno=offset[0] + 1)
 
158
                          auto_comments, user_comments, lineno=offset[0] + 1,
 
159
                          context=msgctxt)
156
160
        if obsolete[0]:
157
161
            if not ignore_obsolete:
158
162
                catalog.obsolete[msgid] = message
159
163
        else:
160
164
            catalog[msgid] = message
161
 
        del messages[:]; del translations[:]; del locations[:];
162
 
        del flags[:]; del auto_comments[:]; del user_comments[:]
 
165
        del messages[:]; del translations[:]; del context[:]; del locations[:];
 
166
        del flags[:]; del auto_comments[:]; del user_comments[:];
163
167
        obsolete[0] = False
164
168
        counter[0] += 1
165
169
 
184
188
                translations.append([int(idx), msg.lstrip()])
185
189
            else:
186
190
                translations.append([0, msg])
 
191
        elif line.startswith('msgctxt'):
 
192
            if messages:
 
193
                _add_message()
 
194
            in_msgid[0] = in_msgstr[0] = False
 
195
            context.append(line[7:].lstrip())
187
196
        elif line.startswith('"'):
188
197
            if in_msgid[0]:
189
198
                messages[-1] += u'\n' + line.rstrip()
190
199
            elif in_msgstr[0]:
191
200
                translations[-1][1] += u'\n' + line.rstrip()
 
201
            elif in_msgctxt[0]:
 
202
                context.append(line.rstrip())
192
203
 
193
204
    for lineno, line in enumerate(fileobj.readlines()):
194
205
        line = line.strip()
195
 
        if not isinstance(line, unicode):
 
206
        if not isinstance(line, text_type):
196
207
            line = line.decode(catalog.charset)
197
208
        if line.startswith('#'):
198
209
            in_msgid[0] = in_msgstr[0] = False
236
247
 
237
248
    return catalog
238
249
 
 
250
 
239
251
WORD_SEP = re.compile('('
240
252
    r'\s+|'                                 # any whitespace
241
253
    r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words
242
254
    r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)'   # em-dash
243
255
')')
244
256
 
 
257
 
245
258
def escape(string):
246
259
    r"""Escape the given string so that it can be included in double-quoted
247
260
    strings in ``PO`` files.
252
265
    '"Say:\\n  \\"hello, world!\\"\\n"'
253
266
 
254
267
    :param string: the string to escape
255
 
    :return: the escaped string
256
 
    :rtype: `str` or `unicode`
257
268
    """
258
269
    return '"%s"' % string.replace('\\', '\\\\') \
259
270
                          .replace('\t', '\\t') \
261
272
                          .replace('\n', '\\n') \
262
273
                          .replace('\"', '\\"')
263
274
 
 
275
 
264
276
def normalize(string, prefix='', width=76):
265
277
    r"""Convert a string into a format that is appropriate for .po files.
266
278
 
284
296
    :param prefix: a string that should be prepended to every line
285
297
    :param width: the maximum line width; use `None`, 0, or a negative number
286
298
                  to completely disable line wrapping
287
 
    :return: the normalized string
288
 
    :rtype: `unicode`
289
299
    """
290
300
    if width and width > 0:
291
301
        prefixlen = len(prefix)
292
302
        lines = []
293
 
        for idx, line in enumerate(string.splitlines(True)):
 
303
        for line in string.splitlines(True):
294
304
            if len(escape(line)) + prefixlen > width:
295
305
                chunks = WORD_SEP.split(line)
296
306
                chunks.reverse()
323
333
        lines[-1] += '\n'
324
334
    return u'""\n' + u'\n'.join([(prefix + escape(l)) for l in lines])
325
335
 
 
336
 
326
337
def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
327
338
             sort_output=False, sort_by_file=False, ignore_obsolete=False,
328
339
             include_previous=False):
332
343
    >>> catalog = Catalog()
333
344
    >>> catalog.add(u'foo %(name)s', locations=[('main.py', 1)],
334
345
    ...             flags=('fuzzy',))
 
346
    <Message...>
335
347
    >>> catalog.add((u'bar', u'baz'), locations=[('main.py', 3)])
336
 
    >>> from StringIO import StringIO
337
 
    >>> buf = StringIO()
 
348
    <Message...>
 
349
    >>> from io import BytesIO
 
350
    >>> buf = BytesIO()
338
351
    >>> write_po(buf, catalog, omit_header=True)
339
352
    >>> print buf.getvalue()
340
353
    #: main.py:1
367
380
                             updating the catalog
368
381
    """
369
382
    def _normalize(key, prefix=''):
370
 
        return normalize(key, prefix=prefix, width=width) \
371
 
            .encode(catalog.charset, 'backslashreplace')
 
383
        return normalize(key, prefix=prefix, width=width)
372
384
 
373
385
    def _write(text):
374
 
        if isinstance(text, unicode):
375
 
            text = text.encode(catalog.charset)
 
386
        if isinstance(text, text_type):
 
387
            text = text.encode(catalog.charset, 'backslashreplace')
376
388
        fileobj.write(text)
377
389
 
378
390
    def _write_comment(comment, prefix=''):
387
399
 
388
400
    def _write_message(message, prefix=''):
389
401
        if isinstance(message.id, (list, tuple)):
 
402
            if message.context:
 
403
                _write('%smsgctxt %s\n' % (prefix,
 
404
                                           _normalize(message.context, prefix)))
390
405
            _write('%smsgid %s\n' % (prefix, _normalize(message.id[0], prefix)))
391
406
            _write('%smsgid_plural %s\n' % (
392
407
                prefix, _normalize(message.id[1], prefix)
401
416
                    prefix, idx, _normalize(string, prefix)
402
417
                ))
403
418
        else:
 
419
            if message.context:
 
420
                _write('%smsgctxt %s\n' % (prefix,
 
421
                                           _normalize(message.context, prefix)))
404
422
            _write('%smsgid %s\n' % (prefix, _normalize(message.id, prefix)))
405
423
            _write('%smsgstr %s\n' % (
406
424
                prefix, _normalize(message.string or '', prefix)
422
440
                for line in comment_header.splitlines():
423
441
                    lines += wraptext(line, width=width,
424
442
                                      subsequent_indent='# ')
425
 
                comment_header = u'\n'.join(lines) + u'\n'
426
 
            _write(comment_header)
 
443
                comment_header = u'\n'.join(lines)
 
444
            _write(comment_header + u'\n')
427
445
 
428
446
        for comment in message.user_comments:
429
447
            _write_comment(comment)
435
453
                              for filename, lineno in message.locations])
436
454
            _write_comment(locs, prefix=':')
437
455
        if message.flags:
438
 
            _write('#%s\n' % ', '.join([''] + list(message.flags)))
 
456
            _write('#%s\n' % ', '.join([''] + sorted(message.flags)))
439
457
 
440
458
        if message.previous_id and include_previous:
441
459
            _write_comment('msgid %s' % _normalize(message.previous_id[0]),