diff --git a/client/package.json b/client/package.json
index c8879ef..d671719 100644
--- a/client/package.json
+++ b/client/package.json
@@ -22,10 +22,13 @@
"@types/react-dom": "^18.3.0",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
+ "i18next": "^25.6.0",
+ "i18next-browser-languagedetector": "^8.2.0",
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
+ "react-i18next": "^16.0.1",
"react-router-dom": "^6.26.2",
"web-vitals": "^2.1.4"
},
diff --git a/client/src/i18n.ts b/client/src/i18n.ts
new file mode 100644
index 0000000..0b98a4f
--- /dev/null
+++ b/client/src/i18n.ts
@@ -0,0 +1,28 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+import en from './locales/en/translation.json';
+import hi from './locales/hi/translation.json';
+
+// i18n instance is initialized once for the whole app
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources: {
+ en: { translation: en },
+ hi: { translation: hi },
+ },
+ fallbackLng: 'en',
+ supportedLngs: ['en', 'hi'],
+ interpolation: { escapeValue: false },
+ detection: {
+ order: ['querystring', 'localStorage', 'navigator', 'htmlTag'],
+ caches: ['localStorage'],
+ },
+ });
+
+export default i18n;
+
+
diff --git a/client/src/index.tsx b/client/src/index.tsx
index f3a62e3..f65401a 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -4,6 +4,7 @@ import { BrowserRouter } from 'react-router-dom';
import { StyledEngineProvider } from '@mui/material/styles';
import { secrets } from './config/secrets';
import './styles.css';
+import './i18n';
import { PreferencesProvider } from './context/PreferencesContext';
import { ApiProvider } from '@/context/ApiContext';
import { LocalizationProvider } from '@mui/x-date-pickers';
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
new file mode 100644
index 0000000..656d293
--- /dev/null
+++ b/client/src/locales/en/translation.json
@@ -0,0 +1,22 @@
+{
+ "nav": {
+ "preferences": "Preferences",
+ "support": "Support",
+ "logout": "Logout"
+ },
+ "preferences": {
+ "noPreference": "No preference",
+ "selectFloor": "Select preferred floor",
+ "selectDuration": "Select preferred meeting duration",
+ "selectCapacity": "Select preferred room capacity",
+ "titlePlaceholder": "Add preferred title",
+ "save": "Save",
+ "saved": "Saved successfully!"
+ },
+ "home": {
+ "newEvent": "New Event",
+ "myEvents": "My Events"
+ }
+}
+
+
diff --git a/client/src/locales/hi/translation.json b/client/src/locales/hi/translation.json
new file mode 100644
index 0000000..b1b4ed2
--- /dev/null
+++ b/client/src/locales/hi/translation.json
@@ -0,0 +1,22 @@
+{
+ "nav": {
+ "preferences": "पसंद",
+ "support": "सहायता",
+ "logout": "लॉग आउट"
+ },
+ "preferences": {
+ "noPreference": "कोई प्राथमिकता नहीं",
+ "selectFloor": "पसंदीदा मंज़िल चुनें",
+ "selectDuration": "पसंदीदा मीटिंग अवधि चुनें",
+ "selectCapacity": "पसंदीदा कक्ष क्षमता चुनें",
+ "titlePlaceholder": "पसंदीदा शीर्षक जोड़ें",
+ "save": "सहेजें",
+ "saved": "सफलतापूर्वक सहेजा गया!"
+ },
+ "home": {
+ "newEvent": "नया इवेंट",
+ "myEvents": "मेरे इवेंट"
+ }
+}
+
+
diff --git a/client/src/pages/Home/TopNavigationBar.tsx b/client/src/pages/Home/TopNavigationBar.tsx
index 3af06d1..c51d1f5 100644
--- a/client/src/pages/Home/TopNavigationBar.tsx
+++ b/client/src/pages/Home/TopNavigationBar.tsx
@@ -2,6 +2,7 @@ import { Box, IconButton, styled, SxProps, Theme, ToggleButton, ToggleButtonGrou
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
import { useNavigate } from 'react-router-dom';
import { ROUTES } from '@/config/routes';
+import { useTranslation } from 'react-i18next';
const TopBar = styled(Box)(({ theme }) => ({
paddingTop: theme.spacing(1.5),
@@ -55,6 +56,7 @@ interface TopNavigationBarProps {
const TopNavigationBar = ({ sx, tabIndex, handleTabChange }: TopNavigationBarProps) => {
const navigate = useNavigate();
+ const { t } = useTranslation();
const handleChange = (_: React.SyntheticEvent | null, newValue: number) => {
if (newValue !== null) {
@@ -83,10 +85,10 @@ const TopNavigationBar = ({ sx, tabIndex, handleTabChange }: TopNavigationBarPro
>
- New Event
+ {t('home.newEvent', 'New Event')}
- My Events
+ {t('home.myEvents', 'My Events')}
diff --git a/client/src/pages/Settings/PreferenceView.tsx b/client/src/pages/Settings/PreferenceView.tsx
index eb02f7a..9d82c07 100644
--- a/client/src/pages/Settings/PreferenceView.tsx
+++ b/client/src/pages/Settings/PreferenceView.tsx
@@ -5,6 +5,7 @@ import { usePreferences } from '@/context/PreferencesContext';
import { createDropdownOptions, isChromeExt, populateDurationOptions, populateRoomCapacity, renderError } from '@/helpers/utility';
import { Box, Button, Typography } from '@mui/material';
import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import toast from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
import EventSeatRoundedIcon from '@mui/icons-material/EventSeatRounded';
@@ -14,6 +15,7 @@ import TitleIcon from '@mui/icons-material/Title';
import { useApi } from '@/context/ApiContext';
export default function PreferenceView() {
+ const { t } = useTranslation();
// Form state
const [formData, setFormData] = useState({
floor: '',
@@ -40,7 +42,7 @@ export default function PreferenceView() {
useEffect(() => {
const init = (floors: string[], capacities: string[]) => {
const floorOptions = createDropdownOptions(floors);
- floorOptions.unshift({ text: 'No preference', value: '' });
+ floorOptions.unshift({ text: t('preferences.noPreference'), value: '' });
setFloorOptions(floorOptions);
setRoomCapacityOptions(createDropdownOptions(capacities));
@@ -95,7 +97,7 @@ export default function PreferenceView() {
duration: Number(formData.duration),
});
- toast.success('Saved successfully!');
+ toast.success(t('preferences.saved'));
};
return (
@@ -127,7 +129,7 @@ export default function PreferenceView() {
sx={{ borderTopLeftRadius: 10, borderTopRightRadius: 10, height: '60px' }}
id="floor"
value={formData.floor}
- placeholder={'Select preferred floor'}
+ placeholder={t('preferences.selectFloor')}
options={floorOptions}
onChange={handleInputChange}
icon={
@@ -147,7 +149,7 @@ export default function PreferenceView() {
value={formData.duration}
options={durationOptions}
onChange={handleInputChange}
- placeholder={'Select preferred meeting duration'}
+ placeholder={t('preferences.selectDuration')}
icon={
}
id="title"
- placeholder="Add preferred title"
+ placeholder={t('preferences.titlePlaceholder')}
onChange={handleInputChange}
/>
@@ -222,7 +224,7 @@ export default function PreferenceView() {
]}
>
- Save
+ {t('preferences.save')}
diff --git a/client/src/pages/Settings/index.tsx b/client/src/pages/Settings/index.tsx
index 034f33f..b93ae46 100644
--- a/client/src/pages/Settings/index.tsx
+++ b/client/src/pages/Settings/index.tsx
@@ -8,9 +8,13 @@ import { useState } from 'react';
import { chromeBackground, isChromeExt } from '@helpers/utility';
import { styled, ToggleButton, ToggleButtonGroup } from '@mui/material';
import ExitToAppRoundedIcon from '@mui/icons-material/ExitToAppRounded';
+import LanguageIcon from '@mui/icons-material/Language';
+import { MenuItem, Select } from '@mui/material';
+import i18n from '@/i18n';
import PreferenceView from '@/pages/Settings/PreferenceView';
import SupportView from '@/pages/Settings/SupportView';
import LogoutView from '@/pages/Settings/LogoutView';
+import { useTranslation } from 'react-i18next';
const TopBar = styled(Box)(({ theme }) => ({
paddingTop: theme.spacing(1.5),
@@ -57,6 +61,8 @@ const StyledToggleButton = styled(ToggleButton)(({ theme }) => ({
export default function Settings() {
const [tabIndex, setTabIndex] = useState(0);
const navigate = useNavigate();
+ const { t } = useTranslation();
+ const [language, setLanguage] = useState(i18n.language || 'en');
const handleTabChange = (_: React.SyntheticEvent | null, newValue: number) => {
if (newValue !== null) {
@@ -68,6 +74,12 @@ export default function Settings() {
navigate(ROUTES.home);
};
+ const handleLanguageChange = (event: any) => {
+ const newLang = event.target.value;
+ setLanguage(newLang);
+ i18n.changeLanguage(newLang);
+ };
+
if (!open) {
return <>>;
}
@@ -119,28 +131,41 @@ export default function Settings() {
>
- Preferences
+ {t('nav.preferences')}
- Support
+ {t('nav.support')}
- {/* logout icon */}
+ {/* language + logout */}
({
borderRadius: 100,
- backgroundColor: tabIndex === 2 ? theme.palette.grey[100] : 'white',
- p: 1,
+ backgroundColor: 'white',
+ px: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
+ gap: 1,
ml: 0,
}),
]}
>
+
+
handleTabChange(null, 2)}>
=6.9.0"
}
@@ -6463,6 +6464,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -6531,6 +6541,46 @@
"ms": "^2.0.0"
}
},
+ "node_modules/i18next": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz",
+ "integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.27.6"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
+ "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -9013,6 +9063,32 @@
"react-dom": ">=16"
}
},
+ "node_modules/react-i18next": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.1.tgz",
+ "integrity": "sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.27.6",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 25.5.2",
+ "react": ">= 16.8.0",
+ "typescript": "^5"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
@@ -9145,11 +9221,6 @@
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
},
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
- },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -10610,7 +10681,7 @@
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
- "dev": true,
+ "devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10820,6 +10891,15 @@
}
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/watchpack": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",