Loader::getTranslator()   A
last analyzed

Complexity

Conditions 6
Paths 14

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 18
c 2
b 0
f 0
dl 0
loc 34
ccs 12
cts 12
cp 1
rs 9.0444
cc 6
nc 14
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
    Copyright (c) 2005 Steven Armstrong <sa at c-area dot ch>
7
    Copyright (c) 2009 Danilo Segan <[email protected]>
8
    Copyright (c) 2016 Michal Čihař <[email protected]>
9
10
    This file is part of MoTranslator.
11
12
    This program is free software; you can redistribute it and/or modify
13
    it under the terms of the GNU General Public License as published by
14
    the Free Software Foundation; either version 2 of the License, or
15
    (at your option) any later version.
16
17
    This program is distributed in the hope that it will be useful,
18
    but WITHOUT ANY WARRANTY; without even the implied warranty of
19
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
    GNU General Public License for more details.
21
22
    You should have received a copy of the GNU General Public License along
23
    with this program; if not, write to the Free Software Foundation, Inc.,
24
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25
*/
26
27
namespace PhpMyAdmin\MoTranslator;
28
29
use PhpMyAdmin\MoTranslator\Cache\CacheFactoryInterface;
30
use PhpMyAdmin\MoTranslator\Cache\InMemoryCache;
31
32
use function file_exists;
33
use function getenv;
34
use function in_array;
35
use function preg_match;
36
use function sprintf;
37
38
class Loader
39
{
40
    /**
41
     * Loader instance.
42
     *
43
     * @static
44
     */
45
    private static Loader|null $instance = null;
46
47
    /**
48
     * Factory to return a factory responsible for returning a `CacheInterface`
49
     *
50
     * @static
51
     */
52
    private static CacheFactoryInterface|null $cacheFactory = null;
53
54
    /**
55
     * Default gettext domain to use.
56
     */
57
    private string $defaultDomain = '';
58
59
    /**
60
     * Configured locale.
61
     */
62
    private string $locale = '';
63
64
    /**
65
     * Loaded domains.
66
     *
67
     * @var array<string,array<string,Translator>>
68
     */
69
    private array $domains = [];
70
71
    /**
72
     * Bound paths for domains.
73
     *
74
     * @var array<string,string>
75
     */
76
    private array $paths = ['' => './'];
77
78
    /**
79
     * Returns the singleton Loader object.
80
     *
81
     * @return Loader object
82
     */
83
    public static function getInstance(): Loader
84
    {
85
        if (self::$instance === null) {
86
            self::$instance = new self();
87
        }
88
89
        return self::$instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::instance could return the type null which is incompatible with the type-hinted return PhpMyAdmin\MoTranslator\Loader. Consider adding an additional type-check to rule them out.
Loading history...
90 84
    }
91
92 84
    /**
93 14
     * Loads global localization functions.
94
     */
95
    public static function loadFunctions(): void
96 84
    {
97
        require_once __DIR__ . '/functions.php';
98
    }
99
100
    /**
101
     * Figure out all possible locale names and start with the most
102 28
     * specific ones.  I.e. for sr_CS.UTF-8@latin, look through all of
103
     * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr.
104 28
     *
105 4
     * @param string $locale Locale code
106
     *
107
     * @return string[] list of locales to try for any POSIX-style locale specification
108
     * @psalm-return list<string>
109
     */
110
    public static function listLocales(string $locale): array
111
    {
112
        $localeNames = [];
113
114
        if ($locale !== '') {
115
            if (
116 294
                preg_match(
117
                    '/^(?P<lang>[a-z]{2,3})' // language code
118 294
                    . '(?:_(?P<country>[A-Z]{2}))?' // country code
119
                    . '(?:\\.(?P<charset>[-A-Za-z0-9_]+))?' // charset
120 294
                    . '(?:@(?P<modifier>[-A-Za-z0-9_]+))?$/', // @ modifier
121
                    $locale,
122 280
                    $matches,
123
                ) === 1
124
            ) {
125
                $lang = $matches['lang'] ?? '';
126 40
                $country = $matches['country'] ?? '';
127 40
                $charset = $matches['charset'] ?? '';
128 40
                $modifier = $matches['modifier'] ?? '';
129
130
                if ($modifier !== '') {
131 266
                    if ($country !== '') {
132 266
                        if ($charset !== '') {
133 266
                            $localeNames[] = sprintf('%s_%s.%s@%s', $lang, $country, $charset, $modifier);
134 266
                        }
135
136 266
                        $localeNames[] = sprintf('%s_%s@%s', $lang, $country, $modifier);
137 70
                    } elseif ($charset !== '') {
138 28
                        $localeNames[] = sprintf('%s.%s@%s', $lang, $charset, $modifier);
139 28
                    }
140 4
141 28
                    $localeNames[] = sprintf('%s@%s', $lang, $modifier);
142
                }
143
144
                if ($country !== '') {
145 28
                    if ($charset !== '') {
146 4
                        $localeNames[] = sprintf('%s_%s.%s', $lang, $country, $charset);
147 28
                    }
148
149 42
                    $localeNames[] = sprintf('%s_%s', $lang, $country);
150 14
                } elseif ($charset !== '') {
151 2
                    $localeNames[] = sprintf('%s.%s', $lang, $charset);
152 14
                }
153
154
                if ($lang !== '') {
155
                    $localeNames[] = $lang;
156 70
                }
157 10
            }
158 70
159
            // If the locale name doesn't match POSIX style, just include it as-is.
160
            if (! in_array($locale, $localeNames, true)) {
161
                $localeNames[] = $locale;
162 266
            }
163 140
        }
164 42
165 6
        return $localeNames;
166 42
    }
167
168
    /**
169
     * Sets factory responsible for composing a `CacheInterface`
170 140
     */
171 20
    public static function setCacheFactory(CacheFactoryInterface|null $cacheFactory): void
172 140
    {
173
        self::$cacheFactory = $cacheFactory;
174 140
    }
175 28
176 4
    /**
177 28
     * Returns Translator object for domain or for default domain.
178
     *
179
     * @param string $domain Translation domain
180
     */
181 266
    public function getTranslator(string $domain = ''): Translator
182
    {
183
        if ($domain === '') {
184
            $domain = $this->defaultDomain;
185 280
        }
186 14
187
        $this->domains[$this->locale] ??= [];
188
189
        if (! isset($this->domains[$this->locale][$domain])) {
190 294
            $base = $this->paths[$domain] ?? './';
191
192
            $localeNames = self::listLocales($this->locale);
193
194
            $filename = '';
195
            foreach ($localeNames as $locale) {
196 14
                $filename = $base . '/' . $locale . '/LC_MESSAGES/' . $domain . '.mo';
197
                if (file_exists($filename)) {
198 14
                    break;
199 2
                }
200
            }
201
202
            // We don't care about invalid path, we will get fallback
203
            // translator here
204
            $moParser = new MoParser($filename);
205
            if (self::$cacheFactory instanceof CacheFactoryInterface) {
206 154
                $cache = self::$cacheFactory->getInstance($moParser, $this->locale, $domain);
207
            } else {
208 154
                $cache = new InMemoryCache($moParser);
209 84
            }
210
211
            $this->domains[$this->locale][$domain] = new Translator($cache);
212 154
        }
213 126
214
        return $this->domains[$this->locale][$domain];
215
    }
216 154
217 140
    /**
218 112
     * Sets the path for a domain.
219
     *
220 28
     * @param string $domain Domain name
221
     * @param string $path   Path where to find locales
222
     */
223 140
    public function bindtextdomain(string $domain, string $path): void
224
    {
225 140
        $this->paths[$domain] = $path;
226 140
    }
227 140
228 140
    /**
229 98
     * Sets the default domain.
230
     *
231
     * @param string $domain Domain name
232
     */
233
    public function textdomain(string $domain): void
234
    {
235 140
        $this->defaultDomain = $domain;
236 140
    }
237 14
238
    /**
239 126
     * Sets a requested locale.
240
     *
241
     * @param string $locale Locale name
242 140
     *
243
     * @return string Set or current locale
244
     */
245 154
    public function setlocale(string $locale): string
246
    {
247
        if (! empty($locale)) {
248
            $this->locale = $locale;
249
        }
250
251
        return $this->locale;
252
    }
253
254 154
    /**
255
     * Detects currently configured locale.
256 154
     *
257 22
     * It checks:
258
     *
259
     * - global lang variable
260
     * - environment for LC_ALL, LC_MESSAGES and LANG
261
     *
262
     * @return string with locale name
263
     */
264 140
    public function detectlocale(): string
265
    {
266 140
        if (isset($GLOBALS['lang'])) {
267 20
            return $GLOBALS['lang'];
268
        }
269
270
        $locale = getenv('LC_ALL');
271
        if ($locale !== false) {
272
            return $locale;
273
        }
274
275
        $locale = getenv('LC_MESSAGES');
276 154
        if ($locale !== false) {
277
            return $locale;
278 154
        }
279 154
280
        $locale = getenv('LANG');
281
        if ($locale !== false) {
282 154
            return $locale;
283
        }
284
285
        return 'en';
286
    }
287
}
288