fisharebest/localization

View on GitHub
src/Locale/AbstractLocale.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace Fisharebest\Localization\Locale;

use Fisharebest\Localization\Language\LanguageInterface;
use Fisharebest\Localization\PluralRule\PluralRuleInterface;
use Fisharebest\Localization\Script\ScriptInterface;
use Fisharebest\Localization\Territory\TerritoryInterface;
use Fisharebest\Localization\Variant\VariantInterface;

/**
 * Class AbstractLocale - The “root” locale, from which all others are derived.
 *
 * @author    Greg Roach <greg@subaqua.co.uk>
 * @copyright (c) 2022 Greg Roach
 * @license   GPL-3.0-or-later
 */
abstract class AbstractLocale
{
    // "Source" strings, when translating numbers
    const DECIMAL  = '.'; // The default decimal mark
    const GROUP    = ','; // The digit group separator
    const NEGATIVE = '-'; // Negative numbers

    // "Target" strings, when translating numbers
    const ADLM_GROUP   = '⹁';
    const ALM          = "\xD8\x9C"; // Arabic Letter Mark
    const APOSTROPHE   = '’';
    const ARAB_DECIMAL = "\xD9\xAB";
    const ARAB_GROUP   = "\xD9\xAC";
    const ARAB_PERCENT = "\xD9\xAA";
    const COMMA        = ',';
    const DOT          = '.';
    const HYPHEN       = '-';
    const LTR_MARK     = "\xE2\x80\x8E"; // Left-to-right marker
    const MINUS_SIGN   = "\xE2\x88\x92";
    const NARROW_NBSP  = "\xE2\x80\xAF"; // Narrow non-breaking space
    const NBSP         = "\xC2\xA0"; // Non-breaking space
    const RTL_MARK     = "\xE2\x80\x8F"; // Right-to-left marker

    // For formatting percentages
    const PERCENT     = '%%';
    const PLACEHOLDER = '%s';

    /**
     * Generate a linux locale code for this locale.  Examples include
     * "fr", “en_GB”, “ca_ES@valencia” and “sr@latin”.
     *
     * @return string
     */
    public function code()
    {
        $code   = $this->language()->code() . '_' . $this->territory()->code();
        $script = $this->script()->unicodeName();

        if ($script !== null && $this->script()->code() !== $this->language()->defaultScript()->code()) {
            $code .= '@' . strtolower($script);
        }

        if ($this->variant() !== null) {
            if ($this->variant()->code() === 'posix') {
                $code = 'POSIX';
            } else {
                $code .= '@' . $this->variant()->code();
            }
        }

        return $code;
    }

    /**
     * Which collation sequence should be used for this locale?
     * “unicode_ci” would mean use “utf8_unicode_ci”, “utf8mb4_unicode_ci”, etc.
     *
     * @link http://dev.mysql.com/doc/refman/5.7/en/charset-unicode-sets.html
     * @return string
     */
    public function collation()
    {
        return 'unicode_ci';
    }

    /**
     * Convert (Hindu-Arabic) digits into a localized form
     *
     * @param string $string e.g. "123.45"
     *
     * @return string
     */
    public function digits($string)
    {
        return strtr($string, $this->numberSymbols() + $this->numerals());
    }

    /**
     * When writing large numbers place a separator after this number of digits.
     *
     * @return int
     */
    protected function digitsFirstGroup()
    {
        return 3;
    }

    /**
     * When writing large numbers place a separator after this number of digits.
     *
     * @return int
     */
    protected function digitsGroup()
    {
        return 3;
    }

    /**
     * Is text written left-to-right “ltr” or right-to-left “rtl”.
     * Most scripts are only written in one direction, but there are a few that
     * can be written in either direction.
     *
     * @return string “ltr” or “rtl”
     */
    public function direction()
    {
        return $this->script()->direction();
    }

    /**
     * The name of this locale, in its own language/script, and with the
     * customary capitalization of the locale.
     *
     * @return string
     */
    abstract public function endonym();

    /**
     * A sortable version of the locale name.  For example, “British English”
     * might sort as “ENGLISH, BRITISH” to keep all the variants of English together.
     *
     * All-capitals makes sorting easier, as we can use a simple strcmp().
     *
     * @return string
     */
    public function endonymSortable()
    {
        return $this->endonym();
    }

    /**
     * Markup for an HTML element
     *
     * @return string e.g. lang="ar" dir="rtl"
     */
    public function htmlAttributes()
    {
        $direction = $this->direction();

        if ($direction === 'rtl' || $direction !== $this->script()->direction()) {
            return 'lang="' . $this->languageTag() . '" dir="' . $this->direction() . '"';
        }

        return 'lang="' . $this->languageTag() . '"';
    }

    /**
     * The language used by this locale.
     *
     * @return LanguageInterface
     */
    abstract public function language();

    /**
     * The IETF language tag for the locale.  Examples include
     * “fr, “en-GB”, “ca-ES-valencia” and “sr-Latn”.
     *
     * @return string
     */
    public function languageTag()
    {
        $language_tag = $this->language()->code();
        if ($this->script()->code() !== $this->language()->defaultScript()->code()) {
            $language_tag .= '-' . $this->script()->code();
        }
        if ($this->territory()->code() !== $this->language()->defaultTerritory()->code()) {
            $language_tag .= '-' . $this->territory()->code();
        }
        if ($this->variant() !== null) {
            $language_tag .= '-' . $this->variant()->code();
        }

        return $language_tag;
    }

    /**
     * When using grouping digits in numbers, keep this many of digits together.
     *
     * @return int
     */
    protected function minimumGroupingDigits()
    {
        return 1;
    }

    /**
     * Convert (Hindu-Arabic) digits into a localized form
     *
     * @param int|float $number The number to be localized
     *
     * @return string
     */
    public function number($number)
    {
        if ($number < 0) {
            $number   = -$number;
            $negative = self::NEGATIVE;
        } else {
            $negative = '';
        }
        $parts    = explode(self::DECIMAL, (string) $number, 2);
        $integers = $parts[0];
        if (strlen($integers) >= $this->digitsFirstGroup() + $this->minimumGroupingDigits()) {
            $todo     = substr($integers, 0, -$this->digitsFirstGroup());
            $integers = self::GROUP . substr($integers, -$this->digitsFirstGroup());
            while (strlen($todo) >= $this->digitsGroup() + $this->minimumGroupingDigits()) {
                $integers = self::GROUP . substr($todo, -$this->digitsGroup()) . $integers;
                $todo     = substr($todo, 0, -$this->digitsGroup());
            }
            $integers = $todo . $integers;
        }
        if (count($parts) > 1) {
            $decimals = self::DECIMAL . $parts[1];
        } else {
            $decimals = '';
        }

        return $this->digits($negative . $integers . $decimals);
    }

    /**
     * The symbols used to format numbers.
     *
     * @return array<string>
     */
    protected function numberSymbols()
    {
        return array();
    }

    /**
     * The numerals (0123456789) used by this locale.
     *
     * @return array<string>
     */
    protected function numerals()
    {
        return $this->script()->numerals();
    }

    /**
     * Convert (Hindu-Arabic) digits into a localized form
     *
     * @param float $number The number to be localized
     *
     * @return string
     */
    public function percent($number)
    {
        return sprintf($this->percentFormat(), $this->number($number * 100.0));
    }

    /**
     * How to format a floating point number (%s) as a percentage.
     *
     * @return string
     */
    protected function percentFormat()
    {
        return self::PLACEHOLDER . self::PERCENT;
    }

    /**
     * Which plural rule is used in this locale
     *
     * @return PluralRuleInterface
     */
    public function pluralRule()
    {
        return $this->language()->pluralRule();
    }

    /**
     * The script used by this locale.
     *
     * @return ScriptInterface
     */
    public function script()
    {
        return $this->language()->defaultScript();
    }

    /**
     * The territory used by this locale.
     *
     * @return TerritoryInterface
     */
    public function territory()
    {
        return $this->language()->defaultTerritory();
    }

    /**
     * The variant, if any of this locale.
     *
     * @return VariantInterface|null
     */
    public function variant()
    {
        return null;
    }
}