fix(i18n): add missing translation keys, update lang/locale logic

This commit is contained in:
Amruth Pillai 2022-03-11 08:43:20 +01:00
parent 8bc7d2599e
commit 7d8828a358
No known key found for this signature in database
GPG Key ID: E3C57DF9B80855AD
35 changed files with 124 additions and 113 deletions

24
.vscode/settings.json vendored
View File

@ -1,15 +1,25 @@
{
"css.validate": false,
"scss.validate": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.wordWrap": "on",
"eslint.workingDirectories": ["schema", "client", "server"],
"i18n-ally.enabledFrameworks": ["i18next"],
"i18n-ally.localesPaths": ["client/public/locales"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.keystyle": "nested"
}
"eslint.workingDirectories": [
"schema",
"client",
"server"
],
"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
}

View File

@ -74,19 +74,19 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
})}
>
<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)}>
<ZoomIn fontSize="medium" />
</ButtonBase>
</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)}>
<ZoomOut fontSize="medium" />
</ButtonBase>
</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)}>
<FilterCenterFocus fontSize="medium" />
</ButtonBase>
@ -96,7 +96,7 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
{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}>
{orientation === 'vertical' ? (
<AlignHorizontalCenter fontSize="medium" />
@ -106,13 +106,13 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
</ButtonBase>
</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}>
<InsertPageBreak fontSize="medium" />
</ButtonBase>
</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}>
<ViewSidebar fontSize="medium" />
</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}>
<Link fontSize="medium" />
</ButtonBase>
</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}>
<Download fontSize="medium" />
</ButtonBase>

View File

@ -184,7 +184,7 @@ const Header = () => {
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
</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>
<MenuItem>
<ListItemIcon>
@ -196,7 +196,7 @@ const Header = () => {
</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}>
<ListItemIcon>
<Delete className="scale-90" />

View File

@ -58,14 +58,14 @@ const PhotoFilters = () => {
<div className="flex items-center">
<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={
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
}
/>
<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)} />}
/>
</div>

View File

@ -67,8 +67,8 @@ const PhotoUpload: React.FC = () => {
<Tooltip
title={
isEmpty(photo.url)
? t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload')
: t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove')
? (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
: (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove') as string)
}
>
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />

View File

@ -32,7 +32,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
return (
<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">
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
</ButtonBase>

View File

@ -62,7 +62,7 @@ const Layout = () => {
path="metadata.layout"
name={t('builder.rightSidebar.sections.layout.heading')}
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}>
<Restore />
</IconButton>
@ -81,7 +81,7 @@ const Layout = () => {
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
<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)}>
<Close fontSize="small" />

View File

@ -20,25 +20,24 @@ import { useMutation } from 'react-query';
import Heading from '@/components/shared/Heading';
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 queryClient from '@/services/react-query';
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 { setResumeState } from '@/store/resume/resumeSlice';
import { dateFormatOptions } from '@/utils/date';
const Settings = () => {
const router = useRouter();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { locale, ...router } = useRouter();
const resume = useAppSelector((state) => state.resume);
const theme = useAppSelector((state) => state.build.theme);
const language = useAppSelector((state) => state.build.language);
const breakLine = useAppSelector((state) => state.build.page.breakLine);
const orientation = useAppSelector((state) => state.build.page.orientation);
@ -62,14 +61,12 @@ const Settings = () => {
dispatch(setResumeState({ path: 'metadata.date.format', value }));
const handleChangeLanguage = (value: Language | null) => {
const { pathname, asPath, query } = router;
const { pathname, asPath, query, push } = router;
const code = value?.code || 'en';
dayjs.locale(code);
dispatch(setLanguage({ language: code }));
document.cookie = `NEXT_LOCALE=${code}; path=/; expires=2147483647`;
router.push({ pathname, query }, asPath, { locale: code });
push({ pathname, query }, asPath, { locale: code });
};
const handleLoadSampleData = async () => {
@ -132,7 +129,7 @@ const Settings = () => {
disableClearable
className="my-2 w-full"
options={languages}
value={language}
value={languageMap[locale ?? 'en']}
isOptionEqualToValue={(a, b) => a.code === b.code}
onChange={(_, value) => handleChangeLanguage(value)}
renderInput={(params) => <TextField {...params} />}

View File

@ -63,7 +63,7 @@ const Sharing = () => {
<div className="mt-1 flex w-full">
<FormControlLabel
label={t<string>('builder.rightSidebar.sections.sharing.short-url.label')}
label={t('builder.rightSidebar.sections.sharing.short-url.label') as string}
control={
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
}

View File

@ -161,7 +161,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
</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>
<MenuItem>
<ListItemIcon>
@ -173,7 +173,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
</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}>
<ListItemIcon>
<DeleteOutline className="scale-90" />

View File

@ -12,7 +12,7 @@ const ColorAvatar: React.FC<Props> = ({ color, size = 20, onClick }) => {
return (
<IconButton onClick={handleClick}>
<Avatar sx={{ bgcolor: color, width: size, height: size }}> </Avatar>
<Avatar sx={{ bgcolor: color, width: size, height: size }}>&nbsp;</Avatar>
</IconButton>
);
};

View File

@ -72,19 +72,19 @@ const Heading: React.FC<Props> = ({
})}
>
{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>
</Tooltip>
)}
{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>
</Tooltip>
)}
{isDeletable && (
<Tooltip title={t<string>('builder.common.tooltip.delete-section')}>
<Tooltip title={t('builder.common.tooltip.delete-section') as string}>
<IconButton onClick={handleDelete}>
<Delete />
</IconButton>

View File

@ -1,19 +1,17 @@
import { Language } from '@mui/icons-material';
import { IconButton, Popover } from '@mui/material';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { MouseEvent, useState } from 'react';
import { languages } from '@/config/languages';
import { setLanguage } from '@/store/build/buildSlice';
import { useAppDispatch } from '@/store/hooks';
import styles from './LanguageSwitcher.module.scss';
const LanguageSwitcher = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
@ -24,10 +22,7 @@ const LanguageSwitcher = () => {
const handleChangeLanguage = (locale: string) => {
const { pathname, asPath, query } = router;
dayjs.locale(locale);
dispatch(setLanguage({ language: locale }));
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
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}>
Missing your language?
{t('common.footer.language.missing')}
</a>
</div>
</div>

View File

@ -3,6 +3,7 @@ import { Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Toolti
import { ListItem as ListItemType } from '@reactive-resume/schema';
import clsx from 'clsx';
import isFunction from 'lodash/isFunction';
import { useTranslation } from 'next-i18next';
import React, { useRef, useState } from 'react';
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 { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
@ -122,29 +125,25 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
<ListItemIcon>
<DriveFileRenameOutline className="scale-90" />
</ListItemIcon>
<ListItemText>Edit</ListItemText>
<ListItemText>{t('builder.common.list.actions.edit')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleDuplicate(item)}>
<ListItemIcon>
<FileCopy className="scale-90" />
</ListItemIcon>
<ListItemText>Duplicate</ListItemText>
<ListItemText>{t('builder.common.list.actions.duplicate')}</ListItemText>
</MenuItem>
<Divider />
<Tooltip
arrow
placement="right"
title="Are you sure you want to delete this item? This is an irreversible action."
>
<Tooltip arrow placement="right" title={t('builder.common.tooltip.delete-item') as string}>
<div>
<MenuItem onClick={() => handleDelete(item)}>
<ListItemIcon>
<DeleteOutline className="scale-90" />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
<ListItemText>{t('builder.common.list.actions.delete')}</ListItemText>
</MenuItem>
</div>
</Tooltip>

View File

@ -2,6 +2,9 @@
export const FONTS_QUERY = 'fonts';
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
export const FILENAME_TIMESTAMP = 'DDMMYYYYHHmmss';

View File

@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -34,9 +35,7 @@ const schema = Joi.object<FormData>().keys({
title: Joi.string().required(),
awarder: Joi.string().required(),
date: Joi.string().allow(''),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
summary: Joi.string().allow(''),
});
@ -154,6 +153,7 @@ const AwardModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}

View File

@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -34,9 +35,7 @@ const schema = Joi.object<FormData>().keys({
name: Joi.string().required(),
issuer: Joi.string().required(),
date: Joi.string().allow(''),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
summary: Joi.string().allow(''),
});
@ -154,6 +153,7 @@ const CertificateModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}

View File

@ -14,6 +14,7 @@ import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -47,9 +48,7 @@ const schema = Joi.object<FormData>().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
level: Joi.string().allow(''),
levelNum: Joi.number().min(0).max(10),
summary: Joi.string().allow(''),
@ -194,6 +193,7 @@ const CustomModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}

View File

@ -14,6 +14,7 @@ import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -46,9 +47,7 @@ const schema = Joi.object<FormData>().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
summary: Joi.string().allow(''),
courses: Joi.array().items(Joi.string().optional()),
});
@ -217,6 +216,7 @@ const EducationModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}

View File

@ -10,6 +10,7 @@ import { useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -21,17 +22,14 @@ const path = 'sections.profile';
const defaultState: FormData = {
network: '',
username: '',
url: 'https://',
url: '',
};
const schema = Joi.object<FormData>({
id: Joi.string(),
network: Joi.string().required(),
username: Joi.string().required(),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.default('https://')
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
});
const ProfileModal: React.FC = () => {
@ -131,6 +129,7 @@ const ProfileModal: React.FC = () => {
<TextField
label={t('builder.common.form.url.label')}
className="col-span-2"
placeholder="https://"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}

View File

@ -14,6 +14,7 @@ import { Controller, useForm } from 'react-hook-form';
import ArrayInput from '@/components/shared/ArrayInput';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -42,9 +43,7 @@ const schema = Joi.object<FormData>().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
summary: Joi.string().allow(''),
keywords: Joi.array().items(Joi.string().optional()),
});
@ -187,6 +186,7 @@ const ProjectModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}

View File

@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -34,9 +35,7 @@ const schema = Joi.object<FormData>().keys({
name: Joi.string().required(),
publisher: Joi.string().required(),
date: Joi.string().allow(''),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
summary: Joi.string().allow(''),
});
@ -154,6 +153,7 @@ const PublicationModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}

View File

@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -40,9 +41,7 @@ const schema = Joi.object<FormData>().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
summary: Joi.string().allow(''),
});
@ -184,6 +183,7 @@ const VolunteerModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}

View File

@ -13,6 +13,7 @@ import { Controller, useForm } from 'react-hook-form';
import BaseModal from '@/components/shared/BaseModal';
import MarkdownSupported from '@/components/shared/MarkdownSupported';
import { VALID_URL_REGEX } from '@/constants/index';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import { addItem, editItem } from '@/store/resume/resumeSlice';
@ -40,9 +41,7 @@ const schema = Joi.object<FormData>().keys({
start: Joi.string().allow(''),
end: Joi.string().allow(''),
}),
url: Joi.string()
.pattern(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, { name: 'valid URL' })
.allow(''),
url: Joi.string().pattern(VALID_URL_REGEX, { name: 'valid URL' }).allow(''),
summary: Joi.string().allow(''),
});
@ -184,6 +183,7 @@ const WorkModal: React.FC = () => {
render={({ field, fieldState }) => (
<TextField
label={t('builder.common.form.url.label')}
placeholder="https://"
className="col-span-2"
error={!!fieldState.error}
helperText={fieldState.error?.message}

View File

@ -125,7 +125,7 @@ const CreateResumeModal: React.FC = () => {
<FormGroup>
<FormControlLabel
label={t<string>('modals.dashboard.create-resume.form.public.label')}
label={t('modals.dashboard.create-resume.form.public.label') as string}
control={
<Controller
name="isPublic"

View File

@ -84,7 +84,7 @@ const ImportExternalModal: React.FC = () => {
<p className="mb-2">
<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.
Head on over to the
Head over to the
<a
href="https://www.linkedin.com/psettings/member-data"
className="underline"
@ -93,7 +93,7 @@ const ImportExternalModal: React.FC = () => {
>
Data Privacy
</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>
</p>

View File

@ -61,12 +61,18 @@
"page": "Page"
},
"list": {
"empty-text": "This list is empty."
"empty-text": "This list is empty.",
"actions": {
"edit": "Edit",
"duplicate": "Duplicate",
"delete": "Delete"
}
},
"tooltip": {
"delete-section": "Delete 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": {

View File

@ -5,9 +5,11 @@
"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": {
"credit": "A passion project by <1>Amruth Pillai</1>",
"language": {
"missing": "Missing your language?"
},
"license": "By the community, for the community."
},
"markdown": {

View File

@ -106,7 +106,7 @@
"actions": {
"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"
},
"reactive-resume": {

View File

@ -5,7 +5,6 @@
"logout": "ಲಾಗ್ ಔಟ್"
}
},
"description": "ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ರೇಸುಮೆವು ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲ ರೇಸುಮೆ ಬಿಲ್ಡರ್ ಆಗಿದ್ದು, ನಿಮ್ಮ ರೇಸುಮೆ ಅನ್ನು 1, 2, 3 ರಂತೆ ಸುಲಭವಾಗಿ ರಚಿಸುವ, ನವೀಕರಿಸುವ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳುವ ಪ್ರಾಪಂಚಿಕ ಕಾರ್ಯಗಳನ್ನು ಮಾಡಲು ನಿರ್ಮಿಸಲಾಗಿದೆ.",
"footer": {
"credit": "<1>ಅಮೃತ್ ಪಿಳ್ಳೈ</1> ಅವರು ಉತ್ಸಾಹದಿಂದ ಮಾಡಿರುವ ಪ್ರಾಜೆಕ್ಟ್",
"license": "ಸಮುದಾಯದಿಂದ, ಸಮುದಾಯಕ್ಕಾಗಿ."

View File

@ -11,7 +11,7 @@
}
},
"heading": "ನಿಮ್ಮ ಪಾಸ್ವರ್ಡ್ ಮರೆತಿರುವಿರಾ?",
"help-text": "%1 ರ ಜೊತೆ ಜೋಡಣೆಯಾಗಿರುವ ಖಾತೆ ಇದ್ದಲ್ಲಿ, ನೀವು ನಿಮ್ಮ ಗುಪ್ತಪದ ಮರುಹೊಂದಿಕೆ ಕೊಂಡಿಯನ್ನು ಹೊಂದಿರುವ ಮಿಂಚೆಯನ್ನು ಪಡೆಯುವಿರಿ."
"help-text": "ಖಾತೆಯು ಅಸ್ತಿತ್ವದಲ್ಲಿದ್ದರೆ, ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಮರುಹೊಂದಿಸಲು ಲಿಂಕ್‌ನೊಂದಿಗೆ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಸ್ವೀಕರಿಸುತ್ತೀರಿ."
},
"login": {
"actions": {
@ -104,16 +104,16 @@
},
"linkedin": {
"actions": {
"upload-archive": "ಜಿಪ್ (ZIP) ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡಿ"
"upload-archive": "ಜಿಪ್ ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡಿ"
},
"body": "ಲಿಂಕ್ಡ್‌ಇನ್‌ನಿಂದ ನಿಮ್ಮ ಡೇಟಾವನ್ನು ರಫ್ತು ಮಾಡುವ ಮೂಲಕ ಮತ್ತು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್‌ನಲ್ಲಿ ಕ್ಷೇತ್ರಗಳನ್ನು ಸ್ವಯಂ ತುಂಬಲು ಅದನ್ನು ಬಳಸುವ ಮೂಲಕ ನೀವು ಸಮಯವನ್ನು ಉಳಿಸಬಹುದು. <1>ಡೇಟಾ ಗೌಪ್ಯತೆಗೆ ಹೋಗಿ</1> ಲಿಂಕ್ಡ್‌ಇನ್‌ನಲ್ಲಿ ವಿಭಾಗ ಮತ್ತು ನಿಮ್ಮ ಡೇಟಾದ ಆರ್ಕೈವ್ ಅನ್ನು ವಿನಂತಿಸಿ. ಒಮ್ಮೆ ಅದು ಲಭ್ಯವಾದ ನಂತರ, ಕೆಳಗಿನ ಜಿಪ್ (ZIP) ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡಿ.",
"heading": "ಲಿಂಕ್ಡಿನ್(LinkedIn) ನಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ"
"body": "ಲಿಂಕ್ಡ್‌ಇನ್‌ನಿಂದ ನಿಮ್ಮ ಡೇಟಾವನ್ನು ರಫ್ತು ಮಾಡುವ ಮೂಲಕ ಮತ್ತು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್‌ನಲ್ಲಿ ಕ್ಷೇತ್ರಗಳನ್ನು ಸ್ವಯಂ ತುಂಬಲು ಅದನ್ನು ಬಳಸುವ ಮೂಲಕ ನೀವು ಸಮಯವನ್ನು ಉಳಿಸಬಹುದು. <1>ಡೇಟಾ ಗೌಪ್ಯತೆಗೆ ಹೋಗಿ</1> ಲಿಂಕ್ಡ್‌ಇನ್‌ನಲ್ಲಿ ವಿಭಾಗ ಮತ್ತು ನಿಮ್ಮ ಡೇಟಾದ ಆರ್ಕೈವ್ ಅನ್ನು ವಿನಂತಿಸಿ. ಒಮ್ಮೆ ಅದು ಲಭ್ಯವಾದ ನಂತರ, ಕೆಳಗಿನ ಜಿಪ್ ಆರ್ಕೈವ್ ಅನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡಿ.",
"heading": "ಲಿಂಕ್ಡಿನ್ ನಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ"
},
"reactive-resume": {
"actions": {
"upload-json": "ಜೆಸನ್ ಅನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡಿ"
},
"body": "ನೀವು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್‌ನ ಪ್ರಸ್ತುತ ಆವೃತ್ತಿಯೊಂದಿಗೆ ರಫ್ತು ಮಾಡಲಾದ ಜೆಸನ್(JSON) ಅನ್ನು ಹೊಂದಿದ್ದರೆ, ಮತ್ತೆ ಸಂಪಾದಿಸಬಹುದಾದ ಆವೃತ್ತಿಯನ್ನು ಪಡೆಯಲು ನೀವು ಅದನ್ನು ಇಲ್ಲಿಗೆ ಆಮದು ಮಾಡಿಕೊಳ್ಳಬಹುದು. ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್‌ನ ಹಿಂದಿನ ಆವೃತ್ತಿಗಳು ದುರದೃಷ್ಟವಶಾತ್ ಸದ್ಯಕ್ಕೆ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ.",
"body": "ನೀವು ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್‌ನ ಪ್ರಸ್ತುತ ಆವೃತ್ತಿಯೊಂದಿಗೆ ರಫ್ತು ಮಾಡಲಾದ ಜೆಸನ್ ಅನ್ನು ಹೊಂದಿದ್ದರೆ, ಮತ್ತೆ ಸಂಪಾದಿಸಬಹುದಾದ ಆವೃತ್ತಿಯನ್ನು ಪಡೆಯಲು ನೀವು ಅದನ್ನು ಇಲ್ಲಿಗೆ ಆಮದು ಮಾಡಿಕೊಳ್ಳಬಹುದು. ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್‌ನ ಹಿಂದಿನ ಆವೃತ್ತಿಗಳು ದುರದೃಷ್ಟವಶಾತ್ ಸದ್ಯಕ್ಕೆ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ.",
"heading": "ಜೆಸನ್ ರೆಸ್ಯೂಮ್‌ನಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಿ"
}
},

View File

@ -5,7 +5,6 @@
"logout": "வெளியேறு"
}
},
"description": "ரியாக்டிவ் ரெஸ்யூம் என்பது ஒரு இலவச மற்றும் ஓப்பன் சோர்ஸ் ரெஸ்யூம் பில்டராகும், இது உங்கள் விண்ணப்பத்தை 1, 2, 3 என எளிதாக உருவாக்குவது, புதுப்பித்தல் மற்றும் பகிர்வது போன்ற சர்வ சாதாரணமான பணிகளைச் செய்ய உருவாக்கப்பட்டுள்ளது.",
"footer": {
"credit": "<1>அம்ருத் பிள்ளை</1>யின் திட்டம்",
"license": "சமூகத்தால், சமூகத்திற்காக."

View File

@ -1,8 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import merge from 'lodash/merge';
import { Language, languageMap } from '@/config/languages';
export type Theme = 'light' | 'dark';
export type Sidebar = 'left' | 'right';
@ -13,7 +11,6 @@ export type Orientation = 'horizontal' | 'vertical';
export type BuildState = {
theme?: Theme;
language: Language;
sidebar: Record<Sidebar, SidebarState>;
page: {
breakLine: boolean;
@ -22,7 +19,6 @@ export type BuildState = {
};
const initialState: BuildState = {
language: languageMap['en'],
sidebar: {
left: { open: false },
right: { open: false },
@ -35,8 +31,6 @@ const initialState: BuildState = {
type SetThemePayload = { theme: Theme };
type SetLanguagePayload = { language: string };
type ToggleSidebarPayload = { sidebar: Sidebar };
type SetSidebarStatePayload = { sidebar: Sidebar; state: SidebarState };
@ -50,11 +44,6 @@ export const buildSlice = createSlice({
state.theme = theme;
},
setLanguage: (state, action: PayloadAction<SetLanguagePayload>) => {
const { language } = action.payload;
state.language = languageMap[language];
},
toggleSidebar: (state, action: PayloadAction<ToggleSidebarPayload>) => {
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;
export default buildSlice.reducer;

View File

@ -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);
}
};

View File

@ -1,14 +1,19 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
const DateWrapper: React.FC = ({ children }) => {
const { locale } = useRouter();
useEffect(() => {
dayjs.extend(relativeTime);
// Locales
require('dayjs/locale/kn');
}, []);
locale && dayjs.locale(locale);
}, [locale]);
return <>{children}</>;
};