first commit
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, languages, currencies, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/Locale.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 Locale
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Provides methods for loading locale data and translating strings identified by unique keys
|
||||
*/
|
||||
|
||||
namespace PKP\i18n;
|
||||
|
||||
use Closure;
|
||||
use DateInterval;
|
||||
use DirectoryIterator;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use InvalidArgumentException;
|
||||
use PKP\config\Config;
|
||||
use PKP\core\PKPRequest;
|
||||
use PKP\facades\Repo;
|
||||
use PKP\i18n\interfaces\LocaleInterface;
|
||||
use PKP\i18n\translation\LocaleBundle;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\plugins\PluginRegistry;
|
||||
use PKP\session\SessionManager;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RecursiveRegexIterator;
|
||||
use RegexIterator;
|
||||
use ResourceBundle;
|
||||
use Sokil\IsoCodes\Database\Countries;
|
||||
use Sokil\IsoCodes\Database\Currencies;
|
||||
use Sokil\IsoCodes\Database\LanguagesInterface;
|
||||
use Sokil\IsoCodes\Database\Scripts;
|
||||
use Sokil\IsoCodes\IsoCodesFactory;
|
||||
use SplFileInfo;
|
||||
|
||||
class Locale implements LocaleInterface
|
||||
{
|
||||
/** Max lifetime for the locale metadata cache, the cache is built by scanning the provided paths */
|
||||
protected const MAX_CACHE_LIFETIME = '1 hour';
|
||||
|
||||
/**
|
||||
* @var callable Formatter for missing locale keys
|
||||
* Receives the locale key and must return a string
|
||||
*/
|
||||
protected ?Closure $missingKeyHandler = null;
|
||||
|
||||
/** Current locale cache */
|
||||
protected ?string $locale = null;
|
||||
|
||||
/** @var int[] Folders where locales can be found, where key = path and value = loading priority */
|
||||
protected array $paths = [];
|
||||
|
||||
/** @var callable[] Custom locale loaders */
|
||||
protected array $loaders = [];
|
||||
|
||||
/** Keeps the request */
|
||||
protected ?PKPRequest $request = null;
|
||||
|
||||
/** @var LocaleMetadata[]|null Discovered locales cache */
|
||||
protected ?array $locales = null;
|
||||
|
||||
/** Primary locale cache */
|
||||
protected ?string $primaryLocale = null;
|
||||
|
||||
/** @var string[]|null Supported form locales cache, where key = locale and value = name */
|
||||
protected ?array $supportedFormLocaleNames = null;
|
||||
|
||||
/** @var string[]|null Supported locales cache, where key = locale and value = name */
|
||||
protected ?array $supportedLocaleNames = null;
|
||||
|
||||
/** @var string[]|null Supported locales cache */
|
||||
protected ?array $supportedLocales = null;
|
||||
|
||||
/** @var LocaleBundle[] Keeps a cache for the locale bundles */
|
||||
protected array $localeBundles = [];
|
||||
|
||||
/** @var string[][][]|null Discovered locale files, keyed first by base path and then by locale */
|
||||
protected array $localeFiles = [];
|
||||
|
||||
/** Keeps cached data related only to the current locale */
|
||||
protected array $cache = [];
|
||||
|
||||
/**
|
||||
* @copy \Illuminate\Contracts\Translation\Translator::get()
|
||||
*
|
||||
* @param null|mixed $locale
|
||||
*/
|
||||
public function get($key, array $params = [], $locale = null): string
|
||||
{
|
||||
return $this->translate($key, null, $params, $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy \Illuminate\Contracts\Translation\Translator::choice()
|
||||
*
|
||||
* @param null|mixed $locale
|
||||
*/
|
||||
public function choice($key, $number, array $params = [], $locale = null): string
|
||||
{
|
||||
return $this->translate($key, $number, $params, $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy \Illuminate\Contracts\Translation\Translator::getLocale()
|
||||
*/
|
||||
public function getLocale(): string
|
||||
{
|
||||
if (isset($this->locale)) {
|
||||
return $this->locale;
|
||||
}
|
||||
$request = $this->_getRequest();
|
||||
$locale = $request->getUserVar('setLocale')
|
||||
?: (SessionManager::hasSession() ? SessionManager::getManager()->getUserSession()->getSessionVar('currentLocale') : null)
|
||||
?: $request->getCookieVar('currentLocale');
|
||||
$this->setLocale($locale);
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy \Illuminate\Contracts\Translation\Translator::setLocale()
|
||||
*/
|
||||
public function setLocale($locale): void
|
||||
{
|
||||
if (!$this->isLocaleValid($locale) || !$this->isSupported($locale)) {
|
||||
if ($locale) {
|
||||
error_log((string) new InvalidArgumentException("Invalid/unsupported locale \"{$locale}\", default locale restored"));
|
||||
}
|
||||
$locale = $this->getPrimaryLocale();
|
||||
}
|
||||
|
||||
$this->locale = $locale;
|
||||
setlocale(LC_ALL, 'C.utf8', 'C');
|
||||
\Locale::setDefault(\Locale::lookup(ResourceBundle::getLocales(''), $locale, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getPrimaryLocale()
|
||||
*/
|
||||
public function getPrimaryLocale(): string
|
||||
{
|
||||
if (isset($this->primaryLocale)) {
|
||||
return $this->primaryLocale;
|
||||
}
|
||||
$request = $this->_getRequest();
|
||||
$locale = SessionManager::isDisabled() ? null : $request->getContext()?->getPrimaryLocale() ?? $request->getSite()?->getPrimaryLocale();
|
||||
return $this->primaryLocale = $this->isLocaleValid($locale) ? $locale : $this->getDefaultLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::registerPath()
|
||||
*/
|
||||
public function registerPath(string $path, int $priority = 0): void
|
||||
{
|
||||
$path = new SplFileInfo($path);
|
||||
if (!$path->isDir()) {
|
||||
throw new InvalidArgumentException("\"{$path}\" isn't a valid folder");
|
||||
}
|
||||
|
||||
// Invalidate the loaded bundles cache
|
||||
$realPath = $path->getRealPath();
|
||||
if (($this->paths[$realPath] ?? null) !== $priority) {
|
||||
$this->paths[$realPath] = $priority;
|
||||
$this->localeBundles = [];
|
||||
$this->locales = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::registerLoader()
|
||||
*/
|
||||
public function registerLoader(callable $fileLoader, int $priority = 0): void
|
||||
{
|
||||
// Invalidate the loaded bundles cache
|
||||
if (array_search($fileLoader, $this->loaders[$priority] ?? [], true) === false) {
|
||||
$this->loaders[$priority][] = $fileLoader;
|
||||
$this->localeBundles = [];
|
||||
ksort($this->loaders, SORT_NUMERIC);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::isLocaleValid()
|
||||
*/
|
||||
public function isLocaleValid(?string $locale): bool
|
||||
{
|
||||
return !empty($locale) && preg_match(LocaleInterface::LOCALE_EXPRESSION, $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getMetadata()
|
||||
*/
|
||||
public function getMetadata(string $locale): ?LocaleMetadata
|
||||
{
|
||||
return $this->getLocales()[$locale] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getLocales()
|
||||
*/
|
||||
public function getLocales(): array
|
||||
{
|
||||
$key = __METHOD__ . static::MAX_CACHE_LIFETIME . array_reduce(
|
||||
array_keys($this->paths),
|
||||
fn (string $hash, string $path): string => sha1($hash . $path),
|
||||
''
|
||||
);
|
||||
$expiration = DateInterval::createFromDateString(static::MAX_CACHE_LIFETIME);
|
||||
return $this->locales ??= Cache::remember($key, $expiration, function () {
|
||||
$locales = [];
|
||||
foreach (array_keys($this->paths) as $folder) {
|
||||
foreach (new DirectoryIterator($folder) as $cursor) {
|
||||
if ($cursor->isDir() && $this->isLocaleValid($cursor->getBasename())) {
|
||||
$locales[$cursor->getBasename()] ??= new LocaleMetadata($cursor->getBasename());
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($locales);
|
||||
return $locales;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::installLocale()
|
||||
*/
|
||||
public function installLocale(string $locale): void
|
||||
{
|
||||
Repo::emailTemplate()->dao->installEmailTemplateLocaleData(Repo::emailTemplate()->dao->getMainEmailTemplatesFilename(), [$locale]);
|
||||
// Load all plugins so they can add locale data if needed
|
||||
PluginRegistry::loadAllPlugins();
|
||||
Hook::call('Locale::installLocale', [&$locale]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::uninstallLocale()
|
||||
*/
|
||||
public function uninstallLocale(string $locale): void
|
||||
{
|
||||
// Delete locale-specific data
|
||||
Repo::emailTemplate()->dao->deleteEmailTemplatesByLocale($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether the given locale is supported
|
||||
*/
|
||||
public function isSupported(string $locale): bool
|
||||
{
|
||||
return isset($this->_getSupportedLocales()[$locale]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getSupportedFormLocales()
|
||||
*/
|
||||
public function getSupportedFormLocales(): array
|
||||
{
|
||||
return $this->supportedFormLocaleNames ??= (SessionManager::isDisabled() ? null : $this->_getRequest()->getContext()?->getSupportedFormLocaleNames())
|
||||
?? $this->getSupportedLocales();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getSupportedLocales()
|
||||
*/
|
||||
public function getSupportedLocales(): array
|
||||
{
|
||||
return $this->supportedLocaleNames ??= array_map(fn (string $locale) => $this->getMetadata($locale)->getDisplayName(), $this->_getSupportedLocales());
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::setMissingKeyHandler()
|
||||
*/
|
||||
public function setMissingKeyHandler(?callable $handler): void
|
||||
{
|
||||
$this->missingKeyHandler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getMissingKeyHandler()
|
||||
*/
|
||||
public function getMissingKeyHandler(): ?callable
|
||||
{
|
||||
return $this->missingKeyHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getBundle()
|
||||
*/
|
||||
public function getBundle(?string $locale = null, bool $useCache = true): LocaleBundle
|
||||
{
|
||||
$locale ??= $this->getLocale();
|
||||
$getter = function () use ($locale): LocaleBundle {
|
||||
$bundle = [];
|
||||
foreach ($this->paths as $folder => $priority) {
|
||||
$bundle += $this->_getLocaleFiles($folder, $locale, $priority);
|
||||
}
|
||||
foreach ($this->loaders as $loader) {
|
||||
$loader($locale, $bundle);
|
||||
}
|
||||
return new LocaleBundle($locale, $bundle);
|
||||
};
|
||||
return $useCache ? $this->localeBundles[$locale] ??= $getter() : $getter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getDefaultLocale()
|
||||
*/
|
||||
public function getDefaultLocale(): string
|
||||
{
|
||||
return Config::getVar('i18n', 'locale');
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getCountries()
|
||||
*/
|
||||
public function getCountries(?string $locale = null): Countries
|
||||
{
|
||||
return $this->_getLocaleCache(__METHOD__, $locale, fn () => $this->_getIsoCodes($locale)->getCountries());
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getCurrencies()
|
||||
*/
|
||||
public function getCurrencies(?string $locale = null): Currencies
|
||||
{
|
||||
return $this->_getLocaleCache(__METHOD__, $locale, fn () => $this->_getIsoCodes($locale)->getCurrencies());
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getLanguages()
|
||||
*/
|
||||
public function getLanguages(?string $locale = null, bool $fromCache = true): LanguagesInterface
|
||||
{
|
||||
if ($fromCache) {
|
||||
return $this->_getLocaleCache(
|
||||
__METHOD__,
|
||||
$locale,
|
||||
fn () => $this->_getIsoCodes($locale)->getLanguages()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->_getIsoCodes($locale)->getLanguages();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getScripts()
|
||||
*/
|
||||
public function getScripts(?string $locale = null): Scripts
|
||||
{
|
||||
return $this->_getLocaleCache(__METHOD__, $locale, fn () => $this->_getIsoCodes($locale)->getScripts());
|
||||
}
|
||||
|
||||
/**
|
||||
* @copy LocaleInterface::getFormattedDisplayNames()
|
||||
*/
|
||||
public function getFormattedDisplayNames(array $filterByLocales = null, array $locales = null, int $langLocaleStatus = LocaleMetadata::LANGUAGE_LOCALE_WITH, bool $omitLocaleCodeInDisplay = true): array
|
||||
{
|
||||
$locales ??= $this->getLocales();
|
||||
|
||||
if ($filterByLocales !== null) {
|
||||
$filterByLocales = array_intersect_key($locales, array_flip($filterByLocales));
|
||||
}
|
||||
|
||||
$locales = $this->getFilteredLocales($locales, $filterByLocales ? array_keys($filterByLocales) : null);
|
||||
|
||||
$localeCodesCount = array_count_values(
|
||||
collect(array_keys($filterByLocales ?? $locales))
|
||||
->map(fn (string $value) => trim(explode('@', explode('_', $value)[0])[0]))
|
||||
->toArray()
|
||||
);
|
||||
|
||||
return collect($locales)
|
||||
->map(function (LocaleMetadata $locale, string $localeKey) use ($localeCodesCount, $langLocaleStatus, $omitLocaleCodeInDisplay) {
|
||||
$localeCode = trim(explode('@', explode('_', $localeKey)[0])[0]);
|
||||
$localeDisplay = $locale->getDisplayName(null, ($localeCodesCount[$localeCode] ?? 0) > 1, $langLocaleStatus);
|
||||
return $localeDisplay . ($omitLocaleCodeInDisplay ? '' : " ({$localeKey})");
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filtered locales by locale codes
|
||||
*
|
||||
* @param array $locales List of available all locales
|
||||
* @param array $filterByLocales List of locales code to filter by the returned formatted names list
|
||||
*
|
||||
* @return array The list of locales with formatted display name
|
||||
*/
|
||||
protected function getFilteredLocales(array $locales, array $filterByLocales = null): array
|
||||
{
|
||||
if (!$filterByLocales) {
|
||||
return $locales;
|
||||
}
|
||||
|
||||
return array_intersect_key($locales, array_flip($filterByLocales));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the texts
|
||||
*/
|
||||
protected function translate(string $key, ?int $number, array $params, ?string $locale): string
|
||||
{
|
||||
if (($key = trim($key)) === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$locale ??= $this->getLocale();
|
||||
$localeBundle = $this->getBundle($locale);
|
||||
$value = $number === null ? $localeBundle->translateSingular($key, $params) : $localeBundle->translatePlural($key, $number, $params);
|
||||
if ($value !== null || Hook::call('Locale::translate', [&$value, $key, $params, $number, $locale, $localeBundle])) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// In order to reduce the noise, we're only logging missing entries for the en locale
|
||||
// TODO: Allow the other missing entries to be logged once the Laravel's logging is setup
|
||||
if ($locale === LocaleInterface::DEFAULT_LOCALE) {
|
||||
error_log("Missing locale key \"{$key}\" for the locale \"{$locale}\"");
|
||||
}
|
||||
return is_callable($this->missingKeyHandler) ? ($this->missingKeyHandler)($key) : '##' . htmlentities($key) . '##';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a cached item only if it belongs to the current locale. If it doesn't exist, the getter will be called
|
||||
*/
|
||||
private function _getLocaleCache(string $key, ?string $locale, callable $getter)
|
||||
{
|
||||
if (($locale ??= $this->getLocale()) !== $this->getLocale()) {
|
||||
return $getter();
|
||||
}
|
||||
if (!isset($this->cache[$key][$locale])) {
|
||||
// Ensures the previous cache is cleared
|
||||
$this->cache[$key] = [$locale => $getter()];
|
||||
}
|
||||
return $this->cache[$key][$locale];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a locale folder, retrieves all locale files (.po)
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function _getLocaleFiles(string $folder, string $locale, int $priority): array
|
||||
{
|
||||
$files = $this->localeFiles[$folder][$locale] ?? null;
|
||||
if ($files === null) {
|
||||
$files = [];
|
||||
if (is_dir($path = "{$folder}/{$locale}")) {
|
||||
$directory = new RecursiveDirectoryIterator($path);
|
||||
$iterator = new RecursiveIteratorIterator($directory);
|
||||
$files = array_keys(iterator_to_array(new RegexIterator($iterator, '/\.po$/i', RecursiveRegexIterator::GET_MATCH)));
|
||||
}
|
||||
$this->localeFiles[$folder][$locale] = $files;
|
||||
}
|
||||
return array_fill_keys($files, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the request
|
||||
*/
|
||||
private function _getRequest(): PKPRequest
|
||||
{
|
||||
return app(PKPRequest::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ISO codes factory
|
||||
*/
|
||||
private function _getIsoCodes(string $locale = null): IsoCodesFactory
|
||||
{
|
||||
return app(IsoCodesFactory::class, $locale ? ['locale' => $locale] : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the supported locales
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function _getSupportedLocales(): array
|
||||
{
|
||||
if (isset($this->supportedLocales)) {
|
||||
return $this->supportedLocales;
|
||||
}
|
||||
$locales = (SessionManager::isDisabled() ? null : $this->_getRequest()->getContext()?->getSupportedLocales() ?? $this->_getRequest()->getSite()?->getSupportedLocales())
|
||||
?? array_map(fn (LocaleMetadata $locale) => $locale->locale, $this->getLocales());
|
||||
return $this->supportedLocales = array_combine($locales, $locales);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/LocaleConversion.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 LocaleConversion
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Provides methods to convert locales from one format to another
|
||||
*/
|
||||
|
||||
namespace PKP\i18n;
|
||||
|
||||
use DateInterval;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use PKP\core\Core;
|
||||
use PKP\facades\Locale;
|
||||
|
||||
class LocaleConversion
|
||||
{
|
||||
/** @var string Max lifetime for the ISO639-2b cache. */
|
||||
protected const MAX_ISO6392B_CACHE_LIFETIME = '1 year';
|
||||
|
||||
/**
|
||||
* Get ISO639-2b array
|
||||
*
|
||||
* @throw Exception
|
||||
*/
|
||||
protected static function getISO6392b(): array
|
||||
{
|
||||
$iso6392bFile = Core::getBaseDir() . '/' . PKP_LIB_PATH . '/lib/vendor/sokil/php-isocodes-db-i18n/databases/iso_639-2.json';
|
||||
if (!file_exists($iso6392bFile)) {
|
||||
throw new Exception("The ISO639-2b file {$iso6392bFile} does not exist.");
|
||||
}
|
||||
$key = __METHOD__ . 'iso639-2b' . self::MAX_ISO6392B_CACHE_LIFETIME . filemtime($iso6392bFile);
|
||||
$expiration = DateInterval::createFromDateString(static::MAX_ISO6392B_CACHE_LIFETIME);
|
||||
return Cache::remember($key, $expiration, function () use ($iso6392bFile) {
|
||||
return json_decode(file_get_contents($iso6392bFile), true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the ISO 2-letter language string (ISO639-1) into a ISO compatible 3-letter string (ISO639-2b).
|
||||
*/
|
||||
public static function get3LetterFrom2LetterIsoLanguage(?string $iso2Letter): ?string
|
||||
{
|
||||
try {
|
||||
$languages = self::getISO6392b();
|
||||
} catch (Exception $e) {
|
||||
error_log($e->getMessage());
|
||||
return null;
|
||||
}
|
||||
foreach (reset($languages) as $languageRaw) {
|
||||
if (($languageRaw['alpha_2'] ?? null) === $iso2Letter) {
|
||||
return $languageRaw['bibliographic'] ?? $languageRaw['alpha_3'];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the ISO 3-letter language string (ISO639-2b) into a ISO compatible 2-letter string (ISO639-1).
|
||||
*/
|
||||
public static function get2LetterFrom3LetterIsoLanguage(?string $iso3Letter): ?string
|
||||
{
|
||||
try {
|
||||
$languages = self::getISO6392b();
|
||||
} catch (Exception $e) {
|
||||
error_log($e->getMessage());
|
||||
return null;
|
||||
}
|
||||
foreach (reset($languages) as $languageRaw) {
|
||||
if (($languageRaw['bibliographic'] ?? null) === $iso3Letter || $languageRaw['alpha_3'] === $iso3Letter) {
|
||||
return $languageRaw['alpha_2'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the PKP locale identifier into an ISO639-2b compatible 3-letter string.
|
||||
*/
|
||||
public static function get3LetterIsoFromLocale(?string $locale): ?string
|
||||
{
|
||||
$iso2Letter = substr($locale, 0, 2);
|
||||
return static::get3LetterFrom2LetterIsoLanguage($iso2Letter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an ISO639-2b compatible 3-letter string into the PKP locale identifier.
|
||||
* This can be ambiguous if several locales are defined for the same language. In this case we'll use the primary locale to disambiguate.
|
||||
* If that still doesn't determine a unique locale then we'll choose the first locale found.
|
||||
*/
|
||||
public static function getLocaleFrom3LetterIso(?string $iso3Letter): ?string
|
||||
{
|
||||
$primaryLocale = Locale::getPrimaryLocale();
|
||||
|
||||
$alpha2Candidates = $localeCandidates = [];
|
||||
try {
|
||||
$languages = self::getISO6392b();
|
||||
} catch (Exception $e) {
|
||||
error_log($e->getMessage());
|
||||
return null;
|
||||
}
|
||||
foreach (reset($languages) as $languageRaw) {
|
||||
if (($languageRaw['bibliographic'] ?? null) === $iso3Letter || $languageRaw['alpha_3'] === $iso3Letter) {
|
||||
if (array_key_exists('alpha_2', $languageRaw)) {
|
||||
$alpha2Candidates[] = $languageRaw['alpha_2'];
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (Locale::getLocales() as $identifier => $locale) {
|
||||
if (in_array($locale->getIsoAlpha2(), $alpha2Candidates)) {
|
||||
if ($identifier === $primaryLocale) {
|
||||
// In case of ambiguity the primary locale overrides all other options so we're done.
|
||||
return $primaryLocale;
|
||||
}
|
||||
$localeCandidates[$identifier] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to retrieve the first matching locale which is in the supported list, otherwise defaults to the first found candidate
|
||||
return Arr::first(array_keys(Locale::getSupportedLocales()), fn (string $locale) => $localeCandidates[$locale] ?? false, array_key_first($localeCandidates));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the ISO 2-letter language string (ISO639-1) into ISO639-3.
|
||||
*/
|
||||
public static function getIso3FromIso1(?string $iso1): ?string
|
||||
{
|
||||
$locale = Arr::first(Locale::getLocales(), fn (LocaleMetadata $locale) => $locale->getIsoAlpha2() === $iso1);
|
||||
return $locale ? $locale->getIsoAlpha3() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the ISO639-3 into ISO639-1.
|
||||
*/
|
||||
public static function getIso1FromIso3(?string $iso3): ?string
|
||||
{
|
||||
$locale = Arr::first(Locale::getLocales(), fn (LocaleMetadata $locale) => $locale->getIsoAlpha3() === $iso3);
|
||||
return $locale ? $locale->getIsoAlpha2() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the PKP locale identifier into an ISO639-3 compatible 3-letter string.
|
||||
*/
|
||||
public static function getIso3FromLocale(?string $locale): ?string
|
||||
{
|
||||
$iso1 = substr($locale, 0, 2);
|
||||
return static::getIso3FromIso1($iso1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the PKP locale identifier into an ISO639-1 compatible 2-letter string.
|
||||
*/
|
||||
public static function getIso1FromLocale(?string $locale): string
|
||||
{
|
||||
return substr($locale, 0, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/LocaleMetadata.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 LocaleMetadata
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Holds metadata about a system locale
|
||||
*/
|
||||
|
||||
namespace PKP\i18n;
|
||||
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use PKP\core\ExportableTrait;
|
||||
use PKP\core\PKPString;
|
||||
use PKP\facades\Locale;
|
||||
use PKP\i18n\interfaces\LocaleInterface;
|
||||
use Sokil\IsoCodes\Database\Languages\Language;
|
||||
|
||||
class LocaleMetadata
|
||||
{
|
||||
use ExportableTrait;
|
||||
|
||||
/**
|
||||
* The following constants define how the locale information will be presented
|
||||
*
|
||||
* LANGUAGE_LOCALE_WITHOUT : The locale will be presented in the current selected language
|
||||
* So, if English and French is available and user selected locale is French, it will be
|
||||
* shown as Français | Anglais
|
||||
*
|
||||
* LANGUAGE_LOCALE_WITH : The locale will be presented in current selected language along
|
||||
* with each locale's own translated name . So, if English and French is available and user
|
||||
* selected locale is French, it will be shown as Français/French | Anglais/English
|
||||
*
|
||||
* LANGUAGE_LOCALE_ONLY : The locale will be presented only in each locale's translated
|
||||
* name . So, if English and French is available and user
|
||||
* selected locale is French, it will be shown as Français | English
|
||||
*/
|
||||
public const LANGUAGE_LOCALE_WITHOUT = 1;
|
||||
public const LANGUAGE_LOCALE_WITH = 2;
|
||||
public const LANGUAGE_LOCALE_ONLY = 3;
|
||||
|
||||
private ?object $_parsedLocale = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(
|
||||
/** Locale identification */
|
||||
public ?string $locale = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the language locale conversion status
|
||||
*
|
||||
*/
|
||||
public static function getLanguageLocaleStatuses(): array
|
||||
{
|
||||
return [
|
||||
self::LANGUAGE_LOCALE_WITHOUT,
|
||||
self::LANGUAGE_LOCALE_WITH,
|
||||
self::LANGUAGE_LOCALE_ONLY
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the display name
|
||||
*
|
||||
* @param string $locale The locale code
|
||||
* @param bool $withCountry Whether to append the country name to language
|
||||
* @param int $langLocaleStatus The language locale conversion value specified by const LocaleMetadata::LANGUAGE_LOCALE_*
|
||||
*
|
||||
* @return string The fully qualified locale with/without own translated locale and with/without country name
|
||||
*/
|
||||
public function getDisplayName(?string $locale = null, bool $withCountry = false, int $langLocaleStatus = self::LANGUAGE_LOCALE_WITHOUT): string
|
||||
{
|
||||
if (!in_array($langLocaleStatus, static::getLanguageLocaleStatuses())) {
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
'Invalid language locale conversion status %s given, must be among [%s]',
|
||||
$langLocaleStatus,
|
||||
implode(',', static::getLanguageLocaleStatuses())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$name = PKPString::regexp_replace(
|
||||
'/\s*\([^)]*\)\s*/',
|
||||
'',
|
||||
PKPString::ucfirst(
|
||||
$this
|
||||
->_getLanguage(
|
||||
$langLocaleStatus === self::LANGUAGE_LOCALE_ONLY ? $this->locale : $locale,
|
||||
$langLocaleStatus === self::LANGUAGE_LOCALE_WITH
|
||||
)
|
||||
->getLocalName()
|
||||
)
|
||||
);
|
||||
|
||||
if ($langLocaleStatus === self::LANGUAGE_LOCALE_WITH) {
|
||||
// Get the translated language name in language's own locale
|
||||
$nameInLangLocale = PKPString::regexp_replace(
|
||||
'/\s*\([^)]*\)\s*/',
|
||||
'',
|
||||
PKPString::ucfirst($this->_getLanguage($this->locale)->getLocalName())
|
||||
);
|
||||
|
||||
$name = __(
|
||||
'common.withForwardSlash',
|
||||
[
|
||||
'item' => $name,
|
||||
'afterSlash' => $nameInLangLocale,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (!$withCountry) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$country = $this->getCountry($locale);
|
||||
|
||||
if (!$country) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
if ($langLocaleStatus !== self::LANGUAGE_LOCALE_WITHOUT) {
|
||||
$localizedCountryName = $this->getCountry($this->locale);
|
||||
|
||||
if ($langLocaleStatus === self::LANGUAGE_LOCALE_ONLY) {
|
||||
$country = $localizedCountryName;
|
||||
} else {
|
||||
if (strcmp($localizedCountryName, $country) !== 0) {
|
||||
$country = __(
|
||||
'common.withForwardSlash',
|
||||
[
|
||||
'item' => $country,
|
||||
'afterSlash' => $localizedCountryName
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return __(
|
||||
'common.withParenthesis',
|
||||
[
|
||||
'item' => $name,
|
||||
'inParenthesis' => $country,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the language name
|
||||
*/
|
||||
public function getLanguage(?string $locale = null): string
|
||||
{
|
||||
return $this->_getLanguage($locale)->getLocalName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the country name
|
||||
*/
|
||||
public function getCountry(?string $locale = null): ?string
|
||||
{
|
||||
return $this->_parse()->country ? Locale::getCountries($locale)->getByAlpha2($this->_parse()->country)?->getLocalName() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the script name
|
||||
*/
|
||||
public function getScript(?string $locale = null): ?string
|
||||
{
|
||||
$script = ucfirst($this->_parse()->script);
|
||||
return $this->_parse()->script ? Locale::getScripts($locale)->getByAlpha4($script)->getLocalName() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the locale expects text on the right-to-left format
|
||||
*/
|
||||
public function isRightToLeft(): bool
|
||||
{
|
||||
$locale = $this->_parse();
|
||||
$language = strtolower($locale->language ?? '');
|
||||
$script = strtolower($locale->script ?? '');
|
||||
$rightToLeftLanguages = array_fill_keys(['ar', 'dv', 'fa', 'he', 'ku', 'nqo', 'prs', 'ps', 'sd', 'syr', 'ug', 'ur', 'yi'], true);
|
||||
$languageScriptExceptions = ['sd-deva' => false, 'tzm-arab' => true, 'pa-arab' => true];
|
||||
return $languageScriptExceptions["{$language}-{$script}"]
|
||||
?? $rightToLeftLanguages[$language]
|
||||
?? $languageScriptExceptions[$this->getIsoAlpha3() . "-{$script}"]
|
||||
?? $rightToLeftLanguages[$this->getIsoAlpha3()]
|
||||
?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two locales and retrieves the completeness ratio (source locale keys which are present in the reference)
|
||||
* If a locale isn't supplied, LocaleInterface::DEFAULT_LOCALE will be used as reference
|
||||
*/
|
||||
public function getCompletenessRatio(string $referenceLocale = null): float
|
||||
{
|
||||
$destiny = Locale::getBundle($referenceLocale ?? LocaleInterface::DEFAULT_LOCALE)->getTranslator()->getEntries();
|
||||
$source = Locale::getBundle($this->locale, false)->getTranslator()->getEntries();
|
||||
$intersection = array_intersect_key($source, $destiny);
|
||||
return min(1, count($intersection) / max(1, count($destiny)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether the locale can be considered complete respecting a threshold level of completeness
|
||||
*/
|
||||
public function isComplete(float $minimumThreshold = 0.9, ?string $referenceLocale = null): bool
|
||||
{
|
||||
return $this->getCompletenessRatio($referenceLocale) >= $minimumThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ISO639-1 representation
|
||||
*/
|
||||
public function getIsoAlpha2(): string
|
||||
{
|
||||
return $this->_getLanguage()->getAlpha2();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ISO639-3 representation
|
||||
*/
|
||||
public function getIsoAlpha3(): string
|
||||
{
|
||||
return $this->_getLanguage()->getAlpha3();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the language
|
||||
*/
|
||||
private function _getLanguage(?string $locale = null, bool $fromCache = true): ?Language
|
||||
{
|
||||
return Locale::getLanguages($locale, $fromCache)->getByAlpha2($this->_parse()->language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the locale string and retrieve its pieces
|
||||
*/
|
||||
private function _parse(): object
|
||||
{
|
||||
if (isset($this->_parsedLocale)) {
|
||||
return $this->_parsedLocale;
|
||||
}
|
||||
if (!preg_match(LocaleInterface::LOCALE_EXPRESSION, $this->locale, $matches)) {
|
||||
throw new DomainException("Invalid locale \"{$this->locale}\"");
|
||||
}
|
||||
return $this->_parsedLocale = (object) [
|
||||
'language' => $matches['language'],
|
||||
'country' => $matches['country'] ?? null,
|
||||
// Updates our script definitions to match the ISO 15924
|
||||
'script' => isset($matches['script']) ? str_replace(['cyrillic', 'latin'], ['latn', 'cyrl'], strtolower($matches['script'])) : null
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/LocaleServiceProvider.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 LocaleServiceProvider
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Service provider for the i18n features
|
||||
*/
|
||||
|
||||
namespace PKP\i18n;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||
use Illuminate\Contracts\Translation\Translator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use PKP\facades\Locale as LocaleFacade;
|
||||
use PKP\i18n\interfaces\LocaleInterface;
|
||||
use PKP\i18n\translation\IsoCodesTranslationDriver;
|
||||
use Sokil\IsoCodes\IsoCodesFactory;
|
||||
|
||||
class LocaleServiceProvider extends ServiceProvider implements DeferrableProvider
|
||||
{
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(LocaleInterface::class, fn () => $this->app->make(Locale::class));
|
||||
// Replaces the default Laravel translator
|
||||
$this->app->alias(LocaleInterface::class, 'translator');
|
||||
$this->app->alias(LocaleInterface::class, Translator::class);
|
||||
|
||||
// Reuses the instance and keeps the user selected language across the application
|
||||
$this->app->singleton(
|
||||
IsoCodesFactory::class,
|
||||
fn (Container $container, array $params): IsoCodesFactory => new IsoCodesFactory(
|
||||
null,
|
||||
new IsoCodesTranslationDriver($params['locale'] ?? LocaleFacade::getLocale())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [LocaleInterface::class, Translator::class, 'translator'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/PKPLocale.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 PKPLocale
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Deprecated class, kept only for backwards compatibility with external plugins
|
||||
*/
|
||||
|
||||
namespace PKP\i18n;
|
||||
|
||||
use PKP\facades\Locale;
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
/**
|
||||
* @deprecated The class \PKP\i18n\PKPLocale has been replaced by PKP\facades\Locale
|
||||
*/
|
||||
class PKPLocale
|
||||
{
|
||||
/**
|
||||
* Return the key name of the user's currently selected locale (default
|
||||
* is "en" English).
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @deprecated 3.4.0.0 The same method is available at \PKP\facades\Locale::getLocale()
|
||||
*/
|
||||
public static function getLocale()
|
||||
{
|
||||
return Locale::getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the primary locale of the current context.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @deprecated 3.4.0.0 The same method is available at \PKP\facades\Locale::getPrimaryLocale(), but before using this method, try to retrieve the locale directly from a nearby context
|
||||
*/
|
||||
public static function getPrimaryLocale()
|
||||
{
|
||||
return Locale::getPrimaryLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a set of locale components. Parameters of mixed length may
|
||||
* be supplied, each a LOCALE_COMPONENT_... constant. An optional final
|
||||
* parameter may be supplied to specify the locale (e.g. 'en').
|
||||
*
|
||||
* @deprecated 3.4.0.0 All the available locale keys are already loaded
|
||||
*/
|
||||
public static function requireComponents()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all available locales.
|
||||
*
|
||||
* @deprecated 3.4.0.0 Use the \PKP\facades\Locale::getLocales()
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function &getAllLocales()
|
||||
{
|
||||
$locales = array_map(fn (LocaleMetadata $locale) => $locale->getDisplayName(), Locale::getLocales());
|
||||
return $locales;
|
||||
}
|
||||
}
|
||||
|
||||
class_alias('\PKP\i18n\PKPLocale', '\PKPLocale');
|
||||
|
||||
// Shared locale components
|
||||
define('LOCALE_COMPONENT_PKP_COMMON', 0x00000001);
|
||||
define('LOCALE_COMPONENT_PKP_ADMIN', 0x00000002);
|
||||
define('LOCALE_COMPONENT_PKP_INSTALLER', 0x00000003);
|
||||
define('LOCALE_COMPONENT_PKP_MANAGER', 0x00000004);
|
||||
define('LOCALE_COMPONENT_PKP_READER', 0x00000005);
|
||||
define('LOCALE_COMPONENT_PKP_SUBMISSION', 0x00000006);
|
||||
define('LOCALE_COMPONENT_PKP_USER', 0x00000007);
|
||||
define('LOCALE_COMPONENT_PKP_GRID', 0x00000008);
|
||||
define('LOCALE_COMPONENT_PKP_DEFAULT', 0x00000009);
|
||||
define('LOCALE_COMPONENT_PKP_EDITOR', 0x0000000A);
|
||||
define('LOCALE_COMPONENT_PKP_REVIEWER', 0x0000000B);
|
||||
define('LOCALE_COMPONENT_PKP_API', 0x0000000C);
|
||||
|
||||
// Application-specific locale components
|
||||
define('LOCALE_COMPONENT_APP_COMMON', 0x00000100);
|
||||
define('LOCALE_COMPONENT_APP_MANAGER', 0x00000101);
|
||||
define('LOCALE_COMPONENT_APP_SUBMISSION', 0x00000102);
|
||||
define('LOCALE_COMPONENT_APP_AUTHOR', 0x00000103);
|
||||
define('LOCALE_COMPONENT_APP_EDITOR', 0x00000104);
|
||||
define('LOCALE_COMPONENT_APP_ADMIN', 0x00000105);
|
||||
define('LOCALE_COMPONENT_APP_DEFAULT', 0x00000106);
|
||||
define('LOCALE_COMPONENT_APP_API', 0x00000107);
|
||||
define('LOCALE_COMPONENT_APP_EMAIL', 0x00000108);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @defgroup i18n I18N
|
||||
* Implements localization concerns such as locale files, time zones, and country lists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/i18n/interfaces/LocaleInterface.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 LocaleInterface
|
||||
*
|
||||
* @ingroup i18n
|
||||
*
|
||||
* @brief Provides methods for loading gettext locale files and translating texts
|
||||
*/
|
||||
|
||||
namespace PKP\i18n\interfaces;
|
||||
|
||||
use PKP\i18n\LocaleMetadata;
|
||||
use PKP\i18n\translation\LocaleBundle;
|
||||
use Sokil\IsoCodes\Database\Countries;
|
||||
use Sokil\IsoCodes\Database\Currencies;
|
||||
use Sokil\IsoCodes\Database\LanguagesInterface;
|
||||
use Sokil\IsoCodes\Database\Scripts;
|
||||
|
||||
interface LocaleInterface extends \Illuminate\Contracts\Translation\Translator
|
||||
{
|
||||
/** Keeps the default locale of the application */
|
||||
public const DEFAULT_LOCALE = 'en';
|
||||
|
||||
/** Regular expression to validate and extract pieces of a locale code */
|
||||
public const LOCALE_EXPRESSION = '/^(?P<language>[a-z]{2})(?:_(?P<country>[A-Za-z]{2,4}))?(?:@(?P<script>[A-Za-z\d]{5,8}|\d[A-Za-z\d]{3}))?$/';
|
||||
|
||||
/**
|
||||
* Attempts to retrieve the primary locale for the current context, if not available, then for the site.
|
||||
*
|
||||
* @deprecated 3.4.0 Use Context::getPrimaryLocale()
|
||||
*/
|
||||
public function getPrimaryLocale(): string;
|
||||
|
||||
/**
|
||||
* Register a locale folder
|
||||
*
|
||||
* @param string $path The given folder is expected to have sub-folders, each one representing a locale (e.g. "./en").
|
||||
* The application will then look for .po files and attempt to lazy load them when requested.
|
||||
* @param int $priority The priority controls which locale should be loaded first, higher priorities overwrite smaller ones (in case of locale key conflicts), the default is 0
|
||||
*/
|
||||
public function registerPath(string $path, int $priority = 0): void;
|
||||
|
||||
/**
|
||||
* Register a locale file loader
|
||||
*
|
||||
* @param callable $fileLoader Receives two arguments.
|
||||
* string $locale The locale string
|
||||
* array $localeFiles An array (key = file path, value = the loading priority) with the locale files to be loaded.
|
||||
* The second argument might be received as a reference (&) in order to update the locales.
|
||||
* The $fileLoader will be invoked once when loading a locale.
|
||||
* @param int $priority Defines the calling priority, higher values will be called later, the default is 0
|
||||
*/
|
||||
public function registerLoader(callable $fileLoader, int $priority = 0): void;
|
||||
|
||||
/**
|
||||
* Check if the supplied locale is valid.
|
||||
*/
|
||||
public function isLocaleValid(?string $locale): bool;
|
||||
|
||||
/**
|
||||
* Retrieves the metadata of a locale
|
||||
*/
|
||||
public function getMetadata(string $locale): ?LocaleMetadata;
|
||||
|
||||
/**
|
||||
* Retrieves a list of available locales with their metadata
|
||||
|
||||
*
|
||||
* @return LocaleMetadata[]
|
||||
*/
|
||||
public function getLocales(): array;
|
||||
|
||||
/**
|
||||
* Install support for a new locale.
|
||||
*/
|
||||
public function installLocale(string $locale): void;
|
||||
|
||||
/**
|
||||
* Uninstall support for an existing locale.
|
||||
*/
|
||||
public function uninstallLocale(string $locale): void;
|
||||
|
||||
/**
|
||||
* Retrieves whether the given locale is in the list of supported locales
|
||||
*/
|
||||
public function isSupported(string $locale): bool;
|
||||
|
||||
/**
|
||||
* Get all supported form locales for the current context (if not available, then from the site).
|
||||
*
|
||||
* @deprecated 3.4.0 Use Context::getSupportedFormLocales()
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSupportedFormLocales(): array;
|
||||
|
||||
/**
|
||||
* Get all supported locales for the current context (if not available, then from the site).
|
||||
*
|
||||
* @deprecated 3.4.0 Use Context::getSupportedLocales()
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSupportedLocales(): array;
|
||||
|
||||
/**
|
||||
* Sets the handler to format missing locale keys
|
||||
*/
|
||||
public function setMissingKeyHandler(?callable $handler): void;
|
||||
|
||||
/**
|
||||
* Retrieves the handler to format missing locale keys
|
||||
*/
|
||||
public function getMissingKeyHandler(): ?callable;
|
||||
|
||||
/**
|
||||
* Retrieves a locale bundle to translate texts.
|
||||
*
|
||||
*/
|
||||
public function getBundle(?string $locale = null, bool $useCache = true): LocaleBundle;
|
||||
|
||||
/**
|
||||
* Retrieves the default locale
|
||||
*/
|
||||
public function getDefaultLocale(): string;
|
||||
|
||||
/**
|
||||
* Retrieve the countries
|
||||
*/
|
||||
public function getCountries(?string $locale = null): Countries;
|
||||
|
||||
/**
|
||||
* Retrieve the currencies
|
||||
*/
|
||||
public function getCurrencies(?string $locale = null): Currencies;
|
||||
|
||||
/**
|
||||
* Retrieve the languages
|
||||
*/
|
||||
public function getLanguages(?string $locale = null, bool $fromCache = true): LanguagesInterface;
|
||||
|
||||
/**
|
||||
* Retrieve the scripts
|
||||
*/
|
||||
public function getScripts(?string $locale = null): Scripts;
|
||||
|
||||
/**
|
||||
* Get the formatted locale display names with country if same language code present multiple times
|
||||
*
|
||||
* @param array $filterByLocales Optional list of locales code to filter by the returned formatted names list
|
||||
* @param array $locales Optional list of available all locales
|
||||
* @param int $langLocaleStatus The const value of one of LocaleMetadata:LANGUAGE_LOCALE_*
|
||||
* @param bool $omitLocaleCodeInDisplay Should leave out the locale code from display. By default leave out.
|
||||
*
|
||||
* @return array The list of locales with formatted display name
|
||||
*/
|
||||
public function getFormattedDisplayNames(array $filterByLocales = null, array $locales = null, int $langLocaleStatus = LocaleMetadata::LANGUAGE_LOCALE_WITH, bool $omitLocaleCodeInDisplay = true): array;
|
||||
}
|
||||
@@ -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