checkers.py 5.9 KB

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