MOON
Server: Apache
System: Linux server1.studioinfinity.com.br 2.6.32-954.3.5.lve1.4.90.el6.x86_64 #1 SMP Tue Feb 21 12:26:30 UTC 2023 x86_64
User: artinside (517)
PHP: 7.4.33
Disabled: exec,passthru,shell_exec,system
Upload Files
File: /home/artinside/sites.artinside.com.br/paliar/vendor/aplus/language/src/Language.php
<?php declare(strict_types=1);
/*
 * This file is part of Aplus Framework Language Library.
 *
 * (c) Natan Felles <natanfelles@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace Framework\Language;

use Framework\Helpers\Isolation;
use Framework\Language\Debug\LanguageCollector;
use InvalidArgumentException;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Pure;

/**
 * Class Language.
 *
 * @see https://www.sitepoint.com/localization-demystified-understanding-php-intl/
 * @see https://unicode-org.github.io/icu-docs/#/icu4c/classMessageFormat.html
 *
 * @package language
 */
class Language
{
    /**
     * The current locale.
     */
    protected string $currentLocale;
    /**
     * The default locale.
     */
    protected string $defaultLocale;
    /**
     * List of directories to find for files.
     *
     * @var array<int,string>
     */
    protected array $directories = [];
    /**
     * The locale fallback level.
     */
    protected FallbackLevel $fallbackLevel = FallbackLevel::default;
    /**
     * List with locales of already scanned directories.
     *
     * @var array<int,string>
     */
    protected array $findedLocales = [];
    /**
     * Language lines.
     *
     * List of "locale" => "file" => "line" => "text"
     *
     * @var array<string,array<string,array<string,string>>>
     */
    protected array $languages = [];
    /**
     * Supported locales. Any other will be ignored.
     *
     * The default locale is always supported.
     *
     * @var array<int,string>
     */
    protected array $supportedLocales = [];
    protected LanguageCollector $debugCollector;

    /**
     * Language constructor.
     *
     * @param string $locale The default (and current) locale code
     * @param array<int,string> $directories List of directory paths to find for language files
     */
    public function __construct(string $locale = 'en', array $directories = [])
    {
        $this->setDefaultLocale($locale);
        $this->setCurrentLocale($locale);
        if ($directories) {
            $this->setDirectories($directories);
        }
    }

    /**
     * Adds a locale to the list of already scanned directories.
     *
     * @param string $locale
     *
     * @return static
     */
    protected function addFindedLocale(string $locale) : static
    {
        $this->findedLocales[] = $locale;
        return $this;
    }

    /**
     * Adds custom lines for a specific locale.
     *
     * Useful to set lines from a database or any parsed file.
     *
     * NOTE: This function will always replace the old lines, as given from files.
     *
     * @param string $locale The locale code
     * @param string $file The file name
     * @param array<string> $lines An array of "line" => "text"
     *
     * @return static
     */
    public function addLines(string $locale, string $file, array $lines) : static
    {
        if (!$this->isFindedLocale($locale)) {
            // Certify that all directories are scanned first
            // So, this method always have priority on replacements
            $this->getLine($locale, $file, '');
        }
        $this->languages[$locale][$file] = isset($this->languages[$locale][$file])
            ? \array_replace($this->languages[$locale][$file], $lines)
            : $lines;
        return $this;
    }

    /**
     * Gets a currency value formatted in a given locale.
     *
     * @param float $value The money value
     * @param string $currency The Currency code. i.e. USD, BRL, JPY
     * @param string|null $locale A custom locale or null to use the current
     *
     * @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
     *
     * @return string
     */
    public function currency(float $value, string $currency, ?string $locale = null) : string
    {
        // @phpstan-ignore-next-line
        return \NumberFormatter::create(
            $locale ?? $this->getCurrentLocale(),
            \NumberFormatter::CURRENCY
        )->formatCurrency($value, $currency);
    }

    /**
     * Gets a formatted date in a given locale.
     *
     * @param int $time An Unix timestamp
     * @param string|null $style One of: short, medium, long or full. Leave null to use short
     * @param string|null $locale A custom locale or null to use the current
     *
     * @throws InvalidArgumentException for invalid style format
     *
     * @return string
     */
    public function date(int $time, ?string $style = null, ?string $locale = null) : string
    {
        if ($style && !\in_array($style, ['short', 'medium', 'long', 'full'], true)) {
            throw new InvalidArgumentException('Invalid date style format: ' . $style);
        }
        $style = $style ?: 'short';
        // @phpstan-ignore-next-line
        return \MessageFormatter::formatMessage(
            $locale ?? $this->getCurrentLocale(),
            "{time, date, {$style}}",
            ['time' => $time]
        );
    }

    /**
     * Find for absolute file paths from where language lines can be loaded.
     *
     * @param string $locale The required locale
     * @param string $file The required file
     *
     * @return array<int,string> a list of valid filenames
     */
    #[Pure]
    protected function findFilenames(string $locale, string $file) : array
    {
        $filenames = [];
        foreach ($this->getDirectories() as $directory) {
            $path = $directory . $locale . \DIRECTORY_SEPARATOR . $file . '.php';
            if (\is_file($path)) {
                $filenames[] = $path;
            }
        }
        return $filenames;
    }

    /**
     * Gets the current locale.
     *
     * @return string
     */
    #[Pure]
    public function getCurrentLocale() : string
    {
        return $this->currentLocale;
    }

    /**
     * Gets the current locale directionality.
     *
     * @return string 'ltr' for Left-To-Right ot 'rtl' for Right-To-Left
     */
    #[Pure]
    public function getCurrentLocaleDirection() : string
    {
        return static::getLocaleDirection($this->getCurrentLocale());
    }

    /**
     * Gets the default locale.
     *
     * @return string
     */
    #[Pure]
    public function getDefaultLocale() : string
    {
        return $this->defaultLocale;
    }

    /**
     * Gets the list of directories where language files can be finded.
     *
     * @return array<int,string>
     */
    #[Pure]
    public function getDirectories() : array
    {
        return $this->directories;
    }

    /**
     * Gets the Fallback Level.
     *
     * @return FallbackLevel
     */
    #[Pure]
    public function getFallbackLevel() : FallbackLevel
    {
        return $this->fallbackLevel;
    }

    /**
     * Gets a text line and locale according the Fallback Level.
     *
     * @param string $locale The locale to get his fallback line
     * @param string $file The file
     * @param string $line The line
     *
     * @return array<int,string|null> Two numeric keys containg the used locale and text
     */
    #[ArrayShape(['string', 'string|null'])]
    protected function getFallbackLine(string $locale, string $file, string $line) : array
    {
        $text = null;
        $level = $this->getFallbackLevel()->value;
        // Fallback to parent
        if ($level > FallbackLevel::none->value && \strpos($locale, '-') > 1) {
            [$locale] = \explode('-', $locale, 2);
            $text = $this->getLine($locale, $file, $line);
        }
        // Fallback to default
        if ($text === null
            && $level > FallbackLevel::parent->value
            && $locale !== $this->getDefaultLocale()
        ) {
            $locale = $this->getDefaultLocale();
            $text = $this->getLine($locale, $file, $line);
        }
        return [
            $locale,
            $text,
        ];
    }

    /**
     * @param string $filename
     *
     * @return array<int,string>
     */
    protected function getFileLines(string $filename) : array
    {
        return Isolation::require($filename);
    }

    /**
     * Gets a language line text.
     *
     * @param string $locale The required locale
     * @param string $file The required file
     * @param string $line The required line
     *
     * @return string|null The text of the line or null if the line is not found
     */
    protected function getLine(string $locale, string $file, string $line) : ?string
    {
        if (isset($this->languages[$locale][$file][$line])) {
            return $this->languages[$locale][$file][$line];
        }
        if (!\in_array($locale, $this->getSupportedLocales(), true)) {
            return null;
        }
        $this->addFindedLocale($locale);
        $this->findLines($locale, $file);
        return $this->languages[$locale][$file][$line] ?? null;
    }

    /**
     * Find and add lines.
     *
     * This method can be overridden to find lines in custom storage, such as
     * in a database table.
     *
     * @param string $locale
     * @param string $file
     *
     * @return static
     */
    protected function findLines(string $locale, string $file) : static
    {
        foreach ($this->findFilenames($locale, $file) as $filename) {
            $this->addLines($locale, $file, $this->getFileLines($filename));
        }
        return $this;
    }

    /**
     * Gets the list of available locales, lines and texts.
     *
     * @return array<string,array<string,array<string,string>>>
     */
    #[Pure]
    public function getLines() : array
    {
        return $this->languages;
    }

    public function resetLines() : static
    {
        $this->languages = [];
        return $this;
    }

    /**
     * Gets the list of Supported Locales.
     *
     * @return array<int,string>
     */
    #[Pure]
    public function getSupportedLocales() : array
    {
        return $this->supportedLocales;
    }

    /**
     * Tells if a locale already was found in the directories.
     *
     * @param string $locale The locale
     *
     * @see \Framework\Language\Language::getLine()
     *
     * @return bool
     */
    #[Pure]
    protected function isFindedLocale(string $locale) : bool
    {
        return \in_array($locale, $this->findedLocales, true);
    }

    /**
     * Renders a language file line with dot notation format.
     *
     * E.g. home.hello matches home for file and hello for line.
     *
     * @param string $line The dot notation file line
     * @param array<mixed> $args The arguments to be used in the formatted text
     * @param string|null $locale A custom locale or null to use the current
     *
     * @return string|null The rendered text or null if not found
     */
    public function lang(string $line, array $args = [], ?string $locale = null) : ?string
    {
        [$file, $line] = \explode('.', $line, 2);
        return $this->render($file, $line, $args, $locale);
    }

    /**
     * Gets an ordinal number in a given locale.
     *
     * @param int $number The number to be converted to ordinal
     * @param string|null $locale A custom locale or null to use the current
     *
     * @return string
     */
    public function ordinal(int $number, ?string $locale = null) : string
    {
        // @phpstan-ignore-next-line
        return \MessageFormatter::formatMessage(
            $locale ?? $this->getCurrentLocale(),
            '{number, ordinal}',
            ['number' => $number]
        );
    }

    /**
     * Renders a language file line.
     *
     * @param string $file The file
     * @param string $line The file line
     * @param array<mixed> $args The arguments to be used in the formatted text
     * @param string|null $locale A custom locale or null to use the current
     *
     * @return string The rendered text or file.line expression
     */
    public function render(
        string $file,
        string $line,
        array $args = [],
        ?string $locale = null
    ) : string {
        if (isset($this->debugCollector)) {
            $start = \microtime(true);
            $rendered = $this->getRenderedLine($file, $line, $args, $locale);
            $end = \microtime(true);
            $this->debugCollector->adddata([
                'start' => $start,
                'end' => $end,
                'file' => $file,
                'line' => $line,
                'locale' => $rendered['locale'],
                'message' => $rendered['message'],
            ]);
            return $rendered['message'];
        }
        return $this->getRenderedLine($file, $line, $args, $locale)['message'];
    }

    /**
     * @param string $file
     * @param string $line
     * @param array<mixed> $args
     * @param string|null $locale
     *
     * @return array<string,string>
     */
    #[ArrayShape(['locale' => 'string', 'message' => 'string'])]
    protected function getRenderedLine(
        string $file,
        string $line,
        array $args = [],
        ?string $locale = null
    ) : array {
        $locale ??= $this->getCurrentLocale();
        $text = $this->getLine($locale, $file, $line);
        if ($text === null) {
            [$locale, $text] = $this->getFallbackLine($locale, $file, $line);
        }
        if ($text !== null) {
            $text = $this->formatMessage($text, $args, $locale);
        }
        return [
            'locale' => $locale,
            'message' => $text ?? ($file . '.' . $line),
        ];
    }

    /**
     * Checks if Language has a line.
     *
     * @param string $file The file
     * @param string $line The file line
     * @param string|null $locale A custom locale or null to use the current
     *
     * @return bool True if the line is found, otherwise false
     */
    public function hasLine(string $file, string $line, ?string $locale = null) : bool
    {
        $locale ??= $this->getCurrentLocale();
        $text = $this->getLine($locale, $file, $line);
        if ($text === null) {
            $text = $this->getFallbackLine($locale, $file, $line)[1];
        }
        return $text !== null;
    }

    /**
     * @param string $text
     * @param array<mixed> $args
     * @param string|null $locale
     *
     * @return string
     */
    public function formatMessage(string $text, array $args = [], ?string $locale = null) : string
    {
        $args = \array_map(static function ($arg) : string {
            return (string) $arg;
        }, $args);
        $locale ??= $this->getCurrentLocale();
        return \MessageFormatter::formatMessage($locale, $text, $args) ?: $text;
    }

    /**
     * Sets the current locale.
     *
     * @param string $locale The current locale. This automatically is set as
     * one of Supported Locales.
     *
     * @return static
     */
    public function setCurrentLocale(string $locale) : static
    {
        $this->currentLocale = $locale;
        $locales = $this->getSupportedLocales();
        $locales[] = $locale;
        $this->setSupportedLocales($locales);
        return $this;
    }

    /**
     * Sets the default locale.
     *
     * @param string $locale The default locale. This automatically is set as
     * one of Supported Locales.
     *
     * @return static
     */
    public function setDefaultLocale(string $locale) : static
    {
        $this->defaultLocale = $locale;
        $locales = $this->getSupportedLocales();
        $locales[] = $locale;
        $this->setSupportedLocales($locales);
        return $this;
    }

    /**
     * Sets a list of directories where language files can be found.
     *
     * @param array<string> $directories a list of valid directory paths
     *
     * @throws InvalidArgumentException if a directory path is inaccessible
     *
     * @return static
     */
    public function setDirectories(array $directories) : static
    {
        $dirs = [];
        foreach ($directories as $directory) {
            $path = \realpath($directory);
            if (!$path || !\is_dir($path)) {
                throw new InvalidArgumentException('Directory path inaccessible: ' . $directory);
            }
            $dirs[] = $path . \DIRECTORY_SEPARATOR;
        }
        $this->directories = $dirs ? \array_unique($dirs) : [];
        $this->reindex();
        return $this;
    }

    /**
     * @param string $directory
     *
     * @return static
     */
    public function addDirectory(string $directory) : static
    {
        $this->setDirectories(\array_merge([
            $directory,
        ], $this->getDirectories()));
        return $this;
    }

    protected function reindex() : void
    {
        $this->findedLocales = [];
        foreach ($this->languages as $locale => $files) {
            foreach (\array_keys($files) as $file) {
                $this->findLines($locale, $file);
            }
            $this->addFindedLocale($locale);
        }
    }

    /**
     * Sets the Fallback Level.
     *
     * @param FallbackLevel|int $level
     *
     * @return static
     */
    public function setFallbackLevel(FallbackLevel | int $level) : static
    {
        if (\is_int($level)) {
            $level = FallbackLevel::from($level);
        }
        $this->fallbackLevel = $level;
        return $this;
    }

    /**
     * Sets a list of Supported Locales.
     *
     * NOTE: the default locale always is supported. But the current can be exclude
     * if this function is called after {@see Language::setCurrentLocale()}.
     *
     * @param array<string> $locales the supported locales
     *
     * @return static
     */
    public function setSupportedLocales(array $locales) : static
    {
        $locales[] = $this->getDefaultLocale();
        $locales = \array_unique($locales);
        \sort($locales);
        $this->supportedLocales = $locales;
        $this->reindex();
        return $this;
    }

    public function setDebugCollector(LanguageCollector $debugCollector) : static
    {
        $this->debugCollector = $debugCollector;
        $this->debugCollector->setLanguage($this);
        return $this;
    }

    /**
     * Gets text directionality based on locale.
     *
     * @param string $locale The locale code
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir
     * @see https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code
     *
     * @return string 'ltr' for Left-To-Right ot 'rtl' for Right-To-Left
     */
    #[Pure]
    public static function getLocaleDirection(string $locale) : string
    {
        $locale = \strtolower($locale);
        $locale = \strtr($locale, ['_' => '-']);
        if (\in_array($locale, [
            'ar',
            'arc',
            'ckb',
            'dv',
            'fa',
            'ha',
            'he',
            'khw',
            'ks',
            'ps',
            'ur',
            'uz-af',
            'yi',
        ], true)) {
            return 'rtl';
        }
        return 'ltr';
    }
}