OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

i18n-scan.php

6 KB
<?php

declare(strict_types=1);

/**
 * Abdeckung der Übersetzung messen.
 *
 *   php bin/i18n-scan.php                     Übersicht je Verzeichnis
 *   php bin/i18n-scan.php --files             Übersicht je Datei (offene zuerst)
 *   php bin/i18n-scan.php templates/tickets   nur diesen Pfad
 *   php bin/i18n-scan.php --open <datei>      offene Stellen einer Datei mit Zeilennummer
 *   php bin/i18n-scan.php --catalog           übersetzte Texte ohne Eintrag in lang/en.php
 *
 * Gezählt werden sichtbare Texte: Textknoten im HTML und die Beschriftungen in
 * placeholder, title, aria-label und alt. Der Zähler ist eine Orientierung, kein
 * Beweis – er findet großzügig und meldet im Zweifel eine Stelle zu viel.
 */

require __DIR__ . '/../src/Core/TemplateScan.php';

use OpenNIT\Core\TemplateScan;

$wurzel = dirname(__DIR__);
$args   = array_slice($argv, 1);
$modus  = 'dirs';
$pfade  = [];
$ziel   = null;

foreach ($args as $arg) {
    if ($arg === '--files') {
        $modus = 'files';
    } elseif ($arg === '--catalog') {
        $modus = 'catalog';
    } elseif ($arg === '--open') {
        $modus = 'open';
    } elseif ($modus === 'open' && $ziel === null) {
        $ziel = $arg;
    } else {
        $pfade[] = $arg;
    }
}

/** Alle Vorlagen unterhalb eines Pfades. */
$sammle = static function (string $basis): array {
    if (is_file($basis)) {
        return [$basis];
    }
    $dateien = [];
    $lauf = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basis, FilesystemIterator::SKIP_DOTS));
    foreach ($lauf as $datei) {
        if ($datei->isFile() && $datei->getExtension() === 'php') {
            $dateien[] = $datei->getPathname();
        }
    }
    sort($dateien);
    return $dateien;
};

if ($modus === 'open') {
    if ($ziel === null || !is_file($ziel)) {
        fwrite(STDERR, "Datei fehlt oder ist nicht lesbar.\n");
        exit(1);
    }
    $offen = TemplateScan::offeneTexte((string)file_get_contents($ziel));
    foreach ($offen as $stelle) {
        printf("%5d  %-12s %s\n", $stelle['zeile'], $stelle['art'], $stelle['text']);
    }
    printf("\n%d offene Stelle(n) in %s\n", count($offen), $ziel);
    exit(0);
}

if ($modus === 'catalog') {
    $katalog = require $wurzel . '/lang/en.php';
    $fehlend = [];

    /**
     * Alle Schlüssel eines t()/te()-Aufrufs.
     *
     * Gelesen werden Aufrufe über `$this` wie über `$view`: Eine Vorlage, die
     * ihren Inhalt in einer benannten Funktion aufbaut, bekommt die Ansicht als
     * Parameter herein und ruft darüber auf.
     *
     * Der erste Parameter ist nicht immer eine einzelne Zeichenkette – eine
     * Auswahl wie `te($einzahl ? 'Anfrage' : 'Anfragen')` bringt zwei
     * Schlüssel mit. Gelesen wird deshalb der gesamte erste Parameter; die
     * Platzhalterwerte dahinter bleiben außen vor, ihre Namen sind keine
     * Oberflächentexte.
     *
     * @return list<string>
     */
    $schluessel = static function (string $quelle): array {
        // Maskierte Anführungszeichen gehören zum Text: 'Let\'s Encrypt' ist
        // eine Zeichenkette, keine zwei.
        $literal = '\'(?:[^\'\\\\]|\\\\.)*\'|"(?:[^"\\\\]|\\\\.)*"';
        $muster  = '/\$(?:this|view)->te?\s*\(((?:' . $literal . '|[^(),\'"]|\((?:' . $literal . '|[^()\'"])*\))*)/s';
        if (preg_match_all($muster, $quelle, $funde, PREG_SET_ORDER) === 0) {
            return [];
        }

        $texte = [];

        // Texte, die ein Skript selbst zusammensetzt, stehen als Liste im Layout.
        if (preg_match('/jsTranslations\\(\\[(.*?)\\]\\)/s', $quelle, $liste) === 1
            && preg_match_all('/' . $literal . '/s', $liste[1], $eintraege) > 0) {
            foreach ($eintraege[0] as $eintrag) {
                $texte[] = stripcslashes(substr($eintrag, 1, -1));
            }
        }

        foreach ($funde as $fund) {
            if (preg_match_all('/' . $literal . '/s', $fund[1], $teile, PREG_OFFSET_CAPTURE) === 0) {
                continue;
            }
            foreach ($teile[0] as [$teil, $pos]) {
                // Ein Feldname in `$wert['schluessel']` und ein Vergleichswert
                // stehen in der Bedingung, nicht in der Beschriftung.
                $davor = rtrim(substr($fund[1], 0, $pos));
                if ($davor !== '' && preg_match('/(\[|={2,3}|!={1,2})$/', $davor) === 1) {
                    continue;
                }
                $texte[] = stripcslashes(substr($teil, 1, -1));
            }
        }
        return $texte;
    };

    foreach ($sammle($wurzel . '/templates') as $datei) {
        foreach ($schluessel((string)file_get_contents($datei)) as $text) {
            if ($text !== '' && !isset($katalog[$text])) {
                $fehlend[$text][] = basename($datei);
            }
        }
    }
    ksort($fehlend);
    foreach ($fehlend as $text => $dateien) {
        printf("%-60s %s\n", mb_substr($text, 0, 58), implode(', ', array_unique($dateien)));
    }
    printf("\n%d übersetzte Texte ohne Eintrag in lang/en.php\n", count($fehlend));
    exit(count($fehlend) === 0 ? 0 : 1);
}

$basen = $pfade ?: [$wurzel . '/templates'];
$zeilen = [];
$gesamtOffen = 0;
$gesamtUebersetzt = 0;

foreach ($basen as $basis) {
    foreach ($sammle($basis) as $datei) {
        $ergebnis = TemplateScan::pruefe((string)file_get_contents($datei));
        $relativ  = str_replace($wurzel . '/', '', $datei);
        $gruppe   = $modus === 'files' ? $relativ : dirname($relativ);

        $zeilen[$gruppe]['offen']      = ($zeilen[$gruppe]['offen'] ?? 0) + $ergebnis['offen'];
        $zeilen[$gruppe]['uebersetzt'] = ($zeilen[$gruppe]['uebersetzt'] ?? 0) + $ergebnis['uebersetzt'];

        $gesamtOffen      += $ergebnis['offen'];
        $gesamtUebersetzt += $ergebnis['uebersetzt'];
    }
}

uasort($zeilen, static fn(array $a, array $b): int => $b['offen'] <=> $a['offen']);

printf("%-52s %8s %8s %7s\n", 'Pfad', 'offen', 'übers.', 'Quote');
printf("%s\n", str_repeat('-', 78));
foreach ($zeilen as $gruppe => $werte) {
    $summe = $werte['offen'] + $werte['uebersetzt'];
    printf(
        "%-52s %8d %8d %6.0f%%\n",
        mb_strlen($gruppe) > 52 ? '…' . mb_substr($gruppe, -51) : $gruppe,
        $werte['offen'],
        $werte['uebersetzt'],
        $summe > 0 ? $werte['uebersetzt'] / $summe * 100 : 100
    );
}

$summe = $gesamtOffen + $gesamtUebersetzt;
printf("%s\n", str_repeat('-', 78));
printf(
    "%-52s %8d %8d %6.0f%%\n",
    'Gesamt',
    $gesamtOffen,
    $gesamtUebersetzt,
    $summe > 0 ? $gesamtUebersetzt / $summe * 100 : 100
);