Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
96 / 96 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
1 / 1 |
I18n | |
100.00% |
96 / 96 |
|
100.00% |
9 / 9 |
53 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
getLanguage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLocale | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadFile | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
22 | |||
__ | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
_e | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLanguages | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
8 | |||
translate | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
10 | |||
loadCurrentLanguage | |
100.00% |
4 / 4 |
|
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 | */ |
14 | namespace Pop\I18n; |
15 | |
16 | use 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 | */ |
28 | class 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 | } |