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