diff --git a/packages/schemas/src/mail/__tests__/mail.test.ts b/packages/schemas/src/mail/__tests__/mail.test.ts
index dfb7935a4..24a434885 100644
--- a/packages/schemas/src/mail/__tests__/mail.test.ts
+++ b/packages/schemas/src/mail/__tests__/mail.test.ts
@@ -113,13 +113,13 @@ describe('isSameMailServer', () => {
});
describe('default templates', () => {
- it.each(['en', 'fr'] as const)('should carry the required placeholders in %s', (language) => {
+ it.each(['en', 'es', 'fr'] as const)('should carry the required placeholders in %s', (language) => {
expect(DEFAULT_NEW_USER_EMAIL_TEMPLATE.body[language]).toContain('{{username}}');
expect(DEFAULT_ASSIGNMENT_EMAIL_TEMPLATE.body[language]).toContain('{{url}}');
expect(DEFAULT_ASSIGNMENT_EMAIL_TEMPLATE.body[language]).toContain('{{expiresAt}}');
});
- it.each(['en', 'fr'] as const)('should have a non-empty subject and body in %s', (language) => {
+ it.each(['en', 'es', 'fr'] as const)('should have a non-empty subject and body in %s', (language) => {
expect(DEFAULT_NEW_USER_EMAIL_TEMPLATE.subject[language].trim()).toBeTruthy();
expect(DEFAULT_ASSIGNMENT_EMAIL_TEMPLATE.subject[language].trim()).toBeTruthy();
});
diff --git a/packages/schemas/src/mail/mail.ts b/packages/schemas/src/mail/mail.ts
index af9b79f7a..ff1823274 100644
--- a/packages/schemas/src/mail/mail.ts
+++ b/packages/schemas/src/mail/mail.ts
@@ -174,6 +174,21 @@ const DEFAULT_NEW_USER_EMAIL_TEMPLATE = {
'New to Open Data Capture? This guide will help you get started:',
'https://opendatacapture.org/en/docs/guides/how-to-get-started-with-odc/'
].join('\n'),
+ es: [
+ 'Hola {{firstName}},',
+ '',
+ 'Se ha creado una cuenta para usted en Open Data Capture.',
+ '',
+ 'Nombre de usuario: {{username}}',
+ 'Grupo(s): {{group}}',
+ '',
+ 'Puede iniciar sesión aquí: {{url}}',
+ '',
+ 'Su administrador le proporcionará su contraseña por separado. Por seguridad, cámbiela después de iniciar sesión por primera vez.',
+ '',
+ '¿Nuevo en Open Data Capture? Esta guía le ayudará a comenzar:',
+ 'https://opendatacapture.org/en/docs/guides/how-to-get-started-with-odc/'
+ ].join('\n'),
fr: [
'Bonjour {{firstName}},',
'',
@@ -192,6 +207,7 @@ const DEFAULT_NEW_USER_EMAIL_TEMPLATE = {
},
subject: {
en: 'Your Open Data Capture account',
+ es: 'Su cuenta de Open Data Capture',
fr: 'Votre compte Open Data Capture'
}
} as const satisfies z.infer;
@@ -211,6 +227,18 @@ const DEFAULT_ASSIGNMENT_EMAIL_TEMPLATE = {
'',
'Thank you.'
].join('\n'),
+ es: [
+ 'Hola,',
+ '',
+ 'Se le ha asignado un cuestionario para completar en Open Data Capture.',
+ '',
+ 'Por favor, complételo utilizando el enlace seguro a continuación:',
+ '{{url}}',
+ '',
+ 'Este enlace expira el {{expiresAt}}.',
+ '',
+ 'Gracias.'
+ ].join('\n'),
fr: [
'Bonjour,',
'',
@@ -226,6 +254,7 @@ const DEFAULT_ASSIGNMENT_EMAIL_TEMPLATE = {
},
subject: {
en: 'Your Open Data Capture assignment',
+ es: 'Su evaluación de Open Data Capture',
fr: 'Votre évaluation Open Data Capture'
}
} as const satisfies z.infer;
From a1c3c0ec2f6dae31fd4c086dec134e1b6686c6fa Mon Sep 17 00:00:00 2001
From: thomasbeaudry
Date: Fri, 16 Oct 2026 23:54:19 -0400
Subject: [PATCH 2/2] test(web): pass the active languages the template dialogs
now require
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`activeLanguages` became a required prop on EditTemplateDialog and
ViewDefaultTemplateDialog, but their own tests were not updated, so
`@opendatacapture/web#lint` stopped at tsc with five TS2741s.
Both fixtures leave Spanish out, which lets a new case assert the picker offers
only the languages the instance has enabled — the behaviour this branch adds.
Co-Authored-By: Claude Opus 5
---
.../__tests__/EditTemplateDialog.test.tsx | 7 +++++++
.../ViewDefaultTemplateDialog.test.tsx | 19 +++++++++++++++----
.../components/MailSettings/MailSettings.tsx | 2 +-
3 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/apps/web/src/components/GroupEmailTemplates/__tests__/EditTemplateDialog.test.tsx b/apps/web/src/components/GroupEmailTemplates/__tests__/EditTemplateDialog.test.tsx
index 957207219..ef88affac 100644
--- a/apps/web/src/components/GroupEmailTemplates/__tests__/EditTemplateDialog.test.tsx
+++ b/apps/web/src/components/GroupEmailTemplates/__tests__/EditTemplateDialog.test.tsx
@@ -1,5 +1,6 @@
import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks';
import { i18n } from '@douglasneuroinformatics/libui/i18n';
+import type { Language } from '@opendatacapture/schemas/core';
import type { GroupEmailTemplate } from '@opendatacapture/schemas/group';
import type { MailTemplate } from '@opendatacapture/schemas/mail';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
@@ -16,7 +17,11 @@ const template: GroupEmailTemplate = {
subject: { en: 'Please complete' }
};
+/** Spanish deliberately left out, so a test can tell a scoped picker from an unscoped one. */
+const ACTIVE_LANGUAGES: readonly Language[] = ['en', 'fr'];
+
type RenderOptions = {
+ activeLanguages?: readonly Language[];
isPending?: boolean;
validateContent?: (template: MailTemplate) => string | undefined;
validateName?: (name: string) => string | undefined;
@@ -24,6 +29,7 @@ type RenderOptions = {
};
const renderDialog = ({
+ activeLanguages = ACTIVE_LANGUAGES,
isPending = false,
validateContent = () => undefined,
validateName = () => undefined,
@@ -33,6 +39,7 @@ const renderDialog = ({
const onSave = vi.fn();
render(
{
beforeEach(() => {
i18n.changeLanguage('en');
@@ -15,12 +19,12 @@ describe('ViewDefaultTemplateDialog', () => {
afterEach(cleanup);
it('should render nothing while closed', () => {
- render();
+ render();
expect(screen.queryByText('Built-in default template')).toBeNull();
});
it('should show the built-in subject and body when open', () => {
- render();
+ render();
expect(screen.getByTestId('template-builtin-subject').value).toBe(
DEFAULT_ASSIGNMENT_EMAIL_TEMPLATE.subject.en
);
@@ -30,15 +34,22 @@ describe('ViewDefaultTemplateDialog', () => {
});
it('should make the built-in subject and body read-only, since the default template cannot be edited', () => {
- render();
+ render();
expect(screen.getByTestId('template-builtin-subject').readOnly).toBe(true);
expect(screen.getByTestId('template-builtin-body').readOnly).toBe(true);
});
it('should request closing when the close button is clicked', () => {
const onOpenChange = vi.fn();
- render();
+ render();
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
expect(onOpenChange).toHaveBeenCalledWith(false);
});
+
+ // Offering a language the instance has disabled invites authoring copy nothing will ever send.
+ it('should offer only the languages the instance has enabled', () => {
+ render();
+ fireEvent.keyDown(screen.getByTestId('template-builtin-language'), { key: 'Enter' });
+ expect(screen.getAllByRole('option').map((option) => option.textContent)).toStrictEqual(['English', 'French']);
+ });
});
diff --git a/apps/web/src/components/MailSettings/MailSettings.tsx b/apps/web/src/components/MailSettings/MailSettings.tsx
index c8d180158..051afc5a0 100644
--- a/apps/web/src/components/MailSettings/MailSettings.tsx
+++ b/apps/web/src/components/MailSettings/MailSettings.tsx
@@ -2,8 +2,8 @@ import React from 'react';
import { Checkbox, Heading } from '@douglasneuroinformatics/libui/components';
import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks';
-import { $UpdateMailConfigData, isSameMailServer } from '@opendatacapture/schemas/mail';
import type { Language } from '@opendatacapture/schemas/core';
+import { $UpdateMailConfigData, isSameMailServer } from '@opendatacapture/schemas/mail';
import type { MailConfigDto, MailTemplate, UpdateMailConfigData } from '@opendatacapture/schemas/mail';
import { z } from 'zod/v4';