mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 06:53:38 +02:00
i18n(events): RSVP page in it/fr/es + extract e2e helper
- strings.ts: add Italian, French, Spanish dictionaries (≈25 keys each) and widen Lang to the full DE/EN/IT/FR/ES set. - +page.server.ts: pickLang now matches any of the five supported locales from Accept-Language; SSR error messages localised the same way. - e2e/helpers.ts: extract the dismissWelcomeModal helper out of events.spec.ts so future module specs can reuse it without duplicating the locale-agnostic dialog locator.
This commit is contained in:
parent
897256c985
commit
3eabbc5e53
4 changed files with 143 additions and 24 deletions
|
|
@ -12,24 +12,8 @@
|
|||
* events-public-rsvp.spec.ts so we don't need a real auth dance here.
|
||||
*/
|
||||
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* The unified Mana app shows a guest-welcome modal on first load that
|
||||
* intercepts every click. Always dismiss it before doing anything else.
|
||||
*/
|
||||
async function dismissWelcomeModal(page: Page) {
|
||||
const dialog = page.locator('[role="dialog"][aria-labelledby="welcome-title"]');
|
||||
// Wait up to 10s for the modal to appear (it's mounted after AuthGate finishes)
|
||||
try {
|
||||
await dialog.waitFor({ state: 'visible', timeout: 10_000 });
|
||||
} catch {
|
||||
// No modal — already dismissed in a previous test or guest-mode disabled
|
||||
return;
|
||||
}
|
||||
await dialog.getByRole('button', { name: /Weiter als Gast|Continue as Guest/i }).click();
|
||||
await dialog.waitFor({ state: 'hidden' });
|
||||
}
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { dismissWelcomeModal } from './helpers';
|
||||
|
||||
// Each test gets its own browser context so IndexedDB starts empty.
|
||||
test.describe('Events module — local flow', () => {
|
||||
|
|
|
|||
25
apps/mana/apps/web/e2e/helpers.ts
Normal file
25
apps/mana/apps/web/e2e/helpers.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Shared Playwright helpers used across the unified Mana app's E2E suites.
|
||||
*/
|
||||
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* The unified Mana app shows a guest-welcome modal on first load that
|
||||
* intercepts every click. Always dismiss it before doing anything else
|
||||
* inside an `(app)` route. Locale-agnostic — matches the German and
|
||||
* English "continue as guest" buttons.
|
||||
*/
|
||||
export async function dismissWelcomeModal(page: Page): Promise<void> {
|
||||
const dialog = page.locator('[role="dialog"][aria-labelledby="welcome-title"]');
|
||||
try {
|
||||
// The modal is mounted after AuthGate finishes its check, so we
|
||||
// give it up to 10s to appear before deciding it isn't there.
|
||||
await dialog.waitFor({ state: 'visible', timeout: 10_000 });
|
||||
} catch {
|
||||
// Already dismissed in a previous test or guest mode disabled
|
||||
return;
|
||||
}
|
||||
await dialog.getByRole('button', { name: /Weiter als Gast|Continue as Guest/i }).click();
|
||||
await dialog.waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ const EVENTS_URL =
|
|||
process.env.PUBLIC_MANA_EVENTS_URL ||
|
||||
'http://localhost:3065';
|
||||
|
||||
type Lang = 'de' | 'en';
|
||||
type Lang = 'de' | 'en' | 'it' | 'fr' | 'es';
|
||||
|
||||
const SUPPORTED: ReadonlySet<Lang> = new Set(['de', 'en', 'it', 'fr', 'es']);
|
||||
|
||||
/** Pick the best supported language from an Accept-Language header. */
|
||||
function pickLang(header: string | null): Lang {
|
||||
|
|
@ -20,8 +22,7 @@ function pickLang(header: string | null): Lang {
|
|||
// Header looks like "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"
|
||||
const parts = header.split(',').map((p) => p.trim().split(';')[0].toLowerCase().slice(0, 2));
|
||||
for (const p of parts) {
|
||||
if (p === 'de') return 'de';
|
||||
if (p === 'en') return 'en';
|
||||
if (SUPPORTED.has(p as Lang)) return p as Lang;
|
||||
}
|
||||
return 'de';
|
||||
}
|
||||
|
|
@ -52,8 +53,22 @@ export const load: PageServerLoad = async ({ params, fetch, request }) => {
|
|||
if (!token) throw error(404, 'Not found');
|
||||
|
||||
const lang = pickLang(request.headers.get('accept-language'));
|
||||
const notFoundMsg = lang === 'de' ? 'Event nicht gefunden' : 'Event not found';
|
||||
const errorMsg = lang === 'de' ? 'Konnte Event nicht laden' : 'Could not load event';
|
||||
const NOT_FOUND_MESSAGES: Record<Lang, string> = {
|
||||
de: 'Event nicht gefunden',
|
||||
en: 'Event not found',
|
||||
it: 'Evento non trovato',
|
||||
fr: 'Événement introuvable',
|
||||
es: 'Evento no encontrado',
|
||||
};
|
||||
const ERROR_MESSAGES: Record<Lang, string> = {
|
||||
de: 'Konnte Event nicht laden',
|
||||
en: 'Could not load event',
|
||||
it: 'Impossibile caricare l’evento',
|
||||
fr: 'Impossible de charger l’événement',
|
||||
es: 'No se pudo cargar el evento',
|
||||
};
|
||||
const notFoundMsg = NOT_FOUND_MESSAGES[lang];
|
||||
const errorMsg = ERROR_MESSAGES[lang];
|
||||
|
||||
try {
|
||||
const res = await fetch(`${EVENTS_URL}/api/v1/rsvp/${encodeURIComponent(token)}`);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Kept tiny and local — this page is the only public consumer.
|
||||
*/
|
||||
|
||||
export type Lang = 'de' | 'en';
|
||||
export type Lang = 'de' | 'en' | 'it' | 'fr' | 'es';
|
||||
|
||||
interface Strings {
|
||||
rsvpTitle: string;
|
||||
|
|
@ -101,6 +101,101 @@ const DICTS: Record<Lang, Strings> = {
|
|||
poweredBy: 'Powered by',
|
||||
dateLocale: 'en-US',
|
||||
},
|
||||
it: {
|
||||
rsvpTitle: 'Conferma la tua presenza',
|
||||
pageTitleSuffix: '— RSVP',
|
||||
allDay: 'Tutto il giorno',
|
||||
peopleAttending: 'persone parteciperanno',
|
||||
cancelledNotice: '⚠️ Questo evento è stato annullato.',
|
||||
successHeading: 'Grazie per la tua risposta!',
|
||||
successYou: 'Hai risposto',
|
||||
successComing: '«Sì, vengo»',
|
||||
successNotComing: '«No»',
|
||||
successMaybe: '«Forse»',
|
||||
successHint:
|
||||
'. Puoi riaprire questa pagina in qualsiasi momento per modificare la tua risposta.',
|
||||
changeAnswer: 'Modifica risposta',
|
||||
formHeading: 'Conferma la tua presenza',
|
||||
yourName: 'Il tuo nome',
|
||||
yourNamePlaceholder: 'es. Anna Bianchi',
|
||||
emailLabel: 'Email (facoltativa)',
|
||||
emailPlaceholder: 'anna@example.com',
|
||||
areYouComing: 'Vieni?',
|
||||
yesComing: '✓ Sì, vengo',
|
||||
maybe: '? Forse',
|
||||
no: '✕ No',
|
||||
bringingPeople: (count) => `Porti qualcuno con te? (${count})`,
|
||||
noteLabel: 'Nota (facoltativa)',
|
||||
notePlaceholder: 'es. «Arrivo solo verso le 20»',
|
||||
send: 'Invia risposta',
|
||||
sending: 'Invio...',
|
||||
genericError: 'Impossibile inviare',
|
||||
poweredBy: 'Powered by',
|
||||
dateLocale: 'it-IT',
|
||||
},
|
||||
fr: {
|
||||
rsvpTitle: 'Confirmez votre présence',
|
||||
pageTitleSuffix: '— RSVP',
|
||||
allDay: 'Toute la journée',
|
||||
peopleAttending: 'personnes participeront',
|
||||
cancelledNotice: '⚠️ Cet événement a été annulé.',
|
||||
successHeading: 'Merci pour ta réponse !',
|
||||
successYou: 'Tu as répondu',
|
||||
successComing: '« Oui, je viens »',
|
||||
successNotComing: '« Non »',
|
||||
successMaybe: '« Peut-être »',
|
||||
successHint: '. Tu peux rouvrir cette page à tout moment pour modifier ta réponse.',
|
||||
changeAnswer: 'Modifier la réponse',
|
||||
formHeading: 'Confirmez votre présence',
|
||||
yourName: 'Ton nom',
|
||||
yourNamePlaceholder: 'p. ex. Anna Martin',
|
||||
emailLabel: 'E-mail (facultatif)',
|
||||
emailPlaceholder: 'anna@example.com',
|
||||
areYouComing: 'Tu viens ?',
|
||||
yesComing: '✓ Oui, je viens',
|
||||
maybe: '? Peut-être',
|
||||
no: '✕ Non',
|
||||
bringingPeople: (count) => `Tu amènes quelqu’un ? (${count})`,
|
||||
noteLabel: 'Note (facultative)',
|
||||
notePlaceholder: 'p. ex. « J’arrive vers 20h »',
|
||||
send: 'Envoyer la réponse',
|
||||
sending: 'Envoi...',
|
||||
genericError: 'Impossible d’envoyer',
|
||||
poweredBy: 'Propulsé par',
|
||||
dateLocale: 'fr-FR',
|
||||
},
|
||||
es: {
|
||||
rsvpTitle: 'Confirma tu asistencia',
|
||||
pageTitleSuffix: '— RSVP',
|
||||
allDay: 'Todo el día',
|
||||
peopleAttending: 'personas asistirán',
|
||||
cancelledNotice: '⚠️ Este evento ha sido cancelado.',
|
||||
successHeading: '¡Gracias por tu respuesta!',
|
||||
successYou: 'Has respondido',
|
||||
successComing: '«Sí, voy»',
|
||||
successNotComing: '«No»',
|
||||
successMaybe: '«Quizás»',
|
||||
successHint:
|
||||
'. Puedes volver a abrir esta página en cualquier momento para cambiar tu respuesta.',
|
||||
changeAnswer: 'Cambiar respuesta',
|
||||
formHeading: 'Confirma tu asistencia',
|
||||
yourName: 'Tu nombre',
|
||||
yourNamePlaceholder: 'p. ej. Ana García',
|
||||
emailLabel: 'Correo (opcional)',
|
||||
emailPlaceholder: 'ana@example.com',
|
||||
areYouComing: '¿Vienes?',
|
||||
yesComing: '✓ Sí, voy',
|
||||
maybe: '? Quizás',
|
||||
no: '✕ No',
|
||||
bringingPeople: (count) => `¿Traes a alguien contigo? (${count})`,
|
||||
noteLabel: 'Nota (opcional)',
|
||||
notePlaceholder: 'p. ej. «Llego sobre las 20h»',
|
||||
send: 'Enviar respuesta',
|
||||
sending: 'Enviando...',
|
||||
genericError: 'No se pudo enviar',
|
||||
poweredBy: 'Powered by',
|
||||
dateLocale: 'es-ES',
|
||||
},
|
||||
};
|
||||
|
||||
export function getStrings(lang: Lang): Strings {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue