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()
							 |