You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			169 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			169 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
    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()
 |