Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
104 / 104
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
1 / 1
I18n
100.00% covered (success)
100.00%
104 / 104
100.00% covered (success)
100.00%
13 / 13
54
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 getLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLocale
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadFile
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 __
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 _e
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguages
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 translate
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
 loadCurrentLanguage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 loadXmlFile
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
10
 loadJsonFile
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 getXmlLanguages
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getJsonLanguages
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\I18n;
16
17use SimpleXMLElement;
18
19/**
20 * I18n and l10n class
21 *
22 * @category   Pop
23 * @package    Pop_I18n
24 * @author     Nick Sagona, III <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    4.1.0
28 */
29class I18n
30{
31
32    /**
33     * Directory with language files in it
34     * @var ?string
35     */
36    protected ?string $directory = null;
37
38    /**
39     * Default system language
40     * @var ?string
41     */
42    protected ?string $language = null;
43
44    /**
45     * Default system locale
46     * @var string
47     */
48    protected ?string $locale = null;
49
50    /**
51     * Language content, keyed by source string. Each value is either the
52     * translated output string, or an array of alternate outputs (keyed by
53     * their 'alt' name when named, otherwise numerically).
54     * @var array
55     */
56    protected array $content = [];
57
58    /**
59     * Constructor
60     *
61     * Instantiate the I18n object
62     *
63     * @param  ?string $lang
64     * @param  ?string $dir
65     */
66    public function __construct(?string $lang = null, ?string $dir = null)
67    {
68        if ($lang === null) {
69            $lang = (defined('POP_LANG')) ? POP_LANG : 'en_US';
70        }
71
72        if (str_contains($lang, '_')) {
73            [$language, $locale] = explode('_', $lang);
74            $this->language = $language;
75            $this->locale   = $locale;
76        } else {
77            $this->language = $lang;
78            $this->locale   = strtoupper($lang);
79        }
80
81        $this->directory = (($dir !== null) && file_exists($dir)) ? realpath($dir) . DIRECTORY_SEPARATOR
82            : __DIR__ . DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR;
83
84        $this->loadCurrentLanguage();
85    }
86
87    /**
88     * Get current language setting
89     *
90     * @return string
91     */
92    public function getLanguage(): string
93    {
94        return $this->language;
95    }
96
97    /**
98     * Get current locale setting
99     *
100     * @return string
101     */
102    public function getLocale(): string
103    {
104        return $this->locale;
105    }
106
107    /**
108     * Load language content from an XML or JSON file
109     *
110     * @param  string $langFile
111     * @throws Exception|\Exception
112     * @return void
113     */
114    public function loadFile(string $langFile): void
115    {
116        if (file_exists($langFile) && (stripos($langFile, '.xml') !== false)) {
117            $this->loadXmlFile($langFile);
118        } else if (file_exists($langFile) && (stripos($langFile, '.json') !== false)) {
119            $this->loadJsonFile($langFile);
120        } else {
121            throw new Exception('Error: The language file ' . $langFile . ' does not exist or is not valid.');
122        }
123    }
124
125    /**
126     * Return the translated string
127     *
128     * @param  string            $str
129     * @param  string|array|null $params
130     * @param  mixed             $variation
131     * @return string
132     */
133    public function __(string $str, string|array|null $params = null, mixed $variation = null): string
134    {
135        return $this->translate($str, $params, $variation);
136    }
137
138    /**
139     * Echo the translated string
140     *
141     * @param  string            $str
142     * @param  string|array|null $params
143     * @param  mixed             $variation
144     * @return void
145     */
146    public function _e(string $str, string|array|null $params = null, mixed $variation = null): void
147    {
148        echo $this->translate($str, $params, $variation);
149    }
150
151    /**
152     * Get languages from the language files in the directory
153     *
154     * @param string $dir
155     * @return array
156     * @throws \Exception
157     */
158    public static function getLanguages(string $dir): array
159    {
160        $langsAry      = [];
161        $langDirectory = $dir;
162
163        if (file_exists($langDirectory)) {
164            $files = scandir($langDirectory);
165            foreach ($files as $file) {
166                if (stripos($file, '.xml') !== false) {
167                    $langsAry = array_merge($langsAry, self::getXmlLanguages($langDirectory . DIRECTORY_SEPARATOR . $file));
168                } else if (stripos($file, '.json') !== false) {
169                    $langsAry = array_merge($langsAry, self::getJsonLanguages($langDirectory . DIRECTORY_SEPARATOR . $file));
170                }
171            }
172        }
173
174        ksort($langsAry);
175        return $langsAry;
176    }
177
178    /**
179     * Translate and return the string
180     *
181     * @param  string            $str
182     * @param  string|array|null $params
183     * @param  mixed             $variation
184     * @return string
185     */
186    protected function translate(string $str, string|array|null $params = null, mixed $variation = null): string
187    {
188        $trans = null;
189
190        if (isset($this->content[$str])) {
191            $output = $this->content[$str];
192            if (($variation !== null) && is_array($output) && isset($output[$variation])) {
193                $trans = $output[$variation];
194            } else {
195                $trans = (is_array($output)) ? reset($output) : $output;
196            }
197        }
198
199        if ($trans === null) {
200            $trans = $str;
201        }
202
203        if ($params !== null) {
204            $replacements = [];
205            foreach ((array)$params as $key => $value) {
206                $replacements['%' . ($key + 1)] = $value;
207            }
208            $trans = strtr($trans, $replacements);
209        }
210
211        return $trans;
212    }
213
214    /**
215     * Get language content from the current language/locale's file, if it exists
216     *
217     * @throws Exception
218     * @return void
219     */
220    protected function loadCurrentLanguage(): void
221    {
222        if (file_exists($this->directory . $this->language . '.xml')) {
223            $this->loadFile($this->directory . $this->language . '.xml');
224        } else if (file_exists($this->directory . $this->language . '.json')) {
225            $this->loadFile($this->directory . $this->language . '.json');
226        }
227    }
228
229    /**
230     * Load language content from an XML file into $content
231     *
232     * @param  string $langFile
233     * @throws \Exception
234     * @return void
235     */
236    protected function loadXmlFile(string $langFile): void
237    {
238        $xml = @new SimpleXMLElement($langFile, LIBXML_NOWARNING, true);
239        $key = null;
240        $i   = 0;
241
242        // Find the locale node key matching the current locale
243        // (SimpleXMLElement's foreach key is the tag name, not a position, so track it manually)
244        foreach ($xml->locale as $locale) {
245            if ($this->locale == (string)$locale->attributes()->region) {
246                $key = $i;
247                break;
248            }
249            $i++;
250        }
251
252        if ($key !== null) {
253            foreach ($xml->locale[$key]->text as $text) {
254                if (isset($text->source) && isset($text->output)) {
255                    $source = (string)$text->source;
256
257                    if (isset($text->output->output)) {
258                        $alternates = [];
259
260                        foreach ($text->output->output as $output) {
261                            $alt = $output->attributes()->alt;
262                            if ($alt !== null) {
263                                $alternates[(string)$alt] = (string)$output;
264                            } else {
265                                $alternates[] = (string)$output;
266                            }
267                        }
268
269                        $this->content[$source] = $alternates;
270                    } else {
271                        $this->content[$source] = (string)$text->output;
272                    }
273                }
274            }
275        }
276    }
277
278    /**
279     * Load language content from a JSON file into $content
280     *
281     * @param  string $langFile
282     * @return void
283     */
284    protected function loadJsonFile(string $langFile): void
285    {
286        $json = json_decode(file_get_contents($langFile), true);
287        $key  = null;
288
289        // Find the locale node key matching the current locale
290        foreach ($json['language']['locale'] as $i => $locale) {
291            if ($this->locale == $locale['region']) {
292                $key = $i;
293                break;
294            }
295        }
296
297        if ($key !== null) {
298            foreach ($json['language']['locale'][$key]['text'] as $text) {
299                if (isset($text['source']) && isset($text['output'])) {
300                    $this->content[(string)$text['source']] = (is_array($text['output'])) ? $text['output'] : (string)$text['output'];
301                }
302            }
303        }
304    }
305
306    /**
307     * Get language info from a single XML language file
308     *
309     * @param  string $file
310     * @return array
311     * @throws \Exception
312     */
313    protected static function getXmlLanguages(string $file): array
314    {
315        $langsAry = [];
316
317        $xml        =@ new SimpleXMLElement($file, LIBXML_NOWARNING, true);
318        $lang       = (string)$xml->attributes()->output;
319        $langName   = (string)$xml->attributes()->name;
320        $langNative = (string)$xml->attributes()->native;
321
322        foreach ($xml->locale as $locale) {
323            $region = (string)$locale->attributes()->region;
324            $name   = (string)$locale->attributes()->name;
325            $native = (string)$locale->attributes()->native;
326            $native .= ' (' . $langName . ', ' . $name . ')';
327            $langsAry[$lang . '_' . $region] = $langNative . ', ' . $native;
328        }
329
330        return $langsAry;
331    }
332
333    /**
334     * Get language info from a single JSON language file
335     *
336     * @param  string $file
337     * @return array
338     */
339    protected static function getJsonLanguages(string $file): array
340    {
341        $langsAry = [];
342
343        $json       = json_decode(file_get_contents($file), true);
344        $lang       = $json['language']['output'];
345        $langName   = $json['language']['name'];
346        $langNative = $json['language']['native'];
347
348        foreach ($json['language']['locale'] as $locale) {
349            $region = $locale['region'];
350            $name   = $locale['name'];
351            $native = $locale['native'];
352            $native .= ' (' . $langName . ', ' . $name . ')';
353            $langsAry[$lang . '_' . $region] = $langNative . ', ' . $native;
354        }
355
356        return $langsAry;
357    }
358
359}