Skip to content

Commit

Permalink
Add language switch & enable 5 languages
Browse files Browse the repository at this point in the history
Closes #134
  • Loading branch information
nop33 committed Jan 9, 2025
1 parent 3d1ebba commit 8e38451
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 8 deletions.
48 changes: 40 additions & 8 deletions bridge_ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions bridge_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"dexie-react-hooks": "^1.1.1",
"ethereum-multicall": "^2.17.0",
"ethers": "^5.7.1",
"framer-motion": "^6.5.1",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.0.2",
"js-base64": "^3.6.1",
"luxon": "^2.3.1",
"notistack": "^1.0.10",
Expand All @@ -43,6 +46,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^14.1.2",
"react-icons": "^5.4.0",
"react-modal": "^3.15.1",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
Expand Down
12 changes: 12 additions & 0 deletions bridge_ui/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { makeStyles, Typography } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import LanguageSwitch from "../localization/LanguageSwitch";
import styled from "styled-components";

const useStyles = makeStyles((theme) => ({
footer: {
Expand Down Expand Up @@ -67,6 +69,16 @@ export default function Footer() {
</Typography>
</div>
</div>
<LanguageSwitchContainer>
<LanguageSwitch />
</LanguageSwitchContainer>
</footer>
);
}

const LanguageSwitchContainer = styled.div`
padding: 15px 30px;
display: flex;
justify-content: space-between;
backdrop-filter: blur(20px);
`
13 changes: 13 additions & 0 deletions bridge_ui/src/hooks/useStateWithLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect, useState } from 'react'

const useStateWithLocalStorage = <T extends string>(localStorageKey: string, defaultValue: T) => {
const [value, setValue] = useState(localStorage.getItem(localStorageKey) || defaultValue)

useEffect(() => {
localStorage.setItem(localStorageKey, value)
}, [localStorageKey, value])

return [value as T, setValue] as const
}

export default useStateWithLocalStorage
40 changes: 40 additions & 0 deletions bridge_ui/src/localization/LanguageSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import i18next from "i18next";
import { useEffect } from "react";
import styled from "styled-components";

import { Language, languageOptions } from "./languages";
import useStateWithLocalStorage from "../hooks/useStateWithLocalStorage";
import Menu from "./Menu";

interface LanguageSwitchProps {
className?: string;
}

const LanguageSwitch: React.FC<LanguageSwitchProps> = ({ className }) => {
const [langValue, setLangValue] = useStateWithLocalStorage<Language>('language', 'en')

useEffect(() => {
i18next.changeLanguage(langValue)
}, [langValue])

const items = languageOptions.map((lang) => ({
text: lang.label,
onClick: () => setLangValue(lang.value)
}))

return (
<Menu
aria-label="Language"
label={languageOptions.find((o) => o.value === langValue)?.label || ''}
items={items}
direction="up"
className={className}
/>
);
};

export default styled(LanguageSwitch)`
border-radius: 8px;
background-color: #1B1B1F;
border: 1px solid rgba(255, 255, 255, 0.08);
`;
155 changes: 155 additions & 0 deletions bridge_ui/src/localization/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
import { RiMore2Line } from 'react-icons/ri'
import styled from 'styled-components'

interface MenuItem {
text: string
icon?: React.ReactNode
onClick: () => void
}

type Direction = 'up' | 'down'

const menuHeight = '45px'

const Menu = ({
label,
icon,
items,
direction,
className
}: {
label: string
icon?: React.ReactNode
items: MenuItem[]
direction: Direction
className?: string
}) => {
const [visible, setVisible] = useState(false)

const animationOrigin = direction === 'up' ? '-95%' : `calc(${menuHeight} - 10px)`
const animationDestination = direction === 'up' ? '-100%' : menuHeight

const handleBlur = () => {
setVisible(false)
}

return (
<MenuContainer
onClick={() => setVisible(!visible)}
className={className}
id="menu-container"
onBlur={handleBlur}
tabIndex={0}
>
<MenuCurrentContent>
{icon && <IconContainer>{icon}</IconContainer>}
<Label>{label}</Label>
<RiMore2Line size={15} />
</MenuCurrentContent>
<AnimatePresence>
{visible && (
<MenuItemsContainer
initial={{ y: animationOrigin, opacity: 0 }}
animate={{ y: animationDestination, opacity: 1 }}
exit={{ y: animationOrigin, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<MenuItemsList
style={{ marginBottom: direction === 'up' ? '8px' : 0, marginTop: direction === 'down' ? '8px' : 0 }}
>
{items.map((item, i) => (
<div key={i}>
<MenuItemComponent onClick={item.onClick}>
{item.icon && <ItemIcon>{item.icon}</ItemIcon>}
<ItemText>{item.text}</ItemText>
</MenuItemComponent>
{i !== items.length - 1 && <Divider />}
</div>
))}
</MenuItemsList>
</MenuItemsContainer>
)}
</AnimatePresence>
</MenuContainer>
)
}

export default Menu

const MenuContainer = styled.div`
position: relative;
height: ${menuHeight};
display: flex;
outline: none;
&:hover {
background-color: rgba(255, 255, 255, 0.02);
}
`

const MenuCurrentContent = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 15px;
cursor: pointer;
gap: 15px;
`

const Label = styled.span`
color: #e3e3e3;
line-height: initial;
flex: 1;
`

const IconContainer = styled.div``

const MenuItemsContainer = styled(motion.div)`
position: absolute;
width: 100%;
z-index: 10000;
`

const MenuItemsList = styled.div`
overflow: hidden;
border-radius: 8px;
background-color: #1B1B1F;
border: 1px solid rgba(255, 255, 255, 0.08);
`

const ItemIcon = styled.div`
width: 23px;
height: 23px;
margin-right: 20px;
opacity: 0.8;
`

const MenuItemComponent = styled.div`
height: 47px;
display: flex;
align-items: center;
padding: 0 20px;
cursor: pointer;
color: #e3e3e3;
&:hover {
background-color: rgba(255, 255, 255, 0.02);
color: #598BED;
${ItemIcon} {
opacity: 1;
}
}
`

const ItemText = styled.div`
text-align: left;
`

const Divider = styled.div`
height: 1px;
background-color: rgba(255, 255, 255, 0.04);
`
35 changes: 35 additions & 0 deletions bridge_ui/src/localization/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";

import de from "../../locales/de-DE/translation.json";
import el from "../../locales/el-GR/translation.json";
import en from "../../locales/en-US/translation.json";
import id from "../../locales/id-ID/translation.json";
import vi from "../../locales/vi-VN/translation.json";
import pt from "../../locales/pt-PT/translation.json";
import { supportedLanguages } from "./languages";

i18next
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources: {
en: { translation: en },
id: { translation: id },
el: { translation: el },
de: { translation: de },
vi: { translation: vi },
pt: { translation: pt },
},
supportedLngs: supportedLanguages,
fallbackLng: "en",
detection: {
lookupLocalStorage: "language",
},
interpolation: {
escapeValue: false,
},
});

export default i18next;
Loading

0 comments on commit 8e38451

Please sign in to comment.