1 # -*- coding: utf-8 -*-
3 babel.messages.checkers
4 ~~~~~~~~~~~~~~~~~~~~~~~
6 Various routines that help with validation of translations.
10 :copyright: (c) 2013 by the Babel Team.
11 :license: BSD, see LICENSE for more details.
14 from babel.messages.catalog import TranslationError, PYTHON_FORMAT
15 from babel._compat import string_types, izip
18 #: list of format chars that are compatible to each other
19 _string_format_compatibilities = [
22 set(['f', 'F', 'g', 'G'])
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 "
34 # skip further tests if no catalog is provided.
38 msgstrs = message.string
39 if not isinstance(msgstrs, (list, tuple)):
41 if len(msgstrs) != catalog.num_plurals:
42 raise TranslationError("Wrong number of plural forms (expected %d)" %
46 def python_format(catalog, message):
47 """Verify the format string placeholders in the translation."""
48 if 'python-format' not in message.flags:
51 if not isinstance(msgids, (list, tuple)):
53 msgstrs = message.string
54 if not isinstance(msgstrs, (list, tuple)):
57 for msgid, msgstr in izip(msgids, msgstrs):
59 _validate_format(msgid, msgstr)
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.
68 The behavior of this function is undefined if the string does not use
71 If the string formatting of `alternative` is compatible to `format` the
72 function returns `None`, otherwise a `TranslationError` is raised.
74 Examples for compatible format strings:
76 >>> _validate_format('Hello %s!', 'Hallo %s!')
77 >>> _validate_format('Hello %i!', 'Hallo %d!')
79 Example for an incompatible format strings:
81 >>> _validate_format('Hello %(name)s!', 'Hallo %s!')
82 Traceback (most recent call last):
84 TranslationError: the format strings are of different kinds
86 This function is used by the `python_format` checker.
88 :param format: The original format string
89 :param alternative: The alternative format string that should be checked
91 :raises TranslationError: on formatting errors
96 for match in PYTHON_FORMAT.finditer(string):
97 name, format, typechar = match.groups()
98 if typechar == '%' and name is None:
100 result.append((name, str(typechar)))
103 def _compatible(a, b):
106 for set in _string_format_compatibilities:
107 if a in set and b in set:
111 def _check_positional(results):
113 for name, char in results:
114 if positional is None:
115 positional = name is None
117 if (name is None) != positional:
118 raise TranslationError('format string mixes positional '
119 'and named placeholders')
120 return bool(positional)
122 a, b = map(_parse, (format, alternative))
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')
131 # if we are operating on positional strings both must have the
132 # same number of format chars and those must be compatible
135 raise TranslationError('positional format placeholders are '
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))
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
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 '
153 '%r and %r are not compatible' %
154 (name, typechar, type_map[name]))
157 def _find_checkers():
160 from pkg_resources import working_set
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]
173 checkers = _find_checkers()