blob: 9af48ebef11b493a587a51e588a7728ed6845fdc [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/**
4 * This file is part of the Carbon package.
5 *
6 * (c) Brian Nesbitt <brian@nesbot.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11namespace Carbon;
12
13use Closure;
14use ReflectionException;
15use ReflectionFunction;
16use Symfony\Component\Translation;
17
18class Translator extends Translation\Translator
19{
20 /**
21 * Translator singletons for each language.
22 *
23 * @var array
24 */
25 protected static $singletons = [];
26
27 /**
28 * List of custom localized messages.
29 *
30 * @var array
31 */
32 protected $messages = [];
33
34 /**
35 * List of custom directories that contain translation files.
36 *
37 * @var string[]
38 */
39 protected $directories = [];
40
41 /**
42 * Set to true while constructing.
43 *
44 * @var bool
45 */
46 protected $initializing = false;
47
48 /**
49 * List of locales aliases.
50 *
51 * @var string[]
52 */
53 protected $aliases = [
54 'me' => 'sr_Latn_ME',
55 'scr' => 'sh',
56 ];
57
58 /**
59 * Return a singleton instance of Translator.
60 *
61 * @param string|null $locale optional initial locale ("en" - english by default)
62 *
63 * @return static
64 */
65 public static function get($locale = null)
66 {
67 $locale = $locale ?: 'en';
68
69 if (!isset(static::$singletons[$locale])) {
70 static::$singletons[$locale] = new static($locale ?: 'en');
71 }
72
73 return static::$singletons[$locale];
74 }
75
76 public function __construct($locale, Translation\Formatter\MessageFormatterInterface $formatter = null, $cacheDir = null, $debug = false)
77 {
78 $this->initializing = true;
79 $this->directories = [__DIR__.'/Lang'];
80 $this->addLoader('array', new Translation\Loader\ArrayLoader());
81 parent::__construct($locale, $formatter, $cacheDir, $debug);
82 $this->initializing = false;
83 }
84
85 /**
86 * Returns the list of directories translation files are searched in.
87 *
88 * @return array
89 */
90 public function getDirectories(): array
91 {
92 return $this->directories;
93 }
94
95 /**
96 * Set list of directories translation files are searched in.
97 *
98 * @param array $directories new directories list
99 *
100 * @return $this
101 */
102 public function setDirectories(array $directories)
103 {
104 $this->directories = $directories;
105
106 return $this;
107 }
108
109 /**
110 * Add a directory to the list translation files are searched in.
111 *
112 * @param string $directory new directory
113 *
114 * @return $this
115 */
116 public function addDirectory(string $directory)
117 {
118 $this->directories[] = $directory;
119
120 return $this;
121 }
122
123 /**
124 * Remove a directory from the list translation files are searched in.
125 *
126 * @param string $directory directory path
127 *
128 * @return $this
129 */
130 public function removeDirectory(string $directory)
131 {
132 $search = rtrim(strtr($directory, '\\', '/'), '/');
133
134 return $this->setDirectories(array_filter($this->getDirectories(), function ($item) use ($search) {
135 return rtrim(strtr($item, '\\', '/'), '/') !== $search;
136 }));
137 }
138
139 /**
140 * Returns the translation.
141 *
142 * @param string $id
143 * @param array $parameters
144 * @param string $domain
145 * @param string $locale
146 *
147 * @return string
148 */
149 public function trans($id, array $parameters = [], $domain = null, $locale = null)
150 {
151 if ($domain === null) {
152 $domain = 'messages';
153 }
154
155 $format = $this->getCatalogue($locale)->get((string) $id, $domain);
156
157 if ($format instanceof Closure) {
158 // @codeCoverageIgnoreStart
159 try {
160 $count = (new ReflectionFunction($format))->getNumberOfRequiredParameters();
161 } catch (ReflectionException $exception) {
162 $count = 0;
163 }
164 // @codeCoverageIgnoreEnd
165
166 return $format(
167 ...array_values($parameters),
168 ...array_fill(0, max(0, $count - \count($parameters)), null)
169 );
170 }
171
172 return parent::trans($id, $parameters, $domain, $locale);
173 }
174
175 /**
176 * Reset messages of a locale (all locale if no locale passed).
177 * Remove custom messages and reload initial messages from matching
178 * file in Lang directory.
179 *
180 * @param string|null $locale
181 *
182 * @return bool
183 */
184 public function resetMessages($locale = null)
185 {
186 if ($locale === null) {
187 $this->messages = [];
188
189 return true;
190 }
191
192 foreach ($this->getDirectories() as $directory) {
193 $data = @include sprintf('%s/%s.php', rtrim($directory, '\\/'), $locale);
194
195 if ($data !== false) {
196 $this->messages[$locale] = $data;
197 $this->addResource('array', $this->messages[$locale], $locale);
198
199 return true;
200 }
201 }
202
203 return false;
204 }
205
206 /**
207 * Returns the list of files matching a given locale prefix (or all if empty).
208 *
209 * @param string $prefix prefix required to filter result
210 *
211 * @return array
212 */
213 public function getLocalesFiles($prefix = '')
214 {
215 $files = [];
216
217 foreach ($this->getDirectories() as $directory) {
218 $directory = rtrim($directory, '\\/');
219
220 foreach (glob("$directory/$prefix*.php") as $file) {
221 $files[] = $file;
222 }
223 }
224
225 return array_unique($files);
226 }
227
228 /**
229 * Returns the list of internally available locales and already loaded custom locales.
230 * (It will ignore custom translator dynamic loading.)
231 *
232 * @param string $prefix prefix required to filter result
233 *
234 * @return array
235 */
236 public function getAvailableLocales($prefix = '')
237 {
238 $locales = [];
239 foreach ($this->getLocalesFiles($prefix) as $file) {
240 $locales[] = substr($file, strrpos($file, '/') + 1, -4);
241 }
242
243 return array_unique(array_merge($locales, array_keys($this->messages)));
244 }
245
246 /**
247 * Init messages language from matching file in Lang directory.
248 *
249 * @param string $locale
250 *
251 * @return bool
252 */
253 protected function loadMessagesFromFile($locale)
254 {
255 if (isset($this->messages[$locale])) {
256 return true;
257 }
258
259 return $this->resetMessages($locale);
260 }
261
262 /**
263 * Set messages of a locale and take file first if present.
264 *
265 * @param string $locale
266 * @param array $messages
267 *
268 * @return $this
269 */
270 public function setMessages($locale, $messages)
271 {
272 $this->loadMessagesFromFile($locale);
273 $this->addResource('array', $messages, $locale);
274 $this->messages[$locale] = array_merge(
275 isset($this->messages[$locale]) ? $this->messages[$locale] : [],
276 $messages
277 );
278
279 return $this;
280 }
281
282 /**
283 * Set messages of the current locale and take file first if present.
284 *
285 * @param array $messages
286 *
287 * @return $this
288 */
289 public function setTranslations($messages)
290 {
291 return $this->setMessages($this->getLocale(), $messages);
292 }
293
294 /**
295 * Get messages of a locale, if none given, return all the
296 * languages.
297 *
298 * @param string|null $locale
299 *
300 * @return array
301 */
302 public function getMessages($locale = null)
303 {
304 return $locale === null ? $this->messages : $this->messages[$locale];
305 }
306
307 /**
308 * Set the current translator locale and indicate if the source locale file exists
309 *
310 * @param string $locale locale ex. en
311 *
312 * @return bool
313 */
314 public function setLocale($locale)
315 {
316 $locale = preg_replace_callback('/[-_]([a-z]{2,}|[0-9]{2,})/', function ($matches) {
317 // _2-letters or YUE is a region, _3+-letters is a variant
318 $upper = strtoupper($matches[1]);
319
320 if ($upper === 'YUE' || $upper === 'ISO' || \strlen($upper) < 3) {
321 return "_$upper";
322 }
323
324 return '_'.ucfirst($matches[1]);
325 }, strtolower($locale));
326
327 $previousLocale = $this->getLocale();
328
329 if ($previousLocale === $locale && isset($this->messages[$locale])) {
330 return true;
331 }
332
333 unset(static::$singletons[$previousLocale]);
334
335 if ($locale === 'auto') {
336 $completeLocale = setlocale(LC_TIME, '0');
337 $locale = preg_replace('/^([^_.-]+).*$/', '$1', $completeLocale);
338 $locales = $this->getAvailableLocales($locale);
339
340 $completeLocaleChunks = preg_split('/[_.-]+/', $completeLocale);
341
342 $getScore = function ($language) use ($completeLocaleChunks) {
343 return static::compareChunkLists($completeLocaleChunks, preg_split('/[_.-]+/', $language));
344 };
345
346 usort($locales, function ($first, $second) use ($getScore) {
347 return $getScore($second) <=> $getScore($first);
348 });
349
350 $locale = $locales[0];
351 }
352
353 if (isset($this->aliases[$locale])) {
354 $locale = $this->aliases[$locale];
355 }
356
357 // If subtag (ex: en_CA) first load the macro (ex: en) to have a fallback
358 if (str_contains($locale, '_') &&
359 $this->loadMessagesFromFile($macroLocale = preg_replace('/^([^_]+).*$/', '$1', $locale))
360 ) {
361 parent::setLocale($macroLocale);
362 }
363
364 if ($this->loadMessagesFromFile($locale) || $this->initializing) {
365 parent::setLocale($locale);
366
367 return true;
368 }
369
370 return false;
371 }
372
373 /**
374 * Show locale on var_dump().
375 *
376 * @return array
377 */
378 public function __debugInfo()
379 {
380 return [
381 'locale' => $this->getLocale(),
382 ];
383 }
384
385 private static function compareChunkLists($referenceChunks, $chunks)
386 {
387 $score = 0;
388
389 foreach ($referenceChunks as $index => $chunk) {
390 if (!isset($chunks[$index])) {
391 $score++;
392
393 continue;
394 }
395
396 if (strtolower($chunks[$index]) === strtolower($chunk)) {
397 $score += 10;
398 }
399 }
400
401 return $score;
402 }
403}