Skip to content
52 changes: 43 additions & 9 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { StoreProvider } from '@/hooks/useStore';
import CallbackPage from '@/pages/callback';
import Endpoint from '@/pages/endpoint';
import { TAuthData } from '@/types/api-types';
import { initializeI18n, localize, TranslationProvider } from '@deriv-com/translations';
import { FILTERED_LANGUAGES } from '@/utils/languages';
import { initializeI18n, localize, TranslationProvider, useTranslations } from '@deriv-com/translations';
import CoreStoreProvider from './CoreStoreProvider';
import './app-root.scss';

Expand All @@ -22,6 +23,37 @@ const i18nInstance = initializeI18n({
cdnUrl: `${TRANSLATIONS_CDN_URL || 'https://translations.deriv.com'}/${R2_PROJECT_NAME}/${CROWDIN_BRANCH_NAME}`,
});

// Component to handle language URL parameter
const LanguageHandler = ({ children }: { children: React.ReactNode }) => {
const { switchLanguage } = useTranslations();

React.useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const langParam = urlParams.get('lang');

if (langParam) {
// Convert to uppercase to match our language codes
const langCode = langParam.toUpperCase() as (typeof FILTERED_LANGUAGES)[number]['code'];
// Use FILTERED_LANGUAGES instead of hard-coded array
const supportedLangCodes = FILTERED_LANGUAGES.map(lang => lang.code);

if (supportedLangCodes.includes(langCode)) {
try {
switchLanguage(langCode);
// Remove lang parameter after processing to avoid URL pollution
const url = new URL(window.location.href);
url.searchParams.delete('lang');
window.history.replaceState({}, '', url.toString());
} catch (error) {
console.error('Failed to switch language:', error);
}
}
}
}, [switchLanguage]);

return <>{children}</>;
};

const router = createBrowserRouter(
createRoutesFromElements(
<Route
Expand All @@ -31,14 +63,16 @@ const router = createBrowserRouter(
fallback={<ChunkLoader message={localize('Please wait while we connect to the server...')} />}
>
<TranslationProvider defaultLang='EN' i18nInstance={i18nInstance}>
<StoreProvider>
<LocalStorageSyncWrapper>
<RoutePromptDialog />
<CoreStoreProvider>
<Layout />
</CoreStoreProvider>
</LocalStorageSyncWrapper>
</StoreProvider>
<LanguageHandler>
<StoreProvider>
<LocalStorageSyncWrapper>
<RoutePromptDialog />
<CoreStoreProvider>
<Layout />
</CoreStoreProvider>
</LocalStorageSyncWrapper>
</StoreProvider>
</LanguageHandler>
</TranslationProvider>
</Suspense>
}
Expand Down
7 changes: 5 additions & 2 deletions src/components/layout/footer/LanguageSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import Text from '@/components/shared_ui/text';
import { LANGUAGES } from '@/utils/languages';
import { FILTERED_LANGUAGES } from '@/utils/languages';
import { useTranslations } from '@deriv-com/translations';
import { Tooltip } from '@deriv-com/ui';

Expand All @@ -12,7 +12,7 @@ const LanguageSettings = ({ openLanguageSettingModal }: TLanguageSettings) => {
const { currentLang, localize } = useTranslations();

const countryIcon = useMemo(
() => LANGUAGES.find(({ code }: { code: string }) => code == currentLang)?.placeholderIcon,
() => FILTERED_LANGUAGES.find(({ code }: { code: string }) => code == currentLang)?.placeholderIcon,
[currentLang]
);

Expand All @@ -22,6 +22,9 @@ const LanguageSettings = ({ openLanguageSettingModal }: TLanguageSettings) => {
className='app-footer__language'
onClick={openLanguageSettingModal}
tooltipContent={localize('Language')}
aria-label={`${localize('Change language')} - ${localize('Current language')}: ${currentLang}`}
aria-expanded='false'
aria-haspopup='dialog'
>
{countryIcon}
<Text size='xs' weight='bold'>
Expand Down
43 changes: 25 additions & 18 deletions src/components/layout/footer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,55 @@
// import useModalManager from '@/hooks/useModalManager';
// import { getActiveTabUrl } from '@/utils/getActiveTabUrl';
// import { LANGUAGES } from '@/utils/languages';
// import { useTranslations } from '@deriv-com/translations';
// import { DesktopLanguagesModal } from '@deriv-com/ui';
import useModalManager from '@/hooks/useModalManager';
import { getActiveTabUrl } from '@/utils/getActiveTabUrl';
import { FILTERED_LANGUAGES } from '@/utils/languages';
import { useTranslations } from '@deriv-com/translations';
import { DesktopLanguagesModal } from '@deriv-com/ui';
import ChangeTheme from './ChangeTheme';
import Endpoint from './Endpoint';
import FullScreen from './FullScreen';
// import LanguageSettings from './LanguageSettings';
import LanguageSettings from './LanguageSettings';
import NetworkStatus from './NetworkStatus';
import ServerTime from './ServerTime';
import './footer.scss';

const Footer = () => {
// const { currentLang = 'EN', localize, switchLanguage } = useTranslations();
// const { hideModal, isModalOpenFor, showModal } = useModalManager();
const { currentLang = 'EN', localize, switchLanguage } = useTranslations();
const { hideModal, isModalOpenFor, showModal } = useModalManager();

// const openLanguageSettingModal = () => showModal('DesktopLanguagesModal');
const openLanguageSettingModal = () => showModal('DesktopLanguagesModal');
return (
<footer className='app-footer'>
<FullScreen />
{/* <LanguageSettings openLanguageSettingModal={openLanguageSettingModal} />
<div className='app-footer__vertical-line' /> */}
<LanguageSettings openLanguageSettingModal={openLanguageSettingModal} />
<div className='app-footer__vertical-line' />
<ChangeTheme />
<div className='app-footer__vertical-line' />
<ServerTime />
<div className='app-footer__vertical-line' />
<NetworkStatus />
<Endpoint />

{/* {isModalOpenFor('DesktopLanguagesModal') && (
{isModalOpenFor('DesktopLanguagesModal') && (
<DesktopLanguagesModal
headerTitle={localize('Select Language')}
isModalOpen
languages={LANGUAGES}
languages={FILTERED_LANGUAGES}
onClose={hideModal}
onLanguageSwitch={code => {
switchLanguage(code);
hideModal();
window.location.replace(getActiveTabUrl());
window.location.reload();
try {
switchLanguage(code);
hideModal();
// Page reload is necessary because Blockly is outside React lifecycle
// and won't re-render with new language without full page refresh
window.location.replace(getActiveTabUrl());
window.location.reload();
} catch (error) {
console.error('Failed to switch language:', error);
hideModal();
}
}}
selectedLanguage={currentLang}
/>
)} */}
)}
</footer>
);
};
Expand Down
12 changes: 9 additions & 3 deletions src/components/layout/header/mobile-menu/menu-header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentProps, useMemo } from 'react';
import { LANGUAGES } from '@/utils/languages';
import { FILTERED_LANGUAGES } from '@/utils/languages';
import { useTranslations } from '@deriv-com/translations';
import { Text, useDevice } from '@deriv-com/ui';

Expand All @@ -13,7 +13,7 @@ const MenuHeader = ({ hideLanguageSetting, openLanguageSetting }: TMenuHeader) =
const { isDesktop } = useDevice();

const countryIcon = useMemo(
() => LANGUAGES.find(({ code }) => code === currentLang)?.placeholderIconInMobile,
() => FILTERED_LANGUAGES.find(({ code }) => code === currentLang)?.placeholderIconInMobile,
[currentLang]
);

Expand All @@ -24,7 +24,13 @@ const MenuHeader = ({ hideLanguageSetting, openLanguageSetting }: TMenuHeader) =
</Text>

{!hideLanguageSetting && (
<button className='mobile-menu__header__language items-center' onClick={openLanguageSetting}>
<button
className='mobile-menu__header__language items-center'
onClick={openLanguageSetting}
aria-label={`${localize('Change language')} - ${localize('Current language')}: ${currentLang}`}
aria-expanded='false'
aria-haspopup='menu'
>
{countryIcon}
<Text className='ml-[0.4rem]' size={isDesktop ? 'xs' : 'sm'} weight='bold'>
{currentLang}
Expand Down
44 changes: 40 additions & 4 deletions src/components/layout/header/mobile-menu/mobile-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useState } from 'react';
import useModalManager from '@/hooks/useModalManager';
import { getActiveTabUrl } from '@/utils/getActiveTabUrl';
import { FILTERED_LANGUAGES } from '@/utils/languages';
import { useTranslations } from '@deriv-com/translations';
import { Drawer, useDevice } from '@deriv-com/ui';
import { Drawer, MobileLanguagesDrawer, useDevice } from '@deriv-com/ui';
import NetworkStatus from './../../footer/NetworkStatus';
import ServerTime from './../../footer/ServerTime';
import BackButton from './back-button';
Expand All @@ -17,7 +20,8 @@ type TMobileMenuProps = {
const MobileMenu = ({ onLogout }: TMobileMenuProps) => {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
const { localize } = useTranslations();
const { currentLang = 'EN', localize, switchLanguage } = useTranslations();
const { hideModal, isModalOpenFor, showModal } = useModalManager();
const { isDesktop } = useDevice();

const openDrawer = () => setIsDrawerOpen(true);
Expand All @@ -28,6 +32,8 @@ const MobileMenu = ({ onLogout }: TMobileMenuProps) => {

const openSubmenu = (submenu: string) => setActiveSubmenu(submenu);
const closeSubmenu = () => setActiveSubmenu(null);
const openLanguageSetting = () => showModal('MobileLanguagesDrawer');
const isLanguageSettingVisible = Boolean(isModalOpenFor('MobileLanguagesDrawer'));

if (isDesktop) return null;
return (
Expand All @@ -38,11 +44,41 @@ const MobileMenu = ({ onLogout }: TMobileMenuProps) => {

<Drawer isOpen={isDrawerOpen} onCloseDrawer={closeDrawer} width='29.5rem'>
<Drawer.Header onCloseDrawer={closeDrawer}>
<MenuHeader hideLanguageSetting={true} openLanguageSetting={() => {}} />
<MenuHeader
hideLanguageSetting={isLanguageSettingVisible}
openLanguageSetting={openLanguageSetting}
/>
</Drawer.Header>

<Drawer.Content>
{activeSubmenu === 'reports' ? (
{isLanguageSettingVisible ? (
<>
<div className='mobile-menu__back-btn'>
<BackButton buttonText={localize('Language')} onClick={hideModal} />
</div>

<MobileLanguagesDrawer
isOpen
languages={FILTERED_LANGUAGES}
onClose={hideModal}
onLanguageSwitch={code => {
try {
switchLanguage(code);
hideModal();
// Page reload is necessary because Blockly is outside React lifecycle
// and won't re-render with new language without full page refresh
window.location.replace(getActiveTabUrl());
window.location.reload();
} catch (error) {
console.error('Failed to switch language:', error);
hideModal();
}
}}
selectedLanguage={currentLang}
wrapperClassName='mobile-menu__language-drawer'
/>
</>
) : activeSubmenu === 'reports' ? (
<>
<div className='mobile-menu__back-btn'>
<BackButton buttonText={localize('Reports')} onClick={closeSubmenu} />
Expand Down
5 changes: 5 additions & 0 deletions src/utils/languages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,8 @@ export const LANGUAGES = [
placeholderIconInMobile: <FlagChinaTraditionalIcon height={14.67} width={22} />,
},
];

// Filtered languages for the language switcher (only show specific languages)
export const FILTERED_LANGUAGES = LANGUAGES.filter(lang =>
['EN', 'ES', 'FR', 'PT', 'AR', 'IT', 'RU'].includes(lang.code)
);
Loading