Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkp/pkp-lib#9707 use weblate locales for ui #10569

Merged
merged 12 commits into from
Dec 11, 2024
4 changes: 2 additions & 2 deletions classes/context/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ public function getSupportedFormLocales(): ?array
/**
* Return associative array of all locales supported by forms on the site.
*
* @param int $langLocaleStatus The const value of one of LocaleMetadata:LANGUAGE_LOCALE_*
* @param int $langLocaleStatus The const value of one of LocaleMetadata::LANGUAGE_LOCALE_*
*
* @return array
*/
Expand Down Expand Up @@ -377,7 +377,7 @@ public function getSupportedLocales()
* Return associative array of all locales supported by the site.
* These locales are used to provide a language toggle on the main site pages.
*
* @param int $langLocaleStatus The const value of one of LocaleMetadata:LANGUAGE_LOCALE_*
* @param int $langLocaleStatus The const value of one of LocaleMetadata::LANGUAGE_LOCALE_*
*
* @return array
*/
Expand Down
2 changes: 1 addition & 1 deletion classes/core/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public function &getData(string $key, string $locale = null)
* @param mixed $value can be either a single value or
* an array of of localized values in the form:
* array(
* 'fr_FR' => 'en français',
* 'fr' => 'en français',
* 'en' => 'in English',
* ...
* )
Expand Down
14 changes: 0 additions & 14 deletions classes/core/traits/LocalizedData.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,6 @@

trait LocalizedData
{
/** @var Conversion table for locales */
public array $_localesTable = [
'be@cyrillic' => 'be',
'bs' => 'bs_Latn',
'fr_FR' => 'fr',
'nb' => 'nb_NO',
'sr@cyrillic' => 'sr_Cyrl',
'sr@latin' => 'sr_Latn',
'uz@cyrillic' => 'uz',
'uz@latin' => 'uz_Latn',
'zh_CN' => 'zh_Hans',
];

/**
* Get a localized value from a multilingual data array
*
Expand Down Expand Up @@ -77,7 +64,6 @@ public function getLocalePrecedence(?string $preferredLocale = null): array
return array_unique(
array_filter([
$preferredLocale ?? Locale::getLocale(),
$this->_localesTable[$preferredLocale ?? Locale::getLocale()] ?? null,
$this->getDefaultLocale(),
$request->getContext()?->getPrimaryLocale(),
$request->getSite()->getPrimaryLocale(),
Expand Down
1 change: 0 additions & 1 deletion classes/galley/Galley.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

use APP\facades\Repo;
use PKP\facades\Locale;
use PKP\i18n\LocaleMetadata;
use PKP\services\PKPSchemaService;
use PKP\submission\Representation;
use PKP\submissionFile\SubmissionFile;
Expand Down
117 changes: 41 additions & 76 deletions classes/i18n/Locale.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
use Closure;
use DateInterval;
use DirectoryIterator;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use PKP\config\Config;
use PKP\core\Core;
use PKP\core\PKPRequest;
use PKP\facades\Repo;
use PKP\core\PKPSessionGuard;
use PKP\facades\Repo;
use PKP\i18n\interfaces\LocaleInterface;
use PKP\i18n\translation\LocaleBundle;
use PKP\i18n\ui\UITranslator;
Expand All @@ -42,7 +44,6 @@
use RecursiveIteratorIterator;
use RecursiveRegexIterator;
use RegexIterator;
use ResourceBundle;
use Sokil\IsoCodes\Database\Countries;
use Sokil\IsoCodes\Database\Currencies;
use Sokil\IsoCodes\Database\LanguagesInterface;
Expand All @@ -56,7 +57,7 @@ class Locale implements LocaleInterface
protected const MAX_CACHE_LIFETIME = '1 hour';

/** @var string Max lifetime for the submission locales cache. */
protected const MAX_SUBMISSION_LOCALES_CACHE_LIFETIME = '1 year';
protected const MAX_WEBLATE_LOCALES_CACHE_LIFETIME = '1 year';

/**
* @var callable Formatter for missing locale keys
Expand Down Expand Up @@ -100,8 +101,8 @@ class Locale implements LocaleInterface
/** Keeps cached data related only to the current locale */
protected array $cache = [];

/** @var string[]|null Available submission locales cache, where key = locale and value = name */
protected ?array $submissionLocaleNames = null;
/** @var string[]|null Available weblate locales cache, where key = locale and value = weblate name */
protected ?array $weblateLocaleNames = null;

/**
* @copy \Illuminate\Contracts\Translation\Translator::get()
Expand Down Expand Up @@ -154,7 +155,8 @@ public function setLocale($locale): void

$this->locale = $locale;
setlocale(LC_ALL, 'C.utf8', 'C');
\Locale::setDefault(\Locale::lookup(ResourceBundle::getLocales(''), $locale, true));
$locales = array_keys($this->getWeblateLocaleNames());
\Locale::setDefault(\Locale::lookup($locales, $locale, true));
}

/**
Expand Down Expand Up @@ -207,15 +209,8 @@ public function registerLoader(callable $fileLoader, int $priority = 0): void
*/
public function isLocaleValid(?string $locale): bool
{
return !empty($locale) && preg_match(LocaleInterface::LOCALE_EXPRESSION, $locale);
}

/**
* @copy LocaleInterface::isSubmissionLocaleValid()
*/
public function isSubmissionLocaleValid(?string $locale): bool
{
return !empty($locale) && preg_match(LocaleInterface::LOCALE_EXPRESSION_SUBMISSION, $locale);
$locales = $this->getWeblateLocaleNames();
return !empty($locale) && array_key_exists($locale, $locales);
}

/**
Expand Down Expand Up @@ -389,13 +384,12 @@ public function getFormattedDisplayNames(?array $filterByLocales = null, ?array
$locales ??= $this->getLocales();

if ($filterByLocales !== null) {
$filterByLocales = array_intersect_key($locales, array_flip($filterByLocales));
$locales = array_intersect_key($locales, array_flip($filterByLocales));
$filterByLocales = array_keys($locales);
}

$locales = $this->getFilteredLocales($locales, $filterByLocales ? array_keys($filterByLocales) : null);

$localeCodesCount = array_count_values(
collect(array_keys($filterByLocales ?? $locales))
collect($filterByLocales ?? array_keys($locales))
->map(fn (string $value) => trim(explode('@', explode('_', $value)[0])[0]))
->toArray()
);
Expand All @@ -419,6 +413,28 @@ public function getUiTranslator(): UITranslator
return new UITranslator($locale, $this->paths, $localeBundleCacheKey);
}

/**
* Get Weblate languages to array
* Combine app's language names with weblate's in English.
* Weblate's names override app's if same locale key
*
* @throws Exception
*
* @return string[]
*
*/
public function getWeblateLocaleNames(): array
{
return $this->weblateLocaleNames ??= (function (): array {
$file = Core::getBaseDir() . '/' . PKP_LIB_PATH . '/lib/weblateLanguages/languages.json';
$key = __METHOD__ . self::MAX_WEBLATE_LOCALES_CACHE_LIFETIME . filemtime($file);
$expiration = DateInterval::createFromDateString(self::MAX_WEBLATE_LOCALES_CACHE_LIFETIME);
return Cache::remember($key, $expiration, fn (): array => collect(json_decode(file_get_contents($file) ?: throw new Exception('Failed to load Weblate locales'), true))
->sortKeys()
->all());
})();
}

/**
* Get appropriately localized display names for submission locales to array
* If $filterByLocales empty, return all languages.
Expand All @@ -431,42 +447,16 @@ public function getUiTranslator(): UITranslator
*/
public function getSubmissionLocaleDisplayNames(array $filterByLocales = [], ?string $displayLocale = null): array
{
$convDispLocale = $this->convertSubmissionLocaleCode($displayLocale ?: $this->getLocale());
return collect($this->_getSubmissionLocaleNames())
->when($filterByLocales, fn ($sln) => $sln->intersectByKeys(array_is_list($filterByLocales) ? array_flip(array_filter($filterByLocales)) : $filterByLocales))
->when($convDispLocale !== 'en', fn ($sln) => $sln->map(function ($nameEn, $l) use ($convDispLocale) {
$cl = $this->convertSubmissionLocaleCode($l);
$dn = locale_get_display_name($cl, $convDispLocale);
return ($dn && $dn !== $cl) ? $dn : "*$nameEn";
$displayLocale = $displayLocale ?: $this->getLocale();
return collect($this->getWeblateLocaleNames())
->when($filterByLocales, fn (Collection $sln) => $sln->intersectByKeys(array_is_list($filterByLocales) ? array_flip(array_filter($filterByLocales)) : $filterByLocales))
->when($displayLocale !== 'en', fn (Collection $sln) => $sln->map(function ($nameEn, $l) use ($displayLocale) {
$dn = locale_get_display_name($l, $displayLocale);
return ($dn && $dn !== $l) ? $dn : "*{$nameEn}";
}))
->toArray();
}

/**
* Convert submission locale code
*/
public function convertSubmissionLocaleCode(string $locale): string
{
return str_replace(['@cyrillic', '@latin'], ['_Cyrl', '_Latn'], $locale);
}

/**
* 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
*
Expand Down Expand Up @@ -559,31 +549,6 @@ private function _getSupportedLocales(): array
return $this->supportedLocales = array_combine($locales, $locales);
}

/**
* Get Weblate submission languages to array
* Combine app's language names with weblate's in English.
* Weblate's names override app's if same locale key
*
* @return string[]
*/
private function _getSubmissionLocaleNames(): array
{
return $this->submissionLocaleNames ??= (function (): array {
$file = Core::getBaseDir() . '/' . PKP_LIB_PATH . '/lib/weblateLanguages/languages.json';
$key = __METHOD__ . self::MAX_SUBMISSION_LOCALES_CACHE_LIFETIME . filemtime($file);
$expiration = DateInterval::createFromDateString(self::MAX_SUBMISSION_LOCALES_CACHE_LIFETIME);
return Cache::remember($key, $expiration, fn (): array => collect($this->getLocales())
->map(function (LocaleMetadata $lm, string $l): string {
$cl = $this->convertSubmissionLocaleCode($l);
$n = locale_get_display_name($cl, 'en');
return ($n && $n !== $cl) ? $n : $lm->getDisplayName('en', true);
})
->merge(json_decode(file_get_contents($file) ?: '', true) ?: [])
->sortKeys()
->toArray());
})();
}

/**
* Retrieve the preferred user locale from our supported locales using the Accept-Language header
* If there's no match, it falls back to the server's primary locale
Expand Down
47 changes: 37 additions & 10 deletions classes/i18n/LocaleConversion.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,36 @@ public static function get2LetterFrom3LetterIsoLanguage(?string $iso3Letter): ?s
/**
* Translate the PKP locale identifier into an ISO639-2b compatible 3-letter string.
*/
public static function get3LetterIsoFromLocale(?string $locale): ?string
public static function get3LetterIsoFromLocale(string $locale): ?string
{
$iso2Letter = substr($locale, 0, 2);
return static::get3LetterFrom2LetterIsoLanguage($iso2Letter);
if (!Locale::isLocaleValid($locale)) {
return null;
}
try {
$languages = self::getISO6392b();
} catch (Exception $e) {
error_log($e->getMessage());
return null;
}
$language = \Locale::getPrimaryLanguage($locale);

foreach (reset($languages) as $languageRaw) {
if (($languageRaw['alpha_2'] ?? null) === $language || $languageRaw['alpha_3'] === $language) {
if ($languageRaw['bibliographic'] ?? null) {
return $languageRaw['bibliographic'];
}
return $languageRaw['alpha_3'] ?? null;
}
}
return null;
}

/**
* 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.
*
* @deprecated 3.5
*/
public static function getLocaleFrom3LetterIso(?string $iso3Letter): ?string
{
Expand Down Expand Up @@ -140,7 +160,7 @@ public static function getLocaleFrom3LetterIso(?string $iso3Letter): ?string
/**
* Translate the ISO 2-letter language string (ISO639-1) into ISO639-3.
*/
public static function getIso3FromIso1(?string $iso1): ?string
public static function getIso3FromIso1(string $iso1): ?string
{
$locale = Arr::first(Locale::getLocales(), fn (LocaleMetadata $locale) => $locale->getIsoAlpha2() === $iso1);
return $locale ? $locale->getIsoAlpha3() : null;
Expand All @@ -149,7 +169,7 @@ public static function getIso3FromIso1(?string $iso1): ?string
/**
* Translate the ISO639-3 into ISO639-1.
*/
public static function getIso1FromIso3(?string $iso3): ?string
public static function getIso1FromIso3(string $iso3): ?string
{
$locale = Arr::first(Locale::getLocales(), fn (LocaleMetadata $locale) => $locale->getIsoAlpha3() === $iso3);
return $locale ? $locale->getIsoAlpha2() : null;
Expand All @@ -158,17 +178,24 @@ public static function getIso1FromIso3(?string $iso3): ?string
/**
* Translate the PKP locale identifier into an ISO639-3 compatible 3-letter string.
*/
public static function getIso3FromLocale(?string $locale): ?string
public static function getIso3FromLocale(string $locale): ?string
{
$iso1 = substr($locale, 0, 2);
return static::getIso3FromIso1($iso1);
if (!Locale::isLocaleValid($locale)) {
return null;
}
$localeMetadata = new LocaleMetadata($locale);
return $localeMetadata->getIsoAlpha3();
}

/**
* Translate the PKP locale identifier into an ISO639-1 compatible 2-letter string.
*/
public static function getIso1FromLocale(?string $locale): string
public static function getIso1FromLocale(string $locale): ?string
{
return substr($locale, 0, 2);
if (!Locale::isLocaleValid($locale)) {
return null;
}
$localeMetadata = new LocaleMetadata($locale);
return $localeMetadata->getIsoAlpha2();
}
}
Loading