xref: /dokuwiki/_test/tests/inc/lang.php (revision 8dbb068ee7ca3cc3e84a1eb5b448221e2b63f97c)
1*8dbb068eSAndreas Gohr<?php
2*8dbb068eSAndreas Gohr
3*8dbb068eSAndreas Gohr/**
4*8dbb068eSAndreas Gohr * Language file tests inspired by the script by schplurtz
5*8dbb068eSAndreas Gohr * @link https://www.dokuwiki.org/teams:i18n:translation-check
6*8dbb068eSAndreas Gohr */
7*8dbb068eSAndreas Gohrclass lang_test extends DokuWikiTest
8*8dbb068eSAndreas Gohr{
9*8dbb068eSAndreas Gohr    /**
10*8dbb068eSAndreas Gohr     * returen all languages except english
11*8dbb068eSAndreas Gohr     *
12*8dbb068eSAndreas Gohr     * @return string[]
13*8dbb068eSAndreas Gohr     */
14*8dbb068eSAndreas Gohr    protected function findLanguages()
15*8dbb068eSAndreas Gohr    {
16*8dbb068eSAndreas Gohr        $languages = glob(DOKU_INC . 'inc/lang/*', GLOB_ONLYDIR);
17*8dbb068eSAndreas Gohr        $languages = array_map('basename', $languages);
18*8dbb068eSAndreas Gohr        $languages = array_filter($languages, function ($in) {
19*8dbb068eSAndreas Gohr            return $in !== 'en';
20*8dbb068eSAndreas Gohr        });
21*8dbb068eSAndreas Gohr        return $languages;
22*8dbb068eSAndreas Gohr    }
23*8dbb068eSAndreas Gohr
24*8dbb068eSAndreas Gohr    /**
25*8dbb068eSAndreas Gohr     * Get all installed plugins
26*8dbb068eSAndreas Gohr     *
27*8dbb068eSAndreas Gohr     * This finds all things that might be a plugin and does not care for enabled or not.
28*8dbb068eSAndreas Gohr     *
29*8dbb068eSAndreas Gohr     * @return string[]
30*8dbb068eSAndreas Gohr     */
31*8dbb068eSAndreas Gohr    protected function findPlugins()
32*8dbb068eSAndreas Gohr    {
33*8dbb068eSAndreas Gohr        $plugins = glob(DOKU_INC . 'lib/plugins/*', GLOB_ONLYDIR);
34*8dbb068eSAndreas Gohr        return $plugins;
35*8dbb068eSAndreas Gohr    }
36*8dbb068eSAndreas Gohr
37*8dbb068eSAndreas Gohr    /**
38*8dbb068eSAndreas Gohr     * Get all installed templates
39*8dbb068eSAndreas Gohr     *
40*8dbb068eSAndreas Gohr     * This finds all things that might be a template and does not care for enabled or not.
41*8dbb068eSAndreas Gohr     *
42*8dbb068eSAndreas Gohr     * @return string[]
43*8dbb068eSAndreas Gohr     */
44*8dbb068eSAndreas Gohr    protected function findTemplates()
45*8dbb068eSAndreas Gohr    {
46*8dbb068eSAndreas Gohr        $templates = glob(DOKU_INC . 'lib/tpl/*', GLOB_ONLYDIR);
47*8dbb068eSAndreas Gohr        return $templates;
48*8dbb068eSAndreas Gohr    }
49*8dbb068eSAndreas Gohr
50*8dbb068eSAndreas Gohr    /**
51*8dbb068eSAndreas Gohr     * Load the strings for the given language
52*8dbb068eSAndreas Gohr     *
53*8dbb068eSAndreas Gohr     * @param string $lang
54*8dbb068eSAndreas Gohr     * @return array
55*8dbb068eSAndreas Gohr     */
56*8dbb068eSAndreas Gohr    protected function loadLanguage($file)
57*8dbb068eSAndreas Gohr    {
58*8dbb068eSAndreas Gohr        $lang = [];
59*8dbb068eSAndreas Gohr        if (file_exists($file)) {
60*8dbb068eSAndreas Gohr            include $file;
61*8dbb068eSAndreas Gohr        }
62*8dbb068eSAndreas Gohr        return $lang;
63*8dbb068eSAndreas Gohr    }
64*8dbb068eSAndreas Gohr
65*8dbb068eSAndreas Gohr    /**
66*8dbb068eSAndreas Gohr     * Provide all the language files to compare
67*8dbb068eSAndreas Gohr     *
68*8dbb068eSAndreas Gohr     * @return Generator
69*8dbb068eSAndreas Gohr     */
70*8dbb068eSAndreas Gohr    public function provideLanguageFiles()
71*8dbb068eSAndreas Gohr    {
72*8dbb068eSAndreas Gohr        $bases = array_merge(
73*8dbb068eSAndreas Gohr            [DOKU_INC . 'inc'],
74*8dbb068eSAndreas Gohr            $this->findPlugins(),
75*8dbb068eSAndreas Gohr            $this->findTemplates()
76*8dbb068eSAndreas Gohr        );
77*8dbb068eSAndreas Gohr
78*8dbb068eSAndreas Gohr        foreach ($this->findLanguages() as $code) {
79*8dbb068eSAndreas Gohr            foreach ($bases as $base) {
80*8dbb068eSAndreas Gohr                foreach (['lang.php', 'settings.php'] as $file) {
81*8dbb068eSAndreas Gohr                    $englishFile = "$base/lang/en/$file";
82*8dbb068eSAndreas Gohr                    $foreignFile = "$base/lang/$code/$file";
83*8dbb068eSAndreas Gohr                    $name = substr($foreignFile, strlen(DOKU_INC));
84*8dbb068eSAndreas Gohr                    $name = '…'.substr($name, -35);
85*8dbb068eSAndreas Gohr
86*8dbb068eSAndreas Gohr                    if (file_exists($foreignFile)) {
87*8dbb068eSAndreas Gohr                        yield ([
88*8dbb068eSAndreas Gohr                            $this->loadLanguage($englishFile),
89*8dbb068eSAndreas Gohr                            $this->loadLanguage($foreignFile),
90*8dbb068eSAndreas Gohr                            $code,
91*8dbb068eSAndreas Gohr                            $name,
92*8dbb068eSAndreas Gohr                        ]);
93*8dbb068eSAndreas Gohr                    }
94*8dbb068eSAndreas Gohr                }
95*8dbb068eSAndreas Gohr            }
96*8dbb068eSAndreas Gohr        }
97*8dbb068eSAndreas Gohr    }
98*8dbb068eSAndreas Gohr
99*8dbb068eSAndreas Gohr    /**
100*8dbb068eSAndreas Gohr     * Check for obsolete language strings
101*8dbb068eSAndreas Gohr     *
102*8dbb068eSAndreas Gohr     * @param array $english key/value language pairs for English
103*8dbb068eSAndreas Gohr     * @param array $foreign key/value language pairs for the foreign language
104*8dbb068eSAndreas Gohr     * @param string $code language code of the foreign file
105*8dbb068eSAndreas Gohr     * @param string $file the base file name the foreign keys came from
106*8dbb068eSAndreas Gohr     * @param string $prefix sub key that is currently checked (used in recursion)
107*8dbb068eSAndreas Gohr     * @dataProvider provideLanguageFiles
108*8dbb068eSAndreas Gohr     */
109*8dbb068eSAndreas Gohr    public function testObsolete($english, $foreign, $code, $file, $prefix = '')
110*8dbb068eSAndreas Gohr    {
111*8dbb068eSAndreas Gohr        $this->assertGreaterThan(0, count($foreign), "$file exists but has no translations");
112*8dbb068eSAndreas Gohr
113*8dbb068eSAndreas Gohr        foreach ($foreign as $key => $value) {
114*8dbb068eSAndreas Gohr            $name = $prefix ? $prefix . $key : $key;
115*8dbb068eSAndreas Gohr            $this->assertArrayHasKey($key, $english, "$file: obsolete/unknown key '$name'");
116*8dbb068eSAndreas Gohr
117*8dbb068eSAndreas Gohr            // sub arrays as for the js translations:
118*8dbb068eSAndreas Gohr            if (is_array($value) && is_array($english[$key])) {
119*8dbb068eSAndreas Gohr                $this->testObsolete($english[$key], $value, $code, $file, $key);
120*8dbb068eSAndreas Gohr            }
121*8dbb068eSAndreas Gohr        }
122*8dbb068eSAndreas Gohr    }
123*8dbb068eSAndreas Gohr
124*8dbb068eSAndreas Gohr    /**
125*8dbb068eSAndreas Gohr     * Check for sprintf format placeholder equality
126*8dbb068eSAndreas Gohr     *
127*8dbb068eSAndreas Gohr     * @param array $english key/value language pairs for English
128*8dbb068eSAndreas Gohr     * @param array $foreign key/value language pairs for the foreign language
129*8dbb068eSAndreas Gohr     * @param string $code language code of the foreign file
130*8dbb068eSAndreas Gohr     * @param string $file the base file name the foreign keys came from
131*8dbb068eSAndreas Gohr     * @param string $prefix sub key that is currently checked (used in recursion)
132*8dbb068eSAndreas Gohr     * @dataProvider provideLanguageFiles
133*8dbb068eSAndreas Gohr     */
134*8dbb068eSAndreas Gohr    public function testPlaceholders($english, $foreign, $code, $file, $prefix = '')
135*8dbb068eSAndreas Gohr    {
136*8dbb068eSAndreas Gohr        $this->assertGreaterThan(0, count($foreign), "$file exists but has no translations");
137*8dbb068eSAndreas Gohr
138*8dbb068eSAndreas Gohr        foreach ($foreign as $key => $value) {
139*8dbb068eSAndreas Gohr            // non existing in english is skipped here, that what testObsolete checks
140*8dbb068eSAndreas Gohr            if (!isset($english[$key])) continue;
141*8dbb068eSAndreas Gohr
142*8dbb068eSAndreas Gohr            // sub arrays as for the js translations:
143*8dbb068eSAndreas Gohr            if (is_array($value) && is_array($english[$key])) {
144*8dbb068eSAndreas Gohr                $this->testPlaceholders($english[$key], $value, $code, $file, $key);
145*8dbb068eSAndreas Gohr                return;
146*8dbb068eSAndreas Gohr            }
147*8dbb068eSAndreas Gohr
148*8dbb068eSAndreas Gohr            $name = $prefix ? $prefix . $key : $key;
149*8dbb068eSAndreas Gohr
150*8dbb068eSAndreas Gohr            $englishPlaceholders = $this->parsePlaceholders($english[$key]);
151*8dbb068eSAndreas Gohr            $foreignPlaceholders = $this->parsePlaceholders($value);
152*8dbb068eSAndreas Gohr            $countEnglish = count($englishPlaceholders);
153*8dbb068eSAndreas Gohr            $countForeign = count($foreignPlaceholders);
154*8dbb068eSAndreas Gohr
155*8dbb068eSAndreas Gohr            $this->assertEquals($countEnglish, $countForeign,
156*8dbb068eSAndreas Gohr                join("\n",
157*8dbb068eSAndreas Gohr                    [
158*8dbb068eSAndreas Gohr                        "$file: unequal amount of sprintf format placeholders in '$name'",
159*8dbb068eSAndreas Gohr                        "en: '" . $english[$key] . "'",
160*8dbb068eSAndreas Gohr                        "$code: '$value'",
161*8dbb068eSAndreas Gohr                    ]
162*8dbb068eSAndreas Gohr                )
163*8dbb068eSAndreas Gohr            );
164*8dbb068eSAndreas Gohr
165*8dbb068eSAndreas Gohr            $this->assertEquals($englishPlaceholders, $foreignPlaceholders,
166*8dbb068eSAndreas Gohr                join("\n",
167*8dbb068eSAndreas Gohr                    [
168*8dbb068eSAndreas Gohr                        "$file: sprintf format mismatch in '$name'",
169*8dbb068eSAndreas Gohr                        "en: '" . $english[$key] . "'",
170*8dbb068eSAndreas Gohr                        "$code: '$value'",
171*8dbb068eSAndreas Gohr                    ]
172*8dbb068eSAndreas Gohr                )
173*8dbb068eSAndreas Gohr            );
174*8dbb068eSAndreas Gohr        }
175*8dbb068eSAndreas Gohr    }
176*8dbb068eSAndreas Gohr
177*8dbb068eSAndreas Gohr    /**
178*8dbb068eSAndreas Gohr     * Parses the placeholders from a string and brings them in the correct order
179*8dbb068eSAndreas Gohr     *
180*8dbb068eSAndreas Gohr     * This has its own test below.
181*8dbb068eSAndreas Gohr     *
182*8dbb068eSAndreas Gohr     * @param string $string
183*8dbb068eSAndreas Gohr     */
184*8dbb068eSAndreas Gohr    protected function parsePlaceholders($string)
185*8dbb068eSAndreas Gohr    {
186*8dbb068eSAndreas Gohr        if (!preg_match_all('/%(?:([0-9]+)\$)?([-.0-9hl]*?[%dufsc])/', $string, $matches, PREG_SET_ORDER)) {
187*8dbb068eSAndreas Gohr            return [];
188*8dbb068eSAndreas Gohr        }
189*8dbb068eSAndreas Gohr
190*8dbb068eSAndreas Gohr        // Given this string : 'schproutch %2$s with %1$04d in %-20s plouf'
191*8dbb068eSAndreas Gohr        // we have this in $matches:
192*8dbb068eSAndreas Gohr        // [
193*8dbb068eSAndreas Gohr        //     0 => ['%2$s', 2, 's'],
194*8dbb068eSAndreas Gohr        //     1 => ['%1$04d', 1, '04d'],
195*8dbb068eSAndreas Gohr        //     2 => ['%-20s', '', '-20s'],
196*8dbb068eSAndreas Gohr        // ]
197*8dbb068eSAndreas Gohr
198*8dbb068eSAndreas Gohr        // sort by the given sorting in key 1
199*8dbb068eSAndreas Gohr        usort($matches, function ($a, $b) {
200*8dbb068eSAndreas Gohr            if ($a[1] === $b[1]) return 0; // keep as is
201*8dbb068eSAndreas Gohr
202*8dbb068eSAndreas Gohr            // sort empties towards the back
203*8dbb068eSAndreas Gohr            if ($a[1] === '') $a[1] = 9999;
204*8dbb068eSAndreas Gohr            if ($b[1] === '') $b[1] = 9999;
205*8dbb068eSAndreas Gohr
206*8dbb068eSAndreas Gohr            // compare sort numbers
207*8dbb068eSAndreas Gohr            if ((int)$a[1] < (int)$b[1]) return -1;
208*8dbb068eSAndreas Gohr            if ((int)$a[1] > (int)$b[1]) return 1;
209*8dbb068eSAndreas Gohr            return 0;
210*8dbb068eSAndreas Gohr        });
211*8dbb068eSAndreas Gohr
212*8dbb068eSAndreas Gohr        // return values in key 2
213*8dbb068eSAndreas Gohr        return array_column($matches, 2);
214*8dbb068eSAndreas Gohr    }
215*8dbb068eSAndreas Gohr
216*8dbb068eSAndreas Gohr    /**
217*8dbb068eSAndreas Gohr     * Dataprovider for the parsePlaceholder test
218*8dbb068eSAndreas Gohr     * @return array[]
219*8dbb068eSAndreas Gohr     */
220*8dbb068eSAndreas Gohr    public function providePlaceholders()
221*8dbb068eSAndreas Gohr    {
222*8dbb068eSAndreas Gohr        return [
223*8dbb068eSAndreas Gohr            ['schproutch %2$s with %1$04d in %-20s plouf', ['04d', 's', '-20s']],
224*8dbb068eSAndreas Gohr        ];
225*8dbb068eSAndreas Gohr    }
226*8dbb068eSAndreas Gohr
227*8dbb068eSAndreas Gohr    /**
228*8dbb068eSAndreas Gohr     * Test the parsePlaceholder utility function above
229*8dbb068eSAndreas Gohr     *
230*8dbb068eSAndreas Gohr     * @param string $input
231*8dbb068eSAndreas Gohr     * @param array $expected
232*8dbb068eSAndreas Gohr     * @dataProvider providePlaceholders
233*8dbb068eSAndreas Gohr     */
234*8dbb068eSAndreas Gohr    public function testParsePlaceholders($input, $expected)
235*8dbb068eSAndreas Gohr    {
236*8dbb068eSAndreas Gohr        $this->assertEquals($expected, $this->parsePlaceholders($input));
237*8dbb068eSAndreas Gohr    }
238*8dbb068eSAndreas Gohr}
239