From b44eabbbcd8b109af4330fca42e5069bfefb14fb Mon Sep 17 00:00:00 2001 From: jyhein <124268211+jyhein@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:39:37 +0200 Subject: [PATCH] pkp/pkp-lib#9425 Make submission language selection and metadata forms independent from website language settings --- .../PKPSubmissionFileController.php | 16 +- api/v1/vocabs/PKPVocabController.php | 4 +- classes/author/Repository.php | 2 +- classes/author/maps/Schema.php | 4 +- .../listPanels/ContributorsListPanel.php | 14 +- classes/context/Context.php | 57 ++++++- classes/dev/ComposerScript.php | 41 +++++ classes/galley/Galley.php | 31 +++- classes/galley/maps/Schema.php | 4 +- classes/i18n/Locale.php | 69 ++++++++ classes/i18n/interfaces/LocaleInterface.php | 6 + .../I9425_SeparateUIAndSubmissionLocales.php | 83 ++++++++++ classes/publication/PKPPublication.php | 40 +++++ classes/publication/Repository.php | 10 +- classes/services/PKPContextService.php | 13 +- classes/submission/PKPSubmission.php | 27 ++++ classes/submission/Repository.php | 10 +- classes/submissionFile/Repository.php | 10 +- classes/submissionFile/SubmissionFile.php | 28 ++++ classes/submissionFile/maps/Schema.php | 4 +- classes/template/PKPTemplateManager.php | 3 +- composer.json | 8 +- .../languages/AdminLanguageGridHandler.php | 5 +- .../languages/LanguageGridCellProvider.php | 21 +++ .../grid/languages/LanguageGridHandler.php | 138 ++++++++++------ .../grid/languages/form/AddLanguageForm.php | 133 +++++++++++++++ .../languages/ManageLanguageGridHandler.php | 5 +- .../SubmissionLanguageGridHandler.php | 153 ++++++++++++++++++ .../fileUpload/FileUploadWizardHandler.php | 3 +- .../form/SubmissionFilesMetadataForm.php | 11 +- lib/weblateLanguages/urlCsv.txt | 1 + locale/en/manager.po | 24 +++ .../PKPAuthorDashboardHandler.php | 44 ++++- pages/submission/PKPSubmissionHandler.php | 52 +++--- pages/workflow/PKPWorkflowHandler.php | 46 ++++-- schemas/context.json | 26 ++- schemas/submission.json | 2 +- templates/admin/contextSettings.tpl | 2 + .../grid/languages/addLanguageForm.tpl | 35 ++++ templates/management/website.tpl | 2 + 40 files changed, 1042 insertions(+), 145 deletions(-) create mode 100644 classes/migration/upgrade/v3_5_0/I9425_SeparateUIAndSubmissionLocales.php create mode 100644 controllers/grid/languages/form/AddLanguageForm.php create mode 100644 controllers/grid/settings/languages/SubmissionLanguageGridHandler.php create mode 100644 lib/weblateLanguages/urlCsv.txt create mode 100644 templates/controllers/grid/languages/addLanguageForm.tpl diff --git a/api/v1/submissions/PKPSubmissionFileController.php b/api/v1/submissions/PKPSubmissionFileController.php index 7f264a4ec34..b3d2dc97791 100644 --- a/api/v1/submissions/PKPSubmissionFileController.php +++ b/api/v1/submissions/PKPSubmissionFileController.php @@ -320,12 +320,12 @@ public function add(Request $illuminateRequest): JsonResponse $params['submissionId'] = $submission->getId(); $params['uploaderUserId'] = (int) $request->getUser()->getId(); - $primaryLocale = $request->getContext()->getPrimaryLocale(); - $allowedLocales = $request->getContext()->getData('supportedSubmissionLocales'); + $submissionLocale = $submission->getData('locale'); + $allowedLocales = $request->getContext()->getSupportedSubmissionMetadataLocales(); // Set the name if not passed with the request if (empty($params['name'])) { - $params['name'][$primaryLocale] = $_FILES['file']['name']; + $params['name'][$submissionLocale] = $_FILES['file']['name']; } // If no genre has been set and there is only one genre possible, set it automatically @@ -344,7 +344,7 @@ public function add(Request $illuminateRequest): JsonResponse null, $params, $allowedLocales, - $primaryLocale + $submissionLocale ); if (!empty($errors)) { @@ -430,15 +430,15 @@ public function edit(Request $illuminateRequest): JsonResponse ], Response::HTTP_BAD_REQUEST); } - $primaryLocale = $request->getContext()->getPrimaryLocale(); - $allowedLocales = $request->getContext()->getData('supportedSubmissionLocales'); + $submissionLocale = $submission->getData('locale'); + $allowedLocales = $request->getContext()->getSupportedSubmissionMetadataLocales(); $errors = Repo::submissionFile() ->validate( $submissionFile, $params, $allowedLocales, - $primaryLocale + $submissionLocale ); if (!empty($errors)) { @@ -466,7 +466,7 @@ public function edit(Request $illuminateRequest): JsonResponse $params['fileId'] = $fileId; $params['uploaderUserId'] = $request->getUser()->getId(); if (empty($params['name'])) { - $params['name'][$primaryLocale] = $_FILES['file']['name']; + $params['name'][$submissionLocale] = $_FILES['file']['name']; } } diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 445bd235235..151893e191e 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -18,6 +18,7 @@ namespace PKP\API\v1\vocabs; use APP\core\Application; +use APP\facades\Repo; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -106,8 +107,9 @@ public function getMany(Request $illuminateRequest): JsonResponse $vocab = $requestParams['vocab'] ?? ''; $locale = $requestParams['locale'] ?? Locale::getLocale(); $term = $requestParams['term'] ?? null; + $locales = array_merge($context->getSupportedSubmissionMetadataLocales(), isset($requestParams['submissionId']) ? Repo::submission()->get((int) $requestParams['submissionId'])?->getPublicationLanguages() ?? [] : []); - if (!in_array($locale, $context->getData('supportedSubmissionLocales'))) { + if (!in_array($locale, $locales)) { return response()->json([ 'error' => __('api.vocabs.400.localeNotSupported', ['locale' => $locale]), ], Response::HTTP_BAD_REQUEST); diff --git a/classes/author/Repository.php b/classes/author/Repository.php index 5a2c97654e2..d44522acb9b 100644 --- a/classes/author/Repository.php +++ b/classes/author/Repository.php @@ -99,8 +99,8 @@ public function getSchemaMap(): maps\Schema public function validate($author, $props, Submission $submission, Context $context) { $schemaService = Services::get('schema'); - $allowedLocales = $context->getSupportedSubmissionLocales(); $primaryLocale = $submission->getData('locale'); + $allowedLocales = $submission->getPublicationLanguages($context->getSupportedSubmissionMetadataLocales()); $validator = ValidatorFactory::make( $props, diff --git a/classes/author/maps/Schema.php b/classes/author/maps/Schema.php index c670ce0112d..45bda26bee5 100644 --- a/classes/author/maps/Schema.php +++ b/classes/author/maps/Schema.php @@ -106,7 +106,9 @@ protected function mapByProperties(array $props, Author $item): array } } - $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->context->getSupportedSubmissionLocales()); + $locales = Repo::submission()->get(Repo::publication()->get($item->getData('publicationId'))->getData('submissionId'))->getPublicationLanguages($this->context->getSupportedSubmissionMetadataLocales()); + + $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $locales); ksort($output); diff --git a/classes/components/listPanels/ContributorsListPanel.php b/classes/components/listPanels/ContributorsListPanel.php index ecea43b9c2a..6071e599d3b 100644 --- a/classes/components/listPanels/ContributorsListPanel.php +++ b/classes/components/listPanels/ContributorsListPanel.php @@ -107,8 +107,6 @@ protected function getPublicationUrlFormat(): string */ protected function getLocalizedForm(): array { - uksort($this->locales, fn ($a, $b) => $a === $this->submission->getData('locale') ? -1 : 1); - $apiUrl = Application::get()->getRequest()->getDispatcher()->url( Application::get()->getRequest(), Application::ROUTE_API, @@ -116,11 +114,15 @@ protected function getLocalizedForm(): array 'submissions/' . $this->submission->getId() . '/publications/__publicationId__/contributors' ); - $form = $this->getForm($apiUrl); + $submissionLocale = $this->submission->getData('locale'); + $data = $this->getForm($apiUrl)->getConfig(); - $data = $form->getConfig(); - $data['primaryLocale'] = $this->submission->getData('locale'); - $data['visibleLocales'] = [$this->submission->getData('locale')]; + $data['primaryLocale'] = $submissionLocale; + $data['visibleLocales'] = [$submissionLocale]; + $data['supportedFormLocales'] = collect($this->locales) + ->sortBy([fn (array $a, array $b) => $b['key'] === $submissionLocale ? 1 : -1]) + ->values() + ->toArray(); return $data; } diff --git a/classes/context/Context.php b/classes/context/Context.php index 748f0ec8269..ae1548cd153 100644 --- a/classes/context/Context.php +++ b/classes/context/Context.php @@ -360,14 +360,10 @@ public function getSupportedSubmissionLocales() /** * Return associative array of all locales supported by submissions on the * context. - * - * @param int $langLocaleStatus The const value of one of LocaleMetadata:LANGUAGE_LOCALE_* - * - * @return array */ - public function getSupportedSubmissionLocaleNames(int $langLocaleStatus = LocaleMetadata::LANGUAGE_LOCALE_WITHOUT) + public function getSupportedSubmissionLocaleNames(): array { - return $this->getData('supportedSubmissionLocaleNames') ?? Locale::getFormattedDisplayNames($this->getSupportedSubmissionLocales(), null, $langLocaleStatus); + return $this->getData('supportedSubmissionLocaleNames') ?? Locale::getSubmissionLocaleDisplayNames($this->getSupportedSubmissionLocales()); } /** @@ -393,6 +389,55 @@ public function getSupportedLocaleNames(int $langLocaleStatus = LocaleMetadata:: return $this->getData('supportedLocaleNames') ?? Locale::getFormattedDisplayNames($this->getSupportedLocales(), null, $langLocaleStatus); } + /** + * Get the supported added submission locales. + */ + public function getSupportedAddedSubmissionLocales(): array + { + return $this->getData('supportedAddedSubmissionLocales'); + } + + /** + * Return associative array of added locales supported by submissions on the + * context. + */ + public function getSupportedAddedSubmissionLocaleNames(): array + { + return Locale::getSubmissionLocaleDisplayNames($this->getSupportedAddedSubmissionLocales()); + } + + /** + * Get the supported default submission locale. + */ + public function getSupportedDefaultSubmissionLocale(): string + { + return $this->getData('supportedDefaultSubmissionLocale'); + } + + /** + * Return string default submission locale supported by the site. + */ + public function getSupportedDefaultSubmissionLocaleName(): string + { + return Locale::getSubmissionLocaleDisplayNames([$l = $this->getSupportedDefaultSubmissionLocale()])[$l]; + } + + /** + * Get the supported metadata locales. + */ + public function getSupportedSubmissionMetadataLocales(): array + { + return $this->getData('supportedSubmissionMetadataLocales'); + } + + /** + * Return associative array of all locales supported by submission metadata forms on the site. + */ + public function getSupportedSubmissionMetadataLocaleNames(): array + { + return Locale::getSubmissionLocaleDisplayNames($this->getSupportedSubmissionMetadataLocales()); + } + /** * Return date or/and time formats available for forms, fallback to the default if not set * diff --git a/classes/dev/ComposerScript.php b/classes/dev/ComposerScript.php index dd348ec0ae7..8e5ac5910c6 100644 --- a/classes/dev/ComposerScript.php +++ b/classes/dev/ComposerScript.php @@ -32,4 +32,45 @@ public static function isoFileCheck(): void throw new Exception("The ISO639-2b file {$iso6392bFile} does not exist."); } } + + /** + * A post-install-cmd custom composer script that + * creates languages.json from downloaded Weblate languages.csv. + */ + public static function weblateFilesDownload(): void + { + try { + $dirPath = dirname(__FILE__, 3) . "/lib/weblateLanguages"; + $urlCsvFilePath = "$dirPath/urlCsv.txt"; + $langFilePath = "$dirPath/languages.json"; + + $urlCsv = !file_exists($urlCsvFilePath) ?: file_get_contents($urlCsvFilePath); + if (!is_string($urlCsv)) { + throw new Exception(__METHOD__ . " : File $urlCsvFilePath does not exist !"); + } + + $streamContext = stream_context_create(['http' => ['method' => 'HEAD']]); + $languagesCsv = str_contains(get_headers($urlCsv, context: $streamContext)[0] ?? "", '200 OK') ? file($urlCsv, FILE_SKIP_EMPTY_LINES) : false; + if (!is_array($languagesCsv) || empty(count($languagesCsv))) { + throw new Exception(__METHOD__ . " : The Weblate file 'languages.csv' cannot be downloaded !"); + } + + array_shift($languagesCsv); + $languages = []; + foreach($languagesCsv as $languageCsv) { + $localeAndName = str_getcsv($languageCsv, ","); + if (isset($localeAndName[0], $localeAndName[1]) && preg_match('/^[\w@-]{2,50}$/', $localeAndName[0]) === 1) { + $displayName = locale_get_display_name($localeAndName[0], 'en'); + $languages[$localeAndName[0]] = (($displayName && $displayName !== $localeAndName[0]) ? $displayName : $localeAndName[1]); + } + } + + $languagesJson = json_encode($languages, JSON_THROW_ON_ERROR); + if (empty($languagesJson) || !file_put_contents($langFilePath, $languagesJson)) { + throw new Exception(__METHOD__ . " : Json file empty, or save unsuccessful: $langFilePath !"); + } + } catch (Exception $e) { + error_log($e->getMessage()); + } + } } diff --git a/classes/galley/Galley.php b/classes/galley/Galley.php index 268e0377898..2d9787aff0b 100644 --- a/classes/galley/Galley.php +++ b/classes/galley/Galley.php @@ -16,8 +16,11 @@ namespace PKP\galley; +use APP\core\Services; use APP\facades\Repo; use PKP\facades\Locale; +use PKP\i18n\LocaleMetadata; +use PKP\services\PKPSchemaService; use PKP\submission\Representation; use PKP\submissionFile\SubmissionFile; @@ -127,8 +130,8 @@ public function isPdfGalley() public function getGalleyLabel() { $label = $this->getLabel(); - if ($this->getLocale() && $this->getLocale() != Locale::getLocale()) { - $label .= ' (' . Locale::getMetadata($this->getLocale())->getDisplayName() . ')'; + if ($this->getLocale() && $this->getLocale() !== Locale::getLocale()) { + $label .= ' (' . Locale::getSubmissionLocaleDisplayNames([$this->getLocale()])[$this->getLocale()] . ')'; } return $label; } @@ -183,6 +186,30 @@ public function setStoredPubId($pubIdType, $pubId) parent::setStoredPubId($pubIdType, $pubId); } } + + /** + * Get metadata language names + */ + public function getLanguageNames(): array + { + return Locale::getSubmissionLocaleDisplayNames($this->getLanguages()); + } + + /** + * Get metadata languages + */ + public function getLanguages(): array + { + $props = Services::get('schema')->getMultilingualProps(PKPSchemaService::SCHEMA_GALLEY); + $locales = array_map(fn (string $prop): array => array_keys($this->getData($prop) ?? []), $props); + return collect([$this->getData('locale')]) + ->concat($locales) + ->flatten() + ->filter() + ->unique() + ->values() + ->toArray(); + } } if (!PKP_STRICT_MODE) { diff --git a/classes/galley/maps/Schema.php b/classes/galley/maps/Schema.php index c28ff66867b..026df6a0088 100644 --- a/classes/galley/maps/Schema.php +++ b/classes/galley/maps/Schema.php @@ -140,7 +140,9 @@ protected function mapByProperties(array $props, Galley $galley): array } } - $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->context->getSupportedFormLocales()); + $locales = $this->publication->getLanguages($this->context->getSupportedSubmissionMetadataLocales(), $galley->getLanguages()); + + $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $locales); ksort($output); diff --git a/classes/i18n/Locale.php b/classes/i18n/Locale.php index 6de3cb6ef3a..643df3ca64d 100644 --- a/classes/i18n/Locale.php +++ b/classes/i18n/Locale.php @@ -29,6 +29,7 @@ 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\i18n\interfaces\LocaleInterface; @@ -95,6 +96,9 @@ 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; + /** * @copy \Illuminate\Contracts\Translation\Translator::get() * @@ -204,6 +208,14 @@ 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); + } + /** * @copy LocaleInterface::getMetadata() */ @@ -405,6 +417,38 @@ public function getUiTranslator(): UITranslator return new UITranslator($locale, $this->paths, $localeBundleCacheKey); } + /** + * Get appropriately localized display names for submission locales to array + * If $filterByLocales empty, return all languages. + * Add '*' (= in English) to display name if no translation available + * + * @param array $filterByLocales Optional list of locale codes/code-name-pairs to filter + * @param ?string $displayLocale Optional display locale + * + * @return array The list of locales with formatted display name + */ + public function getSubmissionLocaleDisplayNames(array $filterByLocales = [], ?string $displayLocale = null): array + { + $convDispLocale = $this->convertSubmissionLocaleCode($displayLocale ?: $this->getLocale()); + return collect($this->_getSubmissionLocaleNames()) + ->when(!empty(count($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"; + })) + ->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 * @@ -513,4 +557,29 @@ private function _getSupportedLocales(): array ?? array_map(fn (LocaleMetadata $locale) => $locale->locale, $this->getLocales()); 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_CACHE_LIFETIME . sha1($file); + $expiration = DateInterval::createFromDateString(static::MAX_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()); + })(); + } } diff --git a/classes/i18n/interfaces/LocaleInterface.php b/classes/i18n/interfaces/LocaleInterface.php index 3f9eed4b2f8..7b3af1bdc7d 100644 --- a/classes/i18n/interfaces/LocaleInterface.php +++ b/classes/i18n/interfaces/LocaleInterface.php @@ -38,6 +38,7 @@ interface LocaleInterface extends \Illuminate\Contracts\Translation\Translator /** Regular expression to validate and extract pieces of a locale code */ public const LOCALE_EXPRESSION = '/^(?P[a-z]{2})(?:_(?P[A-Za-z]{2,4}))?(?:@(?P + +
+ {csrf} + {include file="controllers/notification/inPlaceNotification.tpl" notificationId="installLanguageFormNotification"} + + {fbvFormArea id="availableLocalesFormArea" title="admin.languages.availableLocales"} + {fbvFormSection list="true" description="manager.language.submission.form.description"} + {foreach $availableLocales as $locale => $name} + {fbvElement type="checkbox" id="locale-$locale" name="localesToAdd[$locale]" value=$locale label=$name|unescape:"html" translate=false checked=in_array($locale, $addedLocales)} + {foreachelse} +

{translate key="admin.languages.noLocalesAvailable"}

+ {/foreach} + {/fbvFormSection} + {/fbvFormArea} + + {if not empty($availableLocales)} + {fbvFormButtons id="installLanguageFormSubmit" submitText="common.save"} + {/if} +
diff --git a/templates/management/website.tpl b/templates/management/website.tpl index 69fa6531715..cefde9c8561 100644 --- a/templates/management/website.tpl +++ b/templates/management/website.tpl @@ -64,6 +64,8 @@ {capture assign=languagesUrl}{url router=\PKP\core\PKPApplication::ROUTE_COMPONENT component="grid.settings.languages.ManageLanguageGridHandler" op="fetchGrid" escape=false}{/capture} {load_url_in_div id="languageGridContainer" url=$languagesUrl} + {capture assign=submissionLanguagesUrl}{url router=\PKP\core\PKPApplication::ROUTE_COMPONENT component="grid.settings.languages.SubmissionLanguageGridHandler" op="fetchGrid" escape=false}{/capture} + {load_url_in_div id="submissionLanguageGridContainer" url=$submissionLanguagesUrl} {capture assign=navigationMenusGridUrl}{url router=\PKP\core\PKPApplication::ROUTE_COMPONENT component="grid.navigationMenus.NavigationMenusGridHandler" op="fetchGrid" escape=false}{/capture}