diff --git a/packages/client/package.json b/packages/client/package.json index 3e6af3cd..ef35e637 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -32,11 +32,16 @@ "esbuild": "^0.19.0", "graphql": "^16.8.0", "injection-js": "^2.4.0", + "i18next": "^23.8.2", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-http-backend": "^2.4.3", + "react-i18next": "^14.0.1", "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.12.1", "styled-components": "^5.3.10" + }, "devDependencies": { "@graphql-codegen/cli": "^5.0.0", diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json new file mode 100644 index 00000000..ccc51111 --- /dev/null +++ b/packages/client/public/locales/en/translation.json @@ -0,0 +1,50 @@ +{ + "common": { + "name": "Name", + "description": "Description", + "instruction": "Instruction", + "project": "Project", + "study": "Study", + "submit": "Submit" + }, + "languages": { + "en": "English", + "es": "Spanish" + }, + "home": { + "welcome": "Welcome to SignLab", + "signedIn": "You are signed in", + "logIn": "Please login to continue" + }, + "menu": { + "projects": "Projects", + "newProject": "New Project", + "projectControl": "Project Control", + "userPermissions": "User Permissions", + "studies": "Studies", + "newStudy": "New Study", + "studyControl": "Study Control", + "entryControls": "Entry Controls", + "downloadTags": "Download Tags", + "datasets": "Datasets", + "datasetControl": "Dataset Control", + "projectAccess": "Project Access", + "contribute": "Contribute", + "tagInStudy": "Tag in Study", + "logout": "Logout" + }, + "components": { + "environment": { + "title": "Environment" + }, + "languageSelector": { + "selectLanguage": "Select Language" + }, + "newProject": { + "formLabel": "Create New Project", + "nameDescription": "Please enter project name", + "descriptionDescription": "Please enter project description", + "failMessage": " Failed to create project! Try again." + } + } +} diff --git a/packages/client/public/locales/es/translation.json b/packages/client/public/locales/es/translation.json new file mode 100644 index 00000000..fcd5d96e --- /dev/null +++ b/packages/client/public/locales/es/translation.json @@ -0,0 +1,33 @@ +{ + "home": { + "welcome": "Bienvenido a SignLab", + "signedIn": "Has iniciado sesión", + "logIn": "Por favor inicie sesión para continuar" + }, + "languages": { + "en": "Inglés", + "es": "Español" + }, + "menu": { + "projects": "Proyectos", + "newProject": "Nuevo proyecto", + "projectControl": "Control de Proyecto", + "userPermissions": "Permisos de usuario", + "studies": "Estudios", + "newStudy": "Nuevo estudio", + "studyControl": "Control del estudio", + "entryControls": "Controles de entrada", + "downloadTags": "Descargar Etiquetas", + "datasets": "Conjuntos de datos", + "datasetControl": "Control de conjunto de datos", + "projectAccess": "Acceso al proyecto", + "contribute": "Contribuir", + "tagInStudy": "Etiqueta en estudio", + "logout": "Cerrar sesión" + }, + "components": { + "languageSelector": { + "selectLanguage": "Seleccione el idioma" + } + } +} diff --git a/packages/client/src/components/Environment.component.tsx b/packages/client/src/components/Environment.component.tsx index 186a6204..053ee684 100644 --- a/packages/client/src/components/Environment.component.tsx +++ b/packages/client/src/components/Environment.component.tsx @@ -2,22 +2,24 @@ import { Select, MenuItem, FormControl, InputLabel, Stack, Paper, Typography } f import { useProject } from '../context/Project.context'; import { useStudy } from '../context/Study.context'; import { Dispatch, SetStateAction, FC } from 'react'; +import { useTranslation } from 'react-i18next'; export const Environment: FC = () => { const { project, projects, setProject } = useProject(); const { study, studies, setStudy } = useStudy(); + const { t } = useTranslation(); return ( - Environment + {t('components.environment.title')} {/* Project Selection */} option._id} display={(option) => option.name} @@ -26,7 +28,7 @@ export const Environment: FC = () => { option._id} display={(option) => option.name} diff --git a/packages/client/src/components/LanguageSelector.tsx b/packages/client/src/components/LanguageSelector.tsx new file mode 100644 index 00000000..23434efa --- /dev/null +++ b/packages/client/src/components/LanguageSelector.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import '../i18n'; +import { useTranslation } from 'react-i18next'; +import { Paper } from '@mui/material'; + +const languages = ['en', 'es']; + +export const LanguageSelector: React.FC = () => { + const { t, i18n } = useTranslation(); + const [language, setLanguage] = React.useState(i18n.resolvedLanguage); + + const handleChange = (event: SelectChangeEvent) => { + const newLang = event.target.value as string; + i18n.changeLanguage(newLang); + setLanguage(newLang); + }; + + return ( + + + {t('components.languageSelector.selectLanguage')} + + + + ); +}; diff --git a/packages/client/src/components/SideBar.component.tsx b/packages/client/src/components/SideBar.component.tsx index c43eb55f..98dde7ca 100644 --- a/packages/client/src/components/SideBar.component.tsx +++ b/packages/client/src/components/SideBar.component.tsx @@ -8,6 +8,8 @@ import { Permission } from '../graphql/graphql'; import { useGetRolesQuery } from '../graphql/permission/permission'; import { useProject } from '../context/Project.context'; import { useStudy } from '../context/Study.context'; +import { useTranslation } from 'react-i18next'; +import { LanguageSelector } from './LanguageSelector'; interface SideBarProps { open: boolean; @@ -21,6 +23,7 @@ export const SideBar: FC = ({ open, drawerWidth }) => { const { project } = useProject(); const { study } = useStudy(); const rolesQueryResults = useGetRolesQuery({ variables: { project: project?._id, study: study?._id } }); + const { t } = useTranslation(); useEffect(() => { if (rolesQueryResults.data) { @@ -30,54 +33,62 @@ export const SideBar: FC = ({ open, drawerWidth }) => { const navItems: NavItemProps[] = [ { - name: 'Projects', + name: t('menu.projects'), icon: , action: () => {}, visible: (p) => p!.owner || p!.projectAdmin, permission, subItems: [ - { name: 'New Project', action: () => navigate('/project/new'), visible: (p) => p!.owner }, - { name: 'Project Control', action: () => navigate('/project/controls'), visible: (p) => p!.owner }, - { name: 'User Permissions', action: () => navigate('/project/permissions'), visible: (p) => p!.projectAdmin } + { name: t('menu.newProject'), action: () => navigate('/project/new'), visible: (p) => p!.owner }, + { name: t('menu.projectControl'), action: () => navigate('/project/controls'), visible: (p) => p!.owner }, + { + name: t('menu.userPermissions'), + action: () => navigate('/project/permissions'), + visible: (p) => p!.projectAdmin + } ] }, { - name: 'Studies', + name: t('menu.studies'), action: () => {}, icon: , visible: (p) => p!.projectAdmin || p!.studyAdmin, permission, subItems: [ - { name: 'New Study', action: () => navigate('/study/new'), visible: (p) => p!.projectAdmin }, - { name: 'Study Control', action: () => navigate('/study/controls'), visible: (p) => p!.projectAdmin }, - { name: 'User Permissions', action: () => navigate('/study/permissions'), visible: (p) => p!.studyAdmin }, - { name: 'Entry Controls', action: () => navigate('/study/entries'), visible: (p) => p!.studyAdmin }, - { name: 'Download Tags', action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin } + { name: t('menu.newStudy'), action: () => navigate('/study/new'), visible: (p) => p!.projectAdmin }, + { name: t('menu.studyControl'), action: () => navigate('/study/controls'), visible: (p) => p!.projectAdmin }, + { + name: t('menu.userPermissions'), + action: () => navigate('/study/permissions'), + visible: (p) => p!.studyAdmin + }, + { name: t('menu.entryControls'), action: () => navigate('/study/entries'), visible: (p) => p!.studyAdmin }, + { name: t('menu.downloadTags'), action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin } ] }, { - name: 'Datasets', + name: t('menu.datasets'), action: () => {}, icon: , visible: (p) => p!.owner, permission, subItems: [ - { name: 'Dataset Control', action: () => navigate('/dataset/controls'), visible: (p) => p!.owner }, - { name: 'Project Access', action: () => navigate('/dataset/projectaccess'), visible: (p) => p!.owner } + { name: t('menu.datasetControl'), action: () => navigate('/dataset/controls'), visible: (p) => p!.owner }, + { name: t('menu.projectAccess'), action: () => navigate('/dataset/projectaccess'), visible: (p) => p!.owner } ] }, { - name: 'Contribute', + name: t('menu.contribute'), action: () => {}, icon: , permission, visible: (p) => p!.contributor, subItems: [ - { name: 'Tag in Study', action: () => navigate('/contribute/landing'), visible: (p) => p!.contributor } + { name: t('menu.tagInStudy'), action: () => navigate('/contribute/landing'), visible: (p) => p!.contributor } ] }, { - name: 'Logout', + name: t('menu.logout'), action: logout, icon: , visible: () => true @@ -113,6 +124,8 @@ export const SideBar: FC = ({ open, drawerWidth }) => { )} + + ); }; diff --git a/packages/client/src/i18n.js b/packages/client/src/i18n.js new file mode 100644 index 00000000..38597090 --- /dev/null +++ b/packages/client/src/i18n.js @@ -0,0 +1,9 @@ +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from 'i18next-http-backend'; + +i18next.use(initReactI18next).use(LanguageDetector).use(Backend).init({ + fallbackLang: 'en', + debug: true +}); diff --git a/packages/client/src/main.tsx b/packages/client/src/main.tsx index 2bcefcc9..eb3afe4a 100644 --- a/packages/client/src/main.tsx +++ b/packages/client/src/main.tsx @@ -2,9 +2,12 @@ import * as ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; import { StrictMode } from 'react'; - +import './i18n'; +import * as React from 'react'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - + + + ); diff --git a/packages/client/src/pages/Home.tsx b/packages/client/src/pages/Home.tsx index 05515dfa..7639bde0 100644 --- a/packages/client/src/pages/Home.tsx +++ b/packages/client/src/pages/Home.tsx @@ -1,14 +1,16 @@ import { keyframes } from 'styled-components'; import styled from 'styled-components'; import { useAuth } from '../context/Auth.context'; +import { useTranslation } from 'react-i18next'; export const HomePage: React.FC = () => { const { token, authenticated } = useAuth(); + const { t } = useTranslation(); return (
- Welcome to SignLab - {authenticated && token ?

You are signed in

:

Please login to continue

} + {t('home.welcome')} + {authenticated && token ?

{t('home.signedIn')}

:

{t('home.logIn')}

}
); }; diff --git a/packages/client/src/pages/projects/NewProject.tsx b/packages/client/src/pages/projects/NewProject.tsx index ca515419..62d822ea 100644 --- a/packages/client/src/pages/projects/NewProject.tsx +++ b/packages/client/src/pages/projects/NewProject.tsx @@ -5,44 +5,7 @@ import { materialRenderers, materialCells } from '@jsonforms/material-renderers' import { JsonForms } from '@jsonforms/react'; import { useCreateProjectMutation, useProjectExistsLazyQuery } from '../../graphql/project/project'; import { ErrorObject } from 'ajv'; - -const schema = { - type: 'object', - properties: { - name: { - type: 'string', - pattern: '^[a-zA-Z 0-9]*$', - description: 'Please enter project name' - }, - description: { - type: 'string', - description: 'Please enter project description' - } - }, - required: ['name', 'description'], - errorMessage: { - type: 'data should be an object', - properties: { name: 'Project name should be ...' }, - _: 'data should ...' - } -}; - -const uischema = { - type: 'Group', - label: 'Create New Project', - elements: [ - { - type: 'Control', - label: 'Name', - scope: '#/properties/name' - }, - { - type: 'Control', - label: 'Description', - scope: '#/properties/description' - } - ] -}; +import { useTranslation } from 'react-i18next'; const initialData = { name: '', @@ -57,6 +20,45 @@ export const NewProject: React.FC = () => { }); const [projectExistsQuery, projectExistsResults] = useProjectExistsLazyQuery(); const [additionalErrors, setAdditionalErrors] = useState([]); + const { t } = useTranslation(); + + const schema = { + type: 'object', + properties: { + name: { + type: 'string', + pattern: '^[a-zA-Z 0-9]*$', + description: t('components.newProject.nameDescription') + }, + description: { + type: 'string', + description: t('components.newProject.descriptionDescription') + } + }, + required: ['name', 'description'], + errorMessage: { + type: 'data should be an object', + properties: { name: 'Project name should be ...' }, + _: 'data should ...' + } + }; + + const uischema = { + type: 'Group', + label: t('components.newProject.formLabel'), + elements: [ + { + type: 'Control', + label: t('common.name'), + scope: '#/properties/name' + }, + { + type: 'Control', + label: t('common.description'), + scope: '#/properties/description' + } + ] + }; useEffect(() => { if (projectExistsResults.data?.projectExists) { @@ -101,7 +103,7 @@ export const NewProject: React.FC = () => { <> {error && ( - Failed to create project! Try again. + {t('components.newProject.failMessage')} )} { additionalErrors={additionalErrors} /> );