diff options
Diffstat (limited to 'vendor/ipl/i18n/src')
-rw-r--r-- | vendor/ipl/i18n/src/GettextTranslator.php | 351 | ||||
-rw-r--r-- | vendor/ipl/i18n/src/Locale.php | 124 | ||||
-rw-r--r-- | vendor/ipl/i18n/src/NoopTranslator.php | 31 | ||||
-rw-r--r-- | vendor/ipl/i18n/src/StaticTranslator.php | 14 | ||||
-rw-r--r-- | vendor/ipl/i18n/src/Translation.php | 101 | ||||
-rw-r--r-- | vendor/ipl/i18n/src/functions.php | 34 | ||||
-rw-r--r-- | vendor/ipl/i18n/src/functions_include.php | 6 |
7 files changed, 661 insertions, 0 deletions
diff --git a/vendor/ipl/i18n/src/GettextTranslator.php b/vendor/ipl/i18n/src/GettextTranslator.php new file mode 100644 index 0000000..a7a8144 --- /dev/null +++ b/vendor/ipl/i18n/src/GettextTranslator.php @@ -0,0 +1,351 @@ +<?php + +namespace ipl\I18n; + +use FilesystemIterator; +use ipl\Stdlib\Contract\Translator; + +/** + * Translator using PHP's native [gettext](https://www.php.net/gettext) extension + * + * # Example Usage + * + * ```php + * $translator = (new GettextTranslator()) + * ->addTranslationDirectory('/path/to/locales') + * ->addTranslationDirectory('/path/to/locales-of-domain', 'special') // Could also be the same directory as above + * ->setLocale('de_DE'); + * + * $translator->translate('user'); + * + * printf( + * $translator->translatePlural('%d user', '%d user', 42), + * 42 + * ); + * + * $translator->translateInDomain('special-domain', 'request'); + * + * printf( + * $translator->translatePluralInDomain('special-domain', '%d request', '%d requests', 42), + * 42 + * ); + * + * // All translation functions also accept a context as last parameter + * $translator->translate('group', 'a-context'); + * ``` + * + */ +class GettextTranslator implements Translator +{ + /** @var string Default gettext domain */ + protected $defaultDomain = 'default'; + + /** @var string Default locale code */ + protected $defaultLocale = 'en_US'; + + /** @var array Known translation directories as array[$domain] => $directory */ + protected $translationDirectories = []; + + /** @var array Loaded translations as array[$domain] => $directory */ + protected $loadedTranslations = []; + + /** @var string Primary locale code used for translations */ + protected $locale; + + /** + * Get the default domain + * + * @return string + */ + public function getDefaultDomain() + { + return $this->defaultDomain; + } + + /** + * Set the default domain + * + * @param string $defaultDomain + * + * @return $this + */ + public function setDefaultDomain($defaultDomain) + { + $this->defaultDomain = $defaultDomain; + + return $this; + } + + /** + * Get the default locale + * + * @return string + */ + public function getDefaultLocale() + { + return $this->defaultLocale; + } + + /** + * Set the default locale + * + * @param string $defaultLocale + * + * @return $this + */ + public function setDefaultLocale($defaultLocale) + { + $this->defaultLocale = $defaultLocale; + + return $this; + } + + /** + * Get available translations + * + * @return array Available translations as array[$domain] => $directory + */ + public function getTranslationDirectories() + { + return $this->translationDirectories; + } + + /** + * Add a translation directory + * + * @param string $directory Path to translation files + * @param string $domain Optional domain of the translation + * + * @return $this + */ + public function addTranslationDirectory($directory, $domain = null) + { + $this->translationDirectories[$domain ?: $this->defaultDomain] = $directory; + + return $this; + } + + /** + * Get loaded translations + * + * @return array Loaded translations as array[$domain] => $directory + */ + public function getLoadedTranslations() + { + return $this->loadedTranslations; + } + + /** + * Load a translation so that gettext is able to locate its message catalogs + * + * {@link bindtextdomain()} is called internally for every domain and path + * that has been added with {@link addTranslationDirectory()}. + * + * @return $this + * @throws \Exception If {@link bindtextdomain()} fails for a domain + */ + public function loadTranslations() + { + foreach ($this->translationDirectories as $domain => $directory) { + if ( + isset($this->loadedTranslations[$domain]) + && $this->loadedTranslations[$domain] === $directory + ) { + continue; + } + + if (bindtextdomain($domain, $directory) !== $directory) { + throw new \Exception(sprintf( + "Can't register domain '%s' with path '%s'", + $domain, + $directory + )); + } + + bind_textdomain_codeset($domain, 'UTF-8'); + + $this->loadedTranslations[$domain] = $directory; + } + + return $this; + } + + /** + * Get the primary locale code used for translations + * + * @return string + */ + public function getLocale() + { + return $this->locale; + } + + /** + * Setup the primary locale code to use for translations + * + * Calls {@link loadTranslations()} internally. + * + * @param string $locale Locale code + * + * @return $this + * @throws \Exception If {@link bindtextdomain()} fails for a domain + */ + public function setLocale($locale) + { + putenv("LANGUAGE=$locale.UTF-8"); + setlocale(LC_ALL, $locale . '.UTF-8'); + + $this->loadTranslations(); + + textdomain($this->getDefaultDomain()); + + $this->locale = $locale; + + return $this; + } + + /** + * Encode a message with context to the representation used in .mo files + * + * @param string $message + * @param string $context + * + * @return string The encoded message as context + "\x04" + message + */ + public function encodeMessageWithContext($message, $context) + { + // The encoding of a context and a message in a .mo file is + // context + "\x04" + message (gettext version >= 0.15) + return "{$context}\x04{$message}"; + } + + public function translate($message, $context = null) + { + if ($context !== null) { + $messageForGettext = $this->encodeMessageWithContext($message, $context); + } else { + $messageForGettext = $message; + } + + $translation = gettext($messageForGettext); + + if ($translation === $messageForGettext) { + return $message; + } + + return $translation; + } + + public function translateInDomain($domain, $message, $context = null) + { + if ($context !== null) { + $messageForGettext = $this->encodeMessageWithContext($message, $context); + } else { + $messageForGettext = $message; + } + + $translation = dgettext( + $domain, + $messageForGettext + ); + + if ($translation === $messageForGettext) { + $translation = dgettext( + $this->getDefaultDomain(), + $messageForGettext + ); + } + + if ($translation === $messageForGettext) { + return $message; + } + + return $translation; + } + + public function translatePlural($singular, $plural, $number, $context = null) + { + if ($context !== null) { + $singularForGettext = $this->encodeMessageWithContext($singular, $context); + } else { + $singularForGettext = $singular; + } + + + $translation = ngettext( + $singularForGettext, + $plural, + $number + ); + + if ($translation === $singularForGettext) { + return $number === 1 ? $singular : $plural; + } + + return $translation; + } + + public function translatePluralInDomain($domain, $singular, $plural, $number, $context = null) + { + if ($context !== null) { + $singularForGettext = $this->encodeMessageWithContext($singular, $context); + } else { + $singularForGettext = $singular; + } + + $translation = dngettext( + $domain, + $singularForGettext, + $plural, + $number + ); + + $isSingular = $number === 1; + + if ($translation === ($isSingular ? $singularForGettext : $plural)) { + $translation = dngettext( + $this->getDefaultDomain(), + $singularForGettext, + $plural, + $number + ); + } + + if ($translation === $singularForGettext) { + return $isSingular ? $singular : $plural; + } + + return $translation; + } + + /** + * List available locales by traversing the translation directories from {@link addTranslationDirectory()} + * + * @return string[] Array of available locale codes + */ + public function listLocales() + { + $locales = []; + + foreach (array_unique($this->getTranslationDirectories()) as $directory) { + $fs = new FilesystemIterator( + $directory, + FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS + ); + + foreach ($fs as $file) { + if (! $file->isDir()) { + continue; + } + + $locales[] = $file->getBasename(); + } + } + + $locales = array_filter(array_unique($locales)); + + sort($locales); + + return $locales; + } +} diff --git a/vendor/ipl/i18n/src/Locale.php b/vendor/ipl/i18n/src/Locale.php new file mode 100644 index 0000000..d9e6b05 --- /dev/null +++ b/vendor/ipl/i18n/src/Locale.php @@ -0,0 +1,124 @@ +<?php + +namespace ipl\I18n; + +class Locale +{ + /** @var string Default locale code */ + protected $defaultLocale = 'en_US'; + + /** + * Get the default locale + * + * @return string + */ + public function getDefaultLocale() + { + return $this->defaultLocale; + } + + /** + * Set the default locale + * + * @param string $defaultLocale + * + * @return $this + */ + public function setDefaultLocale($defaultLocale) + { + $this->defaultLocale = $defaultLocale; + + return $this; + } + + /** + * Return the preferred locale based on the given HTTP header and the available translations + * + * @param string $header The HTTP "Accept-Language" header + * @param array $available Available translations + * + * @return string The browser's preferred locale code + */ + public function getPreferred($header, array $available) + { + $headerValues = explode(',', $header); + for ($i = 0; $i < count($headerValues); $i++) { + // In order to accomplish a stable sort we need to take the original + // index into account as well during element comparison + $headerValues[$i] = [$headerValues[$i], $i]; + } + usort( // Sort DESC but keep equal elements ASC + $headerValues, + function ($a, $b) { + $tagA = explode(';', $a[0], 2); + $tagB = explode(';', $b[0], 2); + $qValA = (float) (strpos($a[0], ';') > 0 ? substr(array_pop($tagA), 2) : 1); + $qValB = (float) (strpos($b[0], ';') > 0 ? substr(array_pop($tagB), 2) : 1); + + return $qValA < $qValB ? 1 : ($qValA > $qValB ? -1 : ($a[1] > $b[1] ? 1 : ($a[1] < $b[1] ? -1 : 0))); + } + ); + for ($i = 0; $i < count($headerValues); $i++) { + // We need to reset the array to its original structure once it's sorted + $headerValues[$i] = $headerValues[$i][0]; + } + $requestedLocales = []; + foreach ($headerValues as $headerValue) { + if (strpos($headerValue, ';') > 0) { + $parts = explode(';', $headerValue, 2); + $headerValue = $parts[0]; + } + $requestedLocales[] = str_replace('-', '_', $headerValue); + } + $requestedLocales = array_combine( + array_map('strtolower', array_values($requestedLocales)), + array_values($requestedLocales) + ); + + $available[] = $this->defaultLocale; + $availableLocales = array_combine( + array_map('strtolower', array_values($available)), + array_values($available) + ); + + $similarMatch = null; + + foreach ($requestedLocales as $requestedLocaleLowered => $requestedLocale) { + $localeObj = $this->parseLocale($requestedLocaleLowered); + + if ( + isset($availableLocales[$requestedLocaleLowered]) + && (! $similarMatch || $this->parseLocale($similarMatch)->language === $localeObj->language) + ) { + // Prefer perfect match only if no similar match has been found yet or the perfect match is more precise + // than the similar match + return $availableLocales[$requestedLocaleLowered]; + } + + if (! $similarMatch) { + foreach ($availableLocales as $availableLocaleLowered => $availableLocale) { + if ($this->parseLocale($availableLocaleLowered)->language === $localeObj->language) { + $similarMatch = $availableLocaleLowered; + break; + } + } + } + } + + return $similarMatch ? $availableLocales[$similarMatch] : $this->defaultLocale; + } + + /** + * Parse a locale into its subtags + * + * Converts to output of {@link \Locale::parseLocale()} to an object and returns it. + * + * @param string $locale + * + * @return object Output of {@link \Locale::parseLocale()} converted to an object + */ + public function parseLocale($locale) + { + return (object) \Locale::parseLocale($locale); + } +} diff --git a/vendor/ipl/i18n/src/NoopTranslator.php b/vendor/ipl/i18n/src/NoopTranslator.php new file mode 100644 index 0000000..1f9aab2 --- /dev/null +++ b/vendor/ipl/i18n/src/NoopTranslator.php @@ -0,0 +1,31 @@ +<?php + +namespace ipl\I18n; + +use ipl\Stdlib\Contract\Translator; + +/** + * Translator that just returns the original messages + */ +class NoopTranslator implements Translator +{ + public function translate($message, $context = null) + { + return $message; + } + + public function translateInDomain($domain, $message, $context = null) + { + return $message; + } + + public function translatePlural($singular, $plural, $number, $context = null) + { + return $number === 1 ? $singular : $plural; + } + + public function translatePluralInDomain($domain, $singular, $plural, $number, $context = null) + { + return $number === 1 ? $singular : $plural; + } +} diff --git a/vendor/ipl/i18n/src/StaticTranslator.php b/vendor/ipl/i18n/src/StaticTranslator.php new file mode 100644 index 0000000..d2869bf --- /dev/null +++ b/vendor/ipl/i18n/src/StaticTranslator.php @@ -0,0 +1,14 @@ +<?php + +namespace ipl\I18n; + +use ipl\Stdlib\Contract\Translator; + +/** + * Static entrypoint for a translator instance + */ +class StaticTranslator +{ + /** @var Translator */ + public static $instance; +} diff --git a/vendor/ipl/i18n/src/Translation.php b/vendor/ipl/i18n/src/Translation.php new file mode 100644 index 0000000..52639da --- /dev/null +++ b/vendor/ipl/i18n/src/Translation.php @@ -0,0 +1,101 @@ +<?php + +namespace ipl\I18n; + +trait Translation +{ + /** + * The domain to use in methods {@see Translation::translate()} and {@see Translation::translatePlural()} + * + * Set this to your desired domain and use both mentioned methods as usual, if you never require the + * default translation domain. (It's still being used as a fallback if your domain doesn't provide a + * particular message.) + * + * @var string + */ + protected $translationDomain; + + /** + * Translate a message + * + * @param string $message + * @param string $context Message context + * + * @return string Translated message or original message if no translation is found + */ + public function translate($message, $context = null) + { + return $this->translationDomain === null + ? StaticTranslator::$instance->translate($message, $context) + : StaticTranslator::$instance->translateInDomain($this->translationDomain, $message, $context); + } + + /** + * Translate a message in the given domain + * + * If no translation is found in the specified domain, the translation is also searched for in the default domain. + * + * @param string $domain + * @param string $message + * @param string $context Message context + * + * @return string Translated message or original message if no translation is found + */ + public function translateInDomain($domain, $message, $context = null) + { + return StaticTranslator::$instance->translateInDomain($domain, $message, $context); + } + + /** + * Translate a plural message + * + * The returned message is based on the given number to decide between the singular and plural forms. + * That is also the case if no translation is found. + * + * @param string $singular Singular message + * @param string $plural Plural message + * @param int $number Number to decide between the returned singular and plural forms + * @param string $context Message context + * + * @return string Translated message or original message if no translation is found + */ + public function translatePlural($singular, $plural, $number, $context = null) + { + return $this->translationDomain === null + ? StaticTranslator::$instance->translatePlural($singular, $plural, $number, $context) + : StaticTranslator::$instance->translatePluralInDomain( + $this->translationDomain, + $singular, + $plural, + $number ?? 0, + $context + ); + } + + /** + * Translate a plural message in the given domain + * + * If no translation is found in the specified domain, the translation is also searched for in the default domain. + * + * The returned message is based on the given number to decide between the singular and plural forms. + * That is also the case if no translation is found. + * + * @param string $domain + * @param string $singular Singular message + * @param string $plural Plural message + * @param int $number Number to decide between the returned singular and plural forms + * @param string $context Message context + * + * @return string Translated message or original message if no translation is found + */ + public function translatePluralInDomain($domain, $singular, $plural, $number, $context = null) + { + return StaticTranslator::$instance->translatePluralInDomain( + $domain, + $singular, + $plural, + $number ?? 0, + $context + ); + } +} diff --git a/vendor/ipl/i18n/src/functions.php b/vendor/ipl/i18n/src/functions.php new file mode 100644 index 0000000..74d58df --- /dev/null +++ b/vendor/ipl/i18n/src/functions.php @@ -0,0 +1,34 @@ +<?php + +namespace ipl\I18n; + +/** + * Translate a message + * + * @param string $message + * @param string $context Message context + * + * @return string Translated message or original message if no translation is found + */ +function t($message, $context = null) +{ + return StaticTranslator::$instance->translate($message, $context); +} + +/** + * Translate a plural message + * + * The returned message is based on the given number to decide between the singular and plural forms. + * That is also the case if no translation is found. + * + * @param string $singular Singular message + * @param string $plural Plural message + * @param int $number Number to decide between the returned singular and plural forms + * @param string $context Message context + * + * @return string Translated message or original message if no translation is found + */ +function tp($singular, $plural, $number, $context = null) +{ + return StaticTranslator::$instance->translatePlural($singular, $plural, $number, $context); +} diff --git a/vendor/ipl/i18n/src/functions_include.php b/vendor/ipl/i18n/src/functions_include.php new file mode 100644 index 0000000..68f3806 --- /dev/null +++ b/vendor/ipl/i18n/src/functions_include.php @@ -0,0 +1,6 @@ +<?php + +// Don't redefine the functions if included multiple times +if (! function_exists('ipl\I18n\t')) { + require __DIR__ . '/functions.php'; +} |