From c7b08c470f859305ff4f2c490308656063e19d95 Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 27 Mar 2026 17:11:25 -0400 Subject: [PATCH 1/6] feat: add translations to dialogs in playground --- .../components/Editor/DeleteFileDialog.tsx | 20 ++++-- .../FileUploadDialog/FileUploadDialog.tsx | 16 +++-- .../DeleteInstrumentDialog.tsx | 21 +++++-- .../Header/ActionsDropdown/LoginDialog.tsx | 61 ++++++++++++------- .../ActionsDropdown/RestoreDefaultsDialog.tsx | 22 +++++-- .../ActionsDropdown/StorageUsageDialog.tsx | 30 ++++++--- .../ActionsDropdown/UploadBundleDialog.tsx | 43 +++++++++---- .../ActionsDropdown/UserSettingsDialog.tsx | 27 +++++--- 8 files changed, 166 insertions(+), 74 deletions(-) diff --git a/apps/playground/src/components/Editor/DeleteFileDialog.tsx b/apps/playground/src/components/Editor/DeleteFileDialog.tsx index 73965336e..6cfb73ec5 100644 --- a/apps/playground/src/components/Editor/DeleteFileDialog.tsx +++ b/apps/playground/src/components/Editor/DeleteFileDialog.tsx @@ -1,4 +1,5 @@ import { Button, Dialog } from '@douglasneuroinformatics/libui/components'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { useAppStore } from '@/store'; @@ -10,12 +11,23 @@ export type DeleteFileDialogProps = { export const DeleteFileDialog = ({ filename, isOpen, setIsOpen }: DeleteFileDialogProps) => { const deleteFile = useAppStore((store) => store.deleteFile); + const { t } = useTranslation(); return filename ? ( - {`Are you sure you want to delete "${filename}"?`} - Once deleted, this file cannot be restored + + {t({ + en: `Are you sure you want to delete "${filename}"?`, + fr: `Êtes-vous sûr de vouloir supprimer "${filename}" ?` + })} + + + {t({ + en: 'Once deleted, this file cannot be restored', + fr: 'Une fois supprimé, ce fichier ne peut plus être restauré' + })} + diff --git a/apps/playground/src/components/FileUploadDialog/FileUploadDialog.tsx b/apps/playground/src/components/FileUploadDialog/FileUploadDialog.tsx index c495c7e60..5b07f1207 100644 --- a/apps/playground/src/components/FileUploadDialog/FileUploadDialog.tsx +++ b/apps/playground/src/components/FileUploadDialog/FileUploadDialog.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { Button, Dialog } from '@douglasneuroinformatics/libui/components'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { CloudUploadIcon } from 'lucide-react'; import { useDropzone } from 'react-dropzone'; import type { FileRejection } from 'react-dropzone'; @@ -20,11 +21,12 @@ export type FileUploadDialogProps = { export const FileUploadDialog = ({ accept, isOpen, onSubmit, onValidate, setIsOpen, title }: FileUploadDialogProps) => { const [files, setFiles] = useState([]); const [errorMessage, setErrorMessage] = useState(null); + const { t } = useTranslation(); const handleDrop = useCallback( (acceptedFiles: File[], rejections: FileRejection[]) => { for (const { errors, file } of rejections) { - setErrorMessage(`Invalid file type: ${file.name} `); + setErrorMessage(t({ en: `Invalid file type: ${file.name} `, fr: `Type de fichier invalide : ${file.name} ` })); console.error(errors); return; } @@ -65,9 +67,15 @@ export const FileUploadDialog = ({ accept, isOpen, onSubmit, onValidate, setIsOp } else if (files.length === 1) { dropzoneText = files[0]!.name; } else if (isDragActive) { - dropzoneText = 'Release your cursor to upload file(s)'; + dropzoneText = t({ + en: 'Release your cursor to upload file(s)', + fr: 'Relâchez votre curseur pour téléverser le(s) fichier(s)' + }); } else { - dropzoneText = 'Click here to upload, or drag and drop files into this area'; + dropzoneText = t({ + en: 'Click here to upload, or drag and drop files into this area', + fr: 'Cliquez ici pour téléverser, ou glissez et déposez des fichiers dans cette zone' + }); } return ( @@ -94,7 +102,7 @@ export const FileUploadDialog = ({ accept, isOpen, onSubmit, onValidate, setIsOp type="button" onClick={() => void submitFiles(files)} > - Submit + {t({ en: 'Submit', fr: 'Soumettre' })} diff --git a/apps/playground/src/components/Header/ActionsDropdown/DeleteInstrumentDialog.tsx b/apps/playground/src/components/Header/ActionsDropdown/DeleteInstrumentDialog.tsx index 459cb87a5..e5ecc4bac 100644 --- a/apps/playground/src/components/Header/ActionsDropdown/DeleteInstrumentDialog.tsx +++ b/apps/playground/src/components/Header/ActionsDropdown/DeleteInstrumentDialog.tsx @@ -1,5 +1,5 @@ import { Button, Dialog } from '@douglasneuroinformatics/libui/components'; -import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks'; +import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { useAppStore } from '@/store'; @@ -12,27 +12,36 @@ export const DeleteInstrumentDialog = ({ isOpen, setIsOpen }: DeleteInstrumentDi const addNotification = useNotificationsStore((store) => store.addNotification); const removeInstrument = useAppStore((store) => store.removeInstrument); const selectedInstrument = useAppStore((store) => store.selectedInstrument); + const { t } = useTranslation(); return ( - Are you absolutely sure? - This instrument will be deleted from local storage. + {t({ en: 'Are you absolutely sure?', fr: 'Êtes-vous absolument sûr ?' })} + + {t({ + en: 'This instrument will be deleted from local storage.', + fr: 'Cet instrument sera supprimé du stockage local.' + })} + diff --git a/apps/playground/src/components/Header/ActionsDropdown/LoginDialog.tsx b/apps/playground/src/components/Header/ActionsDropdown/LoginDialog.tsx index 68754ff21..85a3802e7 100644 --- a/apps/playground/src/components/Header/ActionsDropdown/LoginDialog.tsx +++ b/apps/playground/src/components/Header/ActionsDropdown/LoginDialog.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import { asyncResultify } from '@douglasneuroinformatics/libjs'; import { Dialog, Form } from '@douglasneuroinformatics/libui/components'; -import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks'; +import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { $LoginCredentials } from '@opendatacapture/schemas/auth'; import axios from 'axios'; import { CheckIcon, XIcon } from 'lucide-react'; @@ -35,6 +35,7 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => { const revalidateToken = useAppStore((store) => store.revalidateToken); const addNotification = useNotificationsStore((store) => store.addNotification); + const { t } = useTranslation(); useEffect(() => { revalidateToken(); @@ -88,7 +89,11 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => { updateSettings({ apiBaseUrl }); const adminTokenResult = await getAdminToken(credentials, apiBaseUrl); if (adminTokenResult.isErr()) { - addNotification({ type: 'error', title: 'Login Failed', message: adminTokenResult.error }); + addNotification({ + type: 'error', + title: t({ en: 'Login Failed', fr: 'Échec de la connexion' }), + message: adminTokenResult.error + }); return; } @@ -97,7 +102,11 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => { } else { const limitedTokenResult = await getLimitedToken(adminTokenResult.value.accessToken, apiBaseUrl); if (limitedTokenResult.isErr()) { - addNotification({ type: 'error', title: 'Failed to Get Limited Token', message: limitedTokenResult.error }); + addNotification({ + type: 'error', + title: t({ en: 'Failed to Get Limited Token', fr: "Échec de l'obtention d'un jeton limité" }), + message: limitedTokenResult.error + }); return; } login(limitedTokenResult.value.accessToken); @@ -110,10 +119,12 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => { event.preventDefault()}> - Login + {t({ en: 'Login', fr: 'Connexion' })} - Login to your Open Data Capture instance. A special access token is used that grants permissions to create - instruments only. You must have permission to create instruments to use this functionality. + {t({ + en: 'Login to your Open Data Capture instance. A special access token is used that grants permissions to create instruments only. You must have permission to create instruments to use this functionality.', + fr: "Connectez-vous à votre instance Open Data Capture. Un jeton d'accès spécial est utilisé pour accorder l'autorisation de créer uniquement des instruments. Vous devez avoir la permission de créer des instruments pour utiliser cette fonctionnalité." + })}
{isAuthorized ? ( @@ -121,14 +132,14 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => {
- You are already logged in + {t({ en: 'You are already logged in', fr: 'Vous êtes déjà connecté' })} ) : ( <>
- You are not currently logged in + {t({ en: 'You are not currently logged in', fr: "Vous n'êtes actuellement pas connecté" })} )}
@@ -137,42 +148,48 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => {
store.addNotification); const resetInstruments = useAppStore((store) => store.resetInstruments); const resetSettings = useAppStore((store) => store.resetSettings); + const { t } = useTranslation(); return ( - Are you absolutely sure? + {t({ en: 'Are you absolutely sure?', fr: 'Êtes-vous absolument sûr ?' })} - This action will delete all user-defined instruments in local - storage and restore the default configuration. + {t({ en: 'This action will ', fr: 'Cette action va ' })} + + {t({ + en: 'delete all user-defined instruments', + fr: "supprimer tous les instruments définis par l'utilisateur" + })} + {' '} + {t({ + en: 'in local storage and restore the default configuration.', + fr: 'dans le stockage local et restaurer la configuration par défaut.' + })} @@ -33,10 +43,10 @@ export const RestoreDefaultsDialog = ({ isOpen, setIsOpen }: RestoreDefaultsDial setIsOpen(false); }} > - Reset + {t({ en: 'Reset', fr: 'Réinitialiser' })} diff --git a/apps/playground/src/components/Header/ActionsDropdown/StorageUsageDialog.tsx b/apps/playground/src/components/Header/ActionsDropdown/StorageUsageDialog.tsx index c18a9edb0..a360ab0c4 100644 --- a/apps/playground/src/components/Header/ActionsDropdown/StorageUsageDialog.tsx +++ b/apps/playground/src/components/Header/ActionsDropdown/StorageUsageDialog.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { formatByteSize } from '@douglasneuroinformatics/libjs'; import { Button, Dialog } from '@douglasneuroinformatics/libui/components'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { useAppStore } from '@/store'; @@ -13,10 +14,11 @@ export type StorageUsageDialogProps = { export const StorageUsageDialog = ({ isOpen, setIsOpen }: StorageUsageDialogProps) => { const [storageEstimate, setStorageEstimate] = useState(null); const [updateKey, setUpdateKey] = useState(0); - const [message, setMessage] = useState('Loading...'); + const { t } = useTranslation(); + const [message, setMessage] = useState(t({ en: 'Loading...', fr: 'Chargement...' })); const updateStorage = async () => { - setMessage('Loading...'); + setMessage(t({ en: 'Loading...', fr: 'Chargement...' })); const [updated] = await Promise.all([ navigator.storage.estimate(), new Promise((resolve) => setTimeout(resolve, 500)) @@ -33,10 +35,12 @@ export const StorageUsageDialog = ({ isOpen, setIsOpen }: StorageUsageDialogProp - Storage Usage + {t({ en: 'Storage Usage', fr: 'Utilisation du stockage' })} - Check the details below to see how much storage your browser is using for instruments and how much space is - still available. + {t({ + en: 'Check the details below to see how much storage your browser is using for instruments and how much space is still available.', + fr: "Consultez les détails ci-dessous pour voir l'espace de stockage que votre navigateur utilise pour les instruments et combien d'espace est encore disponible." + })} @@ -44,8 +48,14 @@ export const StorageUsageDialog = ({ isOpen, setIsOpen }: StorageUsageDialogProp

{message}

) : ( <> -

Usage: {storageEstimate?.usage ? formatByteSize(storageEstimate.usage, true) : 'N/A'}

-

Quota: {storageEstimate?.quota ? formatByteSize(storageEstimate.quota, true) : 'N/A'}

+

+ {t({ en: 'Usage: ', fr: 'Utilisation : ' })} + {storageEstimate?.usage ? formatByteSize(storageEstimate.usage, true) : 'N/A'} +

+

+ {t({ en: 'Quota: ', fr: 'Quota : ' })} + {storageEstimate?.quota ? formatByteSize(storageEstimate.quota, true) : 'N/A'}{' '} +

)}
@@ -54,16 +64,16 @@ export const StorageUsageDialog = ({ isOpen, setIsOpen }: StorageUsageDialogProp variant="danger" onClick={() => { useAppStore.persist.clearStorage(); - setMessage('Deleting...'); + setMessage(t({ en: 'Deleting...', fr: 'Suppression...' })); void new Promise((resolve) => setTimeout(resolve, 2000)).then(() => { setUpdateKey(updateKey + 1); }); }} > - Drop Database (Irreversible) + {t({ en: 'Drop Database (Irreversible)', fr: 'Supprimer la base de données (Irréversible)' })}
diff --git a/apps/playground/src/components/Header/ActionsDropdown/UploadBundleDialog.tsx b/apps/playground/src/components/Header/ActionsDropdown/UploadBundleDialog.tsx index ccd9b359f..32b8d9b45 100644 --- a/apps/playground/src/components/Header/ActionsDropdown/UploadBundleDialog.tsx +++ b/apps/playground/src/components/Header/ActionsDropdown/UploadBundleDialog.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef } from 'react'; import { Button, Dialog } from '@douglasneuroinformatics/libui/components'; -import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks'; +import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import axios, { isAxiosError } from 'axios'; import type { AxiosResponse } from 'axios'; @@ -23,6 +23,7 @@ export const UploadBundleDialog = ({ isOpen, setIsOpen, onLoginRequired }: Uploa const revalidateToken = useAppStore((store) => store.revalidateToken); const transpilerStateRef = useRef(useAppStore.getState().transpilerState); + const { t } = useTranslation(); useEffect(() => { revalidateToken(); @@ -40,13 +41,25 @@ export const UploadBundleDialog = ({ isOpen, setIsOpen, onLoginRequired }: Uploa const handleSubmit = async () => { const state = transpilerStateRef.current; if (state.status === 'building' || state.status === 'initial') { - addNotification({ message: 'Upload Failed: Transpilation Incomplete', type: 'error' }); + addNotification({ + message: t({ + en: 'Upload Failed: Transpilation Incomplete', + fr: 'Échec du téléversement : Transpilation incomplète' + }), + type: 'error' + }); return; } else if (state.status === 'error') { - addNotification({ message: 'Upload Failed: Transpilation Error', type: 'error' }); + addNotification({ + message: t({ + en: 'Upload Failed: Transpilation Error', + fr: 'Échec du téléversement : Erreur de transpilation' + }), + type: 'error' + }); return; } else if (!auth) { - addNotification({ message: 'Login Required', type: 'error' }); + addNotification({ message: t({ en: 'Login Required', fr: 'Connexion requise' }), type: 'error' }); return; } @@ -75,7 +88,11 @@ export const UploadBundleDialog = ({ isOpen, setIsOpen, onLoginRequired }: Uploa } else { message = 'Unknown Error'; } - addNotification({ message, type: 'error', title: 'HTTP Request Failed' }); + addNotification({ + message, + type: 'error', + title: t({ en: 'HTTP Request Failed', fr: 'Échec de la requête HTTP' }) + }); return; } @@ -89,24 +106,26 @@ export const UploadBundleDialog = ({ isOpen, setIsOpen, onLoginRequired }: Uploa event.preventDefault()}> - Upload Bundle + {t({ en: 'Upload Bundle', fr: 'Téléverser le paquet' })} - Upload an instrument to your Open Data Capture instance. This functionality requires that you have added the - API base URL for your instance to the user settings panel. + {t({ + en: 'Upload an instrument to your Open Data Capture instance. This functionality requires that you have added the API base URL for your instance to the user settings panel.', + fr: "Téléversez un instrument vers votre instance Open Data Capture. Cette fonctionnalité nécessite que vous ayez ajouté l'URL de base de l'API de votre instance dans le panneau des paramètres utilisateur." + })} {!auth && (

- Please{' '} + {t({ en: 'Please ', fr: 'Veuillez ' })} {' '} - to upload a bundle. + {t({ en: ' to upload a bundle.', fr: ' pour téléverser un paquet.' })}

)}
diff --git a/apps/playground/src/components/Header/ActionsDropdown/UserSettingsDialog.tsx b/apps/playground/src/components/Header/ActionsDropdown/UserSettingsDialog.tsx index 9e141f465..d9d4313f8 100644 --- a/apps/playground/src/components/Header/ActionsDropdown/UserSettingsDialog.tsx +++ b/apps/playground/src/components/Header/ActionsDropdown/UserSettingsDialog.tsx @@ -1,7 +1,7 @@ /* eslint-disable perfectionist/sort-objects */ import { Dialog, Form } from '@douglasneuroinformatics/libui/components'; -import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks'; +import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { $Settings } from '@/models/settings.model'; import { useAppStore } from '@/store'; @@ -15,47 +15,54 @@ export const UserSettingsDialog = ({ isOpen, setIsOpen }: UserSettingsDialogProp const addNotification = useNotificationsStore((store) => store.addNotification); const settings = useAppStore((store) => store.settings); const updateSettings = useAppStore((store) => store.updateSettings); + const { t } = useTranslation(); return ( event.preventDefault()}> - User Settings + {t({ en: 'User Settings', fr: 'Paramètres utilisateur' })} { updateSettings(updatedSettings); From 995254c2c7bf4c8bd31f2225f8d00771b6a26fa1 Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 27 Mar 2026 17:11:52 -0400 Subject: [PATCH 2/6] feat: add tranlations to edito components --- .../src/components/Editor/Editor.tsx | 23 ++++++++++++++----- .../components/Editor/EditorFileButton.tsx | 7 +++--- .../src/components/Editor/EditorPane.tsx | 19 +++++++++++---- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/apps/playground/src/components/Editor/Editor.tsx b/apps/playground/src/components/Editor/Editor.tsx index 0ccbf2268..5badfac2e 100644 --- a/apps/playground/src/components/Editor/Editor.tsx +++ b/apps/playground/src/components/Editor/Editor.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { extractInputFileExtension } from '@opendatacapture/instrument-bundler'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { Columns3Icon, FilePlusIcon, FileUpIcon } from 'lucide-react'; import { motion } from 'motion/react'; import { useShallow } from 'zustand/react/shallow'; @@ -29,6 +30,7 @@ export const Editor = () => { const [isFileUploadDialogOpen, setIsFileUploadDialogOpen] = useState(false); const [isDeleteFileDialogOpen, setIsDeleteFileDialogOpen] = useState(false); + const { t } = useTranslation(); const addFile = useAppStore((store) => store.addFile); const addFiles = useAppStore((store) => store.addFiles); @@ -77,10 +79,14 @@ export const Editor = () => { return (
- } tip="View Files" onClick={() => setIsSidebarOpen(!isSidebarOpen)} /> + } + tip={t({ en: 'View Files', fr: 'Voir les fichiers' })} + onClick={() => setIsSidebarOpen(!isSidebarOpen)} + /> } - tip="Add File" + tip={t({ en: 'Add File', fr: 'Ajouter un fichier' })} onClick={() => { setIsSidebarOpen(true); setIsAddingFile(true); @@ -88,7 +94,7 @@ export const Editor = () => { /> } - tip="Upload Files" + tip={t({ en: 'Upload Files', fr: 'Téléverser des fichiers' })} onClick={() => { setIsFileUploadDialogOpen(true); }} @@ -135,7 +141,9 @@ export const Editor = () => { {openFilenames.length ? ( setIsEditorMounted(true)} /> ) : ( - No File Selected + + {t({ en: 'No File Selected', fr: 'Aucun fichier sélectionné' })} + )}
{ }} isOpen={isFileUploadDialogOpen} setIsOpen={setIsFileUploadDialogOpen} - title="Upload Files" + title={t({ en: 'Upload Files', fr: 'Téléverser des fichiers' })} onSubmit={async (files) => { const editorFiles = await loadEditorFilesFromNative(files); addFiles(editorFiles); @@ -166,7 +174,10 @@ export const Editor = () => { onValidate={(files: File[]) => { for (const file of files) { if (filenames.includes(file.name)) { - return { message: `File already exists: ${file.name}`, result: 'error' }; + return { + message: t({ en: `File already exists: ${file.name}`, fr: `Le fichier existe déjà : ${file.name}` }), + result: 'error' + }; } } return { result: 'success' }; diff --git a/apps/playground/src/components/Editor/EditorFileButton.tsx b/apps/playground/src/components/Editor/EditorFileButton.tsx index b27241d05..2427e0e89 100644 --- a/apps/playground/src/components/Editor/EditorFileButton.tsx +++ b/apps/playground/src/components/Editor/EditorFileButton.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; -import { useOnClickOutside } from '@douglasneuroinformatics/libui/hooks'; +import { useOnClickOutside, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { cn } from '@douglasneuroinformatics/libui/utils'; import { PencilIcon, TrashIcon } from 'lucide-react'; @@ -23,6 +23,7 @@ export const EditorFileButton = ({ filename, isActive, onDelete }: EditorFileBut const [displayFilename, setDisplayFilename] = useState(filename); const [isRenaming, setIsRenaming] = useState(false); const renameFile = useAppStore((store) => store.renameFile); + const { t } = useTranslation(); const rename = () => { renameFile(filename, displayFilename); @@ -79,7 +80,7 @@ export const EditorFileButton = ({ filename, isActive, onDelete }: EditorFileBut }} > - Delete + {t({ en: 'Delete', fr: 'Supprimer' })} { @@ -87,7 +88,7 @@ export const EditorFileButton = ({ filename, isActive, onDelete }: EditorFileBut }} > - Rename + {t({ en: 'Rename', fr: 'Renommer' })} diff --git a/apps/playground/src/components/Editor/EditorPane.tsx b/apps/playground/src/components/Editor/EditorPane.tsx index a79eee243..ed99ea9a1 100644 --- a/apps/playground/src/components/Editor/EditorPane.tsx +++ b/apps/playground/src/components/Editor/EditorPane.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; -import { useTheme } from '@douglasneuroinformatics/libui/hooks'; +import { useTheme, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import MonacoEditor from '@monaco-editor/react'; import { useFilesRef } from '@/hooks/useFilesRef'; @@ -29,6 +29,7 @@ export const EditorPane = React.forwardRef(funct const [theme] = useTheme(); const [isMounted, setIsMounted] = useState(false); + const { t } = useTranslation(); const { libs } = useRuntime('v1'); const editorRef = useRef(null); @@ -112,12 +113,18 @@ export const EditorPane = React.forwardRef(funct }; if (!defaultFile) { - return No File Selected; + return ( + {t({ en: 'No File Selected', fr: 'Aucun fichier sélectionné' })} + ); } const fileType = inferFileType(defaultFile.name); if (!fileType) { - return {`Error: Invalid file type "${fileType}"`}; + return ( + + {t({ en: `Error: Invalid file type "${fileType}"`, fr: `Erreur : Type de fichier invalide "${fileType}"` })} + + ); } else if (fileType === 'asset') { if (isBase64EncodedFileType(defaultFile.name)) { return ( @@ -130,7 +137,11 @@ export const EditorPane = React.forwardRef(funct
); } - return Cannot Display Binary Asset; + return ( + + {t({ en: 'Cannot Display Binary Asset', fr: "Impossible d'afficher l'actif binaire" })} + + ); } return ( From df5cea5d5e6d379a712074d3c1457c53fb5059f6 Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 27 Mar 2026 17:13:06 -0400 Subject: [PATCH 3/6] feat: add translations to button components and their dialogs --- .../Header/CloneButton/CloneButton.tsx | 18 +++++++++++------- .../Header/DownloadButton/DownloadButton.tsx | 6 ++++-- .../Header/RefreshButton/RefreshButton.tsx | 4 +++- .../Header/SaveButton/SaveButton.tsx | 6 ++++-- .../Header/ShareButton/ShareButton.tsx | 13 +++++++++---- .../Header/UploadButton/UploadButton.tsx | 18 +++++++++++++----- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/apps/playground/src/components/Header/CloneButton/CloneButton.tsx b/apps/playground/src/components/Header/CloneButton/CloneButton.tsx index 5180106eb..feb1009a1 100644 --- a/apps/playground/src/components/Header/CloneButton/CloneButton.tsx +++ b/apps/playground/src/components/Header/CloneButton/CloneButton.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Dialog, Form, Tooltip } from '@douglasneuroinformatics/libui/components'; -import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks'; +import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { CopyPlusIcon } from 'lucide-react'; import { z } from 'zod/v4'; @@ -16,6 +16,7 @@ export const CloneButton = () => { const addInstrument = useAppStore((store) => store.addInstrument); const setSelectedInstrument = useAppStore((store) => store.setSelectedInstrument); const editorFilesRef = useFilesRef(); + const { t } = useTranslation(); const handleSubmit = ({ label }: { label: string }) => { const files = editorFilesRef.current; @@ -42,9 +43,12 @@ export const CloneButton = () => { - Create New Instrument + {t({ en: 'Create New Instrument', fr: 'Créer un nouvel instrument' })} - This will save the current playground state in local storage as a new instrument. + {t({ + en: 'This will save the current playground state in local storage as a new instrument.', + fr: "Ceci enregistrera l'état actuel du terrain de jeu dans le stockage local en tant que nouvel instrument." + })} { content={{ label: { kind: 'string', - label: 'Label', + label: t({ en: 'Label', fr: 'Nom' }), variant: 'input' } }} - submitBtnLabel="Save" + submitBtnLabel={t({ en: 'Save', fr: 'Enregistrer' })} validationSchema={z.object({ label: z .string() .min(1) .refine( (arg) => !instruments.find((instrument) => instrument.label === arg), - 'An instrument with this label already exists' + t({ en: 'An instrument with this label already exists', fr: 'Un instrument avec ce nom existe déjà' }) ) })} onSubmit={(data) => void handleSubmit(data)} @@ -71,7 +75,7 @@ export const CloneButton = () => {
-

Create New Instrument

+

{t({ en: 'Create New Instrument', fr: 'Créer un nouvel instrument' })}

); diff --git a/apps/playground/src/components/Header/DownloadButton/DownloadButton.tsx b/apps/playground/src/components/Header/DownloadButton/DownloadButton.tsx index e93e55f71..fc828cd09 100644 --- a/apps/playground/src/components/Header/DownloadButton/DownloadButton.tsx +++ b/apps/playground/src/components/Header/DownloadButton/DownloadButton.tsx @@ -1,4 +1,4 @@ -import { useDownload } from '@douglasneuroinformatics/libui/hooks'; +import { useDownload, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import JSZip from 'jszip'; import { DownloadIcon } from 'lucide-react'; @@ -24,10 +24,12 @@ export const DownloadButton = () => { await download(`${baseName}.zip`, file, { blobType: 'application/zip' }); }; + const { t } = useTranslation(); + return ( } - tooltip="Download Archive" + tooltip={t({ en: 'Download Archive', fr: "Télécharger l'archive" })} onClick={() => { void downloadFiles(); }} diff --git a/apps/playground/src/components/Header/RefreshButton/RefreshButton.tsx b/apps/playground/src/components/Header/RefreshButton/RefreshButton.tsx index 7c4d43d48..723b38ad5 100644 --- a/apps/playground/src/components/Header/RefreshButton/RefreshButton.tsx +++ b/apps/playground/src/components/Header/RefreshButton/RefreshButton.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { RefreshCwIcon } from 'lucide-react'; import { useAppStore } from '@/store'; @@ -6,5 +7,6 @@ import { ActionButton } from '../ActionButton'; export const RefreshButton = () => { const onClick = useAppStore((store) => store.viewer.forceRefresh); - return } tooltip="Refresh" onClick={onClick} />; + const { t } = useTranslation(); + return } tooltip={t({ en: 'Refresh', fr: 'Actualiser' })} onClick={onClick} />; }; diff --git a/apps/playground/src/components/Header/SaveButton/SaveButton.tsx b/apps/playground/src/components/Header/SaveButton/SaveButton.tsx index 2045caf7a..223007274 100644 --- a/apps/playground/src/components/Header/SaveButton/SaveButton.tsx +++ b/apps/playground/src/components/Header/SaveButton/SaveButton.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { useInterval } from '@douglasneuroinformatics/libui/hooks'; +import { useInterval, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { isEqual } from 'lodash-es'; import { SaveIcon } from 'lucide-react'; @@ -21,11 +21,13 @@ export const SaveButton = () => { } }, 1000); + const { t } = useTranslation(); + return ( } - tooltip="Save" + tooltip={t({ en: 'Save', fr: 'Enregistrer' })} onClick={() => { updateSelectedInstrument({ files: editorFiles.current }); }} diff --git a/apps/playground/src/components/Header/ShareButton/ShareButton.tsx b/apps/playground/src/components/Header/ShareButton/ShareButton.tsx index be54802ab..74b071114 100644 --- a/apps/playground/src/components/Header/ShareButton/ShareButton.tsx +++ b/apps/playground/src/components/Header/ShareButton/ShareButton.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { formatByteSize } from '@douglasneuroinformatics/libjs'; import { Heading, Input, Popover, Tooltip } from '@douglasneuroinformatics/libui/components'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { CopyButton } from '@opendatacapture/react-core'; import { Share2Icon } from 'lucide-react'; @@ -15,6 +16,7 @@ export const ShareButton = () => { const [shareURL, setShareURL] = useState(encodeShareURL({ files: editorFilesRef.current, label })); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isTooltipOpen, setIsTooltipOpen] = useState(false); + const { t } = useTranslation(); // The user cannot modify the editor without closing the popover useEffect(() => { @@ -40,10 +42,13 @@ export const ShareButton = () => {
- Share Instrument + {t({ en: 'Share Instrument', fr: "Partager l'instrument" })}

- Anyone with this link can open a snapshot of the current code in your playground. The total size of the - URL-encoded source files for this instrument is {formatByteSize(shareURL.size)}. + {t({ + en: 'Anyone with this link can open a snapshot of the current code in your playground. The total size of the URL-encoded source files for this instrument is ', + fr: "Toute personne disposant de ce lien peut ouvrir un aperçu du code actuel dans votre terrain de jeu. La taille totale des fichiers sources encodés dans l'URL pour cet instrument est de " + })} + {formatByteSize(shareURL.size)}.

@@ -53,7 +58,7 @@ export const ShareButton = () => { -

Share

+

{t({ en: 'Share', fr: 'Partager' })}

); diff --git a/apps/playground/src/components/Header/UploadButton/UploadButton.tsx b/apps/playground/src/components/Header/UploadButton/UploadButton.tsx index 10ba8ba4e..8b415385f 100644 --- a/apps/playground/src/components/Header/UploadButton/UploadButton.tsx +++ b/apps/playground/src/components/Header/UploadButton/UploadButton.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { isPlainObject } from '@douglasneuroinformatics/libjs'; -import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks'; +import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import JSZip from 'jszip'; import { UploadIcon } from 'lucide-react'; @@ -18,6 +18,7 @@ export const UploadButton = () => { const addInstrument = useAppStore((store) => store.addInstrument); const setSelectedInstrument = useAppStore((store) => store.setSelectedInstrument); const instruments = useAppStore((store) => store.instruments); + const { t } = useTranslation(); const handleSubmit = async (files: File[]) => { try { @@ -53,8 +54,11 @@ export const UploadButton = () => { } catch (err) { console.error(err); addNotification({ - message: 'Please refer to browser console for details', - title: 'Upload Failed', + message: t({ + en: 'Please refer to browser console for details', + fr: 'Veuillez consulter la console du navigateur pour plus de détails' + }), + title: t({ en: 'Upload Failed', fr: 'Échec du téléversement' }), type: 'error' }); } finally { @@ -64,12 +68,16 @@ export const UploadButton = () => { return ( - } tooltip="Upload Archive" onClick={() => setIsDialogOpen(true)} /> + } + tooltip={t({ en: 'Upload Archive', fr: 'Téléverser une archive' })} + onClick={() => setIsDialogOpen(true)} + /> { return { result: 'success' }; From 7e33be47ea2e87144f39da3c83f7426e25348127 Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 27 Mar 2026 17:13:29 -0400 Subject: [PATCH 4/6] feat: translations to error fallbacks --- .../src/components/Viewer/CompileErrorFallback.tsx | 6 ++++-- .../src/components/Viewer/RuntimeErrorFallback.tsx | 11 ++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/playground/src/components/Viewer/CompileErrorFallback.tsx b/apps/playground/src/components/Viewer/CompileErrorFallback.tsx index aff370666..f4244686a 100644 --- a/apps/playground/src/components/Viewer/CompileErrorFallback.tsx +++ b/apps/playground/src/components/Viewer/CompileErrorFallback.tsx @@ -1,6 +1,8 @@ -import { InstrumentErrorFallback } from '@opendatacapture/react-core'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { InstrumentErrorFallbackProps } from '@opendatacapture/react-core'; +import { InstrumentErrorFallback } from '@opendatacapture/react-core'; export const CompileErrorFallback = (props: Omit) => { - return ; + const { t } = useTranslation(); + return ; }; diff --git a/apps/playground/src/components/Viewer/RuntimeErrorFallback.tsx b/apps/playground/src/components/Viewer/RuntimeErrorFallback.tsx index 38020e1ce..054fa18ae 100644 --- a/apps/playground/src/components/Viewer/RuntimeErrorFallback.tsx +++ b/apps/playground/src/components/Viewer/RuntimeErrorFallback.tsx @@ -1,11 +1,16 @@ -import { InstrumentErrorFallback } from '@opendatacapture/react-core'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { InstrumentErrorFallbackProps } from '@opendatacapture/react-core'; +import { InstrumentErrorFallback } from '@opendatacapture/react-core'; export const RuntimeErrorFallback = (props: Omit) => { + const { t } = useTranslation(); return ( ); From f3097f639bca0f36c8c51f4c231bf61dfd2d4fe2 Mon Sep 17 00:00:00 2001 From: David Roper Date: Fri, 27 Mar 2026 17:13:51 -0400 Subject: [PATCH 5/6] feat: translations for actions main and viewer components --- .../ActionsDropdown/ActionsDropdown.tsx | 14 +++++++----- .../components/MainContent/MainContent.tsx | 7 +++--- .../src/components/Viewer/Viewer.tsx | 22 ++++++++++++++++--- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/apps/playground/src/components/Header/ActionsDropdown/ActionsDropdown.tsx b/apps/playground/src/components/Header/ActionsDropdown/ActionsDropdown.tsx index 0a391d29d..a3907f994 100644 --- a/apps/playground/src/components/Header/ActionsDropdown/ActionsDropdown.tsx +++ b/apps/playground/src/components/Header/ActionsDropdown/ActionsDropdown.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Button, DropdownMenu } from '@douglasneuroinformatics/libui/components'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { EllipsisVerticalIcon } from 'lucide-react'; import { useAppStore } from '@/store'; @@ -21,6 +22,7 @@ export const ActionsDropdown = () => { const [showStorageUsageDialog, setShowStorageUsageDialog] = useState(false); const selectedInstrument = useAppStore((store) => store.selectedInstrument); + const { t } = useTranslation(); return ( @@ -43,22 +45,22 @@ export const ActionsDropdown = () => { setShowLoginDialog(true)}> setShowUploadBundleDialog(true)}> setShowUserSettingsDialog(true)}> setShowStorageUsageDialog(true)}> @@ -68,7 +70,7 @@ export const ActionsDropdown = () => { disabled={selectedInstrument.category !== 'Saved'} type="button" > - Delete Instrument + {t({ en: 'Delete Instrument', fr: "Supprimer l'instrument" })} setShowRestoreDefaultsDialog(true)}> @@ -76,7 +78,7 @@ export const ActionsDropdown = () => { className="w-full cursor-pointer text-red-600 disabled:cursor-not-allowed disabled:opacity-50 dark:text-red-400" type="button" > - Restore Defaults + {t({ en: 'Restore Defaults', fr: 'Restaurer les paramètres par défaut' })} diff --git a/apps/playground/src/components/MainContent/MainContent.tsx b/apps/playground/src/components/MainContent/MainContent.tsx index 35df06b2b..244962df9 100644 --- a/apps/playground/src/components/MainContent/MainContent.tsx +++ b/apps/playground/src/components/MainContent/MainContent.tsx @@ -1,5 +1,5 @@ import { Tabs } from '@douglasneuroinformatics/libui/components'; -import { useMediaQuery } from '@douglasneuroinformatics/libui/hooks'; +import { useMediaQuery, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { Editor } from '../Editor'; import { Resizable } from '../Resizable'; @@ -7,6 +7,7 @@ import { Viewer } from '../Viewer'; export const MainContent = () => { const isDesktop = useMediaQuery('(min-width: 768px)'); + const { t } = useTranslation(); return (
{isDesktop ? ( @@ -22,8 +23,8 @@ export const MainContent = () => { ) : ( - Editor - Viewer + {t({ en: 'Editor', fr: 'Éditeur' })} + {t({ en: 'Viewer', fr: 'Aperçu' })} diff --git a/apps/playground/src/components/Viewer/Viewer.tsx b/apps/playground/src/components/Viewer/Viewer.tsx index 197490cfc..ef778bddf 100644 --- a/apps/playground/src/components/Viewer/Viewer.tsx +++ b/apps/playground/src/components/Viewer/Viewer.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; import { Spinner } from '@douglasneuroinformatics/libui/components'; -import { useInterval } from '@douglasneuroinformatics/libui/hooks'; +import { useInterval, useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { BundlerInput } from '@opendatacapture/instrument-bundler'; import { bundle } from '@opendatacapture/instrument-bundler'; import { ScalarInstrumentRenderer } from '@opendatacapture/react-core'; @@ -25,6 +25,7 @@ export const Viewer = () => { const key = useAppStore((store) => store.viewer.key); const state = useAppStore((store) => store.transpilerState); const setState = useAppStore((store) => store.setTranspilerState); + const { t } = useTranslation(); const transpile = useCallback(async (files: EditorFile[]) => { setState({ status: 'building' }); @@ -33,7 +34,10 @@ export const Viewer = () => { setState({ bundle: await bundle({ inputs }), status: 'built' }); } catch (err) { setState({ - error: err instanceof Error ? err : new Error('Unexpected Error', { cause: err }), + error: + err instanceof Error + ? err + : new Error(t({ en: 'Unexpected Error', fr: 'Erreur inattendue' }), { cause: err }), status: 'error' }); } finally { @@ -71,7 +75,19 @@ export const Viewer = () => { onCompileError={(error) => setState({ error, status: 'error' })} onSubmit={({ data }) => { // eslint-disable-next-line no-alert - alert(JSON.stringify({ _message: 'The following data will be submitted', data }, null, 2)); + alert( + JSON.stringify( + { + _message: t({ + en: 'The following data will be submitted', + fr: 'Les données suivantes seront soumises' + }), + data + }, + null, + 2 + ) + ); }} /> From a53b2fca2632efdfe31eb9d2d0f6537e8a928ea7 Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 31 Mar 2026 10:10:36 -0400 Subject: [PATCH 6/6] fix: adjust name used in invalid file error --- apps/playground/src/components/Editor/EditorPane.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/playground/src/components/Editor/EditorPane.tsx b/apps/playground/src/components/Editor/EditorPane.tsx index ed99ea9a1..4d45e5f19 100644 --- a/apps/playground/src/components/Editor/EditorPane.tsx +++ b/apps/playground/src/components/Editor/EditorPane.tsx @@ -122,7 +122,10 @@ export const EditorPane = React.forwardRef(funct if (!fileType) { return ( - {t({ en: `Error: Invalid file type "${fileType}"`, fr: `Erreur : Type de fichier invalide "${fileType}"` })} + {t({ + en: `Error: Invalid file type for "${defaultFile.name}"`, + fr: `Erreur : Type de fichier invalide pour "${defaultFile.name}"` + })} ); } else if (fileType === 'asset') {