Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
96 / 96
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
I18n
100.00% covered (success)
100.00%
96 / 96
100.00% covered (success)
100.00%
9 / 9
53
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%
33 / 33
100.00% covered (success)
100.00%
1 / 1
22
 __
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%
29 / 29
100.00% covered (success)
100.00%
1 / 1
8
 translate
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
10
 loadCurrentLanguage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\I18n;
15
16use SimpleXMLElement;
17
18/**
19 * I18n and l10n class
20 *
21 * @category   Pop
22 * @package    Pop_I18n
23 * @author     Nick Sagona, III <dev@nolainteractive.com>
24 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
25 * @license    http://www.popphp.org/license     New BSD License
26 * @version    4.0.0
27 */
28class I18n
29{
30
31    /**
32     * Directory with language files in it
33     * @var ?string
34     */
35    protected ?string $directory = null;
36
37    /**
38     * Default system language
39     * @var ?string
40     */
41    protected ?string $language = null;
42
43    /**
44     * Default system locale
45     * @var string
46     */
47    protected ?string $locale = null;
48
49    /**
50     * Language content
51     * @var array
52     */
53    protected array $content = [
54        'source' => [],
55        'output' => []
56    ];
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 file
109     *
110     * @param  string $langFile
111     * @throws Exception|\Exception
112     * @return void
113     */
114    public function loadFile(string $langFile): void
115    {
116        // If an XML file
117        if (file_exists($langFile) && (stripos($langFile, '.xml') !== false)) {
118            if (($xml =@ new SimpleXMLElement($langFile, LIBXML_NOWARNING, true)) !== false) {
119                $key    = 0;
120                $length = count($xml->locale);
121
122                // Find the locale node key
123                for ($i = 0; $i < $length; $i++) {
124                    if ($this->locale == (string)$xml->locale[$i]->attributes()->region) {
125                        $key = $i;
126                    }
127                }
128
129                // If the locale node matches the current locale
130                if ($this->locale == (string)$xml->locale[$key]->attributes()->region) {
131                    foreach ($xml->locale[$key]->text as $text) {
132                        if (isset($text->source) && isset($text->output)) {
133                            $this->content['source'][] = (string)$text->source;
134                            if (isset($text->output->output)) {
135                                $alternates = [];
136
137                                foreach ($text->output->output as $output) {
138                                    $alt = $output->attributes()->alt;
139                                    if ($alt !== null) {
140                                        $alternates[(string)$alt] = (string)$output;
141                                    } else {
142                                        $alternates[] = (string)$output;
143                                    }
144                                }
145
146                                $this->content['output'][] = $alternates;
147                            } else {
148                                $this->content['output'][] = (string)$text->output;
149                            }
150                        }
151                    }
152                }
153            }
154        // Else if a JSON file
155        } else if (file_exists($langFile) && (stripos($langFile, '.json') !== false)) {
156            $json = json_decode(file_get_contents($langFile), true);
157
158            $key    = 0;
159            $length = count($json['language']['locale']);
160
161            // Find the locale node key
162            for ($i = 0; $i < $length; $i++) {
163                if ($this->locale == $json['language']['locale'][$i]['region']) {
164                    $key = $i;
165                }
166            }
167
168            if ($this->locale == $json['language']['locale'][$key]['region']) {
169                foreach ($json['language']['locale'][$key]['text'] as $text) {
170                    if (isset($text['source']) && isset($text['output'])) {
171                        $this->content['source'][] = (string)$text['source'];
172                        $this->content['output'][] = (is_array($text['output'])) ? $text['output'] : (string)$text['output'];
173                    }
174                }
175            }
176        } else {
177            throw new Exception('Error: The language file ' . $langFile . ' does not exist or is not valid.');
178        }
179    }
180
181    /**
182     * Return the translated string
183     *
184     * @param  string            $str
185     * @param  string|array|null $params
186     * @param  mixed             $variation
187     * @return string
188     */
189    public function __(string $str, string|array|null $params = null, mixed $variation = null): string
190    {
191        return $this->translate($str, $params, $variation);
192    }
193
194    /**
195     * Echo the translated string
196     *
197     * @param  string            $str
198     * @param  string|array|null $params
199     * @param  mixed             $variation
200     * @return void
201     */
202    public function _e(string $str, string|array|null $params = null, mixed $variation = null): void
203    {
204        echo $this->translate($str, $params, $variation);
205    }
206
207    /**
208     * Get languages from the XML files
209     *
210     * @param string $dir
211     * @return array
212     * @throws \Exception
213     */
214    public static function getLanguages(string $dir): array
215    {
216        $langsAry      = [];
217        $langDirectory = $dir;
218
219        if (file_exists($langDirectory)) {
220            $files = scandir($langDirectory);
221            foreach ($files as $file) {
222                if (stripos($file, '.xml')) {
223                    if (($xml =@ new SimpleXMLElement($langDirectory . DIRECTORY_SEPARATOR . $file, LIBXML_NOWARNING, true)) !== false) {
224                        $lang       = (string)$xml->attributes()->output;
225                        $langName   = (string)$xml->attributes()->name;
226                        $langNative = (string)$xml->attributes()->native;
227
228                        foreach ($xml->locale as $locale) {
229                            $region = (string)$locale->attributes()->region;
230                            $name   = (string)$locale->attributes()->name;
231                            $native = (string)$locale->attributes()->native;
232                            $native .= ' (' . $langName . ', ' . $name . ')';
233                            $langsAry[$lang . '_' . $region] = $langNative . ', ' . $native;
234                        }
235                    }
236                } else if (stripos($file, '.json')) {
237                    $json = json_decode(file_get_contents($langDirectory . DIRECTORY_SEPARATOR . $file), true);
238                    $lang       = $json['language']['output'];
239                    $langName   = $json['language']['name'];
240                    $langNative = $json['language']['native'];
241
242                    foreach ($json['language']['locale'] as $locale) {
243                        $region = $locale['region'];
244                        $name   = $locale['name'];
245                        $native = $locale['native'];
246                        $native .= ' (' . $langName . ', ' . $name . ')';
247                        $langsAry[$lang . '_' . $region] = $langNative . ', ' . $native;
248                    }
249                }
250            }
251        }
252
253        ksort($langsAry);
254        return $langsAry;
255    }
256
257    /**
258     * Translate and return the string
259     *
260     * @param  string            $str
261     * @param  string|array|null $params
262     * @param  mixed             $variation
263     * @return string
264     */
265    protected function translate(string $str, string|array|null$params = null, mixed $variation = null): string
266    {
267        $key   = array_search($str, $this->content['source']);
268        $trans = null;
269
270        if (($key !== false) && isset($this->content['output'][$key])) {
271            if (($variation !== null) && isset($this->content['output'][$key][$variation])) {
272                $trans = $this->content['output'][$key][$variation];
273            } else {
274                $trans = (is_array($this->content['output'][$key])) ?
275                    reset($this->content['output'][$key]) : $this->content['output'][$key];
276            }
277        }
278
279        if ($trans === null) {
280            $trans = $str;
281        }
282
283        if ($params !== null) {
284            if (is_array($params)) {
285                foreach ($params as $key => $value) {
286                    $trans = str_replace('%' . ($key + 1), $value, $trans);
287                }
288            } else {
289                $trans = str_replace('%1', $params, $trans);
290            }
291        }
292
293        return $trans;
294    }
295
296    /**
297     * Get language content from the XML file
298     *
299     * @throws Exception
300     * @return void
301     */
302    protected function loadCurrentLanguage(): void
303    {
304        if (file_exists($this->directory . $this->language . '.xml')) {
305            $this->loadFile($this->directory . $this->language . '.xml');
306        } else if (file_exists($this->directory . $this->language . '.json')) {
307            $this->loadFile($this->directory . $this->language . '.json');
308        }
309    }
310
311}