1
0
mirror of https://github.com/SomboChea/ui synced 2026-01-18 09:06:14 +07:00

feat(lng): Added change language on the fly (#456)

* feat(lng): added change language on the fly

* fixed dropdown

* applied feedbacks

* added translation

* updated bundlesize

* fixed error

* updated snaps

* added french language

* added language in storage

* updated styles

* fixed tests
This commit is contained in:
Priscila Oliveira
2020-04-23 08:20:41 +02:00
committed by GitHub
parent b17368470d
commit 675ee980ee
21 changed files with 731 additions and 176 deletions

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import Button from '../../muiComponents/Button';
import ThemeContext from '../../design-tokens/ThemeContext';
import LanguageSwitch from '../LanguageSwitch';
import { RightSide } from './styles';
import HeaderToolTip from './HeaderToolTip';
@@ -72,6 +73,7 @@ const HeaderRight: React.FC<Props> = ({
{!withoutSearch && (
<HeaderToolTip onClick={onToggleMobileNav} title={t('search.packages')} tooltipIconType={'search'} />
)}
<LanguageSwitch />
<HeaderToolTip title={t('header.documentation')} tooltipIconType={'help'} />
<HeaderToolTip onClick={onOpenRegistryInfoDialog} title={t('header.registry-info')} tooltipIconType={'info'} />
<HeaderToolTip

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Header /> component with logged in state should load the component in logged in state 1`] = `
.emotion-24 {
.emotion-28 {
background-color: #4b5e40;
color: #fff;
min-height: 60px;
@@ -16,29 +16,29 @@ exports[`<Header /> component with logged in state should load the component in
}
@media (min-width:768px) {
.emotion-24 .emotion-13 {
.emotion-28 .emotion-13 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.emotion-24 .emotion-17 {
.emotion-28 .emotion-17 {
display: none;
}
.emotion-24 .e1jf5lit4 {
.emotion-28 .e1jf5lit4 {
display: none;
}
}
@media (min-width:1024px) {
.emotion-24 .emotion-23 {
.emotion-28 .emotion-27 {
padding: 0 20px;
}
@media (min-width:1275px) {
.emotion-24 .emotion-23 {
.emotion-28 .emotion-27 {
max-width: 1240px;
width: 100%;
margin: 0 auto;
@@ -46,7 +46,7 @@ exports[`<Header /> component with logged in state should load the component in
}
}
.emotion-22 {
.emotion-26 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
@@ -124,7 +124,7 @@ exports[`<Header /> component with logged in state should load the component in
overflow-y: auto;
}
.emotion-20 {
.emotion-24 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@@ -136,16 +136,36 @@ exports[`<Header /> component with logged in state should load the component in
display: block;
}
.emotion-20 {
display: none;
}
@media screen and (min-width:768px) {
.emotion-20 {
display: inline-block;
}
}
.emotion-18 {
display: none;
}
@media screen and (min-width:768px) {
.emotion-18 {
display: inline-block;
}
}
.emotion-22 {
color: #fff;
}
<header
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary"
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-28 emotion-29 MuiAppBar-colorPrimary"
data-testid="header"
>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters"
class="MuiToolbar-root MuiToolbar-regular emotion-26 emotion-27 MuiToolbar-gutters"
>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-14 emotion-15 MuiToolbar-gutters"
@@ -214,7 +234,7 @@ exports[`<Header /> component with logged in state should load the component in
</div>
</div>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters"
class="MuiToolbar-root MuiToolbar-regular emotion-24 emotion-25 MuiToolbar-gutters"
data-testid="header-right"
>
<button
@@ -241,8 +261,53 @@ exports[`<Header /> component with logged in state should load the component in
class="MuiTouchRipple-root"
/>
</button>
<div
class="emotion-20 emotion-21"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit"
tabindex="0"
title="Change language"
type="button"
>
<span
class="MuiButton-label ForwardRef(Button)-label-322"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"
/>
</svg>
<span
class="emotion-18 emotion-19"
>
English
</span>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeSmall"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
<a
class="emotion-18 emotion-19"
class="emotion-22 emotion-23"
data-testid="header--tooltip-documentation"
href="https://verdaccio.org/docs/en/installation"
rel="noopener noreferrer"
@@ -359,7 +424,7 @@ exports[`<Header /> component with logged in state should load the component in
`;
exports[`<Header /> component with logged in state should load the component in logged out state 1`] = `
.emotion-24 {
.emotion-28 {
background-color: #4b5e40;
color: #fff;
min-height: 60px;
@@ -374,29 +439,29 @@ exports[`<Header /> component with logged in state should load the component in
}
@media (min-width:768px) {
.emotion-24 .emotion-13 {
.emotion-28 .emotion-13 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.emotion-24 .emotion-17 {
.emotion-28 .emotion-17 {
display: none;
}
.emotion-24 .e1jf5lit4 {
.emotion-28 .e1jf5lit4 {
display: none;
}
}
@media (min-width:1024px) {
.emotion-24 .emotion-23 {
.emotion-28 .emotion-27 {
padding: 0 20px;
}
@media (min-width:1275px) {
.emotion-24 .emotion-23 {
.emotion-28 .emotion-27 {
max-width: 1240px;
width: 100%;
margin: 0 auto;
@@ -404,7 +469,7 @@ exports[`<Header /> component with logged in state should load the component in
}
}
.emotion-22 {
.emotion-26 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
@@ -482,7 +547,7 @@ exports[`<Header /> component with logged in state should load the component in
overflow-y: auto;
}
.emotion-20 {
.emotion-24 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@@ -494,16 +559,36 @@ exports[`<Header /> component with logged in state should load the component in
display: block;
}
.emotion-20 {
display: none;
}
@media screen and (min-width:768px) {
.emotion-20 {
display: inline-block;
}
}
.emotion-18 {
display: none;
}
@media screen and (min-width:768px) {
.emotion-18 {
display: inline-block;
}
}
.emotion-22 {
color: #fff;
}
<header
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary"
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-28 emotion-29 MuiAppBar-colorPrimary"
data-testid="header"
>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters"
class="MuiToolbar-root MuiToolbar-regular emotion-26 emotion-27 MuiToolbar-gutters"
>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-14 emotion-15 MuiToolbar-gutters"
@@ -572,7 +657,7 @@ exports[`<Header /> component with logged in state should load the component in
</div>
</div>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters"
class="MuiToolbar-root MuiToolbar-regular emotion-24 emotion-25 MuiToolbar-gutters"
data-testid="header-right"
>
<button
@@ -599,8 +684,53 @@ exports[`<Header /> component with logged in state should load the component in
class="MuiTouchRipple-root"
/>
</button>
<div
class="emotion-20 emotion-21"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit"
tabindex="0"
title="Change language"
type="button"
>
<span
class="MuiButton-label ForwardRef(Button)-label-119"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"
/>
</svg>
<span
class="emotion-18 emotion-19"
>
English
</span>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeSmall"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
<a
class="emotion-18 emotion-19"
class="emotion-22 emotion-23"
data-testid="header--tooltip-documentation"
href="https://verdaccio.org/docs/en/installation"
rel="noopener noreferrer"

View File

@@ -11,6 +11,8 @@ import nicaragua from './img/nicaragua.svg';
import pakistan from './img/pakistan.svg';
import austria from './img/austria.svg';
import spain from './img/spain.svg';
import usa from './img/usa.svg';
import france from './img/france.svg';
import earth from './img/earth.svg';
import verdaccio from './img/verdaccio.svg';
import filebinary from './img/filebinary.svg';
@@ -23,9 +25,11 @@ export interface IconsMap {
brazil: string;
spain: string;
china: string;
usa: string;
nicaragua: string;
pakistan: string;
austria: string;
france: string;
germany: string;
india: string;
earth: string;
@@ -54,6 +58,8 @@ export const Icons: IconsMap = {
time,
version,
germany,
usa,
france,
};
export interface Props {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="france"><path d="M38.345 88.273C17.167 88.273 0 105.44 0 126.618v258.759c0 21.177 17.167 38.345 38.345 38.345h132.322V88.273H38.345z" fill="#41479b"/><path fill="#f5f5f5" d="M170.67 88.277h170.67v335.45H170.67z"/><path d="M473.655 88.273H341.333v335.448h132.322c21.177 0 38.345-17.167 38.345-38.345V126.618c0-21.178-17.167-38.345-38.345-38.345z" fill="#ff4b55"/></svg>

After

Width:  |  Height:  |  Size: 434 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,204 @@
import React, { MouseEvent, useCallback, useState, useRef, useContext } from 'react';
import LanguageIcon from '@material-ui/icons/Language';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import i18next, { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import Grow from '@material-ui/core/Grow';
import Popper from '@material-ui/core/Popper';
import MenuList from '@material-ui/core/MenuList';
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import { Language } from '../../../i18n/config';
import ThemeContext from '../../design-tokens/ThemeContext';
import Paper from '../../muiComponents/Paper';
import MenuItem from '../../muiComponents/MenuItem';
import Button from '../../muiComponents/Button';
import Tooltip from '../../muiComponents/Tooltip';
import Divider from '../../muiComponents/Divider';
import Box from '../../muiComponents/Box';
import { Theme } from '../../design-tokens/theme';
import Link from '../Link';
import Icon from '../Icon';
const VERDACCIO_UI_GITHUB_REPOSITORY = 'https://github.com/verdaccio/ui';
const getTranslatedCurrentLanguage = (
t: TFunction
): { [key: string]: { translation: string; icon: React.ComponentProps<typeof Icon>['name'] } } => ({
'en-us': {
translation: t('lng.english'),
icon: 'usa',
},
'fr-fr': {
translation: t('lng.french'),
icon: 'france',
},
'pt-br': {
translation: t('lng.portuguese'),
icon: 'brazil',
},
'de-de': {
translation: t('lng.german'),
icon: 'germany',
},
'es-es': {
translation: t('lng.spanish'),
icon: 'spain',
},
'zh-cn': {
translation: t('lng.chinese'),
icon: 'china',
},
});
const LanguageSwitch = () => {
const themeContext = useContext(ThemeContext);
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const anchorRef = useRef<HTMLButtonElement>(null);
if (!themeContext) {
throw Error(t('theme-context-not-correct-used'));
}
const languages = (i18next.options.resources ? Object.keys(i18next.options.resources) : []) as Array<Language>;
const currentLanguage: Language = i18next.language || i18next.options?.fallbackLng?.[0];
const { translation: userLanguage } = getTranslatedCurrentLanguage(t)[currentLanguage.toLowerCase()];
const handleToggle = useCallback(() => {
setOpen(prevOpen => !prevOpen);
}, []);
const handleClose = useCallback(
(event: MouseEvent<HTMLLIElement | Document | HTMLAnchorElement>) => {
if (anchorRef.current) {
if (anchorRef.current.contains(event.currentTarget)) {
return;
}
}
setOpen(false);
},
[setOpen]
);
const handleSwitchLanguage = useCallback(
(language: Language) => (event: MouseEvent<HTMLLIElement>) => {
themeContext.setLanguage(language);
handleClose(event);
},
[handleClose, themeContext]
);
// return focus to the button when we transitioned from !open -> open
const prevOpen = React.useRef(open);
React.useEffect(() => {
if (prevOpen.current === true && open === false) {
if (anchorRef.current) {
anchorRef.current.focus();
}
}
prevOpen.current = open;
}, [open]);
return (
<Wrapper>
<Tooltip enterDelay={300} title={t('change-language')}>
<SwitchButton color="inherit" onClick={handleToggle} ref={anchorRef}>
<LanguageIcon />
<UserLanguage>{userLanguage}</UserLanguage>
<ExpandMoreIcon fontSize="small" />
</SwitchButton>
</Tooltip>
<Popper anchorEl={anchorRef.current} disablePortal={true} open={open} role={undefined} transition={true}>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<StyledPaper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList>
{languages
.filter(language => language !== currentLanguage)
.map(language => {
const { icon, translation } = getTranslatedCurrentLanguage(t)[language.toLowerCase()];
return (
<StyledMenuItem
key={language}
onClick={handleSwitchLanguage(language)}
selected={userLanguage === translation}>
<Icon name={icon} size="md" />
{translation}
</StyledMenuItem>
);
})}
<Box my={1}>
<Divider />
</Box>
<MenuItem button={true}>
<StyledLink external={true} onClick={handleClose} to={VERDACCIO_UI_GITHUB_REPOSITORY}>
{`${t('help-to-translate')}`}
</StyledLink>
</MenuItem>
</MenuList>
</ClickAwayListener>
</StyledPaper>
</Grow>
)}
</Popper>
</Wrapper>
);
};
export default LanguageSwitch;
const Wrapper = styled('div')<{ theme?: Theme }>(({ theme }) => ({
display: 'none',
[`@media screen and (min-width: ${theme && theme.breakPoints.medium}px)`]: {
display: 'inline-block',
},
}));
const UserLanguage = styled('span')<{ theme?: Theme }>(({ theme }) => ({
display: 'none',
[`@media screen and (min-width: ${theme && theme.breakPoints.medium}px)`]: {
display: 'inline-block',
},
}));
const SwitchButton = withStyles((theme: Theme) => ({
label: {
display: 'grid',
gridGap: theme?.spacing(1),
gridTemplateColumns: '24px 20px',
[`@media screen and (min-width: ${theme && theme.breakPoints.medium}px)`]: {
gridTemplateColumns: '24px 1fr 20px',
},
},
}))(Button);
const StyledMenuItem = withStyles((theme: Theme) => ({
root: {
display: 'grid',
cursor: 'pointer',
gridGap: theme?.spacing(0.5),
gridTemplateColumns: '20px 1fr',
alignItems: 'center',
'&:first-child': {
borderTopLeftRadius: 4,
borderTopRightRadius: 4,
},
},
}))(MenuItem);
const StyledLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
color: theme?.palette.type === 'dark' ? theme?.palette.white : theme?.palette.text.primary,
textDecoration: 'none',
}));
const StyledPaper = styled(Paper)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: theme?.palette.type === 'dark' ? theme?.palette.cyanBlue : theme?.palette.white,
}));

View File

@@ -0,0 +1 @@
export { default } from './LanguageSwitch';