done
This commit is contained in:
		
							
								
								
									
										168
									
								
								lib/python3.11/site-packages/babel/messages/checkers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								lib/python3.11/site-packages/babel/messages/checkers.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,168 @@
 | 
			
		||||
"""
 | 
			
		||||
    babel.messages.checkers
 | 
			
		||||
    ~~~~~~~~~~~~~~~~~~~~~~~
 | 
			
		||||
 | 
			
		||||
    Various routines that help with validation of translations.
 | 
			
		||||
 | 
			
		||||
    :since: version 0.9
 | 
			
		||||
 | 
			
		||||
    :copyright: (c) 2013-2025 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
 | 
			
		||||
 | 
			
		||||
#: list of format chars that are compatible to each other
 | 
			
		||||
_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
 | 
			
		||||
 | 
			
		||||
    # skip further tests if no catalog is provided.
 | 
			
		||||
    elif catalog is None:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    msgstrs = message.string
 | 
			
		||||
    if not isinstance(msgstrs, (list, tuple)):
 | 
			
		||||
        msgstrs = (msgstrs,)
 | 
			
		||||
    if len(msgstrs) != catalog.num_plurals:
 | 
			
		||||
        raise TranslationError("Wrong number of plural forms (expected %d)" %
 | 
			
		||||
                               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,)
 | 
			
		||||
 | 
			
		||||
    for msgid, msgstr in zip(msgids, msgstrs):
 | 
			
		||||
        if msgstr:
 | 
			
		||||
            _validate_format(msgid, 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, b = map(_parse, (format, alternative))
 | 
			
		||||
 | 
			
		||||
    if not a:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # now check if both strings are positional or named
 | 
			
		||||
    a_positional, b_positional = map(_check_positional, (a, 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 we are operating on positional strings both must have the
 | 
			
		||||
    # same number of format chars and those must be compatible
 | 
			
		||||
    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('incompatible format for placeholder '
 | 
			
		||||
                                       '%d: %r and %r are not compatible' %
 | 
			
		||||
                                       (idx + 1, first, second))
 | 
			
		||||
 | 
			
		||||
    # otherwise the second string must not have names the first one
 | 
			
		||||
    # doesn't have and the types of those included must be 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:
 | 
			
		||||
        # if entrypoints are not available or no usable egg-info was found
 | 
			
		||||
        # (see #230), just resort to hard-coded checkers
 | 
			
		||||
        return [num_plurals, python_format]
 | 
			
		||||
    return checkers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
checkers: list[Callable[[Catalog | None, Message], object]] = _find_checkers()
 | 
			
		||||
		Reference in New Issue
	
	Block a user