| <?php |
| |
| /** |
| * This file is part of the Carbon package. |
| * |
| * (c) Brian Nesbitt <brian@nesbot.com> |
| * |
| * For the full copyright and license information, please view the LICENSE |
| * file that was distributed with this source code. |
| */ |
| namespace Carbon; |
| |
| use Closure; |
| use ReflectionException; |
| use ReflectionFunction; |
| use Symfony\Component\Translation; |
| |
| class Translator extends Translation\Translator |
| { |
| /** |
| * Translator singletons for each language. |
| * |
| * @var array |
| */ |
| protected static $singletons = []; |
| |
| /** |
| * List of custom localized messages. |
| * |
| * @var array |
| */ |
| protected $messages = []; |
| |
| /** |
| * List of custom directories that contain translation files. |
| * |
| * @var string[] |
| */ |
| protected $directories = []; |
| |
| /** |
| * Set to true while constructing. |
| * |
| * @var bool |
| */ |
| protected $initializing = false; |
| |
| /** |
| * List of locales aliases. |
| * |
| * @var string[] |
| */ |
| protected $aliases = [ |
| 'me' => 'sr_Latn_ME', |
| 'scr' => 'sh', |
| ]; |
| |
| /** |
| * Return a singleton instance of Translator. |
| * |
| * @param string|null $locale optional initial locale ("en" - english by default) |
| * |
| * @return static |
| */ |
| public static function get($locale = null) |
| { |
| $locale = $locale ?: 'en'; |
| |
| if (!isset(static::$singletons[$locale])) { |
| static::$singletons[$locale] = new static($locale ?: 'en'); |
| } |
| |
| return static::$singletons[$locale]; |
| } |
| |
| public function __construct($locale, Translation\Formatter\MessageFormatterInterface $formatter = null, $cacheDir = null, $debug = false) |
| { |
| $this->initializing = true; |
| $this->directories = [__DIR__.'/Lang']; |
| $this->addLoader('array', new Translation\Loader\ArrayLoader()); |
| parent::__construct($locale, $formatter, $cacheDir, $debug); |
| $this->initializing = false; |
| } |
| |
| /** |
| * Returns the list of directories translation files are searched in. |
| * |
| * @return array |
| */ |
| public function getDirectories(): array |
| { |
| return $this->directories; |
| } |
| |
| /** |
| * Set list of directories translation files are searched in. |
| * |
| * @param array $directories new directories list |
| * |
| * @return $this |
| */ |
| public function setDirectories(array $directories) |
| { |
| $this->directories = $directories; |
| |
| return $this; |
| } |
| |
| /** |
| * Add a directory to the list translation files are searched in. |
| * |
| * @param string $directory new directory |
| * |
| * @return $this |
| */ |
| public function addDirectory(string $directory) |
| { |
| $this->directories[] = $directory; |
| |
| return $this; |
| } |
| |
| /** |
| * Remove a directory from the list translation files are searched in. |
| * |
| * @param string $directory directory path |
| * |
| * @return $this |
| */ |
| public function removeDirectory(string $directory) |
| { |
| $search = rtrim(strtr($directory, '\\', '/'), '/'); |
| |
| return $this->setDirectories(array_filter($this->getDirectories(), function ($item) use ($search) { |
| return rtrim(strtr($item, '\\', '/'), '/') !== $search; |
| })); |
| } |
| |
| /** |
| * Returns the translation. |
| * |
| * @param string $id |
| * @param array $parameters |
| * @param string $domain |
| * @param string $locale |
| * |
| * @return string |
| */ |
| public function trans($id, array $parameters = [], $domain = null, $locale = null) |
| { |
| if ($domain === null) { |
| $domain = 'messages'; |
| } |
| |
| $format = $this->getCatalogue($locale)->get((string) $id, $domain); |
| |
| if ($format instanceof Closure) { |
| // @codeCoverageIgnoreStart |
| try { |
| $count = (new ReflectionFunction($format))->getNumberOfRequiredParameters(); |
| } catch (ReflectionException $exception) { |
| $count = 0; |
| } |
| // @codeCoverageIgnoreEnd |
| |
| return $format( |
| ...array_values($parameters), |
| ...array_fill(0, max(0, $count - \count($parameters)), null) |
| ); |
| } |
| |
| return parent::trans($id, $parameters, $domain, $locale); |
| } |
| |
| /** |
| * Reset messages of a locale (all locale if no locale passed). |
| * Remove custom messages and reload initial messages from matching |
| * file in Lang directory. |
| * |
| * @param string|null $locale |
| * |
| * @return bool |
| */ |
| public function resetMessages($locale = null) |
| { |
| if ($locale === null) { |
| $this->messages = []; |
| |
| return true; |
| } |
| |
| foreach ($this->getDirectories() as $directory) { |
| $data = @include sprintf('%s/%s.php', rtrim($directory, '\\/'), $locale); |
| |
| if ($data !== false) { |
| $this->messages[$locale] = $data; |
| $this->addResource('array', $this->messages[$locale], $locale); |
| |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns the list of files matching a given locale prefix (or all if empty). |
| * |
| * @param string $prefix prefix required to filter result |
| * |
| * @return array |
| */ |
| public function getLocalesFiles($prefix = '') |
| { |
| $files = []; |
| |
| foreach ($this->getDirectories() as $directory) { |
| $directory = rtrim($directory, '\\/'); |
| |
| foreach (glob("$directory/$prefix*.php") as $file) { |
| $files[] = $file; |
| } |
| } |
| |
| return array_unique($files); |
| } |
| |
| /** |
| * Returns the list of internally available locales and already loaded custom locales. |
| * (It will ignore custom translator dynamic loading.) |
| * |
| * @param string $prefix prefix required to filter result |
| * |
| * @return array |
| */ |
| public function getAvailableLocales($prefix = '') |
| { |
| $locales = []; |
| foreach ($this->getLocalesFiles($prefix) as $file) { |
| $locales[] = substr($file, strrpos($file, '/') + 1, -4); |
| } |
| |
| return array_unique(array_merge($locales, array_keys($this->messages))); |
| } |
| |
| /** |
| * Init messages language from matching file in Lang directory. |
| * |
| * @param string $locale |
| * |
| * @return bool |
| */ |
| protected function loadMessagesFromFile($locale) |
| { |
| if (isset($this->messages[$locale])) { |
| return true; |
| } |
| |
| return $this->resetMessages($locale); |
| } |
| |
| /** |
| * Set messages of a locale and take file first if present. |
| * |
| * @param string $locale |
| * @param array $messages |
| * |
| * @return $this |
| */ |
| public function setMessages($locale, $messages) |
| { |
| $this->loadMessagesFromFile($locale); |
| $this->addResource('array', $messages, $locale); |
| $this->messages[$locale] = array_merge( |
| isset($this->messages[$locale]) ? $this->messages[$locale] : [], |
| $messages |
| ); |
| |
| return $this; |
| } |
| |
| /** |
| * Set messages of the current locale and take file first if present. |
| * |
| * @param array $messages |
| * |
| * @return $this |
| */ |
| public function setTranslations($messages) |
| { |
| return $this->setMessages($this->getLocale(), $messages); |
| } |
| |
| /** |
| * Get messages of a locale, if none given, return all the |
| * languages. |
| * |
| * @param string|null $locale |
| * |
| * @return array |
| */ |
| public function getMessages($locale = null) |
| { |
| return $locale === null ? $this->messages : $this->messages[$locale]; |
| } |
| |
| /** |
| * Set the current translator locale and indicate if the source locale file exists |
| * |
| * @param string $locale locale ex. en |
| * |
| * @return bool |
| */ |
| public function setLocale($locale) |
| { |
| $locale = preg_replace_callback('/[-_]([a-z]{2,}|[0-9]{2,})/', function ($matches) { |
| // _2-letters or YUE is a region, _3+-letters is a variant |
| $upper = strtoupper($matches[1]); |
| |
| if ($upper === 'YUE' || $upper === 'ISO' || \strlen($upper) < 3) { |
| return "_$upper"; |
| } |
| |
| return '_'.ucfirst($matches[1]); |
| }, strtolower($locale)); |
| |
| $previousLocale = $this->getLocale(); |
| |
| if ($previousLocale === $locale && isset($this->messages[$locale])) { |
| return true; |
| } |
| |
| unset(static::$singletons[$previousLocale]); |
| |
| if ($locale === 'auto') { |
| $completeLocale = setlocale(LC_TIME, '0'); |
| $locale = preg_replace('/^([^_.-]+).*$/', '$1', $completeLocale); |
| $locales = $this->getAvailableLocales($locale); |
| |
| $completeLocaleChunks = preg_split('/[_.-]+/', $completeLocale); |
| |
| $getScore = function ($language) use ($completeLocaleChunks) { |
| return static::compareChunkLists($completeLocaleChunks, preg_split('/[_.-]+/', $language)); |
| }; |
| |
| usort($locales, function ($first, $second) use ($getScore) { |
| return $getScore($second) <=> $getScore($first); |
| }); |
| |
| $locale = $locales[0]; |
| } |
| |
| if (isset($this->aliases[$locale])) { |
| $locale = $this->aliases[$locale]; |
| } |
| |
| // If subtag (ex: en_CA) first load the macro (ex: en) to have a fallback |
| if (str_contains($locale, '_') && |
| $this->loadMessagesFromFile($macroLocale = preg_replace('/^([^_]+).*$/', '$1', $locale)) |
| ) { |
| parent::setLocale($macroLocale); |
| } |
| |
| if ($this->loadMessagesFromFile($locale) || $this->initializing) { |
| parent::setLocale($locale); |
| |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Show locale on var_dump(). |
| * |
| * @return array |
| */ |
| public function __debugInfo() |
| { |
| return [ |
| 'locale' => $this->getLocale(), |
| ]; |
| } |
| |
| private static function compareChunkLists($referenceChunks, $chunks) |
| { |
| $score = 0; |
| |
| foreach ($referenceChunks as $index => $chunk) { |
| if (!isset($chunks[$index])) { |
| $score++; |
| |
| continue; |
| } |
| |
| if (strtolower($chunks[$index]) === strtolower($chunk)) { |
| $score += 10; |
| } |
| } |
| |
| return $score; |
| } |
| } |