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