first commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/translation/IsoCodesTranslationDriver.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class IsoCodesTranslationDriver
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Translation provider for the IsoCodes package, faster and optimized to not keep items in memory
|
||||
*/
|
||||
|
||||
namespace PKP\i18n\translation;
|
||||
|
||||
use APP\core\Application;
|
||||
use DateInterval;
|
||||
use DomainException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use PKP\i18n\interfaces\LocaleInterface;
|
||||
use Sokil\IsoCodes\TranslationDriver\TranslationDriverInterface;
|
||||
|
||||
class IsoCodesTranslationDriver implements TranslationDriverInterface
|
||||
{
|
||||
/** @var string Max lifetime for the cache, a new cache file is created whenever the translation file is modified */
|
||||
protected const MAX_CACHE_LIFETIME = '1 year';
|
||||
|
||||
protected ?Translator $translator = null;
|
||||
protected string $locale;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(string $locale)
|
||||
{
|
||||
$this->setLocale($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups the translator
|
||||
*/
|
||||
public function configureDirectory(string $isoNumber, string $directory): void
|
||||
{
|
||||
if (!preg_match(LocaleInterface::LOCALE_EXPRESSION, $this->locale, $matches)) {
|
||||
throw new DomainException("Invalid locale \"{$this->locale}\"");
|
||||
}
|
||||
$locale = (object) [
|
||||
'language' => $matches['language'],
|
||||
'country' => $matches['country'] ?? null,
|
||||
'script' => $matches['script'] ?? null
|
||||
];
|
||||
$locales = [$this->locale, ...($locale->script ? [$locale->language . '@' . $locale->script] : []), $locale->language];
|
||||
// Attempts to find the best locale
|
||||
foreach ($locales as $locale) {
|
||||
$path = "{$directory}/{$locale}/LC_MESSAGES/{$isoNumber}.mo";
|
||||
if (file_exists($path)) {
|
||||
// Check if it's installed before caching the ISO codes (huge dataset), just to avoid a slow installation page
|
||||
$loader = fn () => Translator::createFromTranslationsArray(LocaleFile::loadArray($path, Application::isInstalled()));
|
||||
$key = __METHOD__ . static::MAX_CACHE_LIFETIME . $path . filemtime($path);
|
||||
$expiration = DateInterval::createFromDateString(static::MAX_CACHE_LIFETIME);
|
||||
$this->translator = Application::isInstalled() ? Cache::remember($key, $expiration, $loader) : $loader();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the driver locale
|
||||
*/
|
||||
public function setLocale(string $locale): void
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to translate an entry
|
||||
*/
|
||||
public function translate(string $isoNumber, string $message): string
|
||||
{
|
||||
return $this->translator?->getSingular($message) ?: $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @file classes/i18n/translation/LocaleBundle.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class LocaleBundle
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Bundles several locale files for a given locale into a single object
|
||||
*/
|
||||
|
||||
namespace PKP\i18n\translation;
|
||||
|
||||
use DateInterval;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use PKP\facades\Locale;
|
||||
|
||||
class LocaleBundle
|
||||
{
|
||||
/** @var string Max lifetime for the bundle cache. A new cache is created anytime a locale file in the bundle is modified */
|
||||
protected const MAX_CACHE_LIFETIME = '1 year';
|
||||
|
||||
/** The locale assigned to this bundle */
|
||||
public string $locale;
|
||||
|
||||
/** @var int[] Keeps the locale filenames (key) and their loading priorities (value) */
|
||||
protected array $paths = [];
|
||||
|
||||
/** Keeps the translations, lazy initialized when a translation is requested */
|
||||
protected ?Translator $translator = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $locale Locale assigned to this locale bundle
|
||||
* @param int[] $paths Optional list of gettext files to load, where the key must contain the locale path and the value its priority
|
||||
*/
|
||||
public function __construct(string $locale, ?array $paths = null)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
$this->paths = $paths ?? [];
|
||||
asort($this->paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a string using the selected locale.
|
||||
* Substitution works by replacing tokens like "{$foo}" with the value of
|
||||
* the parameter named "foo" (if supplied).
|
||||
*
|
||||
* @param string $key Locale key
|
||||
* @param array $params Named substitution parameters
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function translateSingular(string $key, array $params = []): ?string
|
||||
{
|
||||
$message = $this->getTranslator()->getSingular($key);
|
||||
return $message !== null ? $this->_format($message, $params) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a string using the selected locale with support for plurals.
|
||||
* Substitution works by replacing tokens like "{$foo}" with the value of
|
||||
* the parameter named "foo" (if supplied).
|
||||
*
|
||||
* @param string $key Locale key
|
||||
* @param int $count Count of items
|
||||
* @param array $params Named substitution parameters
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function translatePlural(string $key, int $count, array $params = []): ?string
|
||||
{
|
||||
$message = $this->getTranslator()->getPlural($key, $count);
|
||||
return $message !== null ? $this->_format($message, $params) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new locale to the bundle
|
||||
*/
|
||||
public function addPath(string $path, int $priority = 0): void
|
||||
{
|
||||
$this->paths[$path] = $priority;
|
||||
$this->setEntries($this->paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the locale paths (keys) that are part of this bundle together with their priorities (values)
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
return $this->paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the locale paths (keys) that are part of this bundle together with their priorities (values)
|
||||
*
|
||||
* @param int[] $paths
|
||||
*/
|
||||
public function setEntries(array $paths): void
|
||||
{
|
||||
$this->paths = $paths;
|
||||
asort($this->paths);
|
||||
// Clears the cache
|
||||
$this->translator = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily build and retrieves the Translator instance
|
||||
*/
|
||||
public function getTranslator(): Translator
|
||||
{
|
||||
if($this->translator) {
|
||||
return $this->translator;
|
||||
}
|
||||
// Caches only the supported locales (avoid spending time with one-offs)
|
||||
$isSupported = Locale::isSupported($this->locale);
|
||||
$loader = function () use ($isSupported): Translator {
|
||||
$translator = new Translator();
|
||||
// Merge all the locale files into a single structure
|
||||
$firstPath = array_key_first($this->paths);
|
||||
foreach (array_keys($this->paths) as $path) {
|
||||
$translations = LocaleFile::loadArray($path, $isSupported);
|
||||
// Once the first locale file is added, ensures only messages are merged
|
||||
$translator->addTranslations($firstPath === $path ? $translations : ['messages' => $translations['messages']]);
|
||||
}
|
||||
return $translator;
|
||||
};
|
||||
$key = __METHOD__ . static::MAX_CACHE_LIFETIME . array_reduce(array_keys($this->paths), fn (string $hash, string $path): string => sha1($hash . $path . filemtime($path)), '');
|
||||
$expiration = DateInterval::createFromDateString(static::MAX_CACHE_LIFETIME);
|
||||
return $this->translator ??= $isSupported ? Cache::remember($key, $expiration, $loader) : $loader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the translation
|
||||
*/
|
||||
private function _format(string $message, array $params = [])
|
||||
{
|
||||
return count($params) ? str_replace(array_map(fn (string $search): string => "{\${$search}}", array_keys($params)), array_values($params), $message) : $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/translation/LocaleFile.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class LocaleFile
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Loads translations from a locale file
|
||||
*/
|
||||
|
||||
namespace PKP\i18n\translation;
|
||||
|
||||
use DateInterval;
|
||||
use Exception;
|
||||
use Gettext\Generator\ArrayGenerator;
|
||||
use Gettext\Loader\LoaderInterface;
|
||||
use Gettext\Loader\MoLoader;
|
||||
use Gettext\Loader\PoLoader;
|
||||
use Gettext\Translations;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use SplFileInfo;
|
||||
|
||||
abstract class LocaleFile
|
||||
{
|
||||
/** @var string Max lifetime for the cache, a new cache file is created whenever the translation file is modified */
|
||||
protected const MAX_CACHE_LIFETIME = '1 year';
|
||||
|
||||
/**
|
||||
* Retrieves a suitable loader
|
||||
*/
|
||||
public static function getLoader(string $path): LoaderInterface
|
||||
{
|
||||
return match (strtolower((new SplFileInfo($path))->getExtension())) {
|
||||
'po' => new PoLoader(),
|
||||
'mo' => new MoLoader(),
|
||||
default => throw new Exception("There's no suitable gettext loader for this file type")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the translations from a file
|
||||
*/
|
||||
public static function loadTranslations(string $path): Translations
|
||||
{
|
||||
return self::getLoader($path)->loadFile($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the translations from a file as an array and caches the content physically as a PHP file in order to use the opcache
|
||||
*/
|
||||
public static function loadArray(string $path, bool $useCache = false): array
|
||||
{
|
||||
$loader = fn () => (new ArrayGenerator(['includeEmpty' => false]))->generateArray(static::loadTranslations($path));
|
||||
$key = __METHOD__ . static::MAX_CACHE_LIFETIME . $path . filemtime($path);
|
||||
return $useCache ? Cache::remember($key, DateInterval::createFromDateString(static::MAX_CACHE_LIFETIME), $loader) : $loader();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/translation/Translator.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Translator
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Extends the default GetText translator with serialization and the possibility to detect when translations failed
|
||||
*/
|
||||
|
||||
namespace PKP\i18n\translation;
|
||||
|
||||
use Gettext\Translator as GetTextTranslator;
|
||||
use PKP\core\ExportableTrait;
|
||||
|
||||
class Translator extends GetTextTranslator
|
||||
{
|
||||
use ExportableTrait;
|
||||
|
||||
/**
|
||||
* Builds a translator instance from arrays
|
||||
*/
|
||||
public static function createFromTranslationsArray(array ...$translations): static
|
||||
{
|
||||
$translator = new static();
|
||||
foreach ($translations as $translationSet) {
|
||||
$translator->addTranslations($translationSet);
|
||||
}
|
||||
return $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a singular translation
|
||||
*/
|
||||
public function getSingular(string $original): ?string
|
||||
{
|
||||
$translation = $this->getTranslation(null, null, $original);
|
||||
return $translation[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a plural translation
|
||||
*/
|
||||
public function getPlural(string $original, int $value): ?string
|
||||
{
|
||||
$translation = $this->getTranslation(null, null, $original);
|
||||
$key = $this->getPluralIndex(null, $value, $translation === null);
|
||||
|
||||
return $translation[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the raw translator data
|
||||
*/
|
||||
public function getEntries(string $context = ''): array
|
||||
{
|
||||
return $this->dictionary[$this->domain][$context] ?? [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user