| """ |
| babel.messages.checkers |
| ~~~~~~~~~~~~~~~~~~~~~~~ |
| |
| Various routines that help with validation of translations. |
| |
| :since: version 0.9 |
| |
| :copyright: (c) 2013-2026 by the Babel Team. |
| :license: BSD, see LICENSE for more details. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from collections.abc import Callable |
|
|
| from babel.messages.catalog import PYTHON_FORMAT, Catalog, Message, TranslationError |
|
|
| |
| _string_format_compatibilities = [ |
| {'i', 'd', 'u'}, |
| {'x', 'X'}, |
| {'f', 'F', 'g', 'G'}, |
| ] |
|
|
|
|
| def num_plurals(catalog: Catalog | None, message: Message) -> None: |
| """Verify the number of plurals in the translation.""" |
| if not message.pluralizable: |
| if not isinstance(message.string, str): |
| raise TranslationError("Found plural forms for non-pluralizable message") |
| return |
|
|
| |
| elif catalog is None: |
| return |
|
|
| msgstrs = message.string |
| if not isinstance(msgstrs, (list, tuple)): |
| msgstrs = (msgstrs,) |
| if len(msgstrs) != catalog.num_plurals: |
| raise TranslationError( |
| f"Wrong number of plural forms (expected {catalog.num_plurals})", |
| ) |
|
|
|
|
| def python_format(catalog: Catalog | None, message: Message) -> None: |
| """Verify the format string placeholders in the translation.""" |
| if 'python-format' not in message.flags: |
| return |
| msgids = message.id |
| if not isinstance(msgids, (list, tuple)): |
| msgids = (msgids,) |
| msgstrs = message.string |
| if not isinstance(msgstrs, (list, tuple)): |
| msgstrs = (msgstrs,) |
|
|
| if msgstrs[0]: |
| _validate_format(msgids[0], msgstrs[0]) |
| if message.pluralizable: |
| for msgstr in msgstrs[1:]: |
| if msgstr: |
| _validate_format(msgids[1], msgstr) |
|
|
|
|
| def _validate_format(format: str, alternative: str) -> None: |
| """Test format string `alternative` against `format`. `format` can be the |
| msgid of a message and `alternative` one of the `msgstr`\\s. The two |
| arguments are not interchangeable as `alternative` may contain less |
| placeholders if `format` uses named placeholders. |
| |
| If the string formatting of `alternative` is compatible to `format` the |
| function returns `None`, otherwise a `TranslationError` is raised. |
| |
| Examples for compatible format strings: |
| |
| >>> _validate_format('Hello %s!', 'Hallo %s!') |
| >>> _validate_format('Hello %i!', 'Hallo %d!') |
| |
| Example for an incompatible format strings: |
| |
| >>> _validate_format('Hello %(name)s!', 'Hallo %s!') |
| Traceback (most recent call last): |
| ... |
| TranslationError: the format strings are of different kinds |
| |
| This function is used by the `python_format` checker. |
| |
| :param format: The original format string |
| :param alternative: The alternative format string that should be checked |
| against format |
| :raises TranslationError: on formatting errors |
| """ |
|
|
| def _parse(string: str) -> list[tuple[str, str]]: |
| result: list[tuple[str, str]] = [] |
| for match in PYTHON_FORMAT.finditer(string): |
| name, format, typechar = match.groups() |
| if typechar == '%' and name is None: |
| continue |
| result.append((name, str(typechar))) |
| return result |
|
|
| def _compatible(a: str, b: str) -> bool: |
| if a == b: |
| return True |
| for set in _string_format_compatibilities: |
| if a in set and b in set: |
| return True |
| return False |
|
|
| def _check_positional(results: list[tuple[str, str]]) -> bool: |
| positional = None |
| for name, _char in results: |
| if positional is None: |
| positional = name is None |
| else: |
| if (name is None) != positional: |
| raise TranslationError( |
| 'format string mixes positional and named placeholders', |
| ) |
| return bool(positional) |
|
|
| a = _parse(format) |
| b = _parse(alternative) |
|
|
| if not a: |
| return |
|
|
| |
| a_positional = _check_positional(a) |
| b_positional = _check_positional(b) |
| if a_positional and not b_positional and not b: |
| raise TranslationError('placeholders are incompatible') |
| elif a_positional != b_positional: |
| raise TranslationError('the format strings are of different kinds') |
|
|
| |
| |
| if a_positional: |
| if len(a) != len(b): |
| raise TranslationError('positional format placeholders are unbalanced') |
| for idx, ((_, first), (_, second)) in enumerate(zip(a, b)): |
| if not _compatible(first, second): |
| raise TranslationError( |
| f'incompatible format for placeholder {idx + 1:d}: ' |
| f'{first!r} and {second!r} are not compatible', |
| ) |
|
|
| |
| |
| else: |
| type_map = dict(a) |
| for name, typechar in b: |
| if name not in type_map: |
| raise TranslationError(f'unknown named placeholder {name!r}') |
| elif not _compatible(typechar, type_map[name]): |
| raise TranslationError( |
| f'incompatible format for placeholder {name!r}: ' |
| f'{typechar!r} and {type_map[name]!r} are not compatible', |
| ) |
|
|
|
|
| def _find_checkers() -> list[Callable[[Catalog | None, Message], object]]: |
| from babel.messages._compat import find_entrypoints |
|
|
| checkers: list[Callable[[Catalog | None, Message], object]] = [] |
| checkers.extend(load() for (name, load) in find_entrypoints('babel.checkers')) |
| if len(checkers) == 0: |
| |
| |
| return [num_plurals, python_format] |
| return checkers |
|
|
|
|
| checkers: list[Callable[[Catalog | None, Message], object]] = _find_checkers() |
|
|