Ctrl K
ComunidadGuías…

tests/TestTranslations.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (c) 2008-2026 Andrew Ziem.
#
# This work is licensed under the terms of the GNU GPL, version 3 or
# later.  See the COPYING file in the top-level directory.


"""
Tests for translation (.po) files.

These checks cover bugs that ``msgfmt --check`` cannot catch, such as
NSIS ``${placeholder}`` typos in translations (see commit 104a6211 which
fixed ``${proname}`` -> ``${prodname}`` in Italian).

For format-string validation run ``make -C po tests``.
"""

import ast
import os
import re
import unittest
import warnings

_PO_DIR = os.path.join(os.path.dirname(__file__), '..', 'po')

# Matches ${name} placeholders used in NSIS strings, e.g. ${prodname}.
_NSIS_PLACEHOLDER = re.compile(r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}')


def _parse_po_string(token):
    """Return the decoded PO string literal from the raw token."""
    token = token.strip()
    if not token:
        return ''
    with warnings.catch_warnings():
        # An invalid escape sequence (e.g. ``\q``) raises SyntaxWarning,
        # which CI turns into an error under PYTHONWARNINGS=error. Decode
        # it the way Python does (keeping the backslash) instead.
        warnings.simplefilter('ignore')
        return ast.literal_eval(token)


def _parse_po(path):
    """Yield (msgctxt, msgid, msgstr) tuples from a .po file.

    Minimal stdlib parser; handles multi-line quoted strings and ignores
    fuzzy/obsolete entries. Sufficient for placeholder consistency checks.
    """
    msgctxt = None
    msgid = None
    msgstr = None
    fuzzy = False
    field = None  # which multiline field we are accumulating into

    results = []
    with open(path, 'r', encoding='utf-8') as f:
        for raw_line in [*f, '\n']:
            line = raw_line.rstrip('\n')
            if not line.strip():
                if msgid and msgstr and not fuzzy:
                    results.append((msgctxt, msgid, msgstr))
                msgctxt, msgid, msgstr = None, None, None
                fuzzy = False
                field = None
            elif line.startswith('#,') and 'fuzzy' in line:
                fuzzy = True
            elif line.startswith('#'):
                continue
            elif line.startswith('msgctxt '):
                msgctxt = _parse_po_string(line[len('msgctxt '):])
                field = 'msgctxt'
            elif line.startswith('msgid '):
                msgid = _parse_po_string(line[len('msgid '):])
                field = 'msgid'
            elif line.startswith('msgstr '):
                msgstr = _parse_po_string(line[len('msgstr '):])
                field = 'msgstr'
            elif line.startswith('"'):
                text = _parse_po_string(line)
                if field == 'msgctxt' and msgctxt is not None:
                    msgctxt += text
                elif field == 'msgid' and msgid is not None:
                    msgid += text
                elif field == 'msgstr' and msgstr is not None:
                    msgstr += text
    return results


class TestTranslations(unittest.TestCase):
    """Validate .po files beyond what msgfmt --check covers."""

    def _po_entries(self, po_path, failures):
        """Parse a .po file, recording a clean failure if it is malformed."""
        try:
            return _parse_po(po_path)
        except (SyntaxError, ValueError) as e:
            failures.append(
                f"{os.path.basename(po_path)}: parse error: {e!r}")
            return []

    def _po_files(self):
        po_dir = os.path.abspath(_PO_DIR)
        return [os.path.join(po_dir, f)
                for f in os.listdir(po_dir) if f.endswith('.po')]

    def test_nsis_placeholders_match(self):
        """NSIS translations must keep the same ${...} placeholders as msgid.

        Regression test for commit 104a6211 (Italian ${proname} typo).
        """
        failures = []
        for po_path in self._po_files():
            for msgctxt, msgid, msgstr in self._po_entries(po_path, failures):
                if not msgctxt or not msgctxt.startswith('nsis:'):
                    continue
                expected = sorted(_NSIS_PLACEHOLDER.findall(msgid))
                actual = sorted(_NSIS_PLACEHOLDER.findall(msgstr))
                if expected != actual:
                    failures.append(
                        f"{os.path.basename(po_path)}: msgctxt={msgctxt} "
                        f"expected={expected} actual={actual}")
        self.assertEqual([], failures,
                         "NSIS placeholder mismatch in translations:\n" +
                         "\n".join(failures))

    def test_no_excess_backslash_escapes(self):
        """Translations must not contain excess backslash escapes.

        Regression test for excess escaping in Indonesian and Sinhala, where
        quotes or newlines had extra backslashes (``\\\\\\"`` or ``\\\\\\n``).
        """
        failures = []
        for po_path in self._po_files():
            for msgctxt, msgid, msgstr in self._po_entries(po_path, failures):
                if '\\"' in msgstr or '\\\n' in msgstr:
                    failures.append(
                        f"{os.path.basename(po_path)}: msgctxt={msgctxt!r} "
                        f"msgid={msgid!r} msgstr={msgstr!r}")
        self.assertEqual([], failures,
                         "Unexpected backslash escapes in translations:\n" +
                         "\n".join(failures))

    def test_parse_po_string_decodes_escapes(self):
        """PO string parser decodes escaped quotes and backslash-newline."""
        self.assertEqual(_parse_po_string('"Say \\"hello\\""'), 'Say "hello"')
        self.assertEqual(_parse_po_string('"Line 1\\\\\\n"'), 'Line 1\\\n')


if __name__ == '__main__':
    unittest.main()