summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/i18n/src
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ipl/i18n/src')
-rw-r--r--vendor/ipl/i18n/src/GettextTranslator.php353
-rw-r--r--vendor/ipl/i18n/src/Locale.php127
-rw-r--r--vendor/ipl/i18n/src/NoopTranslator.php31
-rw-r--r--vendor/ipl/i18n/src/StaticTranslator.php14
-rw-r--r--vendor/ipl/i18n/src/Translation.php101
-rw-r--r--vendor/ipl/i18n/src/functions.php34
-rw-r--r--vendor/ipl/i18n/src/functions_include.php6
7 files changed, 666 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..288a489
--- /dev/null
+++ b/vendor/ipl/i18n/src/GettextTranslator.php
@@ -0,0 +1,353 @@
+<?php
+
+namespace ipl\I18n;
+
+use FilesystemIterator;
+use ipl\Stdlib\Contract\Translator;
+use SplFileInfo;
+
+/**
+ * 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<string, string> Known translation directories as array[$domain] => $directory */
+ protected $translationDirectories = [];
+
+ /** @var array<string, string> 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<string, string> 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<string, string> 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
+ );
+
+ /** @var SplFileInfo $file */
+ 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..48e345f
--- /dev/null
+++ b/vendor/ipl/i18n/src/Locale.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace ipl\I18n;
+
+use ipl\Stdlib\Str;
+use stdClass;
+
+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<string> $available Available translations
+ *
+ * @return string The browser's preferred locale code
+ */
+ public function getPreferred($header, array $available)
+ {
+ $headerValues = Str::trimSplit($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 = Str::trimSplit($a[0], ';', 2);
+ $tagB = Str::trimSplit($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 = Str::trimSplit($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 stdClass 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..eb40287
--- /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 ?? 0, $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';
+}