Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Status
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
6 / 6
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 inspect
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
3
 inspectSection
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 combine
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 obsolete
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Verba\Tool;
6
7/**
8 * Reports translation gaps for a domain without touching any files: per locale,
9 * how many ordinary and contextual source messages are missing, untranslated,
10 * translated, and how many catalog entries are now obsolete.
11 *
12 * @api
13 */
14final class Status
15{
16    public function __construct(
17        private readonly Domain $domain,
18    ) {}
19
20    public function run(): StatusReport
21    {
22        $extraction = new Extractor($this->domain)->extract();
23        $fresh = $extraction['messages'];
24        $freshContexts = $extraction['contexts'];
25
26        $locales = [];
27
28        foreach ($this->domain->locales as $locale) {
29            $locales[$locale] = $this->inspect($locale, $fresh, $freshContexts);
30        }
31
32        return new StatusReport($this->domain->name, $locales, $extraction['warnings']);
33    }
34
35    /**
36     * @param array<string, Message> $fresh
37     * @param array<string, array<string, Message>> $freshContexts
38     * @return array{
39     *     missing: int,
40     *     untranslated: int,
41     *     translated: int,
42     *     obsolete: int,
43     *     total: int,
44     *     locations: list<string>,
45     * }
46     */
47    private function inspect(string $locale, array $fresh, array $freshContexts): array
48    {
49        $catalog = CatalogFile::load($this->domain->file($locale));
50        $summary = self::inspectSection($fresh, $catalog->messages);
51        $obsolete = self::obsolete($catalog->messages, $catalog->obsolete, $fresh);
52
53        foreach ($freshContexts as $context => $contextFresh) {
54            $contextSummary = self::inspectSection(
55                $contextFresh,
56                $catalog->contexts[$context] ?? [],
57            );
58            $summary = self::combine($summary, $contextSummary);
59        }
60
61        $contextNames = array_unique([
62            ...array_keys($catalog->contexts),
63            ...array_keys($catalog->obsoleteContexts),
64        ]);
65
66        foreach ($contextNames as $context) {
67            $obsolete += self::obsolete(
68                $catalog->contexts[$context] ?? [],
69                $catalog->obsoleteContexts[$context] ?? [],
70                $freshContexts[$context] ?? [],
71            );
72        }
73
74        return [
75            'missing' => $summary['missing'],
76            'untranslated' => $summary['untranslated'],
77            'translated' => $summary['translated'],
78            'obsolete' => $obsolete,
79            'total' => $summary['total'],
80            'locations' => $summary['locations'],
81        ];
82    }
83
84    /**
85     * @param array<string, Message> $fresh
86     * @param array<string, string|list<string>|null> $messages
87     * @return array{
88     *     missing: int,
89     *     untranslated: int,
90     *     translated: int,
91     *     total: int,
92     *     locations: list<string>,
93     * }
94     */
95    private static function inspectSection(array $fresh, array $messages): array
96    {
97        $missing = 0;
98        $untranslated = 0;
99        $translated = 0;
100        $locations = [];
101
102        foreach ($fresh as $id => $message) {
103            if (!array_key_exists($id, $messages)) {
104                $missing++;
105                $locations = [...$locations, ...$message->locations];
106            } elseif ($messages[$id] === null || $messages[$id] === []) {
107                $untranslated++;
108                $locations = [...$locations, ...$message->locations];
109            } else {
110                $translated++;
111            }
112        }
113
114        return [
115            'missing' => $missing,
116            'untranslated' => $untranslated,
117            'translated' => $translated,
118            'total' => count($fresh),
119            'locations' => $locations,
120        ];
121    }
122
123    /**
124     * @param array{missing: int, untranslated: int, translated: int, total: int, locations: list<string>} $left
125     * @param array{missing: int, untranslated: int, translated: int, total: int, locations: list<string>} $right
126     * @return array{missing: int, untranslated: int, translated: int, total: int, locations: list<string>}
127     */
128    private static function combine(array $left, array $right): array
129    {
130        return [
131            'missing' => $left['missing'] + $right['missing'],
132            'untranslated' => $left['untranslated'] + $right['untranslated'],
133            'translated' => $left['translated'] + $right['translated'],
134            'total' => $left['total'] + $right['total'],
135            'locations' => [...$left['locations'], ...$right['locations']],
136        ];
137    }
138
139    /**
140     * @param array<string, string|list<string>|null> $messages
141     * @param array<string, string|list<string>|null> $parked
142     * @param array<string, Message> $fresh
143     */
144    private static function obsolete(array $messages, array $parked, array $fresh): int
145    {
146        $count = count($parked);
147
148        foreach (array_keys($messages) as $id) {
149            if (array_key_exists($id, $fresh) || array_key_exists($id, $parked)) {
150                continue;
151            }
152
153            $count++;
154        }
155
156        return $count;
157    }
158}