From d180bd58e82ffc2cb21e202b1cd386f4d9f057bf Mon Sep 17 00:00:00 2001 From: MDshouabemanzar Date: Wed, 15 Oct 2025 15:16:07 +0530 Subject: [PATCH] multilingual --- client/package.json | 2 +- client/src/components/LanguageToggle.tsx | 36 +++++++++++++++ client/src/context/TranslationContext.tsx | 48 ++++++++++++++++++++ client/src/i18n.ts | 3 ++ client/src/index.tsx | 9 ++-- client/src/locales/de.json | 8 ++++ client/src/locales/en.json | 6 +++ client/src/locales/es.json | 6 +++ client/src/locales/fr.json | 8 ++++ client/src/locales/hi.json | 8 ++++ client/src/pages/Home/MyEventsView/index.tsx | 12 +++-- client/src/pages/Home/TopNavigationBar.tsx | 9 +++- server/.env.example | 10 +++- server/src/auth/encryption.service.ts | 11 ++++- 14 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 client/src/components/LanguageToggle.tsx create mode 100644 client/src/context/TranslationContext.tsx create mode 100644 client/src/i18n.ts create mode 100644 client/src/locales/de.json create mode 100644 client/src/locales/en.json create mode 100644 client/src/locales/es.json create mode 100644 client/src/locales/fr.json create mode 100644 client/src/locales/hi.json diff --git a/client/package.json b/client/package.json index c8879ef..6b70604 100644 --- a/client/package.json +++ b/client/package.json @@ -27,7 +27,7 @@ "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", "react-router-dom": "^6.26.2", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4" }, "scripts": { "start": "vite --port 3000", diff --git a/client/src/components/LanguageToggle.tsx b/client/src/components/LanguageToggle.tsx new file mode 100644 index 0000000..207b9ba --- /dev/null +++ b/client/src/components/LanguageToggle.tsx @@ -0,0 +1,36 @@ +import { IconButton, Menu, MenuItem } from '@mui/material'; +import TranslateIcon from '@mui/icons-material/Translate'; +import { useState } from 'react'; +import { useTranslation } from '@/context/TranslationContext'; + +const LanguageToggle = () => { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + const onOpen = (e: React.MouseEvent) => setAnchorEl(e.currentTarget); + const onClose = () => setAnchorEl(null); + + const { changeLanguage } = useTranslation(); + + const changeLang = (lng: string) => { + changeLanguage(lng); + onClose(); + }; + + return ( + <> + + + + + changeLang('en')}>English + changeLang('es')}>Español + changeLang('hi')}>हिंदी + changeLang('fr')}>Français + changeLang('de')}>Deutsch + + + ); +}; + +export default LanguageToggle; diff --git a/client/src/context/TranslationContext.tsx b/client/src/context/TranslationContext.tsx new file mode 100644 index 0000000..f85428f --- /dev/null +++ b/client/src/context/TranslationContext.tsx @@ -0,0 +1,48 @@ +import { createContext, useContext, useMemo, useState, ReactNode } from 'react'; +import en from '@/locales/en.json'; +import es from '@/locales/es.json'; +import hi from '@/locales/hi.json'; +import fr from '@/locales/fr.json'; +import de from '@/locales/de.json'; + +type Translations = { [key: string]: string }; +type Resources = { [lang: string]: { translation: Translations } }; + +const resources: Resources = { + en: { translation: en as Translations }, + es: { translation: es as Translations }, + hi: { translation: hi as Translations }, + fr: { translation: fr as Translations }, + de: { translation: de as Translations }, +}; + +type TranslationContextType = { + language: string; + changeLanguage: (lng: string) => void; + t: (key: string) => string; +}; + +const TranslationContext = createContext(undefined); + +export const TranslationProvider = ({ children }: { children: ReactNode }) => { + const [language, setLanguage] = useState('en'); + + const changeLanguage = (lng: string) => setLanguage(lng); + + const t = (key: string) => { + const res = resources[language]?.translation[key]; + return res ?? key; + }; + + const value = useMemo(() => ({ language, changeLanguage, t }), [language]); + + return {children}; +}; + +export const useTranslation = () => { + const ctx = useContext(TranslationContext); + if (!ctx) throw new Error('useTranslation must be used within TranslationProvider'); + return ctx; +}; + +export default TranslationContext; diff --git a/client/src/i18n.ts b/client/src/i18n.ts new file mode 100644 index 0000000..abb9803 --- /dev/null +++ b/client/src/i18n.ts @@ -0,0 +1,3 @@ +// i18n stub kept for compatibility. Translations are handled via TranslationContext. +const noop = {}; +export default noop; diff --git a/client/src/index.tsx b/client/src/index.tsx index f3a62e3..ae85bc5 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -6,6 +6,7 @@ import { secrets } from './config/secrets'; import './styles.css'; import { PreferencesProvider } from './context/PreferencesContext'; import { ApiProvider } from '@/context/ApiContext'; +import { TranslationProvider } from './context/TranslationContext'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; @@ -15,9 +16,11 @@ root.render( - - - + + + + + diff --git a/client/src/locales/de.json b/client/src/locales/de.json new file mode 100644 index 0000000..e0219ed --- /dev/null +++ b/client/src/locales/de.json @@ -0,0 +1,8 @@ +{ + "newEvent": "Neues Ereignis", + "myEvents": "Meine Ereignisse", + "settings": "Einstellungen", + "language": "Sprache", + "noEvents": "Keine Ereignisse zum Anzeigen", + "createContent": "Inhalt erstellen" +} diff --git a/client/src/locales/en.json b/client/src/locales/en.json new file mode 100644 index 0000000..6e40682 --- /dev/null +++ b/client/src/locales/en.json @@ -0,0 +1,6 @@ +{ + "newEvent": "New Event", + "myEvents": "My Events", + "settings": "Settings", + "language": "Language" +} diff --git a/client/src/locales/es.json b/client/src/locales/es.json new file mode 100644 index 0000000..8648a53 --- /dev/null +++ b/client/src/locales/es.json @@ -0,0 +1,6 @@ +{ + "newEvent": "Nuevo Evento", + "myEvents": "Mis Eventos", + "settings": "Configuración", + "language": "Idioma" +} diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json new file mode 100644 index 0000000..afbe892 --- /dev/null +++ b/client/src/locales/fr.json @@ -0,0 +1,8 @@ +{ + "newEvent": "Nouvel événement", + "myEvents": "Mes événements", + "settings": "Paramètres", + "language": "Langue", + "noEvents": "Aucun événement à afficher", + "createContent": "Créer du contenu" +} diff --git a/client/src/locales/hi.json b/client/src/locales/hi.json new file mode 100644 index 0000000..51a7e6a --- /dev/null +++ b/client/src/locales/hi.json @@ -0,0 +1,8 @@ +{ + "newEvent": "नया कार्यक्रम", + "myEvents": "मेरे कार्यक्रम", + "settings": "सेटिंग्स", + "language": "भाषा", + "noEvents": "प्रदर्शित करने के लिए कोई कार्यक्रम नहीं", + "createContent": "संदर्भ बनाएं" +} diff --git a/client/src/pages/Home/MyEventsView/index.tsx b/client/src/pages/Home/MyEventsView/index.tsx index f050814..40c466b 100644 --- a/client/src/pages/Home/MyEventsView/index.tsx +++ b/client/src/pages/Home/MyEventsView/index.tsx @@ -5,10 +5,11 @@ import DeleteConfirmationView from '@components/DeleteConfirmationView'; import EventCard from '@components/EventCard'; import { ROUTES } from '@config/routes'; import { FormData } from '@helpers/types'; -import { Box, Divider, Skeleton, Stack, Typography } from '@mui/material'; +import { Box, Divider, Skeleton, Stack, Typography, Link } from '@mui/material'; import React, { useEffect, useRef, useState } from 'react'; import { BookRoomDto, EventResponse, IConferenceRoom } from '@quickmeet/shared'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from '@/context/TranslationContext'; import { convertToRFC3339, getTimeZoneString, renderError } from '@helpers/utility'; import toast from 'react-hot-toast'; import EditEventsView from './EditEventsView'; @@ -288,9 +289,12 @@ export default function MyEventsView({ redirectedDate }: MyEventsViewProps) { ) : ( <> {events.length == 0 ? ( - - No events to show - + + {t('noEvents')} + + {t('createContent')} + + ) : ( ({ paddingTop: theme.spacing(1.5), @@ -66,6 +68,8 @@ const TopNavigationBar = ({ sx, tabIndex, handleTabChange }: TopNavigationBarPro navigate(ROUTES.settings) }; + const { t } = useTranslation(); + return ( - New Event + {t('newEvent')} - My Events + {t('myEvents')} @@ -104,6 +108,7 @@ const TopNavigationBar = ({ sx, tabIndex, handleTabChange }: TopNavigationBarPro mr: 2, }} > + diff --git a/server/.env.example b/server/.env.example index e5f5f31..c4c8add 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,6 +1,12 @@ +# APP_PORT=3001 +# NODE_ENV=development +# ENCRYPTION_KEY=93fc7ebce3eab10f5514b286b8503dbcdaa9b9d0af0c71e3924bca5efc3148db +# OAUTH_REDIRECT_URL=http://localhost:3000/oauthcallback +# OAUTH_CLIENT_SECRET=secret +# OAUTH_CLIENT_ID=secret APP_PORT=3001 NODE_ENV=development ENCRYPTION_KEY=93fc7ebce3eab10f5514b286b8503dbcdaa9b9d0af0c71e3924bca5efc3148db OAUTH_REDIRECT_URL=http://localhost:3000/oauthcallback -OAUTH_CLIENT_SECRET=secret -OAUTH_CLIENT_ID=secret +OAUTH_CLIENT_SECRET=test_secret +OAUTH_CLIENT_ID=test_client diff --git a/server/src/auth/encryption.service.ts b/server/src/auth/encryption.service.ts index 0190402..e08dd35 100644 --- a/server/src/auth/encryption.service.ts +++ b/server/src/auth/encryption.service.ts @@ -9,7 +9,16 @@ export class EncryptionService { private key: Buffer; constructor(@Inject(appConfig.KEY) private config: ConfigType) { - this.key = Buffer.from(this.config.encryptionKey, 'hex'); + // Ensure the encryption key is provided before attempting to convert it to a Buffer + if (!this.config.encryptionKey) { + throw new InternalServerErrorException('Missing ENCRYPTION_KEY environment variable. See server/.env or server/.env.example'); + } + + try { + this.key = Buffer.from(this.config.encryptionKey, 'hex'); + } catch (err) { + throw new InternalServerErrorException('Invalid ENCRYPTION_KEY: must be a hex string.'); + } if (this.key.length !== 32) { throw new InternalServerErrorException('Invalid ENCRYPTION_KEY: Must be 32 bytes.');