view i18n/check-translation.py @ 52268:2e26c21f77ca stable

contrib: install the arm64 compiler tools in the Windows dependency script This lets us build arm64 wheels on Windows. We should update from VS 2017 to VS 2019, but that can wait until there's a clean Windows system available. There's a little bit of handwaving here because I originally installed some packages on the CI system by checking boxes in the GUI installer to get arm64 support, since I didn't know the name(s) of the things needed. Exporting the configuration from the GUI installer showed the list of things present, and then I was able to run `vs_buildtools.exe` on another system that previously had this dependency script run a few years ago, with just this new arg. That allowed the wheels to be built (the process failed on this second system prior to this). The only difference between the CI system configuration prior to installing arm64 stuff and this second system after, is the second system has the WinXP support component. Surprisingly, the arm64 WDK and arm64 CRT or Universal CRT doesn't seem to be required.
author Matt Harbison <matt_harbison@yahoo.com>
date Thu, 14 Nov 2024 15:36:41 -0500
parents f4733654f144
children
line wrap: on
line source

#!/usr/bin/env python3
#
# check-translation.py - check Mercurial specific translation problems

from __future__ import annotations

import re

import polib

scanners = []
checkers = []


def scanner():
    def decorator(func):
        scanners.append(func)
        return func

    return decorator


def levelchecker(level, msgidpat):
    def decorator(func):
        if msgidpat:
            match = re.compile(msgidpat).search
        else:
            match = lambda msgid: True
        checkers.append((func, level))
        func.match = match
        return func

    return decorator


def match(checker, pe):
    """Examine whether POEntry "pe" is target of specified checker or not"""
    if not checker.match(pe.msgid):
        return
    # examine suppression by translator comment
    nochecker = 'no-%s-check' % checker.__name__
    for tc in pe.tcomment.split():
        if nochecker == tc:
            return
    return True


####################


def fatalchecker(msgidpat=None):
    return levelchecker('fatal', msgidpat)


@fatalchecker(r'\$\$')
def promptchoice(pe):
    """Check translation of the string given to "ui.promptchoice()"

    >>> pe = polib.POEntry(
    ...     msgid ='prompt$$missing &sep$$missing &amp$$followed by &none',
    ...     msgstr='prompt  missing &sep$$missing  amp$$followed by none&')
    >>> match(promptchoice, pe)
    True
    >>> for e in promptchoice(pe): print(e)
    number of choices differs between msgid and msgstr
    msgstr has invalid choice missing '&'
    msgstr has invalid '&' followed by none
    """
    idchoices = [c.rstrip(' ') for c in pe.msgid.split('$$')[1:]]
    strchoices = [c.rstrip(' ') for c in pe.msgstr.split('$$')[1:]]

    if len(idchoices) != len(strchoices):
        yield "number of choices differs between msgid and msgstr"

    indices = [(c, c.find('&')) for c in strchoices]
    if [c for c, i in indices if i == -1]:
        yield "msgstr has invalid choice missing '&'"
    if [c for c, i in indices if len(c) == i + 1]:
        yield "msgstr has invalid '&' followed by none"


deprecatedpe = None


@scanner()
def deprecatedsetup(pofile):
    pes = [p for p in pofile if p.msgid == '(DEPRECATED)' and p.msgstr]
    if len(pes):
        global deprecatedpe
        deprecatedpe = pes[0]


@fatalchecker(r'\(DEPRECATED\)')
def deprecated(pe):
    """Check for DEPRECATED
    >>> ped = polib.POEntry(
    ...     msgid = '(DEPRECATED)',
    ...     msgstr= '(DETACERPED)')
    >>> deprecatedsetup([ped])
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED)',
    ...     msgstr= 'something (DEPRECATED)')
    >>> match(deprecated, pe)
    True
    >>> for e in deprecated(pe): print(e)
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED)',
    ...     msgstr= 'something (DETACERPED)')
    >>> match(deprecated, pe)
    True
    >>> for e in deprecated(pe): print(e)
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED)',
    ...     msgstr= 'something')
    >>> match(deprecated, pe)
    True
    >>> for e in deprecated(pe): print(e)
    msgstr inconsistently translated (DEPRECATED)
    >>> pe = polib.POEntry(
    ...     msgid = 'Something (DEPRECATED, foo bar)',
    ...     msgstr= 'something (DETACERPED, foo bar)')
    >>> match(deprecated, pe)
    """
    if not (
        '(DEPRECATED)' in pe.msgstr
        or (deprecatedpe and deprecatedpe.msgstr in pe.msgstr)
    ):
        yield "msgstr inconsistently translated (DEPRECATED)"


####################


def warningchecker(msgidpat=None):
    return levelchecker('warning', msgidpat)


@warningchecker()
def taildoublecolons(pe):
    """Check equality of tail '::'-ness between msgid and msgstr

    >>> pe = polib.POEntry(
    ...     msgid ='ends with ::',
    ...     msgstr='ends with ::')
    >>> for e in taildoublecolons(pe): print(e)
    >>> pe = polib.POEntry(
    ...     msgid ='ends with ::',
    ...     msgstr='ends without double-colons')
    >>> for e in taildoublecolons(pe): print(e)
    tail '::'-ness differs between msgid and msgstr
    >>> pe = polib.POEntry(
    ...     msgid ='ends without double-colons',
    ...     msgstr='ends with ::')
    >>> for e in taildoublecolons(pe): print(e)
    tail '::'-ness differs between msgid and msgstr
    """
    if pe.msgid.endswith('::') != pe.msgstr.endswith('::'):
        yield "tail '::'-ness differs between msgid and msgstr"


@warningchecker()
def indentation(pe):
    """Check equality of initial indentation between msgid and msgstr

    This may report unexpected warning, because this doesn't aware
    the syntax of rst document and the context of msgstr.

    >>> pe = polib.POEntry(
    ...     msgid ='    indented text',
    ...     msgstr='  narrowed indentation')
    >>> for e in indentation(pe): print(e)
    initial indentation width differs betweeen msgid and msgstr
    """
    idindent = len(pe.msgid) - len(pe.msgid.lstrip())
    strindent = len(pe.msgstr) - len(pe.msgstr.lstrip())
    if idindent != strindent:
        yield "initial indentation width differs betweeen msgid and msgstr"


####################


def check(pofile, fatal=True, warning=False):
    targetlevel = {'fatal': fatal, 'warning': warning}
    targetcheckers = [
        (checker, level) for checker, level in checkers if targetlevel[level]
    ]
    if not targetcheckers:
        return []

    detected = []
    for checker in scanners:
        checker(pofile)
    for pe in pofile.translated_entries():
        errors = []
        for checker, level in targetcheckers:
            if match(checker, pe):
                errors.extend(
                    (level, checker.__name__, error) for error in checker(pe)
                )
        if errors:
            detected.append((pe, errors))
    return detected


########################################

if __name__ == "__main__":
    import sys
    import optparse

    optparser = optparse.OptionParser(
        """%prog [options] pofile ...

This checks Mercurial specific translation problems in specified
'*.po' files.

Each detected problems are shown in the format below::

    filename:linenum:type(checker): problem detail .....

"type" is "fatal" or "warning". "checker" is the name of the function
detecting corresponded error.

Checking by checker "foo" on the specific msgstr can be suppressed by
the "translator comment" like below. Multiple "no-xxxx-check" should
be separated by whitespaces::

    # no-foo-check
    msgid = "....."
    msgstr = "....."
"""
    )
    optparser.add_option(
        "",
        "--warning",
        help="show also warning level problems",
        action="store_true",
    )
    optparser.add_option(
        "",
        "--doctest",
        help="run doctest of this tool, instead of check",
        action="store_true",
    )
    (options, args) = optparser.parse_args()

    if options.doctest:
        import os

        if 'TERM' in os.environ:
            del os.environ['TERM']
        import doctest

        failures, tests = doctest.testmod()
        sys.exit(failures and 1 or 0)

    detected = []
    warning = options.warning
    for f in args:
        detected.extend(
            (f, pe, errors)
            for pe, errors in check(polib.pofile(f), warning=warning)
        )
    if detected:
        for f, pe, errors in detected:
            for level, checker, error in errors:
                sys.stderr.write(
                    '%s:%d:%s(%s): %s\n'
                    % (f, pe.linenum, level, checker, error)
                )
        sys.exit(1)