diff --git a/pages/preprint/PreprintHandler.php b/pages/preprint/PreprintHandler.php index d8e0bd6bb7..642c58cb2e 100644 --- a/pages/preprint/PreprintHandler.php +++ b/pages/preprint/PreprintHandler.php @@ -22,18 +22,23 @@ use APP\facades\Repo; use APP\handler\Handler; use APP\observers\events\UsageEvent; +use APP\publication\Publication; use APP\security\authorization\OpsServerMustPublishPolicy; use APP\template\TemplateManager; use Firebase\JWT\JWT; +use Illuminate\Support\Arr; +use PKP\author\Author; use PKP\citation\CitationDAO; use PKP\config\Config; use PKP\core\Core; use PKP\core\PKPApplication; use PKP\db\DAORegistry; +use PKP\facades\Locale; use PKP\orcid\OrcidManager; use PKP\plugins\Hook; use PKP\plugins\PluginRegistry; use PKP\security\authorization\ContextRequiredPolicy; +use PKP\services\PKPSchemaService; use PKP\submission\Genre; use PKP\submission\GenreDAO; use PKP\submission\PKPSubmission; @@ -302,6 +307,16 @@ public function view($args, $request) $templateMgr->addHeader('canonical', ''); } + $templateMgr->assign('pubLocaleData', $this->getMultilingualMetadataOpts( + $publication, + $templateMgr->getTemplateVars('currentLocale'), + $templateMgr->getTemplateVars('activeTheme')->getOption('showMultilingualMetadata') ?: [], + )); + $templateMgr->registerPlugin('modifier', 'wrapData', fn (...$args) => $this->smartyWrapData($templateMgr, ...$args)); + $templateMgr->registerPlugin('modifier', 'useFilters', fn (...$args) => $this->smartyUseFilters($templateMgr, ...$args)); + $templateMgr->registerPlugin('modifier', 'getAuthorFullNames', $this->smartyGetAuthorFullNames(...)); + $templateMgr->registerPlugin('modifier', 'getAffiliationNamesWithRors', $this->smartyGetAffiliationNamesWithRors(...)); + if (!Hook::call('PreprintHandler::view', [&$request, &$preprint, $publication])) { $templateMgr->display('frontend/pages/preprint.tpl'); event(new UsageEvent(Application::ASSOC_TYPE_SUBMISSION, $context, $preprint)); @@ -424,4 +439,128 @@ public function userCanViewGalley($request) } return false; } + + /** + * Multilingual publication metadata for template: + * showMultilingualMetadataOpts - Show metadata in other languages: title (+ subtitle), keywords, abstract, etc. + */ + protected function getMultilingualMetadataOpts(Publication $publication, string $currentUILocale, array $showMultilingualMetadataOpts): array + { + // Affiliation languages are not in multiligual props + $authorsLocales = collect($publication->getData('authors')) + ->map(fn ($author): array => $this->getAuthorLocales($author)) + ->flatten() + ->unique() + ->values() + ->toArray(); + $langNames = collect($publication->getLanguageNames() + Locale::getSubmissionLocaleDisplayNames($authorsLocales)) + ->sortKeys(); + $langs = $langNames->keys(); + + return [ + 'opts' => array_flip($showMultilingualMetadataOpts), + 'uiLocale' => $currentUILocale, + 'localeNames' => $langNames, + 'localeOrder' => collect($publication->getLocalePrecedence()) + ->intersect($langs) /* remove locales not in publication's languages */ + ->concat($langs) + ->unique() + ->values() + ->toArray(), + 'accessibility' => [ + 'localeNames' => $langNames, + 'langAttrs' => $langNames->map(fn ($_, $l) => preg_replace(['/@.+$/', '/_/'], ['', '-'], $l))->toArray() /* remove @ and text after */, + ], + ]; + } + + /** + * Publication's multilingual data to array for js and page + */ + protected function smartyWrapData(TemplateManager $templateMgr, array $data, string $switcher, ?array $filters = null, ?string $separator = null): array + { + return [ + 'switcher' => $switcher, + 'data' => collect($data) + ->map( + fn ($val): string => collect(Arr::wrap($val)) + ->when($filters, fn ($value) => $value->map(fn ($v) => $this->smartyUseFilters($templateMgr, $v, $filters))) + ->when($separator, fn ($value): string => $value->join($separator), fn ($value): string => $value->first()) + ) + ->toArray(), + 'defaultLocale' => collect($templateMgr->getTemplateVars('pubLocaleData')['localeOrder']) + ->first(fn (string $locale) => isset($data[$locale])), + ]; + } + + /** + * Smarty template: Apply filters to given value + */ + protected function smartyUseFilters(TemplateManager $templateMgr, string $value, ?array $filters): string + { + if (!$filters) { + return $value; + } + foreach ($filters as $filter) { + $params = Arr::wrap($filter); + $funcName = array_shift($params); + if ($func = $templateMgr->registered_plugins['modifier'][$funcName][0] ?? null) { + $value = $func($value, ...$params); + } else { + error_log("{$funcName} : No such modifier in template registered plugins."); + } + } + return $value; + } + + /** + * Smarty template: Get author's full names to multilingual array including all multilingual and affiliation languages as default localized name + */ + protected function smartyGetAuthorFullNames(Author $author): array + { + return collect($this->getAuthorLocales($author)) + ->mapWithKeys(fn (string $locale) => [$locale => $author->getFullName(preferredLocale: $locale)]) + ->toArray(); + } + + /** + * Smarty template: Get authors' affiliations with rors + */ + protected function smartyGetAffiliationNamesWithRors(Author $author): array + { + $affiliations = collect($author->getAffiliations()); + + return collect($this->getAuthorLocales($author)) + ->flip() + ->map( + fn ($_, string $locale) => $affiliations + ->map(fn ($affiliation): array => [ + 'name' => $affiliation->getAffiliationName($locale), + 'ror' => $affiliation->getRor(), + ]) + ->filter(fn (array $nameRor) => $nameRor['name']) + ->toArray() + ) + ->filter() + ->toArray(); + } + + /** + * Aux for smarty template functions: Get author's locales from multilingual props and affiliations + */ + protected function getAuthorLocales(Author $author): array + { + $multilingualLocales = collect(app()->get('schema')->getMultilingualProps(PKPSchemaService::SCHEMA_AUTHOR)) + ->map(fn (string $prop): array => array_keys($author->getData($prop) ?? [])); + $affiliationLocales = collect($author->getAffiliations()) + ->flatten() + ->map(fn ($affiliation): array => array_keys($affiliation->getData('name') ?? [])); + + return $multilingualLocales + ->concat($affiliationLocales) + ->flatten() + ->unique() + ->values() + ->toArray(); + } } diff --git a/plugins/themes/default/DefaultThemePlugin.php b/plugins/themes/default/DefaultThemePlugin.php index 448f1fceca..b431ab74fc 100755 --- a/plugins/themes/default/DefaultThemePlugin.php +++ b/plugins/themes/default/DefaultThemePlugin.php @@ -125,6 +125,30 @@ public function init() 'default' => 'none', ]); + $this->addOption('showMultilingualMetadata', 'FieldOptions', [ + 'label' => __('plugins.themes.default.option.metadata.label'), + 'description' => __('plugins.themes.default.option.metadata.description'), + 'options' => [ + [ + 'value' => 'title', + 'label' => __('submission.title'), + ], + [ + 'value' => 'keywords', + 'label' => __('common.keywords'), + ], + [ + 'value' => 'abstract', + 'label' => __('common.abstract'), + ], + [ + 'value' => 'author', + 'label' => __('default.groups.name.author'), + ], + ], + 'default' => [], + ]); + // Load primary stylesheet $this->addStyle('stylesheet', 'styles/index.less'); diff --git a/plugins/themes/default/js/main.js b/plugins/themes/default/js/main.js index 0775189b43..4b7e3326cb 100755 --- a/plugins/themes/default/js/main.js +++ b/plugins/themes/default/js/main.js @@ -114,3 +114,198 @@ }); })(jQuery); + +/** + * Create language buttons to show multilingual metadata + * [data-pkp-switcher-data]: Publication data for the switchers to control + * [data-pkp-switcher]: Switchers' containers + */ +(() => { + function createSwitcher(listbox, data, localeOrder, localeNames, accessibility) { + // Get all locales for the switcher from the data + const locales = Object.keys(Object.assign({}, ...Object.values(data))); + // The initially selected locale + let selectedLocale = null; + // Create and sort to alphabetical order + const buttons = localeOrder + .map((locale) => { + if (locales.indexOf(locale) === -1) { + return null; + } + if (!selectedLocale) { + selectedLocale = locale; + } + + const isSelectedLocale = locale === selectedLocale; + const button = document.createElement('button'); + + button.type = 'button'; + button.classList.add('pkpBadge', 'pkpBadge--button'); + button.value = locale; + // For safari losing button focus; Tabindex maintains it + button.tabIndex = '0'; + if (!isSelectedLocale) { + button.classList.add('pkp_screen_reader'); + } + + // Text content + /// SR + const srText = document.createElement('span'); + srText.classList.add('pkp_screen_reader'); + srText.textContent = accessibility.localeNames[locale]; + button.appendChild(srText); + // Visual + const text = document.createElement('span'); + text.ariaHidden = 'true'; + text.textContent = localeNames[locale]; + button.appendChild(text); + + return button; + }) + .filter((btn) => btn) + .sort((a, b) => a.value.localeCompare(b.value)); + + // If only one button, set it disabled + if (buttons.length === 1) { + buttons[0].ariaDisabled = 'true'; + } + + buttons.forEach((btn) => { + const option = document.createElement('li'); + option.role = 'option'; + option.ariaSelected = `${btn.value === selectedLocale}`; + option.appendChild(btn); + // Listbox: Ul element + listbox.appendChild(option); + }); + + return buttons; + } + + /** + * Sync data in elements to match the selected locale + */ + function syncDataElContents(locale, propsData, accessibility) { + for (prop in propsData.data) { + propsData.dataEls[prop].lang = accessibility.langAttrs[locale]; + propsData.dataEls[prop].innerHTML = propsData.data[prop][locale] ?? ''; + } + } + + /** + * Toggle 'visually hidden' of the buttons; Current selected is always visual + * pkp_screen_reader: button visibility hidden + */ + function setVisibility(buttons, currentSelected, visible) { + buttons.forEach((btn) => { + if (visible) { + btn.classList.remove('pkp_screen_reader'); + } else if (btn !== currentSelected.btn) { + btn.classList.add('pkp_screen_reader'); + } + }); + } + + function setSwitcher(propsData, switcherContainer, localeOrder, localeNames, accessibility) { + // Create buttons and append them to the switcher container + const listbox = switcherContainer.querySelector('[role="listbox"]'); + const buttons = createSwitcher(listbox, propsData.data, localeOrder, localeNames, accessibility); + const currentSelected = {btn: switcherContainer.querySelector('.pkpBadge--button:not(.pkp_screen_reader')}; + + // Sync contents in data elements to match the selected locale (currentSelected.btn.value) + syncDataElContents(currentSelected.btn.value, propsData, accessibility); + + // Do not add listeners if just one button, it is disabled + if (buttons.length < 2) { + return; + } + + const isButtonsHidden = () => buttons.some(b => b.classList.contains('pkp_screen_reader')); + + // New button switches language and syncs data contents + switcherContainer.addEventListener('click', (evt) => { + // Choices are li > button > span + const newSelectedBtn = evt.target.classList.contains('pkpBadge--button') + ? evt.target + : (evt.target.querySelector('.pkpBadge--button') ?? evt.target.closest('.pkpBadge--button')); + if (buttons.some(b => b === newSelectedBtn)) { + // Set visibility + setVisibility(buttons, currentSelected, true); + if (newSelectedBtn !== currentSelected.btn) { + // Sync contents + syncDataElContents(newSelectedBtn.value, propsData, accessibility); + // Listbox option: Aria + currentSelected.btn.parentElement.ariaSelected = 'false'; + newSelectedBtn.parentElement.ariaSelected = 'true'; + // Update current button + currentSelected.btn = newSelectedBtn; + } + // Reset focus to current selected + currentSelected.btn.focus(); + } + }); + + // Visually hide buttons when focus out + switcherContainer.addEventListener('focusout', (evt) => { + if (evt.target !== evt.currentTarget && evt.relatedTarget?.closest('[data-pkp-switcher]') !== switcherContainer) { + setVisibility(buttons, currentSelected, false); + } + }); + + // For tabbed browsing: Show all visually hidden buttons when one of the buttons receives focus + switcherContainer.addEventListener('focusin', (evt) => { + if (isButtonsHidden() && buttons.find(b => b === evt.target)) { + setVisibility(buttons, currentSelected, true); + // Reset focus to current selected + currentSelected.btn.focus(); + } + }); + + // Arrow keys left and right cycles button focus when all buttons are visible. Set focused button. + switcherContainer.addEventListener('keydown', (evt) => { + if (evt.key === 'ArrowRight' || evt.key === 'ArrowLeft') { + const i = buttons.findIndex(b => b === evt.target); + if (i !== -1 && !isButtonsHidden()) { + const focusedBtn = (evt.key === 'ArrowRight') + ? (buttons[i + 1] ?? buttons[0]) + : (buttons[i - 1] ?? buttons[buttons.length - 1]); + focusedBtn.focus(); + } + } + }); + } + + /** + * Set all multilingual data and elements for the switchers + */ + function setSwitchersData(dataEls, pubLocaleData) { + const propsData = {}; + dataEls.forEach((dataEl) => { + const propName = dataEl.getAttribute('data-pkp-switcher-data'); + const switcherName = pubLocaleData[propName].switcher; + if (!propsData[switcherName]) { + propsData[switcherName] = {data: [], dataEls: []}; + } + propsData[switcherName].data[propName] = pubLocaleData[propName].data; + propsData[switcherName].dataEls[propName] = dataEl; + }); + return propsData; + } + + (() => { + const switcherContainers = document.querySelectorAll('[data-pkp-switcher]'); + + if (!switcherContainers.length) return; + + const pubLocaleData = JSON.parse(pubLocaleDataJson); + const switchersDataEls = document.querySelectorAll('[data-pkp-switcher-data]'); + const switchersData = setSwitchersData(switchersDataEls, pubLocaleData); + // Create and set switchers, and sync data on the page + switcherContainers.forEach((switcherContainer) => { + const switcherName = switcherContainer.getAttribute('data-pkp-switcher'); + if (switchersData[switcherName]) { + setSwitcher(switchersData[switcherName], switcherContainer, pubLocaleData.localeOrder, pubLocaleData.localeNames, pubLocaleData.accessibility); + } + }); + })(); +})(); \ No newline at end of file diff --git a/plugins/themes/default/locale/en/locale.po b/plugins/themes/default/locale/en/locale.po index 22f90fc34b..9a1979066c 100644 --- a/plugins/themes/default/locale/en/locale.po +++ b/plugins/themes/default/locale/en/locale.po @@ -97,4 +97,25 @@ msgid "plugins.themes.default.nextSlide" msgstr "Next slide" msgid "plugins.themes.default.prevSlide" -msgstr "Previous slide" \ No newline at end of file +msgstr "Previous slide" + +msgid "plugins.themes.default.option.metadata.label" +msgstr "Show preprint metadata on the preprint landing page" + +msgid "plugins.themes.default.option.metadata.description" +msgstr "Select the preprint metadata to show in other languages." + +msgid "plugins.themes.default.languageSwitcher.ariaDescription.select" +msgstr "Select language" + +msgid "plugins.themes.default.languageSwitcher.ariaDescription.titles" +msgstr "The preprint title and subtitle languages" + +msgid "plugins.themes.default.languageSwitcher.ariaDescription.author" +msgstr "The author's affiliation languages" + +msgid "plugins.themes.default.languageSwitcher.ariaDescription.keywords" +msgstr "The keywords languages" + +msgid "plugins.themes.default.languageSwitcher.ariaDescription.abstract" +msgstr "The abstract languages" \ No newline at end of file diff --git a/plugins/themes/default/styles/objects/preprint_details.less b/plugins/themes/default/styles/objects/preprint_details.less index ae1533d13a..1fa3e528b9 100755 --- a/plugins/themes/default/styles/objects/preprint_details.less +++ b/plugins/themes/default/styles/objects/preprint_details.less @@ -10,15 +10,17 @@ */ .obj_preprint_details { - > .page_title { - margin: 0; - } - - > .subtitle { - margin: 0; - font-size: @font-base; - line-height: @line-lead; - font-weight: @normal; + .page_titles { + .page_title { + margin: 0; + } + + .subtitle { + margin: 0; + font-size: @font-base; + line-height: @line-lead; + font-weight: @normal; + } } .row { @@ -60,6 +62,12 @@ font-size: @font-bump; font-weight: @bold; } + + &.keywords .label, + &.abstract .label { + display: inline-block; + font-size: @font-base; + } } .sub_item { @@ -80,7 +88,7 @@ .name { font-weight: bold; - display: block; + display: inline-block; } .orcid { @@ -273,6 +281,77 @@ } } + /** + * Language switcher + */ + + [data-pkp-switcher] { + .pkpBadge { + padding: 0.25em 1em; + font-size: @font-tiny; + font-weight: @normal; + line-height: 1.5em; + border: 1px solid @bg-border-color-light; + border-radius: 1.2em; + color: @text; + } + + .pkpBadge--button { + background: inherit; + text-decoration: none; + cursor: pointer; + + &:hover { + border-color: @text; + outline: 0; + } + } + // Button + [aria-disabled="true"], + [aria-disabled="true"]:hover { + color: #fff; + background: @bg-dark; + border-color: @bg-dark; + cursor: not-allowed; + } + + display: inline-block; + + * { + display: inline-block; + } + + [role="listbox"], + [role="option"] { + padding: 0; + margin: 0; + } + + [aria-selected="true"] .pkpBadge--button { + font-weight: @bold; + } + + .pkpBadge--button:not(.pkp_screen_reader){ + animation: fadeIn 0.7s ease-in-out; + + @keyframes fadeIn { + 0% { + display: none; + opacity: 0; + } + + 1% { + display: inline-block; + opacity: 0; + } + + 100% { + opacity: 1; + } + } + } + } + @media(min-width: @screen-phone) { .entry_details { @@ -307,8 +386,7 @@ font-weight: @bold; } - &.doi .label, - &.keywords .label { + &.doi .label { display: inline; font-size: @font-base; } diff --git a/templates/frontend/objects/preprint_details.tpl b/templates/frontend/objects/preprint_details.tpl index f23e3d6b21..a672c1a695 100644 --- a/templates/frontend/objects/preprint_details.tpl +++ b/templates/frontend/objects/preprint_details.tpl @@ -64,7 +64,65 @@ * @uses $licenseUrl string URL to license. Only assigned if license should be * included with published submissions. * @uses $ccLicenseBadge string An image and text with details about the license + * @uses $pubLocaleData Array of e.g. publication's locales and metadata field names to show in multiple languages *} + +{** + * Function useFilters: Filter text content of the metadata shown on the page + * E.g. usage $value|useFilters:$filters + * Filter format with params: e.g. '[funcName, param2, ...]' + * @param string $value, Required, The value to be filtered + * @param array|null $filters, Required, E.g. [ 'escape', ['default', ''] ] + * @return string, filtered value + * + * $authorName|useFilters:['escape'] + *} + +{** + * Function wrapData: Publication's multilingual data to array for js and page + * Filters texts using function useFilters + * @param array $data, Required, E.g. $publication->getTitles('html') + * @param string $switcher, Required, Switcher's name + * @param array $filters, Optional, E.g. [ 'escape', ['default', ''] ] + * @param string $separator, Optional but required to join array (e.g. keywords) + * @return array, [ 'switcher' => string name, 'data' => array multilingual, 'defaultLocale' => string locale code ] + * + * $publication->getFullTitles('html')|wrapData:"titles":['strip_unsafe_html'] + *} + +{** + * Authors' affiliations: concat affiliation and ror icon + * @param array $affiliationNamesWithRors, Required + * @param string $rorIdIcon, Required + * @param array $filters, Optional, E.g. ['escape'] + * @return array, As variable $affiliationNamesWithRors + *} + {function concatAuthorAffiliationsWithRors} + {foreach from=$affiliationNamesWithRors item=$namesPerLocale key=$locale} + {foreach from=$namesPerLocale item=$nameWithRor key=$key} + {* Affiliation name *} + {$affiliationRor=$nameWithRor.name|useFilters:$filters} + {* Ror *} + {if $nameWithRor.ror} + {capture assign="ror"}{$rorIdIcon}{/capture} + {$affiliationRor="{$affiliationRor}{$ror}"} + {/if} + {$affiliationNamesWithRors[$locale][$key]=$affiliationRor} + {/foreach} + {/foreach} + {assign "affiliationNamesWithRors" value=$affiliationNamesWithRors scope="parent" nocache} +{/function} + +{** + * Switchers' listbox container + *} + {function switcherContainer} + +{/function} + +{* Language switchers' buttons text contents: locale names can be used instead of lang attribute codes by removing the line under this comment. *} +{$pubLocaleData.localeNames = $pubLocaleData.accessibility.langAttrs} +
{* Indicate if this is only a preview *} @@ -109,15 +167,45 @@ {translate key="navigation.breadcrumbSeparator"} {translate key="publication.version" version=$publication->getData('version')} -

- {$publication->getLocalizedTitle(null, 'html')|strip_unsafe_html} -

+
+ {** Example usage of the language switcher, title and subtitle + * In h1, the attribute data-pkp-switcher-data="title" and, in h2, the attribute data-pkp-switcher-data="subtitle" are used to sync the text content in elements when + * the language is switched. + * The attribute data-pkp-switcher="titles" is the container for the switcher's buttons, it needs to match json's switcher-key's value, e.g. {"title":{"switcher":"titles"... + * The function wrapData wraps publication data for the json and the tpl, e.g. $pubLocaleData.title=$publication->getTitles('html')|wrapData:"titles":['strip_unsafe_html'], + * $pubLocaleData's title-key needs to match data-pkp-switcher-data'-attribute's value, e.g. data-pkp-switcher-data="title". This is for to sync the data in the correct element. + * See all the examples below. + * The rest of the work is handled by the js code. + *} + {* Publication title for json *} + {$pubLocaleData.title=$publication->getTitles('html')|wrapData:"titles":['strip_unsafe_html']} +

+ {$pubLocaleData.title.data[$pubLocaleData.title.defaultLocale]} +

- {if $publication->getLocalizedData('subtitle')} -

- {$publication->getLocalizedSubTitle(null, 'html')|strip_unsafe_html} -

- {/if} + {if $publication->getSubTitles('html')} + {* Publication subtitle for json *} + {$pubLocaleData.subtitle=$publication->getSubTitles('html')|wrapData:"titles":['strip_unsafe_html']} +

+ {$pubLocaleData.subtitle.data[$pubLocaleData.subtitle.defaultLocale]} +

+ {/if} + {* Title and subtitle common switcher *} + {if isset($pubLocaleData.opts.title)} + {switcherContainer} + {/if} +
+ + {* Comma list separator for authors' affiliations and keywords *} + {capture assign=commaListSeparator}{translate key="common.commaListSeparator"}{/capture}
@@ -126,17 +214,35 @@

{translate key="submission.authors"}

    {foreach from=$publication->getData('authors') item=author} -
  • - - {$author->getFullName()|escape} - - {if count($author->getAffiliations()) > 0} +
  • +
    + {* Publication author name for json *} + {$pubLocaleData["author{$author@index}Name"]=$author|getAuthorFullNames|wrapData:"author{$author@index}":['escape']} + {* Name *} + + {$pubLocaleData["author{$author@index}Name"].data[$pubLocaleData["author{$author@index}Name"].defaultLocale]} + + {* Author switcher *} + {if isset($pubLocaleData.opts.author)} + {switcherContainer} + {/if} +
    + {if $author->getAffiliations()} + {* Publication author affiliations for json *} + {concatAuthorAffiliationsWithRors affiliationNamesWithRors=$author|getAffiliationNamesWithRors rorIdIcon=$rorIdIcon filters=['escape']} + {$pubLocaleData["author{$author@index}Affiliation"]=$affiliationNamesWithRors|wrapData:"author{$author@index}":null:$commaListSeparator} + {* Affiliation *} - {foreach name="affiliations" from=$author->getAffiliations() item="affiliation"} - {$affiliation->getLocalizedName()|escape} - {if $affiliation->getRor()}{$rorIdIcon}{/if} - {if !$smarty.foreach.affiliations.last}{translate key="common.commaListSeparator"}{/if} - {/foreach} + + {$pubLocaleData["author{$author@index}Affiliation"].data[$pubLocaleData["author{$author@index}Affiliation"].defaultLocale]} + {/if} {if $author->getData('orcid')} @@ -175,25 +281,50 @@ {/if} {* Keywords *} - {if !empty($publication->getLocalizedData('keywords'))} -
    -

    - {capture assign=translatedKeywords}{translate key="preprint.subject"}{/capture} - {translate key="semicolon" label=$translatedKeywords} -

    - - {foreach name="keywords" from=$publication->getLocalizedData('keywords') item="keyword"} - {$keyword|escape}{if !$smarty.foreach.keywords.last}{translate key="common.commaListSeparator"}{/if} - {/foreach} - -
    + {if $publication->getData('keywords')} + {* Publication keywords for json *} + {$pubLocaleData.keywords=$publication->getData('keywords')|wrapData:"keywords":['escape']:$keywordSeparator} +
    +

    + {capture assign=translatedKeywords}{translate key="common.keywords"}{/capture} + {translate key="semicolon" label=$translatedKeywords} +

    + + + {$pubLocaleData.keywords.data[$pubLocaleData.keywords.defaultLocale]} + + {* Keyword switcher *} + {if isset($pubLocaleData.opts.keywords)} + {switcherContainer} + {/if} + +
    {/if} {* Abstract *} - {if $publication->getLocalizedData('abstract')} -
    -

    {translate key="common.abstract"}

    - {$publication->getLocalizedData('abstract')|strip_unsafe_html} + {if $publication->getData('abstract')} + {* Publication abstract for json *} + {$pubLocaleData.abstract=$publication->getData('abstract')|wrapData:"abstract":['strip_unsafe_html']} +
    +
    +

    + {translate key="common.abstract"} +

    + {* Abstract switcher *} + {if isset($pubLocaleData.opts.abstract)} + {switcherContainer} + {/if} +
    +
    + {$pubLocaleData.abstract.data[$pubLocaleData.abstract.defaultLocale]} +
    {/if} @@ -493,3 +624,11 @@
+ +