{t({ en: 'This site is where you complete questionnaires assigned to you on Open Data Capture. To begin, open the secure link you were sent.', + es: 'En este sitio puede completar los cuestionarios que se le han asignado en Open Data Capture. Para comenzar, abra el enlace seguro que se le envió.', fr: 'Ce site vous permet de compléter les questionnaires qui vous ont été assignés sur Open Data Capture. Pour commencer, ouvrez le lien sécurisé qui vous a été envoyé.' })}
diff --git a/apps/gateway/src/services/axios.ts b/apps/gateway/src/services/axios.ts index 8fc2f899c..c1df9cea0 100644 --- a/apps/gateway/src/services/axios.ts +++ b/apps/gateway/src/services/axios.ts @@ -7,6 +7,7 @@ axios.interceptors.request.use((config) => { config.timeout = 10000; // abort request after 10 seconds config.timeoutErrorMessage = i18n.t({ en: 'Network Error', + es: 'Error de red', fr: 'Erreur de réseau' }); return config; @@ -20,6 +21,7 @@ axios.interceptors.response.use( notifications.addNotification({ message: i18n.t({ en: 'Unknown Error', + es: 'Error desconocido', fr: 'Erreur inconnue' }), type: 'error' @@ -30,6 +32,7 @@ axios.interceptors.response.use( notifications.addNotification({ message: i18n.t({ en: 'HTTP Request Failed', + es: 'Error en la solicitud HTTP', fr: 'Échec de la requête HTTP' }), title: error.response?.status.toString(), diff --git a/apps/gateway/src/services/i18n.ts b/apps/gateway/src/services/i18n.ts index c32af3fc4..82098fa06 100644 --- a/apps/gateway/src/services/i18n.ts +++ b/apps/gateway/src/services/i18n.ts @@ -1,5 +1,16 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +/* eslint-disable @typescript-eslint/no-namespace */ + import { i18n } from '@douglasneuroinformatics/libui/i18n'; +declare module '@douglasneuroinformatics/libui/i18n' { + export namespace UserConfig { + export interface Options { + requireCompleteTranslations: true; + } + } +} + // `defaultLanguage` is deliberately left unset. It is what `t()` falls back to when a string has no // entry in the resolved language, so pointing it at the patient's language would make every string // missing a translation render as nothing. The session's language is set with `changeLanguage`. diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index 86df8f258..1a178d53b 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -117,12 +117,17 @@ In components use a selector: `useAppStore((store) => store.currentGroup)`. Outs `useTranslation` comes from `@douglasneuroinformatics/libui/hooks`. Two forms: ```tsx -t({ en: 'Connection Problem', fr: 'Problème de connexion' }); // inline — prefer this +t({ en: 'Connection Problem', es: 'Problema de conexión', fr: 'Problème de connexion' }); // inline — prefer this t('layout.tabs.table'); // keyed, from a namespace JSON ``` +**Every interface language needs an entry.** libui resolves `obj[resolvedLanguage] ?? obj.en`, so a +missing language renders in English and nothing complains — which is why +`requireCompleteTranslations` is set in `src/services/i18n.ts`: omitting a language from an inline +`t()` call is a type error caught by `pnpm lint`. + Use the keyed form only when a string is reused. Resource files are `src/translations/*.json`, keyed -per-leaf as `{ "en": ..., "fr": ... }` and kept sorted by +per-leaf as `{ "en": ..., "es": ..., "fr": ... }` and kept sorted by `pnpm --filter @opendatacapture/web format:translations`. **Adding a namespace means three edits in `src/services/i18n.ts`** — the import, the `declare module` diff --git a/apps/web/src/components/AppErrorComponent.tsx b/apps/web/src/components/AppErrorComponent.tsx index ed9b048c4..cfcc72aa4 100644 --- a/apps/web/src/components/AppErrorComponent.tsx +++ b/apps/web/src/components/AppErrorComponent.tsx @@ -45,22 +45,24 @@ export const AppErrorComponent = ({ error, reset }: ErrorComponentProps) => {{isOnline ? t({ en: "We couldn't reach the server. This is usually temporary — please try again.", + es: 'No se pudo contactar con el servidor. Suele ser algo temporal: vuelva a intentarlo.', fr: 'Impossible de joindre le serveur. Le problème est généralement temporaire — veuillez réessayer.' }) : t({ en: "You appear to be offline. We'll reconnect automatically as soon as your connection returns.", + es: 'Parece que no tiene conexión. Nos reconectaremos automáticamente en cuanto se restablezca.', fr: 'Vous semblez être hors ligne. La reconnexion se fera automatiquement dès le retour de votre connexion.' })}