From 3fbc316a09aabf77098c34737ace4936a68ffc2b Mon Sep 17 00:00:00 2001 From: jyhein <124268211+jyhein@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:00:05 +0200 Subject: [PATCH] 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/core/DataObject.php | 14 ++ classes/dev/ComposerScript.php | 40 +++++ classes/galley/Galley.php | 31 +++- classes/galley/maps/Schema.php | 4 +- classes/i18n/Locale.php | 71 ++++++++ classes/i18n/interfaces/LocaleInterface.php | 6 + .../install/AnnouncementsMigration.php | 4 +- .../migration/install/CategoriesMigration.php | 2 +- classes/migration/install/CommonMigration.php | 12 +- .../install/ControlledVocabMigration.php | 2 +- classes/migration/install/DoiMigration.php | 2 +- classes/migration/install/GenresMigration.php | 2 +- .../migration/install/HighlightsMigration.php | 2 +- .../install/InstitutionsMigration.php | 2 +- .../install/LibraryFilesMigration.php | 2 +- classes/migration/install/LogMigration.php | 2 +- .../migration/install/MetadataMigration.php | 4 +- .../install/NavigationMenusMigration.php | 4 +- .../install/ReviewFormsMigration.php | 4 +- .../install/RolesAndUserGroupsMigration.php | 2 +- .../install/SubmissionFilesMigration.php | 2 +- .../install/SubmissionsMigration.php | 8 +- .../migration/install/TombstoneMigration.php | 2 +- .../I9425_SeparateUIAndSubmissionLocales.php | 131 +++++++++++++++ classes/publication/PKPPublication.php | 40 +++++ classes/publication/Repository.php | 8 +- 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 | 131 +++++++++++++++ .../languages/ManageLanguageGridHandler.php | 5 +- .../SubmissionLanguageGridHandler.php | 152 ++++++++++++++++++ .../fileUpload/FileUploadWizardHandler.php | 3 +- .../form/SubmissionFilesMetadataForm.php | 11 +- locale/en/manager.po | 24 +++ .../PKPAuthorDashboardHandler.php | 44 ++++- pages/submission/PKPSubmissionHandler.php | 52 +++--- pages/workflow/PKPWorkflowHandler.php | 47 ++++-- schemas/context.json | 26 ++- schemas/submission.json | 2 +- templates/admin/contextSettings.tpl | 2 + .../grid/languages/addLanguageForm.tpl | 35 ++++ templates/management/website.tpl | 2 + 57 files changed, 1130 insertions(+), 173 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 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 abbf1defdcc..a35e10927dd 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; @@ -104,8 +105,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/core/DataObject.php b/classes/core/DataObject.php index 5a79b731fcd..2f0cdbfeb03 100644 --- a/classes/core/DataObject.php +++ b/classes/core/DataObject.php @@ -47,6 +47,19 @@ class DataObject /** @var bool whether injection adapters have already been loaded from the database */ public $_injectionAdaptersLoaded = false; + /** @var array conversion table for locales */ + public $_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", + ]; + /** * Constructor */ @@ -101,6 +114,7 @@ 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(), diff --git a/classes/dev/ComposerScript.php b/classes/dev/ComposerScript.php index dd348ec0ae7..5d17e85fa6c 100644 --- a/classes/dev/ComposerScript.php +++ b/classes/dev/ComposerScript.php @@ -32,4 +32,44 @@ 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"; + $langFilePath = "$dirPath/languages.json"; + $urlCsv = 'https://raw.githubusercontent.com/WeblateOrg/language-data/main/languages.csv'; + + if (!is_dir($dirPath)) { + mkdir($dirPath); + } + + $streamContext = stream_context_create(['http' => ['method' => 'HEAD']]); + $languagesCsv = !preg_match('/200 OK/', get_headers($urlCsv, false, $streamContext)[0] ?? "") ?: file($urlCsv, FILE_SKIP_EMPTY_LINES); + if (!is_array($languagesCsv) || !$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])) { + $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 (!$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 b8831116ee1..c4665a989e6 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; @@ -54,6 +55,9 @@ 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 string Max lifetime for the submission locales cache. */ + protected const MAX_SUBMISSION_LOCALES_CACHE_LIFETIME = '1 year'; + /** * @var callable Formatter for missing locale keys * Receives the locale key and must return a string @@ -96,6 +100,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() * @@ -202,6 +209,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() */ @@ -403,6 +418,37 @@ 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. + * Adds '*' (= 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($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 * @@ -511,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_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()); + })(); + } } diff --git a/classes/i18n/interfaces/LocaleInterface.php b/classes/i18n/interfaces/LocaleInterface.php index 3f9eed4b2f8..5782428a28a 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 28c77ce2027..82af05a0f1a 100644 --- a/templates/management/website.tpl +++ b/templates/management/website.tpl @@ -70,6 +70,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}