24ecdcfedbdc0b7b1a5ad6911b8b65637d72162d
[sdc/sdc-distribution-client.git] /
1 # -*- coding: utf-8 -*-
2 """
3     babel.messages.checkers
4     ~~~~~~~~~~~~~~~~~~~~~~~
5
6     Various routines that help with validation of translations.
7
8     :since: version 0.9
9
10     :copyright: (c) 2013 by the Babel Team.
11     :license: BSD, see LICENSE for more details.
12 """
13
14 from babel.messages.catalog import TranslationError, PYTHON_FORMAT
15 from babel._compat import string_types, izip
16
17
18 #: list of format chars that are compatible to each other
19 _string_format_compatibilities = [
20     set(['i', 'd', 'u']),
21     set(['x', 'X']),
22     set(['f', 'F', 'g', 'G'])
23 ]
24
25
26 def num_plurals(catalog, message):
27     """Verify the number of plurals in the translation."""
28     if not message.pluralizable:
29         if not isinstance(message.string, string_types):
30             raise TranslationError("Found plural forms for non-pluralizable "
31                                    "message")
32         return
33
34     # skip further tests if no catalog is provided.
35     elif catalog is None:
36         return
37
38     msgstrs = message.string
39     if not isinstance(msgstrs, (list, tuple)):
40         msgstrs = (msgstrs,)
41     if len(msgstrs) != catalog.num_plurals:
42         raise TranslationError("Wrong number of plural forms (expected %d)" %
43                                catalog.num_plurals)
44
45
46 def python_format(catalog, message):
47     """Verify the format string placeholders in the translation."""
48     if 'python-format' not in message.flags:
49         return
50     msgids = message.id
51     if not isinstance(msgids, (list, tuple)):
52         msgids = (msgids,)
53     msgstrs = message.string
54     if not isinstance(msgstrs, (list, tuple)):
55         msgstrs = (msgstrs,)
56
57     for msgid, msgstr in izip(msgids, msgstrs):
58         if msgstr:
59             _validate_format(msgid, msgstr)
60
61
62 def _validate_format(format, alternative):
63     """Test format string `alternative` against `format`.  `format` can be the
64     msgid of a message and `alternative` one of the `msgstr`\s.  The two
65     arguments are not interchangeable as `alternative` may contain less
66     placeholders if `format` uses named placeholders.
67
68     The behavior of this function is undefined if the string does not use
69     string formattings.
70
71     If the string formatting of `alternative` is compatible to `format` the
72     function returns `None`, otherwise a `TranslationError` is raised.
73
74     Examples for compatible format strings:
75
76     >>> _validate_format('Hello %s!', 'Hallo %s!')
77     >>> _validate_format('Hello %i!', 'Hallo %d!')
78
79     Example for an incompatible format strings:
80
81     >>> _validate_format('Hello %(name)s!', 'Hallo %s!')
82     Traceback (most recent call last):
83       ...
84     TranslationError: the format strings are of different kinds
85
86     This function is used by the `python_format` checker.
87
88     :param format: The original format string
89     :param alternative: The alternative format string that should be checked
90                         against format
91     :raises TranslationError: on formatting errors
92     """
93
94     def _parse(string):
95         result = []
96         for match in PYTHON_FORMAT.finditer(string):
97             name, format, typechar = match.groups()
98             if typechar == '%' and name is None:
99                 continue
100             result.append((name, str(typechar)))
101         return result
102
103     def _compatible(a, b):
104         if a == b:
105             return True
106         for set in _string_format_compatibilities:
107             if a in set and b in set:
108                 return True
109         return False
110
111     def _check_positional(results):
112         positional = None
113         for name, char in results:
114             if positional is None:
115                 positional = name is None
116             else:
117                 if (name is None) != positional:
118                     raise TranslationError('format string mixes positional '
119                                            'and named placeholders')
120         return bool(positional)
121
122     a, b = map(_parse, (format, alternative))
123
124     # now check if both strings are positional or named
125     a_positional, b_positional = map(_check_positional, (a, b))
126     if a_positional and not b_positional and not b:
127         raise TranslationError('placeholders are incompatible')
128     elif a_positional != b_positional:
129         raise TranslationError('the format strings are of different kinds')
130
131     # if we are operating on positional strings both must have the
132     # same number of format chars and those must be compatible
133     if a_positional:
134         if len(a) != len(b):
135             raise TranslationError('positional format placeholders are '
136                                    'unbalanced')
137         for idx, ((_, first), (_, second)) in enumerate(izip(a, b)):
138             if not _compatible(first, second):
139                 raise TranslationError('incompatible format for placeholder '
140                                        '%d: %r and %r are not compatible' %
141                                        (idx + 1, first, second))
142
143     # otherwise the second string must not have names the first one
144     # doesn't have and the types of those included must be compatible
145     else:
146         type_map = dict(a)
147         for name, typechar in b:
148             if name not in type_map:
149                 raise TranslationError('unknown named placeholder %r' % name)
150             elif not _compatible(typechar, type_map[name]):
151                 raise TranslationError('incompatible format for '
152                                        'placeholder %r: '
153                                        '%r and %r are not compatible' %
154                                        (name, typechar, type_map[name]))
155
156
157 def _find_checkers():
158     checkers = []
159     try:
160         from pkg_resources import working_set
161     except ImportError:
162         pass
163     else:
164         for entry_point in working_set.iter_entry_points('babel.checkers'):
165             checkers.append(entry_point.load())
166     if len(checkers) == 0:
167         # if pkg_resources is not available or no usable egg-info was found
168         # (see #230), just resort to hard-coded checkers
169         return [num_plurals, python_format]
170     return checkers
171
172
173 checkers = _find_checkers()