blob: ddd2b4e9839947b528c847d8da343f172648e760 [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 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010011
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020012namespace Carbon\Traits;
13
14use Carbon\CarbonInterface;
15use Carbon\Exceptions\InvalidTypeException;
16use Carbon\Exceptions\NotLocaleAwareException;
17use Carbon\Language;
18use Carbon\Translator;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010019use Carbon\TranslatorStrongTypeInterface;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020020use Closure;
21use Symfony\Component\Translation\TranslatorBagInterface;
22use Symfony\Component\Translation\TranslatorInterface;
23use Symfony\Contracts\Translation\LocaleAwareInterface;
24use Symfony\Contracts\Translation\TranslatorInterface as ContractsTranslatorInterface;
25
26if (!interface_exists('Symfony\\Component\\Translation\\TranslatorInterface')) {
27 class_alias(
28 'Symfony\\Contracts\\Translation\\TranslatorInterface',
29 'Symfony\\Component\\Translation\\TranslatorInterface'
30 );
31}
32
33/**
34 * Trait Localization.
35 *
36 * Embed default and locale translators and translation base methods.
37 */
38trait Localization
39{
40 /**
41 * Default translator.
42 *
43 * @var \Symfony\Component\Translation\TranslatorInterface
44 */
45 protected static $translator;
46
47 /**
48 * Specific translator of the current instance.
49 *
50 * @var \Symfony\Component\Translation\TranslatorInterface
51 */
52 protected $localTranslator;
53
54 /**
55 * Options for diffForHumans().
56 *
57 * @var int
58 */
59 protected static $humanDiffOptions = CarbonInterface::NO_ZERO_DIFF;
60
61 /**
62 * @deprecated To avoid conflict between different third-party libraries, static setters should not be used.
63 * You should rather use the ->settings() method.
64 * @see settings
65 *
66 * @param int $humanDiffOptions
67 */
68 public static function setHumanDiffOptions($humanDiffOptions)
69 {
70 static::$humanDiffOptions = $humanDiffOptions;
71 }
72
73 /**
74 * @deprecated To avoid conflict between different third-party libraries, static setters should not be used.
75 * You should rather use the ->settings() method.
76 * @see settings
77 *
78 * @param int $humanDiffOption
79 */
80 public static function enableHumanDiffOption($humanDiffOption)
81 {
82 static::$humanDiffOptions = static::getHumanDiffOptions() | $humanDiffOption;
83 }
84
85 /**
86 * @deprecated To avoid conflict between different third-party libraries, static setters should not be used.
87 * You should rather use the ->settings() method.
88 * @see settings
89 *
90 * @param int $humanDiffOption
91 */
92 public static function disableHumanDiffOption($humanDiffOption)
93 {
94 static::$humanDiffOptions = static::getHumanDiffOptions() & ~$humanDiffOption;
95 }
96
97 /**
98 * Return default humanDiff() options (merged flags as integer).
99 *
100 * @return int
101 */
102 public static function getHumanDiffOptions()
103 {
104 return static::$humanDiffOptions;
105 }
106
107 /**
108 * Get the default translator instance in use.
109 *
110 * @return \Symfony\Component\Translation\TranslatorInterface
111 */
112 public static function getTranslator()
113 {
114 return static::translator();
115 }
116
117 /**
118 * Set the default translator instance to use.
119 *
120 * @param \Symfony\Component\Translation\TranslatorInterface $translator
121 *
122 * @return void
123 */
124 public static function setTranslator(TranslatorInterface $translator)
125 {
126 static::$translator = $translator;
127 }
128
129 /**
130 * Return true if the current instance has its own translator.
131 *
132 * @return bool
133 */
134 public function hasLocalTranslator()
135 {
136 return isset($this->localTranslator);
137 }
138
139 /**
140 * Get the translator of the current instance or the default if none set.
141 *
142 * @return \Symfony\Component\Translation\TranslatorInterface
143 */
144 public function getLocalTranslator()
145 {
146 return $this->localTranslator ?: static::translator();
147 }
148
149 /**
150 * Set the translator for the current instance.
151 *
152 * @param \Symfony\Component\Translation\TranslatorInterface $translator
153 *
154 * @return $this
155 */
156 public function setLocalTranslator(TranslatorInterface $translator)
157 {
158 $this->localTranslator = $translator;
159
160 return $this;
161 }
162
163 /**
164 * Returns raw translation message for a given key.
165 *
166 * @param \Symfony\Component\Translation\TranslatorInterface $translator the translator to use
167 * @param string $key key to find
168 * @param string|null $locale current locale used if null
169 * @param string|null $default default value if translation returns the key
170 *
171 * @return string
172 */
173 public static function getTranslationMessageWith($translator, string $key, ?string $locale = null, ?string $default = null)
174 {
175 if (!($translator instanceof TranslatorBagInterface && $translator instanceof TranslatorInterface)) {
176 throw new InvalidTypeException(
177 'Translator does not implement '.TranslatorInterface::class.' and '.TranslatorBagInterface::class.'. '.
178 (\is_object($translator) ? \get_class($translator) : \gettype($translator)).' has been given.'
179 );
180 }
181
182 if (!$locale && $translator instanceof LocaleAwareInterface) {
183 $locale = $translator->getLocale();
184 }
185
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100186 $result = self::getFromCatalogue($translator, $translator->getCatalogue($locale), $key);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200187
188 return $result === $key ? $default : $result;
189 }
190
191 /**
192 * Returns raw translation message for a given key.
193 *
194 * @param string $key key to find
195 * @param string|null $locale current locale used if null
196 * @param string|null $default default value if translation returns the key
197 * @param \Symfony\Component\Translation\TranslatorInterface $translator an optional translator to use
198 *
199 * @return string
200 */
201 public function getTranslationMessage(string $key, ?string $locale = null, ?string $default = null, $translator = null)
202 {
203 return static::getTranslationMessageWith($translator ?: $this->getLocalTranslator(), $key, $locale, $default);
204 }
205
206 /**
207 * Translate using translation string or callback available.
208 *
209 * @param \Symfony\Component\Translation\TranslatorInterface $translator
210 * @param string $key
211 * @param array $parameters
212 * @param null $number
213 *
214 * @return string
215 */
216 public static function translateWith(TranslatorInterface $translator, string $key, array $parameters = [], $number = null): string
217 {
218 $message = static::getTranslationMessageWith($translator, $key, null, $key);
219 if ($message instanceof Closure) {
220 return (string) $message(...array_values($parameters));
221 }
222
223 if ($number !== null) {
224 $parameters['%count%'] = $number;
225 }
226 if (isset($parameters['%count%'])) {
227 $parameters[':count'] = $parameters['%count%'];
228 }
229
230 // @codeCoverageIgnoreStart
231 $choice = $translator instanceof ContractsTranslatorInterface
232 ? $translator->trans($key, $parameters)
233 : $translator->transChoice($key, $number, $parameters);
234 // @codeCoverageIgnoreEnd
235
236 return (string) $choice;
237 }
238
239 /**
240 * Translate using translation string or callback available.
241 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100242 * @param string $key
243 * @param array $parameters
244 * @param string|int|float|null $number
245 * @param \Symfony\Component\Translation\TranslatorInterface|null $translator
246 * @param bool $altNumbers
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200247 *
248 * @return string
249 */
250 public function translate(string $key, array $parameters = [], $number = null, ?TranslatorInterface $translator = null, bool $altNumbers = false): string
251 {
252 $translation = static::translateWith($translator ?: $this->getLocalTranslator(), $key, $parameters, $number);
253
254 if ($number !== null && $altNumbers) {
255 return str_replace($number, $this->translateNumber($number), $translation);
256 }
257
258 return $translation;
259 }
260
261 /**
262 * Returns the alternative number for a given integer if available in the current locale.
263 *
264 * @param int $number
265 *
266 * @return string
267 */
268 public function translateNumber(int $number): string
269 {
270 $translateKey = "alt_numbers.$number";
271 $symbol = $this->translate($translateKey);
272
273 if ($symbol !== $translateKey) {
274 return $symbol;
275 }
276
277 if ($number > 99 && $this->translate('alt_numbers.99') !== 'alt_numbers.99') {
278 $start = '';
279 foreach ([10000, 1000, 100] as $exp) {
280 $key = "alt_numbers_pow.$exp";
281 if ($number >= $exp && $number < $exp * 10 && ($pow = $this->translate($key)) !== $key) {
282 $unit = floor($number / $exp);
283 $number -= $unit * $exp;
284 $start .= ($unit > 1 ? $this->translate("alt_numbers.$unit") : '').$pow;
285 }
286 }
287 $result = '';
288 while ($number) {
289 $chunk = $number % 100;
290 $result = $this->translate("alt_numbers.$chunk").$result;
291 $number = floor($number / 100);
292 }
293
294 return "$start$result";
295 }
296
297 if ($number > 9 && $this->translate('alt_numbers.9') !== 'alt_numbers.9') {
298 $result = '';
299 while ($number) {
300 $chunk = $number % 10;
301 $result = $this->translate("alt_numbers.$chunk").$result;
302 $number = floor($number / 10);
303 }
304
305 return $result;
306 }
307
308 return (string) $number;
309 }
310
311 /**
312 * Translate a time string from a locale to an other.
313 *
314 * @param string $timeString date/time/duration string to translate (may also contain English)
315 * @param string|null $from input locale of the $timeString parameter (`Carbon::getLocale()` by default)
316 * @param string|null $to output locale of the result returned (`"en"` by default)
317 * @param int $mode specify what to translate with options:
318 * - CarbonInterface::TRANSLATE_ALL (default)
319 * - CarbonInterface::TRANSLATE_MONTHS
320 * - CarbonInterface::TRANSLATE_DAYS
321 * - CarbonInterface::TRANSLATE_UNITS
322 * - CarbonInterface::TRANSLATE_MERIDIEM
323 * You can use pipe to group: CarbonInterface::TRANSLATE_MONTHS | CarbonInterface::TRANSLATE_DAYS
324 *
325 * @return string
326 */
327 public static function translateTimeString($timeString, $from = null, $to = null, $mode = CarbonInterface::TRANSLATE_ALL)
328 {
329 // Fallback source and destination locales
330 $from = $from ?: static::getLocale();
331 $to = $to ?: 'en';
332
333 if ($from === $to) {
334 return $timeString;
335 }
336
337 // Standardize apostrophe
338 $timeString = strtr($timeString, ['’' => "'"]);
339
340 $fromTranslations = [];
341 $toTranslations = [];
342
343 foreach (['from', 'to'] as $key) {
344 $language = $$key;
345 $translator = Translator::get($language);
346 $translations = $translator->getMessages();
347
348 if (!isset($translations[$language])) {
349 return $timeString;
350 }
351
352 $translationKey = $key.'Translations';
353 $messages = $translations[$language];
354 $months = $messages['months'] ?? [];
355 $weekdays = $messages['weekdays'] ?? [];
356 $meridiem = $messages['meridiem'] ?? ['AM', 'PM'];
357
358 if ($key === 'from') {
359 foreach (['months', 'weekdays'] as $variable) {
360 $list = $messages[$variable.'_standalone'] ?? null;
361
362 if ($list) {
363 foreach ($$variable as $index => &$name) {
364 $name .= '|'.$messages[$variable.'_standalone'][$index];
365 }
366 }
367 }
368 }
369
370 $$translationKey = array_merge(
371 $mode & CarbonInterface::TRANSLATE_MONTHS ? static::getTranslationArray($months, 12, $timeString) : [],
372 $mode & CarbonInterface::TRANSLATE_MONTHS ? static::getTranslationArray($messages['months_short'] ?? [], 12, $timeString) : [],
373 $mode & CarbonInterface::TRANSLATE_DAYS ? static::getTranslationArray($weekdays, 7, $timeString) : [],
374 $mode & CarbonInterface::TRANSLATE_DAYS ? static::getTranslationArray($messages['weekdays_short'] ?? [], 7, $timeString) : [],
375 $mode & CarbonInterface::TRANSLATE_DIFF ? static::translateWordsByKeys([
376 'diff_now',
377 'diff_today',
378 'diff_yesterday',
379 'diff_tomorrow',
380 'diff_before_yesterday',
381 'diff_after_tomorrow',
382 ], $messages, $key) : [],
383 $mode & CarbonInterface::TRANSLATE_UNITS ? static::translateWordsByKeys([
384 'year',
385 'month',
386 'week',
387 'day',
388 'hour',
389 'minute',
390 'second',
391 ], $messages, $key) : [],
392 $mode & CarbonInterface::TRANSLATE_MERIDIEM ? array_map(function ($hour) use ($meridiem) {
393 if (\is_array($meridiem)) {
394 return $meridiem[$hour < 12 ? 0 : 1];
395 }
396
397 return $meridiem($hour, 0, false);
398 }, range(0, 23)) : []
399 );
400 }
401
402 return substr(preg_replace_callback('/(?<=[\d\s+.\/,_-])('.implode('|', $fromTranslations).')(?=[\d\s+.\/,_-])/iu', function ($match) use ($fromTranslations, $toTranslations) {
403 [$chunk] = $match;
404
405 foreach ($fromTranslations as $index => $word) {
406 if (preg_match("/^$word\$/iu", $chunk)) {
407 return $toTranslations[$index] ?? '';
408 }
409 }
410
411 return $chunk; // @codeCoverageIgnore
412 }, " $timeString "), 1, -1);
413 }
414
415 /**
416 * Translate a time string from the current locale (`$date->locale()`) to an other.
417 *
418 * @param string $timeString time string to translate
419 * @param string|null $to output locale of the result returned ("en" by default)
420 *
421 * @return string
422 */
423 public function translateTimeStringTo($timeString, $to = null)
424 {
425 return static::translateTimeString($timeString, $this->getTranslatorLocale(), $to);
426 }
427
428 /**
429 * Get/set the locale for the current instance.
430 *
431 * @param string|null $locale
432 * @param string ...$fallbackLocales
433 *
434 * @return $this|string
435 */
436 public function locale(string $locale = null, ...$fallbackLocales)
437 {
438 if ($locale === null) {
439 return $this->getTranslatorLocale();
440 }
441
442 if (!$this->localTranslator || $this->getTranslatorLocale($this->localTranslator) !== $locale) {
443 $translator = Translator::get($locale);
444
445 if (!empty($fallbackLocales)) {
446 $translator->setFallbackLocales($fallbackLocales);
447
448 foreach ($fallbackLocales as $fallbackLocale) {
449 $messages = Translator::get($fallbackLocale)->getMessages();
450
451 if (isset($messages[$fallbackLocale])) {
452 $translator->setMessages($fallbackLocale, $messages[$fallbackLocale]);
453 }
454 }
455 }
456
457 $this->setLocalTranslator($translator);
458 }
459
460 return $this;
461 }
462
463 /**
464 * Get the current translator locale.
465 *
466 * @return string
467 */
468 public static function getLocale()
469 {
470 return static::getLocaleAwareTranslator()->getLocale();
471 }
472
473 /**
474 * Set the current translator locale and indicate if the source locale file exists.
475 * Pass 'auto' as locale to use closest language from the current LC_TIME locale.
476 *
477 * @param string $locale locale ex. en
478 *
479 * @return bool
480 */
481 public static function setLocale($locale)
482 {
483 return static::getLocaleAwareTranslator()->setLocale($locale) !== false;
484 }
485
486 /**
487 * Set the fallback locale.
488 *
489 * @see https://symfony.com/doc/current/components/translation.html#fallback-locales
490 *
491 * @param string $locale
492 */
493 public static function setFallbackLocale($locale)
494 {
495 $translator = static::getTranslator();
496
497 if (method_exists($translator, 'setFallbackLocales')) {
498 $translator->setFallbackLocales([$locale]);
499
500 if ($translator instanceof Translator) {
501 $preferredLocale = $translator->getLocale();
502 $translator->setMessages($preferredLocale, array_replace_recursive(
503 $translator->getMessages()[$locale] ?? [],
504 Translator::get($locale)->getMessages()[$locale] ?? [],
505 $translator->getMessages($preferredLocale)
506 ));
507 }
508 }
509 }
510
511 /**
512 * Get the fallback locale.
513 *
514 * @see https://symfony.com/doc/current/components/translation.html#fallback-locales
515 *
516 * @return string|null
517 */
518 public static function getFallbackLocale()
519 {
520 $translator = static::getTranslator();
521
522 if (method_exists($translator, 'getFallbackLocales')) {
523 return $translator->getFallbackLocales()[0] ?? null;
524 }
525
526 return null;
527 }
528
529 /**
530 * Set the current locale to the given, execute the passed function, reset the locale to previous one,
531 * then return the result of the closure (or null if the closure was void).
532 *
533 * @param string $locale locale ex. en
534 * @param callable $func
535 *
536 * @return mixed
537 */
538 public static function executeWithLocale($locale, $func)
539 {
540 $currentLocale = static::getLocale();
541 $result = $func(static::setLocale($locale) ? static::getLocale() : false, static::translator());
542 static::setLocale($currentLocale);
543
544 return $result;
545 }
546
547 /**
548 * Returns true if the given locale is internally supported and has short-units support.
549 * Support is considered enabled if either year, day or hour has a short variant translated.
550 *
551 * @param string $locale locale ex. en
552 *
553 * @return bool
554 */
555 public static function localeHasShortUnits($locale)
556 {
557 return static::executeWithLocale($locale, function ($newLocale, TranslatorInterface $translator) {
558 return $newLocale &&
559 (
560 ($y = static::translateWith($translator, 'y')) !== 'y' &&
561 $y !== static::translateWith($translator, 'year')
562 ) || (
563 ($y = static::translateWith($translator, 'd')) !== 'd' &&
564 $y !== static::translateWith($translator, 'day')
565 ) || (
566 ($y = static::translateWith($translator, 'h')) !== 'h' &&
567 $y !== static::translateWith($translator, 'hour')
568 );
569 });
570 }
571
572 /**
573 * Returns true if the given locale is internally supported and has diff syntax support (ago, from now, before, after).
574 * Support is considered enabled if the 4 sentences are translated in the given locale.
575 *
576 * @param string $locale locale ex. en
577 *
578 * @return bool
579 */
580 public static function localeHasDiffSyntax($locale)
581 {
582 return static::executeWithLocale($locale, function ($newLocale, TranslatorInterface $translator) {
583 if (!$newLocale) {
584 return false;
585 }
586
587 foreach (['ago', 'from_now', 'before', 'after'] as $key) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100588 if ($translator instanceof TranslatorBagInterface &&
589 self::getFromCatalogue($translator, $translator->getCatalogue($newLocale), $key) instanceof Closure
590 ) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200591 continue;
592 }
593
594 if ($translator->trans($key) === $key) {
595 return false;
596 }
597 }
598
599 return true;
600 });
601 }
602
603 /**
604 * Returns true if the given locale is internally supported and has words for 1-day diff (just now, yesterday, tomorrow).
605 * Support is considered enabled if the 3 words are translated in the given locale.
606 *
607 * @param string $locale locale ex. en
608 *
609 * @return bool
610 */
611 public static function localeHasDiffOneDayWords($locale)
612 {
613 return static::executeWithLocale($locale, function ($newLocale, TranslatorInterface $translator) {
614 return $newLocale &&
615 $translator->trans('diff_now') !== 'diff_now' &&
616 $translator->trans('diff_yesterday') !== 'diff_yesterday' &&
617 $translator->trans('diff_tomorrow') !== 'diff_tomorrow';
618 });
619 }
620
621 /**
622 * Returns true if the given locale is internally supported and has words for 2-days diff (before yesterday, after tomorrow).
623 * Support is considered enabled if the 2 words are translated in the given locale.
624 *
625 * @param string $locale locale ex. en
626 *
627 * @return bool
628 */
629 public static function localeHasDiffTwoDayWords($locale)
630 {
631 return static::executeWithLocale($locale, function ($newLocale, TranslatorInterface $translator) {
632 return $newLocale &&
633 $translator->trans('diff_before_yesterday') !== 'diff_before_yesterday' &&
634 $translator->trans('diff_after_tomorrow') !== 'diff_after_tomorrow';
635 });
636 }
637
638 /**
639 * Returns true if the given locale is internally supported and has period syntax support (X times, every X, from X, to X).
640 * Support is considered enabled if the 4 sentences are translated in the given locale.
641 *
642 * @param string $locale locale ex. en
643 *
644 * @return bool
645 */
646 public static function localeHasPeriodSyntax($locale)
647 {
648 return static::executeWithLocale($locale, function ($newLocale, TranslatorInterface $translator) {
649 return $newLocale &&
650 $translator->trans('period_recurrences') !== 'period_recurrences' &&
651 $translator->trans('period_interval') !== 'period_interval' &&
652 $translator->trans('period_start_date') !== 'period_start_date' &&
653 $translator->trans('period_end_date') !== 'period_end_date';
654 });
655 }
656
657 /**
658 * Returns the list of internally available locales and already loaded custom locales.
659 * (It will ignore custom translator dynamic loading.)
660 *
661 * @return array
662 */
663 public static function getAvailableLocales()
664 {
665 $translator = static::getLocaleAwareTranslator();
666
667 return $translator instanceof Translator
668 ? $translator->getAvailableLocales()
669 : [$translator->getLocale()];
670 }
671
672 /**
673 * Returns list of Language object for each available locale. This object allow you to get the ISO name, native
674 * name, region and variant of the locale.
675 *
676 * @return Language[]
677 */
678 public static function getAvailableLocalesInfo()
679 {
680 $languages = [];
681 foreach (static::getAvailableLocales() as $id) {
682 $languages[$id] = new Language($id);
683 }
684
685 return $languages;
686 }
687
688 /**
689 * Initialize the default translator instance if necessary.
690 *
691 * @return \Symfony\Component\Translation\TranslatorInterface
692 */
693 protected static function translator()
694 {
695 if (static::$translator === null) {
696 static::$translator = Translator::get();
697 }
698
699 return static::$translator;
700 }
701
702 /**
703 * Get the locale of a given translator.
704 *
705 * If null or omitted, current local translator is used.
706 * If no local translator is in use, current global translator is used.
707 *
708 * @param null $translator
709 *
710 * @return string|null
711 */
712 protected function getTranslatorLocale($translator = null): ?string
713 {
714 if (\func_num_args() === 0) {
715 $translator = $this->getLocalTranslator();
716 }
717
718 $translator = static::getLocaleAwareTranslator($translator);
719
720 return $translator ? $translator->getLocale() : null;
721 }
722
723 /**
724 * Throw an error if passed object is not LocaleAwareInterface.
725 *
726 * @param LocaleAwareInterface|null $translator
727 *
728 * @return LocaleAwareInterface|null
729 */
730 protected static function getLocaleAwareTranslator($translator = null)
731 {
732 if (\func_num_args() === 0) {
733 $translator = static::translator();
734 }
735
736 if ($translator && !($translator instanceof LocaleAwareInterface || method_exists($translator, 'getLocale'))) {
737 throw new NotLocaleAwareException($translator);
738 }
739
740 return $translator;
741 }
742
743 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100744 * @param mixed $translator
745 * @param \Symfony\Component\Translation\MessageCatalogueInterface $catalogue
746 *
747 * @return mixed
748 */
749 private static function getFromCatalogue($translator, $catalogue, string $id, string $domain = 'messages')
750 {
751 return $translator instanceof TranslatorStrongTypeInterface
752 ? $translator->getFromCatalogue($catalogue, $id, $domain) // @codeCoverageIgnore
753 : $catalogue->get($id, $domain);
754 }
755
756 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200757 * Return the word cleaned from its translation codes.
758 *
759 * @param string $word
760 *
761 * @return string
762 */
763 private static function cleanWordFromTranslationString($word)
764 {
765 $word = str_replace([':count', '%count', ':time'], '', $word);
766 $word = strtr($word, ['’' => "'"]);
767 $word = preg_replace('/({\d+(,(\d+|Inf))?}|[\[\]]\d+(,(\d+|Inf))?[\[\]])/', '', $word);
768
769 return trim($word);
770 }
771
772 /**
773 * Translate a list of words.
774 *
775 * @param string[] $keys keys to translate.
776 * @param string[] $messages messages bag handling translations.
777 * @param string $key 'to' (to get the translation) or 'from' (to get the detection RegExp pattern).
778 *
779 * @return string[]
780 */
781 private static function translateWordsByKeys($keys, $messages, $key): array
782 {
783 return array_map(function ($wordKey) use ($messages, $key) {
784 $message = $key === 'from' && isset($messages[$wordKey.'_regexp'])
785 ? $messages[$wordKey.'_regexp']
786 : ($messages[$wordKey] ?? null);
787
788 if (!$message) {
789 return '>>DO NOT REPLACE<<';
790 }
791
792 $parts = explode('|', $message);
793
794 return $key === 'to'
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100795 ? self::cleanWordFromTranslationString(end($parts))
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200796 : '(?:'.implode('|', array_map([static::class, 'cleanWordFromTranslationString'], $parts)).')';
797 }, $keys);
798 }
799
800 /**
801 * Get an array of translations based on the current date.
802 *
803 * @param callable $translation
804 * @param int $length
805 * @param string $timeString
806 *
807 * @return string[]
808 */
809 private static function getTranslationArray($translation, $length, $timeString): array
810 {
811 $filler = '>>DO NOT REPLACE<<';
812
813 if (\is_array($translation)) {
814 return array_pad($translation, $length, $filler);
815 }
816
817 $list = [];
818 $date = static::now();
819
820 for ($i = 0; $i < $length; $i++) {
821 $list[] = $translation($date, $timeString, $i) ?? $filler;
822 }
823
824 return $list;
825 }
826}