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 */ 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 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 "$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} 809