404 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
		
		
			
		
	
	
			404 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
|  | <?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; | ||
|  |     } | ||
|  | } |