Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/issue 164 Added support for multiple languages #194

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
"@types/react-dom": "^18.3.0",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"i18next": "^24.2.2",
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.4.0",
"react-router-dom": "^6.26.2",
"web-vitals": "^2.1.4"
},
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/AttendeeInput.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useLocales } from '@/config/i18n';
import { useApi } from '@/context/ApiContext';
import { isEmailValid } from '@/helpers/utility';
import PeopleAltRoundedIcon from '@mui/icons-material/PeopleAltRounded';
Expand All @@ -19,6 +20,7 @@ interface AttendeeInputProps {
export default function AttendeeInput({ id, onChange, value, type }: AttendeeInputProps) {
const [options, setOptions] = useState<IPeopleInformation[]>([]);
const [textInput, setTextInput] = useState('');
const { locale } = useLocales();

const api = useApi();

Expand Down Expand Up @@ -161,7 +163,7 @@ export default function AttendeeInput({ id, onChange, value, type }: AttendeeInp
onChange={(e) => setTextInput(e.target.value)}
type={type}
variant="standard"
placeholder="Attendees"
placeholder={locale.placeholder.attendees}
slotProps={{
input: {
...params.InputProps,
Expand Down
10 changes: 6 additions & 4 deletions client/src/components/DeleteConfirmationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ArrowBackIosRoundedIcon from '@mui/icons-material/ArrowBackIosRounded';
import { EventResponse } from '@quickmeet/shared';
import { chromeBackground, isChromeExt } from '@helpers/utility';
import EventCard from './EventCard';
import { useLocales } from '@/config/i18n';

interface DeleteConfirmationViewProps {
handleNegativeClick: () => void;
Expand All @@ -19,6 +20,7 @@ export default function DeleteConfirmationView({ event, open, handleNegativeClic
if (!open) return <></>;

const background: SxProps<Theme> = isChromeExt ? { ...chromeBackground } : { background: '#F8F8F8' };
const { locale } = useLocales();
return (
<Box
sx={{
Expand Down Expand Up @@ -57,7 +59,7 @@ export default function DeleteConfirmationView({ event, open, handleNegativeClic
}}
>
<Typography variant="h4" fontWeight={700}>
Are you sure you want to permanently delete this event?
{locale.info.deleteEventConfirmation}
</Typography>
{event && (
<Box
Expand All @@ -74,7 +76,7 @@ export default function DeleteConfirmationView({ event, open, handleNegativeClic
borderRadius: 2,
}}
>
<EventCard event={event} hideMenu={true} handleEditClick={() => { }} onDelete={() => { }} />
<EventCard event={event} hideMenu={true} handleEditClick={() => {}} onDelete={() => {}} />
</Box>
)}

Expand Down Expand Up @@ -108,7 +110,7 @@ export default function DeleteConfirmationView({ event, open, handleNegativeClic
textTransform: 'none',
}}
>
Delete
{locale.buttonText.delete}
</Typography>
</Button>

Expand Down Expand Up @@ -136,7 +138,7 @@ export default function DeleteConfirmationView({ event, open, handleNegativeClic
]}
>
<Typography variant="subtitle2" fontWeight={700}>
Cancel
{locale.buttonText.cancel}
</Typography>
</Button>
</Box>
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/RoomsDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ReactElement } from 'react';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { IConferenceRoom } from '@quickmeet/shared';
import { IAvailableRoomsDropdownOption } from '@/helpers/types';
import { useLocales } from '@/config/i18n';

interface DropdownProps {
id: string;
Expand Down Expand Up @@ -86,6 +87,7 @@ const StyledHintTypography = styled(Typography)(({ theme }) => ({
}));

const RenderNoRooms = ({ icon }: { icon?: ReactElement }) => {
const { locale } = useLocales();
return (
<Box
sx={{
Expand All @@ -96,7 +98,7 @@ const RenderNoRooms = ({ icon }: { icon?: ReactElement }) => {
{icon && icon}

<StyledHintTypography ml={2} variant="subtitle2">
No rooms available
{locale.info.noRooms}
</StyledHintTypography>
</Box>
);
Expand Down
32 changes: 32 additions & 0 deletions client/src/config/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { LOCALES, type LocaleType } from '@/config/locales';
import i18next from 'i18next';
import { initReactI18next, useTranslation } from 'react-i18next';

export const initI18n = () => {
i18next.use(initReactI18next).init({
fallbackLng: 'en',
resources: LOCALES.reduce(
(acc, { code, locale }) => {
acc[code] = { translation: locale };
return acc;
},
{} as Record<string, { translation: LocaleType }>,
),
});
};

export const useLocales = () => {
const { i18n } = useTranslation();
const currentLanguage = i18n.language;
const locale = i18n.getResourceBundle(currentLanguage, 'translation') as LocaleType;

const changeLanguage = (language: string) => {
i18n.changeLanguage(language);
};

return {
locale,
currentLanguage,
changeLanguage,
};
};
13 changes: 13 additions & 0 deletions client/src/config/locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import en from '@/locales/en.json';
import no from '@/locales/no.json';

export type LocaleType = typeof en;
interface ILocale {
code: string;
name: string;
locale: LocaleType;
}
export const LOCALES: ILocale[] = [
{ code: 'en', name: 'English', locale: en },
{ code: 'no', name: 'Norwegian', locale: no },
] as const;
8 changes: 8 additions & 0 deletions client/src/context/PreferencesContext.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { constants } from '@/config/constants';
import { CacheService, CacheServiceFactory } from '@/helpers/cache';
import { useLocales } from '@/config/i18n';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';

interface Preferences {
duration: number;
seats: number;
title?: string;
floor?: string;
language?: string;
}

interface PreferencesContextType {
Expand All @@ -24,6 +26,7 @@ const defaultPreferences = {
duration: 30,
seats: 1,
title: constants.defaultTitle,
language: 'en',
};

export const PreferencesProvider = ({ children }: PreferencesProviderProps) => {
Expand All @@ -32,8 +35,10 @@ export const PreferencesProvider = ({ children }: PreferencesProviderProps) => {
duration: defaultPreferences.duration,
seats: defaultPreferences.seats,
title: defaultPreferences.title,
language: defaultPreferences.language,
});
const [loading, setLoading] = useState(true);
const { changeLanguage, currentLanguage } = useLocales();

useEffect(() => {
const loadPreferences = async () => {
Expand All @@ -53,6 +58,9 @@ export const PreferencesProvider = ({ children }: PreferencesProviderProps) => {
if (!preferences.title) {
preferences.title = defaultPreferences.title;
}
if (preferences.language && preferences.language !== currentLanguage) {
changeLanguage(preferences.language);
}

cacheService.save('preferences', JSON.stringify(preferences));
}, [preferences]);
Expand Down
9 changes: 8 additions & 1 deletion client/src/helpers/utility.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Api from '@/api/api';
import { LOCALES } from '@/config/locales';
import { ROUTES } from '@config/routes';
import { secrets } from '@config/secrets';
import { ApiResponse } from '@quickmeet/shared';
Expand Down Expand Up @@ -141,14 +142,20 @@ export function convertToLocaleDate(dateStr?: string) {
export const createDropdownOptions = (options: string[], type: 'time' | 'default' = 'default') => {
return (options || []).map((option) => ({ value: option, text: type === 'time' ? formatMinsToHM(Number(option), 'm') : option }));
};
export const createLanguageOptions = () => {
return LOCALES.map(({ code, name }) => ({
value: code,
text: name,
}));
};

export const renderError = async (err: ApiResponse<any>, navigate: NavigateFunction) => {
const { status, statusCode, message, redirect } = err;
if (status === 'error') {
if (statusCode === 401) {
try {
await new Api().logout();
} catch (error) { }
} catch (error) {}
navigate(ROUTES.signIn);
} else if (statusCode === 400) {
toast.error('Input missing fields');
Expand Down
3 changes: 2 additions & 1 deletion client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { PreferencesProvider } from './context/PreferencesContext';
import { ApiProvider } from '@/context/ApiContext';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';

import { initI18n } from '@/config/i18n';
initI18n();
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<StyledEngineProvider injectFirst>
Expand Down
45 changes: 45 additions & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"buttonText": {
"preferences": "Preferences",
"support": "Support",
"save": "Save",
"saveChanges": "Save Changes",
"newEvent": "New Event",
"myEvents": "My Events",
"bookNow": "Book Now",
"reportBug": "REPORT A BUG",
"requestFeature": "REQUEST A FEATURE",
"confirm": "Confirm",
"cancel": "Cancel",
"delete": "Delete"
},
"error": {
"invalidEmail": "Invalid email entered",
"duplicateEmail": "Duplicate email entered",
"failedToRetieveCallbackUrl": "Failed to retrieve oauth callback url",
"missingFields": "Input missing fields",
"selectEventToDelete": "Please select the event to delete",
"roomNotUpdated": "Room was not updated",
"loginNotComplete": "Couldn't complete request. Redirecting to login page"
},
"success": {
"savedSuccessfully": "Saved successfully"
},
"info": {
"createMeeting": "Create meet link",
"defaultTitle": "Quick Meeting",
"deleteEventConfirmation": "Are you sure you want to permanently delete this event?",
"noEvents": "No events to show",
"noRooms": "No rooms available",
"logoutConfirmation": "Are you sure you want to logout?",
"logoutInfo": "This will revoke the application permissions and would require to approve them again when trying to log back in"
},
"placeholder": {
"attendees": "Attendees",
"selectFloor": "Select preferred floor",
"selectDuration": "Select preferred meeting duration",
"selectRoomCapacity": "Select preferred room capacity",
"selectLanguage": "Select preferred language",
"invalidEmail": "Invalid email entered"
}
}
45 changes: 45 additions & 0 deletions client/src/locales/no.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"buttonText": {
"preferences": "Preferanser",
"support": "Støtte",
"save": "Spare",
"saveChanges": "Lagre endringer",
"newEvent": "Ny begivenhet",
"myEvents": "Mine hendelser",
"bookNow": "Bestill nå",
"reportBug": "RAPPORTER EN FEIL",
"requestFeature": "BE EN FUNKSJON",
"confirm": "Bekrefte",
"cancel": "Kansellere",
"delete": "Slett"
},
"error": {
"invalidEmail": "Ugyldig e-post angitt",
"duplicateEmail": "Duplikat e-post oppgitt",
"failedToRetieveCallbackUrl": "Kunne ikke hente oauth tilbakeringingsadress",
"missingFields": "Skriv inn manglende felt",
"selectEventToDelete": "Velg arrangementet du vil slette",
"roomNotUpdated": "Rommet ble ikke oppdatert",
"loginNotComplete": "Kunne ikke fullføre forespørselen. Omdirigerer til påloggingssiden"
},
"success": {
"savedSuccessfully": "Lagret vellykket"
},
"info": {
"createMeeting": "Opprett møtelink",
"defaultTitle": "Rask møte",
"deleteEventConfirmation": "Er du sikker på at du vil slette denne hendelsen permanent?",
"noEvents": "Ingen hendelser å vise",
"noRooms": "Ingen rom tilgjengelig",
"logoutConfirmation": "Er du sikker på at du vil logge ut?",
"logoutInfo": "Dette vil tilbakekalle applikasjonstillatelsene og vil kreve å godkjenne dem på nytt når du prøver å logge på igjen"
},
"placeholder": {
"attendees": "Deltakere",
"selectFloor": "Velg ønsket etasje",
"selectDuration": "Velg ønsket møtevarighet",
"selectRoomCapacity": "Velg ønsket romkapasitet",
"selectLanguage": "Velg ønsket språk",
"invalidEmail": "Ugyldig e-postadresse"
}
}
8 changes: 6 additions & 2 deletions client/src/pages/Home/BookRoomView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'dayjs/locale/en-gb';
import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
import { useLocales } from '@/config/i18n';

const createRoomDropdownOptions = (rooms: IConferenceRoom[]) => {
return (rooms || []).map((room) => ({ value: room.email, text: room.name, seats: room.seats, floor: room.floor }) as RoomsDropdownOption);
Expand All @@ -42,6 +43,9 @@ export default function BookRoomView({ onRoomBooked }: BookRoomViewProps) {
// Context or global state
const { preferences } = usePreferences();

// Locales
const { locale } = useLocales();

// loading states
const [bookClickLoading, setBookClickLoading] = useState(false);
const [roomLoading, setRoomLoading] = useState(false);
Expand Down Expand Up @@ -411,7 +415,7 @@ export default function BookRoomView({ onRoomBooked }: BookRoomViewProps) {
>
<Checkbox checked={formData.conference} value={formData.conference} onChange={(e) => handleInputChange('conference', e.target.checked)} />
<Typography variant="subtitle1" ml={0.5}>
Create meet link
{locale.info.createMeeting}
</Typography>
</Box>
</Box>
Expand Down Expand Up @@ -452,7 +456,7 @@ export default function BookRoomView({ onRoomBooked }: BookRoomViewProps) {
]}
>
<Typography variant="h6" fontWeight={700}>
Book now
{locale.buttonText.bookNow}
</Typography>
</LoadingButton>
</Box>
Expand Down
Loading