fix(i18n): add missing translation keys, update lang/locale logic
This commit is contained in:
parent
8bc7d2599e
commit
7d8828a358
|
@ -1,15 +1,25 @@
|
||||||
{
|
{
|
||||||
"css.validate": false,
|
"css.validate": false,
|
||||||
"scss.validate": false,
|
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.wordWrap": "on",
|
"editor.wordWrap": "on",
|
||||||
"eslint.workingDirectories": ["schema", "client", "server"],
|
"eslint.workingDirectories": [
|
||||||
"i18n-ally.enabledFrameworks": ["i18next"],
|
"schema",
|
||||||
"i18n-ally.localesPaths": ["client/public/locales"],
|
"client",
|
||||||
"i18n-ally.sourceLanguage": "en",
|
"server"
|
||||||
"i18n-ally.keystyle": "nested"
|
],
|
||||||
}
|
"i18n-ally.enabledFrameworks": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"client/public/locales"
|
||||||
|
],
|
||||||
|
"i18n-ally.namespace": true,
|
||||||
|
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
|
||||||
|
"i18n-ally.sortKeys": true,
|
||||||
|
"scss.validate": false
|
||||||
|
}
|
|
@ -74,19 +74,19 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={styles.controller}>
|
<div className={styles.controller}>
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-in')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.zoom-in') as string}>
|
||||||
<ButtonBase onClick={() => zoomIn(0.25)}>
|
<ButtonBase onClick={() => zoomIn(0.25)}>
|
||||||
<ZoomIn fontSize="medium" />
|
<ZoomIn fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-out')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.zoom-out') as string}>
|
||||||
<ButtonBase onClick={() => zoomOut(0.25)}>
|
<ButtonBase onClick={() => zoomOut(0.25)}>
|
||||||
<ZoomOut fontSize="medium" />
|
<ZoomOut fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.center-artboard')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.center-artboard') as string}>
|
||||||
<ButtonBase onClick={() => centerView(0.95)}>
|
<ButtonBase onClick={() => centerView(0.95)}>
|
||||||
<FilterCenterFocus fontSize="medium" />
|
<FilterCenterFocus fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
@ -96,7 +96,7 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||||
|
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<>
|
<>
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-orientation') as string}>
|
||||||
<ButtonBase onClick={handleTogglePageOrientation}>
|
<ButtonBase onClick={handleTogglePageOrientation}>
|
||||||
{orientation === 'vertical' ? (
|
{orientation === 'vertical' ? (
|
||||||
<AlignHorizontalCenter fontSize="medium" />
|
<AlignHorizontalCenter fontSize="medium" />
|
||||||
|
@ -106,13 +106,13 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-page-break-line')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-page-break-line') as string}>
|
||||||
<ButtonBase onClick={handleTogglePageBreakLine}>
|
<ButtonBase onClick={handleTogglePageBreakLine}>
|
||||||
<InsertPageBreak fontSize="medium" />
|
<InsertPageBreak fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-sidebars')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-sidebars') as string}>
|
||||||
<ButtonBase onClick={handleToggleSidebar}>
|
<ButtonBase onClick={handleToggleSidebar}>
|
||||||
<ViewSidebar fontSize="medium" />
|
<ViewSidebar fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
@ -122,13 +122,13 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.copy-link')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.copy-link') as string}>
|
||||||
<ButtonBase onClick={handleCopyLink}>
|
<ButtonBase onClick={handleCopyLink}>
|
||||||
<Link fontSize="medium" />
|
<Link fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.export-pdf')}>
|
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.export-pdf') as string}>
|
||||||
<ButtonBase onClick={handleExportPDF} disabled={isLoading}>
|
<ButtonBase onClick={handleExportPDF} disabled={isLoading}>
|
||||||
<Download fontSize="medium" />
|
<Download fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|
|
@ -184,7 +184,7 @@ const Header = () => {
|
||||||
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip arrow placement="right" title={t<string>('builder.header.menu.tooltips.share-link')}>
|
<Tooltip arrow placement="right" title={t('builder.header.menu.tooltips.share-link') as string}>
|
||||||
<div>
|
<div>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
@ -196,7 +196,7 @@ const Header = () => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip arrow placement="right" title={t<string>('builder.header.menu.tooltips.delete')}>
|
<Tooltip arrow placement="right" title={t('builder.header.menu.tooltips.delete') as string}>
|
||||||
<MenuItem onClick={handleDelete}>
|
<MenuItem onClick={handleDelete}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Delete className="scale-90" />
|
<Delete className="scale-90" />
|
||||||
|
|
|
@ -58,14 +58,14 @@ const PhotoFilters = () => {
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.grayscale.label')}
|
label={t('builder.leftSidebar.sections.basics.photo-filters.effects.grayscale.label') as string}
|
||||||
control={
|
control={
|
||||||
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
|
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.border.label')}
|
label={t('builder.leftSidebar.sections.basics.photo-filters.effects.border.label') as string}
|
||||||
control={<Checkbox color="secondary" checked={border} onChange={(_, value) => handleSetBorder(value)} />}
|
control={<Checkbox color="secondary" checked={border} onChange={(_, value) => handleSetBorder(value)} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,8 +67,8 @@ const PhotoUpload: React.FC = () => {
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
isEmpty(photo.url)
|
isEmpty(photo.url)
|
||||||
? t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload')
|
? (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
|
||||||
: t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove')
|
: (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove') as string)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />
|
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />
|
||||||
|
|
|
@ -32,7 +32,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip title={t<string>('builder.common.columns.tooltip')}>
|
<Tooltip title={t('builder.common.columns.tooltip') as string}>
|
||||||
<ButtonBase onClick={handleClick} sx={{ padding: 1, borderRadius: 1 }} className="opacity-50 hover:opacity-75">
|
<ButtonBase onClick={handleClick} sx={{ padding: 1, borderRadius: 1 }} className="opacity-50 hover:opacity-75">
|
||||||
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
|
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|
|
@ -62,7 +62,7 @@ const Layout = () => {
|
||||||
path="metadata.layout"
|
path="metadata.layout"
|
||||||
name={t('builder.rightSidebar.sections.layout.heading')}
|
name={t('builder.rightSidebar.sections.layout.heading')}
|
||||||
action={
|
action={
|
||||||
<Tooltip title={t<string>('builder.rightSidebar.sections.layout.tooltip.reset-layout')}>
|
<Tooltip title={t('builder.rightSidebar.sections.layout.tooltip.reset-layout') as string}>
|
||||||
<IconButton onClick={handleResetLayout}>
|
<IconButton onClick={handleResetLayout}>
|
||||||
<Restore />
|
<Restore />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -81,7 +81,7 @@ const Layout = () => {
|
||||||
|
|
||||||
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
|
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={t<string>('builder.common.actions.delete', { token: t('builder.common.glossary.page') })}
|
title={t('builder.common.actions.delete', { token: t('builder.common.glossary.page') }) as string}
|
||||||
>
|
>
|
||||||
<IconButton size="small" onClick={() => handleDeletePage(pageIndex)}>
|
<IconButton size="small" onClick={() => handleDeletePage(pageIndex)}>
|
||||||
<Close fontSize="small" />
|
<Close fontSize="small" />
|
||||||
|
|
|
@ -20,25 +20,24 @@ import { useMutation } from 'react-query';
|
||||||
|
|
||||||
import Heading from '@/components/shared/Heading';
|
import Heading from '@/components/shared/Heading';
|
||||||
import ThemeSwitch from '@/components/shared/ThemeSwitch';
|
import ThemeSwitch from '@/components/shared/ThemeSwitch';
|
||||||
import { Language, languages } from '@/config/languages';
|
import { Language, languageMap, languages } from '@/config/languages';
|
||||||
import { ServerError } from '@/services/axios';
|
import { ServerError } from '@/services/axios';
|
||||||
import queryClient from '@/services/react-query';
|
import queryClient from '@/services/react-query';
|
||||||
import { loadSampleData, LoadSampleDataParams, resetResume, ResetResumeParams } from '@/services/resume';
|
import { loadSampleData, LoadSampleDataParams, resetResume, ResetResumeParams } from '@/services/resume';
|
||||||
import { setLanguage, setTheme, togglePageBreakLine, togglePageOrientation } from '@/store/build/buildSlice';
|
import { setTheme, togglePageBreakLine, togglePageOrientation } from '@/store/build/buildSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setResumeState } from '@/store/resume/resumeSlice';
|
import { setResumeState } from '@/store/resume/resumeSlice';
|
||||||
import { dateFormatOptions } from '@/utils/date';
|
import { dateFormatOptions } from '@/utils/date';
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { locale, ...router } = useRouter();
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
const resume = useAppSelector((state) => state.resume);
|
||||||
const theme = useAppSelector((state) => state.build.theme);
|
const theme = useAppSelector((state) => state.build.theme);
|
||||||
const language = useAppSelector((state) => state.build.language);
|
|
||||||
const breakLine = useAppSelector((state) => state.build.page.breakLine);
|
const breakLine = useAppSelector((state) => state.build.page.breakLine);
|
||||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||||
|
|
||||||
|
@ -62,14 +61,12 @@ const Settings = () => {
|
||||||
dispatch(setResumeState({ path: 'metadata.date.format', value }));
|
dispatch(setResumeState({ path: 'metadata.date.format', value }));
|
||||||
|
|
||||||
const handleChangeLanguage = (value: Language | null) => {
|
const handleChangeLanguage = (value: Language | null) => {
|
||||||
const { pathname, asPath, query } = router;
|
const { pathname, asPath, query, push } = router;
|
||||||
const code = value?.code || 'en';
|
const code = value?.code || 'en';
|
||||||
|
|
||||||
dayjs.locale(code);
|
|
||||||
dispatch(setLanguage({ language: code }));
|
|
||||||
document.cookie = `NEXT_LOCALE=${code}; path=/; expires=2147483647`;
|
document.cookie = `NEXT_LOCALE=${code}; path=/; expires=2147483647`;
|
||||||
|
|
||||||
router.push({ pathname, query }, asPath, { locale: code });
|
push({ pathname, query }, asPath, { locale: code });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadSampleData = async () => {
|
const handleLoadSampleData = async () => {
|
||||||
|
@ -132,7 +129,7 @@ const Settings = () => {
|
||||||
disableClearable
|
disableClearable
|
||||||
className="my-2 w-full"
|
className="my-2 w-full"
|
||||||
options={languages}
|
options={languages}
|
||||||
value={language}
|
value={languageMap[locale ?? 'en']}
|
||||||
isOptionEqualToValue={(a, b) => a.code === b.code}
|
isOptionEqualToValue={(a, b) => a.code === b.code}
|
||||||
onChange={(_, value) => handleChangeLanguage(value)}
|
onChange={(_, value) => handleChangeLanguage(value)}
|
||||||
renderInput={(params) => <TextField {...params} />}
|
renderInput={(params) => <TextField {...params} />}
|
||||||
|
|
|
@ -63,7 +63,7 @@ const Sharing = () => {
|
||||||
|
|
||||||
<div className="mt-1 flex w-full">
|
<div className="mt-1 flex w-full">
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t<string>('builder.rightSidebar.sections.sharing.short-url.label')}
|
label={t('builder.rightSidebar.sections.sharing.short-url.label') as string}
|
||||||
control={
|
control={
|
||||||
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
|
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.share-link')}>
|
<Tooltip arrow placement="right" title={t('dashboard.resume.menu.tooltips.share-link') as string}>
|
||||||
<div>
|
<div>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
@ -173,7 +173,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.delete')}>
|
<Tooltip arrow placement="right" title={t('dashboard.resume.menu.tooltips.delete') as string}>
|
||||||
<MenuItem onClick={handleDelete}>
|
<MenuItem onClick={handleDelete}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeleteOutline className="scale-90" />
|
<DeleteOutline className="scale-90" />
|
||||||
|
|
|
@ -12,7 +12,7 @@ const ColorAvatar: React.FC<Props> = ({ color, size = 20, onClick }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton onClick={handleClick}>
|
<IconButton onClick={handleClick}>
|
||||||
<Avatar sx={{ bgcolor: color, width: size, height: size }}> </Avatar>
|
<Avatar sx={{ bgcolor: color, width: size, height: size }}> </Avatar>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,19 +72,19 @@ const Heading: React.FC<Props> = ({
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isEditable && (
|
{isEditable && (
|
||||||
<Tooltip title={t<string>('builder.common.tooltip.rename-section')}>
|
<Tooltip title={t('builder.common.tooltip.rename-section') as string}>
|
||||||
<IconButton onClick={toggleEditMode}>{editMode ? <Check /> : <DriveFileRenameOutline />}</IconButton>
|
<IconButton onClick={toggleEditMode}>{editMode ? <Check /> : <DriveFileRenameOutline />}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isHideable && (
|
{isHideable && (
|
||||||
<Tooltip title={t<string>('builder.common.tooltip.toggle-visibility')}>
|
<Tooltip title={t('builder.common.tooltip.toggle-visibility') as string}>
|
||||||
<IconButton onClick={toggleVisibility}>{visibility ? <Visibility /> : <VisibilityOff />}</IconButton>
|
<IconButton onClick={toggleVisibility}>{visibility ? <Visibility /> : <VisibilityOff />}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDeletable && (
|
{isDeletable && (
|
||||||
<Tooltip title={t<string>('builder.common.tooltip.delete-section')}>
|
<Tooltip title={t('builder.common.tooltip.delete-section') as string}>
|
||||||
<IconButton onClick={handleDelete}>
|
<IconButton onClick={handleDelete}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
import { Language } from '@mui/icons-material';
|
import { Language } from '@mui/icons-material';
|
||||||
import { IconButton, Popover } from '@mui/material';
|
import { IconButton, Popover } from '@mui/material';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import { MouseEvent, useState } from 'react';
|
import { MouseEvent, useState } from 'react';
|
||||||
|
|
||||||
import { languages } from '@/config/languages';
|
import { languages } from '@/config/languages';
|
||||||
import { setLanguage } from '@/store/build/buildSlice';
|
|
||||||
import { useAppDispatch } from '@/store/hooks';
|
|
||||||
|
|
||||||
import styles from './LanguageSwitcher.module.scss';
|
import styles from './LanguageSwitcher.module.scss';
|
||||||
|
|
||||||
const LanguageSwitcher = () => {
|
const LanguageSwitcher = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
@ -24,10 +22,7 @@ const LanguageSwitcher = () => {
|
||||||
const handleChangeLanguage = (locale: string) => {
|
const handleChangeLanguage = (locale: string) => {
|
||||||
const { pathname, asPath, query } = router;
|
const { pathname, asPath, query } = router;
|
||||||
|
|
||||||
dayjs.locale(locale);
|
|
||||||
dispatch(setLanguage({ language: locale }));
|
|
||||||
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
||||||
|
|
||||||
router.push({ pathname, query }, asPath, { locale });
|
router.push({ pathname, query }, asPath, { locale });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,7 +54,7 @@ const LanguageSwitcher = () => {
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<a href="https://translate.rxresu.me" target="_blank" rel="noreferrer" className={styles.language}>
|
<a href="https://translate.rxresu.me" target="_blank" rel="noreferrer" className={styles.language}>
|
||||||
Missing your language?
|
{t('common.footer.language.missing')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Toolti
|
||||||
import { ListItem as ListItemType } from '@reactive-resume/schema';
|
import { ListItem as ListItemType } from '@reactive-resume/schema';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import isFunction from 'lodash/isFunction';
|
import isFunction from 'lodash/isFunction';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { DropTargetMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
|
import { DropTargetMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||||
|
|
||||||
|
@ -26,6 +27,8 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
|
const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
||||||
|
|
||||||
|
@ -122,29 +125,25 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Edit</ListItemText>
|
<ListItemText>{t('builder.common.list.actions.edit')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={() => handleDuplicate(item)}>
|
<MenuItem onClick={() => handleDuplicate(item)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<FileCopy className="scale-90" />
|
<FileCopy className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Duplicate</ListItemText>
|
<ListItemText>{t('builder.common.list.actions.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip arrow placement="right" title={t('builder.common.tooltip.delete-item') as string}>
|
||||||
arrow
|
|
||||||
placement="right"
|
|
||||||
title="Are you sure you want to delete this item? This is an irreversible action."
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<MenuItem onClick={() => handleDelete(item)}>
|
<MenuItem onClick={() => handleDelete(item)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeleteOutline className="scale-90" />
|
<DeleteOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Delete</ListItemText>
|
<ListItemText>{t('builder.common.list.actions.delete')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
export const FONTS_QUERY = 'fonts';
|
export const FONTS_QUERY = 'fonts';
|
||||||
export const RESUMES_QUERY = 'resumes';
|
export const RESUMES_QUERY = 'resumes';
|
||||||
|
|
||||||
|
// Regular Expressions
|
||||||
|
export const VALID_URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||||
|
|
||||||
// Date Formats
|
// Date Formats
|
||||||
export const FILENAME_TIMESTAMP = 'DDMMYYYYHHmmss';
|
export const FILENAME_TIMESTAMP = 'DDMMYYYYHHmmss';
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -34,9 +35,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
title: Joi.string().required(),
|
title: Joi.string().required(),
|
||||||
awarder: Joi.string().required(),
|
awarder: Joi.string().required(),
|
||||||
date: Joi.string().allow(''),
|
date: Joi.string().allow(''),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -154,6 +153,7 @@ const AwardModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -34,9 +35,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
issuer: Joi.string().required(),
|
issuer: Joi.string().required(),
|
||||||
date: Joi.string().allow(''),
|
date: Joi.string().allow(''),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -154,6 +153,7 @@ const CertificateModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
import ArrayInput from '@/components/shared/ArrayInput';
|
import ArrayInput from '@/components/shared/ArrayInput';
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -47,9 +48,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
start: Joi.string().allow(''),
|
start: Joi.string().allow(''),
|
||||||
end: Joi.string().allow(''),
|
end: Joi.string().allow(''),
|
||||||
}),
|
}),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
level: Joi.string().allow(''),
|
level: Joi.string().allow(''),
|
||||||
levelNum: Joi.number().min(0).max(10),
|
levelNum: Joi.number().min(0).max(10),
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
|
@ -194,6 +193,7 @@ const CustomModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
import ArrayInput from '@/components/shared/ArrayInput';
|
import ArrayInput from '@/components/shared/ArrayInput';
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -46,9 +47,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
start: Joi.string().allow(''),
|
start: Joi.string().allow(''),
|
||||||
end: Joi.string().allow(''),
|
end: Joi.string().allow(''),
|
||||||
}),
|
}),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
courses: Joi.array().items(Joi.string().optional()),
|
courses: Joi.array().items(Joi.string().optional()),
|
||||||
});
|
});
|
||||||
|
@ -217,6 +216,7 @@ const EducationModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { useEffect, useMemo } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -21,17 +22,14 @@ const path = 'sections.profile';
|
||||||
const defaultState: FormData = {
|
const defaultState: FormData = {
|
||||||
network: '',
|
network: '',
|
||||||
username: '',
|
username: '',
|
||||||
url: 'https://',
|
url: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = Joi.object<FormData>({
|
const schema = Joi.object<FormData>({
|
||||||
id: Joi.string(),
|
id: Joi.string(),
|
||||||
network: Joi.string().required(),
|
network: Joi.string().required(),
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.default('https://')
|
|
||||||
.allow(''),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ProfileModal: React.FC = () => {
|
const ProfileModal: React.FC = () => {
|
||||||
|
@ -131,6 +129,7 @@ const ProfileModal: React.FC = () => {
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
import ArrayInput from '@/components/shared/ArrayInput';
|
import ArrayInput from '@/components/shared/ArrayInput';
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -42,9 +43,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
start: Joi.string().allow(''),
|
start: Joi.string().allow(''),
|
||||||
end: Joi.string().allow(''),
|
end: Joi.string().allow(''),
|
||||||
}),
|
}),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
keywords: Joi.array().items(Joi.string().optional()),
|
keywords: Joi.array().items(Joi.string().optional()),
|
||||||
});
|
});
|
||||||
|
@ -187,6 +186,7 @@ const ProjectModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -34,9 +35,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
publisher: Joi.string().required(),
|
publisher: Joi.string().required(),
|
||||||
date: Joi.string().allow(''),
|
date: Joi.string().allow(''),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -154,6 +153,7 @@ const PublicationModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -40,9 +41,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
start: Joi.string().allow(''),
|
start: Joi.string().allow(''),
|
||||||
end: Joi.string().allow(''),
|
end: Joi.string().allow(''),
|
||||||
}),
|
}),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -184,6 +183,7 @@ const VolunteerModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
import MarkdownSupported from '@/components/shared/MarkdownSupported';
|
||||||
|
import { VALID_URL_REGEX } from '@/constants/index';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||||
|
@ -40,9 +41,7 @@ const schema = Joi.object<FormData>().keys({
|
||||||
start: Joi.string().allow(''),
|
start: Joi.string().allow(''),
|
||||||
end: Joi.string().allow(''),
|
end: Joi.string().allow(''),
|
||||||
}),
|
}),
|
||||||
url: Joi.string()
|
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
|
||||||
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
|
|
||||||
.allow(''),
|
|
||||||
summary: Joi.string().allow(''),
|
summary: Joi.string().allow(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -184,6 +183,7 @@ const WorkModal: React.FC = () => {
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t('builder.common.form.url.label')}
|
||||||
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
|
|
|
@ -125,7 +125,7 @@ const CreateResumeModal: React.FC = () => {
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t<string>('modals.dashboard.create-resume.form.public.label')}
|
label={t('modals.dashboard.create-resume.form.public.label') as string}
|
||||||
control={
|
control={
|
||||||
<Controller
|
<Controller
|
||||||
name="isPublic"
|
name="isPublic"
|
||||||
|
|
|
@ -84,7 +84,7 @@ const ImportExternalModal: React.FC = () => {
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
<Trans t={t} i18nKey="modals.dashboard.import-external.linkedin.body">
|
<Trans t={t} i18nKey="modals.dashboard.import-external.linkedin.body">
|
||||||
You can save time by exporting your data from LinkedIn and using it to auto-fill fields on Reactive Resume.
|
You can save time by exporting your data from LinkedIn and using it to auto-fill fields on Reactive Resume.
|
||||||
Head on over to the
|
Head over to the
|
||||||
<a
|
<a
|
||||||
href="https://www.linkedin.com/psettings/member-data"
|
href="https://www.linkedin.com/psettings/member-data"
|
||||||
className="underline"
|
className="underline"
|
||||||
|
@ -93,7 +93,7 @@ const ImportExternalModal: React.FC = () => {
|
||||||
>
|
>
|
||||||
Data Privacy
|
Data Privacy
|
||||||
</a>
|
</a>
|
||||||
section on LinkedIn and request an archive of your data. Once it is available, upload the ZIP archive below.
|
section on LinkedIn and request an archive of your data. Once it is available, upload the ZIP file below.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -61,12 +61,18 @@
|
||||||
"page": "Page"
|
"page": "Page"
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"empty-text": "This list is empty."
|
"empty-text": "This list is empty.",
|
||||||
|
"actions": {
|
||||||
|
"edit": "Edit",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"delete": "Delete"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"delete-section": "Delete Section",
|
"delete-section": "Delete Section",
|
||||||
"rename-section": "Rename Section",
|
"rename-section": "Rename Section",
|
||||||
"toggle-visibility": "Toggle Visibility"
|
"toggle-visibility": "Toggle Visibility",
|
||||||
|
"delete-item": "Are you sure you want to delete this item? This is an irreversible action."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"controller": {
|
"controller": {
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Reactive Resume is a free and open source resume builder that's built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3.",
|
|
||||||
"footer": {
|
"footer": {
|
||||||
"credit": "A passion project by <1>Amruth Pillai</1>",
|
"credit": "A passion project by <1>Amruth Pillai</1>",
|
||||||
|
"language": {
|
||||||
|
"missing": "Missing your language?"
|
||||||
|
},
|
||||||
"license": "By the community, for the community."
|
"license": "By the community, for the community."
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
"actions": {
|
"actions": {
|
||||||
"upload-archive": "Upload ZIP Archive"
|
"upload-archive": "Upload ZIP Archive"
|
||||||
},
|
},
|
||||||
"body": "You can save time by exporting your data from LinkedIn and using it to auto-fill fields on Reactive Resume. Head on over to the <1>Data Privacy</1> section on LinkedIn and request an archive of your data. Once it is available, upload the ZIP archive below.",
|
"body": "You can save time by exporting your data from LinkedIn and using it to auto-fill fields on Reactive Resume. Head over to the <1>Data Privacy</1> section on LinkedIn and request an archive of your data. Once it is available, upload the ZIP file below.",
|
||||||
"heading": "Import From LinkedIn"
|
"heading": "Import From LinkedIn"
|
||||||
},
|
},
|
||||||
"reactive-resume": {
|
"reactive-resume": {
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
"logout": "ಲಾಗ್ ಔಟ್"
|
"logout": "ಲಾಗ್ ಔಟ್"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ರೇಸುಮೆವು ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲ ರೇಸುಮೆ ಬಿಲ್ಡರ್ ಆಗಿದ್ದು, ನಿಮ್ಮ ರೇಸುಮೆ ಅನ್ನು 1, 2, 3 ರಂತೆ ಸುಲಭವಾಗಿ ರಚಿಸುವ, ನವೀಕರಿಸುವ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳುವ ಪ್ರಾಪಂಚಿಕ ಕಾರ್ಯಗಳನ್ನು ಮಾಡಲು ನಿರ್ಮಿಸಲಾಗಿದೆ.",
|
|
||||||
"footer": {
|
"footer": {
|
||||||
"credit": "<1>ಅಮೃತ್ ಪಿಳ್ಳೈ</1> ಅವರು ಉತ್ಸಾಹದಿಂದ ಮಾಡಿರುವ ಪ್ರಾಜೆಕ್ಟ್",
|
"credit": "<1>ಅಮೃತ್ ಪಿಳ್ಳೈ</1> ಅವರು ಉತ್ಸಾಹದಿಂದ ಮಾಡಿರುವ ಪ್ರಾಜೆಕ್ಟ್",
|
||||||
"license": "ಸಮುದಾಯದಿಂದ, ಸಮುದಾಯಕ್ಕಾಗಿ."
|
"license": "ಸಮುದಾಯದಿಂದ, ಸಮುದಾಯಕ್ಕಾಗಿ."
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heading": "ನಿಮ್ಮ ಪಾಸ್ವರ್ಡ್ ಮರೆತಿರುವಿರಾ?",
|
"heading": "ನಿಮ್ಮ ಪಾಸ್ವರ್ಡ್ ಮರೆತಿರುವಿರಾ?",
|
||||||
"help-text": "%1 ರ ಜೊತೆ ಜೋಡಣೆಯಾಗಿರುವ ಖಾತೆ ಇದ್ದಲ್ಲಿ, ನೀವು ನಿಮ್ಮ ಗುಪ್ತಪದ ಮರುಹೊಂದಿಕೆ ಕೊಂಡಿಯನ್ನು ಹೊಂದಿರುವ ಮಿಂಚೆಯನ್ನು ಪಡೆಯುವಿರಿ."
|
"help-text": "ಖಾತೆಯು ಅಸ್ತಿತ್ವದಲ್ಲಿದ್ದರೆ, ನಿಮ್ಮ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಮರುಹೊಂದಿಸಲು ಲಿಂಕ್ನೊಂದಿಗೆ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಸ್ವೀಕರಿಸುತ್ತೀರಿ."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
@ -104,16 +104,16 @@
|
||||||
},
|
},
|
||||||
"linkedin": {
|
"linkedin": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"upload-archive": "ಜಿಪ್ (ZIP) ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಿ"
|
"upload-archive": "ಜಿಪ್ ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಿ"
|
||||||
},
|
},
|
||||||
"body": "ಲಿಂಕ್ಡ್ಇನ್ನಿಂದ ನಿಮ್ಮ ಡೇಟಾವನ್ನು ರಫ್ತು ಮಾಡುವ ಮೂಲಕ ಮತ್ತು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ನಲ್ಲಿ ಕ್ಷೇತ್ರಗಳನ್ನು ಸ್ವಯಂ ತುಂಬಲು ಅದನ್ನು ಬಳಸುವ ಮೂಲಕ ನೀವು ಸಮಯವನ್ನು ಉಳಿಸಬಹುದು. <1>ಡೇಟಾ ಗೌಪ್ಯತೆಗೆ ಹೋಗಿ</1> ಲಿಂಕ್ಡ್ಇನ್ನಲ್ಲಿ ವಿಭಾಗ ಮತ್ತು ನಿಮ್ಮ ಡೇಟಾದ ಆರ್ಕೈವ್ ಅನ್ನು ವಿನಂತಿಸಿ. ಒಮ್ಮೆ ಅದು ಲಭ್ಯವಾದ ನಂತರ, ಕೆಳಗಿನ ಜಿಪ್ (ZIP) ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಿ.",
|
"body": "ಲಿಂಕ್ಡ್ಇನ್ನಿಂದ ನಿಮ್ಮ ಡೇಟಾವನ್ನು ರಫ್ತು ಮಾಡುವ ಮೂಲಕ ಮತ್ತು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ನಲ್ಲಿ ಕ್ಷೇತ್ರಗಳನ್ನು ಸ್ವಯಂ ತುಂಬಲು ಅದನ್ನು ಬಳಸುವ ಮೂಲಕ ನೀವು ಸಮಯವನ್ನು ಉಳಿಸಬಹುದು. <1>ಡೇಟಾ ಗೌಪ್ಯತೆಗೆ ಹೋಗಿ</1> ಲಿಂಕ್ಡ್ಇನ್ನಲ್ಲಿ ವಿಭಾಗ ಮತ್ತು ನಿಮ್ಮ ಡೇಟಾದ ಆರ್ಕೈವ್ ಅನ್ನು ವಿನಂತಿಸಿ. ಒಮ್ಮೆ ಅದು ಲಭ್ಯವಾದ ನಂತರ, ಕೆಳಗಿನ ಜಿಪ್ ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಿ.",
|
||||||
"heading": "ಲಿಂಕ್ಡಿನ್(LinkedIn) ನಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ"
|
"heading": "ಲಿಂಕ್ಡಿನ್ ನಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ"
|
||||||
},
|
},
|
||||||
"reactive-resume": {
|
"reactive-resume": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"upload-json": "ಜೆಸನ್ ಅನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಿ"
|
"upload-json": "ಜೆಸನ್ ಅನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಿ"
|
||||||
},
|
},
|
||||||
"body": "ನೀವು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ನ ಪ್ರಸ್ತುತ ಆವೃತ್ತಿಯೊಂದಿಗೆ ರಫ್ತು ಮಾಡಲಾದ ಜೆಸನ್(JSON) ಅನ್ನು ಹೊಂದಿದ್ದರೆ, ಮತ್ತೆ ಸಂಪಾದಿಸಬಹುದಾದ ಆವೃತ್ತಿಯನ್ನು ಪಡೆಯಲು ನೀವು ಅದನ್ನು ಇಲ್ಲಿಗೆ ಆಮದು ಮಾಡಿಕೊಳ್ಳಬಹುದು. ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ನ ಹಿಂದಿನ ಆವೃತ್ತಿಗಳು ದುರದೃಷ್ಟವಶಾತ್ ಸದ್ಯಕ್ಕೆ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ.",
|
"body": "ನೀವು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ನ ಪ್ರಸ್ತುತ ಆವೃತ್ತಿಯೊಂದಿಗೆ ರಫ್ತು ಮಾಡಲಾದ ಜೆಸನ್ ಅನ್ನು ಹೊಂದಿದ್ದರೆ, ಮತ್ತೆ ಸಂಪಾದಿಸಬಹುದಾದ ಆವೃತ್ತಿಯನ್ನು ಪಡೆಯಲು ನೀವು ಅದನ್ನು ಇಲ್ಲಿಗೆ ಆಮದು ಮಾಡಿಕೊಳ್ಳಬಹುದು. ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ನ ಹಿಂದಿನ ಆವೃತ್ತಿಗಳು ದುರದೃಷ್ಟವಶಾತ್ ಸದ್ಯಕ್ಕೆ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ.",
|
||||||
"heading": "ಜೆಸನ್ ರೆಸ್ಯೂಮ್ನಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ"
|
"heading": "ಜೆಸನ್ ರೆಸ್ಯೂಮ್ನಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
"logout": "வெளியேறு"
|
"logout": "வெளியேறு"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "ரியாக்டிவ் ரெஸ்யூம் என்பது ஒரு இலவச மற்றும் ஓப்பன் சோர்ஸ் ரெஸ்யூம் பில்டராகும், இது உங்கள் விண்ணப்பத்தை 1, 2, 3 என எளிதாக உருவாக்குவது, புதுப்பித்தல் மற்றும் பகிர்வது போன்ற சர்வ சாதாரணமான பணிகளைச் செய்ய உருவாக்கப்பட்டுள்ளது.",
|
|
||||||
"footer": {
|
"footer": {
|
||||||
"credit": "<1>அம்ருத் பிள்ளை</1>யின் திட்டம்",
|
"credit": "<1>அம்ருத் பிள்ளை</1>யின் திட்டம்",
|
||||||
"license": "சமூகத்தால், சமூகத்திற்காக."
|
"license": "சமூகத்தால், சமூகத்திற்காக."
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
|
|
||||||
import { Language, languageMap } from '@/config/languages';
|
|
||||||
|
|
||||||
export type Theme = 'light' | 'dark';
|
export type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
export type Sidebar = 'left' | 'right';
|
export type Sidebar = 'left' | 'right';
|
||||||
|
@ -13,7 +11,6 @@ export type Orientation = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
export type BuildState = {
|
export type BuildState = {
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
language: Language;
|
|
||||||
sidebar: Record<Sidebar, SidebarState>;
|
sidebar: Record<Sidebar, SidebarState>;
|
||||||
page: {
|
page: {
|
||||||
breakLine: boolean;
|
breakLine: boolean;
|
||||||
|
@ -22,7 +19,6 @@ export type BuildState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: BuildState = {
|
const initialState: BuildState = {
|
||||||
language: languageMap['en'],
|
|
||||||
sidebar: {
|
sidebar: {
|
||||||
left: { open: false },
|
left: { open: false },
|
||||||
right: { open: false },
|
right: { open: false },
|
||||||
|
@ -35,8 +31,6 @@ const initialState: BuildState = {
|
||||||
|
|
||||||
type SetThemePayload = { theme: Theme };
|
type SetThemePayload = { theme: Theme };
|
||||||
|
|
||||||
type SetLanguagePayload = { language: string };
|
|
||||||
|
|
||||||
type ToggleSidebarPayload = { sidebar: Sidebar };
|
type ToggleSidebarPayload = { sidebar: Sidebar };
|
||||||
|
|
||||||
type SetSidebarStatePayload = { sidebar: Sidebar; state: SidebarState };
|
type SetSidebarStatePayload = { sidebar: Sidebar; state: SidebarState };
|
||||||
|
@ -50,11 +44,6 @@ export const buildSlice = createSlice({
|
||||||
|
|
||||||
state.theme = theme;
|
state.theme = theme;
|
||||||
},
|
},
|
||||||
setLanguage: (state, action: PayloadAction<SetLanguagePayload>) => {
|
|
||||||
const { language } = action.payload;
|
|
||||||
|
|
||||||
state.language = languageMap[language];
|
|
||||||
},
|
|
||||||
toggleSidebar: (state, action: PayloadAction<ToggleSidebarPayload>) => {
|
toggleSidebar: (state, action: PayloadAction<ToggleSidebarPayload>) => {
|
||||||
const { sidebar } = action.payload;
|
const { sidebar } = action.payload;
|
||||||
|
|
||||||
|
@ -76,7 +65,7 @@ export const buildSlice = createSlice({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setTheme, setLanguage, toggleSidebar, setSidebarState, togglePageBreakLine, togglePageOrientation } =
|
export const { setTheme, toggleSidebar, setSidebarState, togglePageBreakLine, togglePageOrientation } =
|
||||||
buildSlice.actions;
|
buildSlice.actions;
|
||||||
|
|
||||||
export default buildSlice.reducer;
|
export default buildSlice.reducer;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const getCookie = (name: string): string | undefined => {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return parts.at(-1);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,14 +1,19 @@
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const DateWrapper: React.FC = ({ children }) => {
|
const DateWrapper: React.FC = ({ children }) => {
|
||||||
|
const { locale } = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
// Locales
|
// Locales
|
||||||
require('dayjs/locale/kn');
|
require('dayjs/locale/kn');
|
||||||
}, []);
|
|
||||||
|
locale && dayjs.locale(locale);
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue