From ff2b0403aeedf025511103acaf05f9ae91b34235 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Sat, 9 May 2026 09:37:03 -0400 Subject: [PATCH 1/2] add multi language support to banner message --- public/config.js | 7 +++- src/config.ts | 4 +- src/sections/layout/Layout.tsx | 40 +++++++++++++++++-- .../component/sections/layout/Layout.spec.tsx | 40 +++++++++++++++++++ 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/public/config.js b/public/config.js index b2b9d2087..e05146a4d 100644 --- a/public/config.js +++ b/public/config.js @@ -6,8 +6,11 @@ window.__APP_CONFIG__ = { // Base URL of your Dataverse backend backendUrl: 'http://localhost:8000', // Optional banner shown at the top of the app when set. Basic HTML markup is supported. - bannerMessage: - "You are using the new Dataverse Modern version. This is an early release and some features from the original site are not yet available. Please see the Project Roadmap for details.", + // Use a string for one message across all languages, or map language codes to localized messages. + bannerMessage: { + en: "You are using the new Dataverse Modern version. This is an early release and some features from the original site are not yet available. Please see the Project Roadmap for details.", + es: "Está utilizando la nueva versión moderna de Dataverse. Esta es una versión preliminar y algunas funciones del sitio original aún no están disponibles. Consulte la hoja de ruta del proyecto para más detalles." + }, // OIDC provider settings oidc: { clientId: 'test', diff --git a/src/config.ts b/src/config.ts index fb4530cef..1b63d2ee0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,9 +8,11 @@ declare global { let CONFIG: AppConfig | undefined +const BannerMessageSchema = z.union([z.string(), z.record(z.string(), z.string())]) + const AppConfigSchema = z.object({ backendUrl: z.url(), - bannerMessage: z.string().optional(), + bannerMessage: BannerMessageSchema.optional(), oidc: z.object({ clientId: z.string(), authorizationEndpoint: z.url(), diff --git a/src/sections/layout/Layout.tsx b/src/sections/layout/Layout.tsx index 5833d63cc..8fc1d3f07 100644 --- a/src/sections/layout/Layout.tsx +++ b/src/sections/layout/Layout.tsx @@ -1,17 +1,25 @@ import DOMPurify from 'dompurify' import { Outlet } from 'react-router-dom' import { Container } from '@iqss/dataverse-design-system' +import { useTranslation } from 'react-i18next' import styles from './Layout.module.scss' import { FooterFactory } from './footer/FooterFactory' import TopBarProgressIndicator from './topbar-progress-indicator/TopbarProgressIndicator' import { HeaderFactory } from './header/HeaderFactory' import { HistoryTrackerProvider } from '@/router/HistoryTrackerProvider' import { requireAppConfig } from '@/config' +import type { AppConfig } from '@/config' export function Layout() { - const { bannerMessage } = requireAppConfig() - const sanitizedBannerMessage = bannerMessage - ? DOMPurify.sanitize(bannerMessage, { USE_PROFILES: { html: true } }) + const { i18n } = useTranslation() + const { bannerMessage, defaultLanguage } = requireAppConfig() + const localizedBannerMessage = getLocalizedBannerMessage( + bannerMessage, + i18n.resolvedLanguage ?? i18n.language, + defaultLanguage + ) + const sanitizedBannerMessage = localizedBannerMessage + ? DOMPurify.sanitize(localizedBannerMessage, { USE_PROFILES: { html: true } }) : null return ( @@ -34,3 +42,29 @@ export function Layout() { ) } + +function getLocalizedBannerMessage( + bannerMessage: AppConfig['bannerMessage'], + selectedLanguage: string | undefined, + defaultLanguage: string +): string | undefined { + if (!bannerMessage || typeof bannerMessage === 'string') return bannerMessage + + const normalizedMessages = Object.fromEntries( + Object.entries(bannerMessage).map(([language, message]) => [language.toLowerCase(), message]) + ) + const selectedLanguageCode = selectedLanguage?.toLowerCase() + const defaultLanguageCode = defaultLanguage.toLowerCase() + const candidateLanguages = [ + selectedLanguageCode, + selectedLanguageCode?.split('-')[0], + defaultLanguageCode, + defaultLanguageCode.split('-')[0] + ].filter((language): language is string => Boolean(language)) + + for (const language of candidateLanguages) { + if (normalizedMessages[language]) return normalizedMessages[language] + } + + return Object.values(bannerMessage)[0] +} diff --git a/tests/component/sections/layout/Layout.spec.tsx b/tests/component/sections/layout/Layout.spec.tsx index 591349ef5..75196c568 100644 --- a/tests/component/sections/layout/Layout.spec.tsx +++ b/tests/component/sections/layout/Layout.spec.tsx @@ -4,6 +4,7 @@ import { FooterMother } from './footer/FooterMother' import { Layout } from '../../../../src/sections/layout/Layout' import { applyTestAppConfig } from '../../../support/bootstrapAppConfig' import type { AppConfig } from '@/config' +import i18next from '@/i18n' describe('Layout', () => { const sandbox: SinonSandbox = createSandbox() @@ -17,6 +18,8 @@ describe('Layout', () => { sandbox.restore() Cypress.env('bannerMessage', defaultBannerMessageEnv) applyTestAppConfig() + cy.clearAllLocalStorage() + cy.wrap(i18next.changeLanguage('en')) }) it('renders the header', () => { @@ -60,4 +63,41 @@ describe('Layout', () => { cy.get('script').should('not.exist') }) }) + + it('renders the banner message for the selected header language', () => { + Cypress.env('bannerMessage', { + en: 'English banner', + es: 'Banner en español moderno' + }) + applyTestAppConfig() + + cy.customMount() + + cy.findByRole('alert').within(() => { + cy.findByText('English', { exact: false }).should('exist') + cy.findByText('Banner en español', { exact: false }).should('not.exist') + }) + + cy.findByRole('button', { name: 'Toggle navigation' }).click() + cy.get('#language-switcher-dropdown').click() + cy.findByText('Español').click() + + cy.findByRole('alert').within(() => { + cy.findByText('Banner en español', { exact: false }).should('exist') + cy.get('strong').should('contain.text', 'moderno') + cy.findByText('English', { exact: false }).should('not.exist') + }) + }) + + it('falls back to the default language banner when the selected language is not configured', () => { + Cypress.env('bannerMessage', { + en: 'Default language banner' + }) + applyTestAppConfig() + + cy.wrap(i18next.changeLanguage('es')) + cy.customMount() + + cy.findByRole('alert').should('contain.text', 'Default language banner') + }) }) From 7ac91dfec16c468781764e28a1d1f91226827d4d Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 20 May 2026 14:52:42 -0400 Subject: [PATCH 2/2] update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a05b41c..3192a043b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Changed -### Fixed +- Add multilingual support for the banner message. ### Removed