diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-01-29 18:21:37 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-01-29 19:02:57 +0100 |
| commit | e4d5b8151802517b2943756fc0d09ffa95e2c4e2 (patch) | |
| tree | 9e99137a7b64ea7993a8311a7162336a551be8b2 /src | |
| parent | 47b854de26dea24e7838fd0804df103dee99635f (diff) | |
chore: replace lingui functions with react-intl
Diffstat (limited to 'src')
54 files changed, 2314 insertions, 341 deletions
diff --git a/src/components/Branding/Branding.tsx b/src/components/Branding/Branding.tsx index 01948e9..efb3a49 100644 --- a/src/components/Branding/Branding.tsx +++ b/src/components/Branding/Branding.tsx @@ -1,17 +1,18 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { ReactElement } from 'react'; -import { t } from '@lingui/macro'; import photo from '@assets/images/armand-philippot.jpg'; import { config } from '@config/website'; -import styles from './Branding.module.scss'; import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import { ReactElement } from 'react'; +import { useIntl } from 'react-intl'; import { Person, WithContext } from 'schema-dts'; +import styles from './Branding.module.scss'; import Logo from './Logo/Logo'; type BrandingReturn = ({ isHome }: { isHome: boolean }) => ReactElement; const Branding: BrandingReturn = ({ isHome = false }) => { + const intl = useIntl(); const TitleTag = isHome ? 'h1' : 'p'; const schemaJsonLd: WithContext<Person> = { @@ -38,10 +39,15 @@ const Branding: BrandingReturn = ({ isHome = false }) => { <div className={styles.logo__front}> <Image src={photo} - alt={t({ - message: `${config.name} picture`, - comment: 'Branding logo.', - })} + alt={intl.formatMessage( + { + defaultMessage: '{brandingName} picture', + description: 'Branding: branding name picture.', + }, + { + brandingName: config.name, + } + )} layout="responsive" /> </div> diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 7c8eb5c..30179be 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -1,12 +1,13 @@ import { config } from '@config/website'; -import { t } from '@lingui/macro'; import Head from 'next/head'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import { BreadcrumbList, WithContext } from 'schema-dts'; import styles from './Breadcrumb.module.scss'; const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { + const intl = useIntl(); const router = useRouter(); const isHome = router.pathname === '/'; @@ -20,14 +21,24 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { <> <li className={styles.item}> <Link href="/"> - <a>{t`Home`}</a> + <a> + {intl.formatMessage({ + defaultMessage: 'Home', + description: 'Breadcrumb: Home item', + })} + </a> </Link> </li> {(isArticle || isThematic || isSubject) && ( <> <li className={styles.item}> <Link href="/blog"> - <a>{t`Blog`}</a> + <a> + {intl.formatMessage({ + defaultMessage: 'Blog', + description: 'Breadcrumb: Blog item', + })} + </a> </Link> </li> </> @@ -36,7 +47,12 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { <> <li className={styles.item}> <Link href="/projets"> - <a>{t`Projects`}</a> + <a> + {intl.formatMessage({ + defaultMessage: 'Projects', + description: 'Breadcrumb: Projects item', + })} + </a> </Link> </li> </> @@ -51,7 +67,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { const homepage: BreadcrumbList['itemListElement'] = { '@type': 'ListItem', position: 1, - name: t`Home`, + name: intl.formatMessage({ + defaultMessage: 'Home', + description: 'Breadcrumb: Home item', + }), item: config.url, }; @@ -61,7 +80,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { const blog: BreadcrumbList['itemListElement'] = { '@type': 'ListItem', position: 2, - name: t`Blog`, + name: intl.formatMessage({ + defaultMessage: 'Blog', + description: 'Breadcrumb: Blog item', + }), item: `${config.url}/blog`, }; @@ -72,7 +94,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { const blog: BreadcrumbList['itemListElement'] = { '@type': 'ListItem', position: 2, - name: t`Projects`, + name: intl.formatMessage({ + defaultMessage: 'Projects', + description: 'Breadcrumb: Projects item', + }), item: `${config.url}/projets`, }; @@ -108,7 +133,12 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { </Head> {!isHome && ( <nav id="breadcrumb" className={styles.wrapper}> - <span className="screen-reader-text">{t`You are here:`}</span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'You are here:', + description: 'Breadcrumb: You are here prefix', + })} + </span> <ol className={styles.list}>{getItems()}</ol> </nav> )} diff --git a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx b/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx index 246ad80..e9f6079 100644 --- a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx +++ b/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx @@ -1,6 +1,6 @@ import { CloseIcon, CogIcon, SearchIcon } from '@components/Icons'; -import { t } from '@lingui/macro'; import { ForwardedRef, forwardRef, SetStateAction } from 'react'; +import { useIntl } from 'react-intl'; import styles from '../Buttons.module.scss'; type ButtonType = 'search' | 'settings'; @@ -17,6 +17,7 @@ const ButtonToolbar = ( }, ref: ForwardedRef<HTMLButtonElement> ) => { + const intl = useIntl(); const ButtonIcon = () => (type === 'search' ? <SearchIcon /> : <CogIcon />); const btnClasses = isActivated ? `${styles.toolbar} ${styles['toolbar--activated']}` @@ -38,9 +39,29 @@ const ButtonToolbar = ( </span> </span> {isActivated ? ( - <span className="screen-reader-text">{t`Close ${type}`}</span> + <span className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: 'Close {type}', + description: 'ButtonToolbar: Close button', + }, + { + type, + } + )} + </span> ) : ( - <span className="screen-reader-text">{t`Open ${type}`}</span> + <span className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: 'Open {type}', + description: 'ButtonToolbar: Open button', + }, + { + type, + } + )} + </span> )} </button> ); diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx index 6eb0184..e95a378 100644 --- a/src/components/Comment/Comment.tsx +++ b/src/components/Comment/Comment.tsx @@ -1,7 +1,6 @@ import { Button } from '@components/Buttons'; import CommentForm from '@components/CommentForm/CommentForm'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { Comment as CommentData } from '@ts/types/comments'; import { getFormattedDate } from '@utils/helpers/format'; import Head from 'next/head'; @@ -9,6 +8,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import { Comment as CommentSchema, WithContext } from 'schema-dts'; import styles from './Comment.module.scss'; @@ -21,6 +21,7 @@ const Comment = ({ comment: CommentData; isNested?: boolean; }) => { + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; const [isReply, setIsReply] = useState<boolean>(false); @@ -48,7 +49,16 @@ const Comment = ({ minute: 'numeric', }) .replace(':', 'h'); - return t`${date} at ${time}`; + return intl.formatMessage( + { + defaultMessage: '{date} at {time}', + description: 'Comment: publication date', + }, + { + date, + time, + } + ); }; const getApprovedComment = () => { @@ -68,7 +78,12 @@ const Comment = ({ {getCommentAuthor()} </header> <dl className={styles.date}> - <dt>{t`Published on:`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'Comment: publication date label', + })} + </dt> <dd> <time dateTime={comment.date}> <Link href={`#comment-${comment.commentId}`}> @@ -83,9 +98,12 @@ const Comment = ({ ></div> {!isNested && ( <footer className={styles.footer}> - <Button - clickHandler={() => setIsReply((prev) => !prev)} - >{t`Reply`}</Button> + <Button clickHandler={() => setIsReply((prev) => !prev)}> + {intl.formatMessage({ + defaultMessage: 'Reply', + description: 'Comment: reply button', + })} + </Button> </footer> )} </article> @@ -116,7 +134,14 @@ const Comment = ({ }; const getCommentStatus = () => { - return <p>{t`This comment is awaiting moderation.`}</p>; + return ( + <p> + {intl.formatMessage({ + defaultMessage: 'This comment is awaiting moderation.', + description: 'Comment: awaiting moderation message', + })} + </p> + ); }; const schemaJsonLd: WithContext<CommentSchema> = { diff --git a/src/components/CommentForm/CommentForm.tsx b/src/components/CommentForm/CommentForm.tsx index 1ed219c..0ea3276 100644 --- a/src/components/CommentForm/CommentForm.tsx +++ b/src/components/CommentForm/CommentForm.tsx @@ -1,9 +1,9 @@ import { ButtonSubmit } from '@components/Buttons'; import { Form, FormItem, Input, TextArea } from '@components/Form'; import Notice from '@components/Notice/Notice'; -import { t } from '@lingui/macro'; import { createComment } from '@services/graphql/mutations'; import { ForwardedRef, forwardRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './CommentForm.module.scss'; const CommentForm = ( @@ -18,6 +18,7 @@ const CommentForm = ( }, ref: ForwardedRef<HTMLInputElement> ) => { + const intl = useIntl(); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [website, setWebsite] = useState(''); @@ -68,7 +69,12 @@ const CommentForm = ( return ( <div className={wrapperClasses}> - <h2 className={styles.title}>{t`Leave a comment`}</h2> + <h2 className={styles.title}> + {intl.formatMessage({ + defaultMessage: 'Leave a comment', + description: 'CommentForm: form title', + })} + </h2> <Form submitHandler={submitHandler} modifier={isReply ? 'centered' : undefined} @@ -77,7 +83,10 @@ const CommentForm = ( <Input id="commenter-name" name="commenter-name" - label={t`Name`} + label={intl.formatMessage({ + defaultMessage: 'Name', + description: 'CommentForm: Name field label', + })} required={true} value={name} setValue={setName} @@ -88,7 +97,10 @@ const CommentForm = ( <Input id="commenter-email" name="commenter-email" - label={t`Email`} + label={intl.formatMessage({ + defaultMessage: 'Email', + description: 'CommentForm: Email field label', + })} required={true} value={email} setValue={setEmail} @@ -98,7 +110,10 @@ const CommentForm = ( <Input id="commenter-website" name="commenter-website" - label={t`Website`} + label={intl.formatMessage({ + defaultMessage: 'Website', + description: 'CommentForm: Website field label', + })} value={website} setValue={setWebsite} /> @@ -107,17 +122,31 @@ const CommentForm = ( <TextArea id="commenter-message" name="commenter-message" - label={t`Comment`} + label={intl.formatMessage({ + defaultMessage: 'Comment', + description: 'CommentForm: Comment field label', + })} value={message} setValue={setMessage} required={true} /> </FormItem> <FormItem> - <ButtonSubmit>{t`Send`}</ButtonSubmit> + <ButtonSubmit> + {intl.formatMessage({ + defaultMessage: 'Send', + description: 'CommentForm: Send button', + })} + </ButtonSubmit> </FormItem> {isSuccess && !isApproved && ( - <Notice type="success">{t`Thanks for your comment! It is now awaiting moderation.`}</Notice> + <Notice type="success"> + {intl.formatMessage({ + defaultMessage: + 'Thanks for your comment! It is now awaiting moderation.', + description: 'CommentForm: Comment sent success message', + })} + </Notice> )} </Form> </div> diff --git a/src/components/CommentsList/CommentsList.tsx b/src/components/CommentsList/CommentsList.tsx index bdca00b..6630a03 100644 --- a/src/components/CommentsList/CommentsList.tsx +++ b/src/components/CommentsList/CommentsList.tsx @@ -1,6 +1,6 @@ -import { Comment as CommentData } from '@ts/types/comments'; import Comment from '@components/Comment/Comment'; -import { t } from '@lingui/macro'; +import { Comment as CommentData } from '@ts/types/comments'; +import { useIntl } from 'react-intl'; import styles from './CommentsList.module.scss'; const CommentsList = ({ @@ -10,6 +10,8 @@ const CommentsList = ({ articleId: number; comments: CommentData[]; }) => { + const intl = useIntl(); + const getCommentsList = () => { return comments.map((comment) => { return ( @@ -20,11 +22,21 @@ const CommentsList = ({ return ( <> - <h2 className={styles.title}>{t`Comments`}</h2> + <h2 className={styles.title}> + {intl.formatMessage({ + defaultMessage: 'Comments', + description: 'CommentsList: Comments section title', + })} + </h2> {comments.length > 0 ? ( <ol className={styles.list}>{getCommentsList()}</ol> ) : ( - <p className={styles['no-comments']}>{t`No comments yet.`}</p> + <p className={styles['no-comments']}> + {intl.formatMessage({ + defaultMessage: 'No comments yet.', + description: 'CommentsList: No comment message', + })} + </p> )} </> ); diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 15a4660..4aa980d 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -2,11 +2,12 @@ import { ButtonLink } from '@components/Buttons'; import Copyright from '@components/Copyright/Copyright'; import FooterNav from '@components/FooterNav/FooterNav'; import { ArrowIcon } from '@components/Icons'; -import { t } from '@lingui/macro'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './Footer.module.scss'; const Footer = () => { + const intl = useIntl(); const [backToTopClasses, setBackToTopClasses] = useState( `${styles['back-to-top']} ${styles['back-to-top--hidden']}` ); @@ -36,7 +37,12 @@ const Footer = () => { <FooterNav /> <div className={backToTopClasses}> <ButtonLink target="#top" position="center"> - <span className="screen-reader-text">{t`Back to top`}</span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Back to top', + description: 'Footer: Back to top button', + })} + </span> <ArrowIcon direction="top" /> </ButtonLink> </div> diff --git a/src/components/FooterNav/FooterNav.tsx b/src/components/FooterNav/FooterNav.tsx index 7266e7e..918fed7 100644 --- a/src/components/FooterNav/FooterNav.tsx +++ b/src/components/FooterNav/FooterNav.tsx @@ -1,9 +1,23 @@ import Link from 'next/link'; import styles from './FooterNav.module.scss'; -import { footerNav } from '@config/nav'; +import { NavItem } from '@ts/types/nav'; +import { useIntl } from 'react-intl'; const FooterNav = () => { - const navItems = footerNav.map((item) => { + const intl = useIntl(); + + const footerNavConfig: NavItem[] = [ + { + id: 'legal-notice', + name: intl.formatMessage({ + defaultMessage: 'Legal notice', + description: 'FooterNav: legal notice link', + }), + slug: '/mentions-legales', + }, + ]; + + const navItems = footerNavConfig.map((item) => { return ( <li key={item.id} className={styles.item}> <Link href={item.slug}> diff --git a/src/components/Icons/Copyright/Copyright.tsx b/src/components/Icons/Copyright/Copyright.tsx index 396c127..d27c042 100644 --- a/src/components/Icons/Copyright/Copyright.tsx +++ b/src/components/Icons/Copyright/Copyright.tsx @@ -1,4 +1,3 @@ -import { t } from '@lingui/macro'; import styles from './Copyright.module.scss'; const CopyrightIcon = () => { @@ -8,7 +7,7 @@ const CopyrightIcon = () => { viewBox="0 0 211.99811 63.999996" xmlns="http://www.w3.org/2000/svg" > - <title>{t`CC BY SA`}</title> + <title>CC BY SA</title> <path d="m 175.53911,15.829498 c 0,-3.008 1.485,-4.514 4.458,-4.514 2.973,0 4.457,1.504 4.457,4.514 0,2.971 -1.486,4.457 -4.457,4.457 -2.971,0 -4.458,-1.486 -4.458,-4.457 z" /> <path d="m 188.62611,24.057498 v 13.085 h -3.656 v 15.542 h -9.944 v -15.541 h -3.656 v -13.086 c 0,-0.572 0.2,-1.057 0.599,-1.457 0.401,-0.399 0.887,-0.6 1.457,-0.6 h 13.144 c 0.533,0 1.01,0.2 1.428,0.6 0.417,0.4 0.628,0.886 0.628,1.457 z" /> <path d="m 179.94147,-1.9073486e-6 c -8.839,0 -16.34167,3.0848125073486 -22.51367,9.2578125073486 -6.285,6.4000004 -9.42969,13.9811874 -9.42969,22.7421874 0,8.762 3.14469,16.284312 9.42969,22.570312 6.361,6.286 13.86467,9.429688 22.51367,9.429688 8.799,0 16.43611,-3.181922 22.91211,-9.544922 6.096,-5.98 9.14453,-13.464078 9.14453,-22.455078 0,-8.952 -3.10646,-16.532188 -9.31446,-22.7421874 -6.172,-6.172 -13.75418,-9.2578125073486 -22.74218,-9.2578125073486 z M 180.05475,5.7714825 c 7.238,0 13.40967,2.55225 18.51367,7.6562495 5.103,5.106 7.65625,11.294313 7.65625,18.570313 0,7.391 -2.51397,13.50575 -7.54297,18.34375 -5.295,5.221 -11.50591,7.828125 -18.6289,7.828125 -7.162,0 -13.33268,-2.589484 -18.51368,-7.771484 -5.18,-5.178001 -7.76953,-11.310485 -7.76953,-18.396485 0,-7.047 2.60813,-13.238266 7.82813,-18.572265 5.029,-5.1040004 11.18103,-7.6582035 18.45703,-7.6582035 z" /> diff --git a/src/components/Icons/Moon/Moon.tsx b/src/components/Icons/Moon/Moon.tsx index 62e7203..acdf6ae 100644 --- a/src/components/Icons/Moon/Moon.tsx +++ b/src/components/Icons/Moon/Moon.tsx @@ -1,14 +1,21 @@ -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './Moon.module.scss'; const MoonIcon = () => { + const intl = useIntl(); + return ( <svg className={styles.moon} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" > - <title>{t`Dark theme`}</title> + <title> + {intl.formatMessage({ + defaultMessage: 'Dark theme', + description: 'Icons: Moon icon (dark theme)', + })} + </title> <path d="M 51.077315,1.9893942 A 43.319985,43.319985 0 0 1 72.840039,39.563145 43.319985,43.319985 0 0 1 29.520053,82.88313 43.319985,43.319985 0 0 1 5.4309911,75.569042 48.132997,48.132997 0 0 0 46.126047,98 48.132997,48.132997 0 0 0 94.260004,49.867002 48.132997,48.132997 0 0 0 51.077315,1.9893942 Z" /> </svg> ); diff --git a/src/components/Icons/Sun/Sun.tsx b/src/components/Icons/Sun/Sun.tsx index 612d3fa..44945c2 100644 --- a/src/components/Icons/Sun/Sun.tsx +++ b/src/components/Icons/Sun/Sun.tsx @@ -1,14 +1,21 @@ -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './Sun.module.scss'; const SunIcon = () => { + const intl = useIntl(); + return ( <svg className={styles.sun} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" > - <title>{t`Light theme`}</title> + <title> + {intl.formatMessage({ + defaultMessage: 'Light theme', + description: 'Icons: Sun icon (light theme)', + })} + </title> <path d="M 69.398043,50.000437 A 19.399259,19.399204 0 0 1 49.998784,69.399641 19.399259,19.399204 0 0 1 30.599525,50.000437 19.399259,19.399204 0 0 1 49.998784,30.601234 19.399259,19.399204 0 0 1 69.398043,50.000437 Z m 27.699233,1.125154 c 2.657696,0.0679 1.156196,12.061455 -1.435545,11.463959 L 80.113224,59.000697 c -2.589801,-0.597494 -1.625657,-8.345536 1.032041,-8.278609 z m -18.06653,37.251321 c 1.644087,2.091234 -9.030355,8.610337 -10.126414,6.188346 L 62.331863,80.024585 c -1.096058,-2.423931 5.197062,-6.285342 6.839209,-4.194107 z M 38.611418,97.594444 C 38.02653,100.18909 26.24148,95.916413 27.436475,93.54001 l 7.168026,-14.256474 c 1.194024,-2.376403 8.102101,0.151313 7.517214,2.744986 z M 6.1661563,71.834242 C 3.7916868,73.028262 -0.25499873,61.16274 2.3386824,60.577853 L 17.905618,57.067567 c 2.593681,-0.584886 4.894434,6.403678 2.518995,7.598668 z M 6.146757,30.055146 c -2.3764094,-1.194991 4.46571,-11.714209 6.479353,-9.97798 l 12.090589,10.414462 c 2.014613,1.736229 -1.937017,7.926514 -4.314396,6.731524 z M 38.56777,4.2639045 C 37.982883,1.6682911 50.480855,0.41801247 50.415868,3.0766733 L 50.020123,19.028638 c -0.06596,2.657691 -7.357169,3.394862 -7.943027,0.800218 z m 40.403808,9.1622435 c 1.635357,-2.098023 10.437771,6.872168 8.339742,8.506552 l -12.58818,9.805327 c -2.099,1.634383 -7.192276,-3.626682 -5.557888,-5.724706 z M 97.096306,50.69105 c 2.657696,-0.06596 1.164926,12.462047 -1.425846,11.863582 L 80.122924,58.96578 c -2.590771,-0.597496 -1.636327,-7.814 1.021371,-7.879957 z" /> </svg> ); diff --git a/src/components/Layouts/Layout.tsx b/src/components/Layouts/Layout.tsx index 599cfe2..b479ef3 100644 --- a/src/components/Layouts/Layout.tsx +++ b/src/components/Layouts/Layout.tsx @@ -1,12 +1,12 @@ -import { ReactElement, ReactNode, useEffect, useRef } from 'react'; import Footer from '@components/Footer/Footer'; import Header from '@components/Header/Header'; import Main from '@components/Main/Main'; import Breadcrumb from '@components/Breadcrumb/Breadcrumb'; -import { t } from '@lingui/macro'; -import Head from 'next/head'; import { config } from '@config/website'; +import Head from 'next/head'; import { useRouter } from 'next/router'; +import { ReactElement, ReactNode, useEffect, useRef } from 'react'; +import { useIntl } from 'react-intl'; import { WebSite, WithContext } from 'schema-dts'; const Layout = ({ @@ -16,6 +16,7 @@ const Layout = ({ children: ReactNode; isHome?: boolean; }) => { + const intl = useIntl(); const ref = useRef<HTMLSpanElement>(null); const { asPath } = useRouter(); @@ -91,7 +92,12 @@ const Layout = ({ ></script> </Head> <span ref={ref} tabIndex={-1} /> - <a href="#main" className="screen-reader-text">{t`Skip to content`}</a> + <a href="#main" className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Skip to content', + description: 'Layout: Skip to content button', + })} + </a> <Header isHome={isHome} /> <Main>{children}</Main> <Footer /> diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx index ef8b587..59386af 100644 --- a/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/src/components/MDX/CodeBlock/CodeBlock.tsx @@ -3,6 +3,7 @@ import { translateCopyButton } from '@utils/helpers/prism'; import { useRouter } from 'next/router'; import Prism from 'prismjs'; import { ReactChildren, useEffect } from 'react'; +import { useIntl } from 'react-intl'; const CodeBlock = ({ className, @@ -15,6 +16,7 @@ const CodeBlock = ({ const languageClass = classNames.find((name: string) => name.startsWith('language-') ); + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; @@ -23,8 +25,8 @@ const CodeBlock = ({ }); useEffect(() => { - translateCopyButton(locale); - }, [locale]); + translateCopyButton(locale, intl); + }, [intl, locale]); return ( <div> diff --git a/src/components/MainNav/MainNav.tsx b/src/components/MainNav/MainNav.tsx index afc4193..a866b9c 100644 --- a/src/components/MainNav/MainNav.tsx +++ b/src/components/MainNav/MainNav.tsx @@ -1,6 +1,3 @@ -import { SetStateAction } from 'react'; -import Link from 'next/link'; -import { t } from '@lingui/macro'; import { BlogIcon, ContactIcon, @@ -9,9 +6,12 @@ import { HomeIcon, ProjectsIcon, } from '@components/Icons'; -import { mainNav } from '@config/nav'; -import styles from './MainNav.module.scss'; +import { NavItem } from '@ts/types/nav'; +import Link from 'next/link'; import { useRouter } from 'next/router'; +import { SetStateAction } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './MainNav.module.scss'; const MainNav = ({ isOpened, @@ -20,8 +20,52 @@ const MainNav = ({ isOpened: boolean; setIsOpened: (value: SetStateAction<boolean>) => void; }) => { + const intl = useIntl(); const router = useRouter(); + const mainNavConfig: NavItem[] = [ + { + id: 'home', + name: intl.formatMessage({ + defaultMessage: 'Home', + description: 'MainNav: home link', + }), + slug: '/', + }, + { + id: 'blog', + name: intl.formatMessage({ + defaultMessage: 'Blog', + description: 'MainNav: blog link', + }), + slug: '/blog', + }, + { + id: 'projects', + name: intl.formatMessage({ + defaultMessage: 'Projects', + description: 'MainNav: projects link', + }), + slug: '/projets', + }, + { + id: 'cv', + name: intl.formatMessage({ + defaultMessage: 'Resume', + description: 'MainNav: resume link', + }), + slug: '/cv', + }, + { + id: 'contact', + name: intl.formatMessage({ + defaultMessage: 'Contact', + description: 'MainNav: contact link', + }), + slug: '/contact', + }, + ]; + const getIcon = (id: string) => { switch (id) { case 'home': @@ -39,7 +83,7 @@ const MainNav = ({ } }; - const navItems = mainNav.map((item) => { + const navItems = mainNavConfig.map((item) => { const currentClass = router.asPath === item.slug ? styles.current : ''; return ( @@ -73,7 +117,15 @@ const MainNav = ({ > <HamburgerIcon isActive={isOpened} /> <span className="screen-reader-text"> - {isOpened ? t`Close menu` : t`Open menu`} + {isOpened + ? intl.formatMessage({ + defaultMessage: 'Close menu', + description: 'MainNav: close button', + }) + : intl.formatMessage({ + defaultMessage: 'Open menu', + description: 'MainNav: open button', + })} </span> </label> <nav className={styles.nav}> diff --git a/src/components/PaginationCursor/PaginationCursor.tsx b/src/components/PaginationCursor/PaginationCursor.tsx index bcbb555..a8c6265 100644 --- a/src/components/PaginationCursor/PaginationCursor.tsx +++ b/src/components/PaginationCursor/PaginationCursor.tsx @@ -1,4 +1,4 @@ -import { plural, t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './PaginationCursor.module.scss'; const PaginationCursor = ({ @@ -8,6 +8,8 @@ const PaginationCursor = ({ current: number; total: number; }) => { + const intl = useIntl(); + return ( <div className={styles.wrapper}> <progress @@ -16,12 +18,19 @@ const PaginationCursor = ({ value={current} aria-valuemin={0} aria-valuemax={total} - aria-label={t`Number of articles loaded out of the total available.`} - title={plural(current, { - zero: `# articles out of a total of ${total}`, - one: `# article out of a total of ${total}`, - other: `# articles out of a total of ${total}`, + aria-label={intl.formatMessage({ + defaultMessage: + 'Number of articles loaded out of the total available.', + description: 'PaginationCursor: loaded articles count aria-label', })} + title={intl.formatMessage( + { + defaultMessage: + '{articlesCount, plural, =0 {# articles} one {# article} other {# articles}} out of a total of {total}', + description: 'PaginationCursor: loaded articles count message', + }, + { articlesCount: current, total } + )} ></progress> </div> ); diff --git a/src/components/PostFooter/PostFooter.tsx b/src/components/PostFooter/PostFooter.tsx index ad471eb..6c97ec2 100644 --- a/src/components/PostFooter/PostFooter.tsx +++ b/src/components/PostFooter/PostFooter.tsx @@ -1,10 +1,12 @@ import { ButtonLink } from '@components/Buttons'; -import { t } from '@lingui/macro'; import { TopicPreview } from '@ts/types/taxonomies'; import Image from 'next/image'; +import { useIntl } from 'react-intl'; import styles from './PostFooter.module.scss'; const PostFooter = ({ topics }: { topics: TopicPreview[] }) => { + const intl = useIntl(); + const getTopics = () => { return topics.map((topic) => { return ( @@ -31,7 +33,12 @@ const PostFooter = ({ topics }: { topics: TopicPreview[] }) => { {topics.length > 0 && ( <> <dl className={styles.meta}> - <dt>{t`Read more articles about:`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Read more articles about:', + description: 'PostFooter: read more posts about given subjects', + })} + </dt> <dd> <ul className={styles.list}>{getTopics()}</ul> </dd> diff --git a/src/components/PostMeta/PostMeta.tsx b/src/components/PostMeta/PostMeta.tsx index f95707a..86e4e71 100644 --- a/src/components/PostMeta/PostMeta.tsx +++ b/src/components/PostMeta/PostMeta.tsx @@ -1,9 +1,9 @@ import { config } from '@config/website'; -import { plural, t } from '@lingui/macro'; import { ArticleMeta } from '@ts/types/articles'; import { getFormattedDate } from '@utils/helpers/format'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import styles from './PostMeta.module.scss'; type PostMetaMode = 'list' | 'single'; @@ -26,6 +26,7 @@ const PostMeta = ({ website, wordsCount, } = meta; + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; const isThematic = () => router.asPath.includes('/thematique/'); @@ -62,24 +63,31 @@ const PostMeta = ({ }; const getCommentsCount = () => { - switch (commentCount) { - case 0: - return t`No comments`; - case 1: - return t`1 comment`; - default: - return t`${commentCount} comments`; - } + return intl.formatMessage( + { + defaultMessage: + '{commentCount, plural, =0 {No comments} one {# comment} other {# comments}}', + description: 'PostMeta: comment count value', + }, + { commentCount } + ); }; const getReadingTime = () => { if (!readingTime) return; - if (readingTime < 0) return t`less than 1 minute`; - return plural(readingTime, { - zero: '# minutes', - one: '# minute', - other: '# minutes', - }); + if (readingTime < 0) + return intl.formatMessage({ + defaultMessage: 'less than 1 minute', + description: 'PostMeta: Reading time value', + }); + return intl.formatMessage( + { + defaultMessage: + '{readingTime, plural, =0 {# minutes} one {# minute} other {# minutes}}', + description: 'PostMeta: reading time value', + }, + { readingTime } + ); }; const getDates = () => { @@ -91,14 +99,24 @@ const PostMeta = ({ return ( <> <div className={styles.item}> - <dt className={styles.term}>{t`Published on:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'PostMeta: publication date label', + })} + </dt> <dd className={styles.description}> <time dateTime={dates.publication}>{publicationDate}</time> </dd> </div> {publicationDate !== updateDate && ( <div className={styles.item}> - <dt className={styles.term}>{t`Updated on:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Updated on:', + description: 'PostMeta: update date label', + })} + </dt> <dd className={styles.description}> <time dateTime={dates.update}>{updateDate}</time> </dd> @@ -114,14 +132,24 @@ const PostMeta = ({ <dl className={wrapperClass}> {author && ( <div className={styles.item}> - <dt className={styles.term}>{t`Written by:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Written by:', + description: 'Article meta', + })} + </dt> <dd className={styles.description}>{author.name}</dd> </div> )} {getDates()} {readingTime !== undefined && wordsCount !== undefined && ( <div className={styles.item}> - <dt className={styles.term}>{t`Reading time:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Reading time:', + description: 'Article meta', + })} + </dt> <dd className={styles.description} title={`Approximately ${wordsCount.toLocaleString(locale)} words`} @@ -132,20 +160,35 @@ const PostMeta = ({ )} {results && ( <div className={styles.item}> - <dt className={styles.term}>{t`Total: `}</dt> - <dd className={styles.description}> - {plural(results, { - zero: '# articles', - one: '# article', - other: '# articles', + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Total:', + description: 'Article meta', })} + </dt> + <dd className={styles.description}> + {intl.formatMessage( + { + defaultMessage: + '{results, plural, =0 {No articles} one {# article} other {# articles}}', + description: 'PostMeta: total found articles', + }, + { results } + )} </dd> </div> )} {!isThematic() && thematics && thematics.length > 0 && ( <div className={styles.item}> <dt className={styles.term}> - {thematics.length > 1 ? t`Thematics:` : t`Thematic:`} + {intl.formatMessage( + { + defaultMessage: + '{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}', + description: 'PostMeta: thematics list label', + }, + { thematicsCount: thematics.length } + )} </dt> {getThematics()} </div> @@ -153,14 +196,26 @@ const PostMeta = ({ {isThematic() && topics && topics.length > 0 && ( <div className={styles.item}> <dt className={styles.term}> - {topics.length > 1 ? t`Topics:` : t`Topic:`} + {intl.formatMessage( + { + defaultMessage: + '{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}', + description: 'PostMeta: topics list label', + }, + { topicsCount: topics.length } + )} </dt> {getTopics()} </div> )} {website && ( <div className={styles.item}> - <dt className={styles.term}>{t`Website:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Website:', + description: 'PostMeta: website label', + })} + </dt> <dd className={styles.description}> <a href={website}>{website}</a> </dd> @@ -168,7 +223,12 @@ const PostMeta = ({ )} {commentCount !== undefined && ( <div className={styles.item}> - <dt className={styles.term}>{t`Comments:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Comments:', + description: 'PostMeta: comment count label', + })} + </dt> <dd className={styles.description}> {isArticle() ? ( <a href="#comments">{getCommentsCount()}</a> diff --git a/src/components/PostPreview/PostPreview.tsx b/src/components/PostPreview/PostPreview.tsx index b084ca1..72ba638 100644 --- a/src/components/PostPreview/PostPreview.tsx +++ b/src/components/PostPreview/PostPreview.tsx @@ -1,15 +1,15 @@ -import PostMeta from '@components/PostMeta/PostMeta'; -import { t } from '@lingui/macro'; -import { ArticleMeta, ArticlePreview } from '@ts/types/articles'; -import Link from 'next/link'; -import styles from './PostPreview.module.scss'; -import Image from 'next/image'; import { ButtonLink } from '@components/Buttons'; import { ArrowIcon } from '@components/Icons'; +import PostMeta from '@components/PostMeta/PostMeta'; +import { config } from '@config/website'; import { TitleLevel } from '@ts/types/app'; -import { BlogPosting, WithContext } from 'schema-dts'; +import { ArticleMeta, ArticlePreview } from '@ts/types/articles'; +import Image from 'next/image'; import Head from 'next/head'; -import { config } from '@config/website'; +import Link from 'next/link'; +import { FormattedMessage } from 'react-intl'; +import { BlogPosting, WithContext } from 'schema-dts'; +import styles from './PostPreview.module.scss'; const PostPreview = ({ post, @@ -97,11 +97,16 @@ const PostPreview = ({ ></div> <footer className={styles.footer}> <ButtonLink target={`/article/${slug}`} position="left"> - {t`Read more`} - <span className="screen-reader-text"> - {' '} - {t({ message: `about ${title}`, comment: 'Post title' })} - </span> + <FormattedMessage + defaultMessage="Read more<a11y> about {title}</a11y>" + description="PostPreview: read more link" + values={{ + title, + a11y: (chunks: string) => ( + <span className="screen-reader-text">{chunks}</span> + ), + }} + /> <ArrowIcon /> </ButtonLink> </footer> diff --git a/src/components/PostsList/PostsList.tsx b/src/components/PostsList/PostsList.tsx index df9dfe4..16deee3 100644 --- a/src/components/PostsList/PostsList.tsx +++ b/src/components/PostsList/PostsList.tsx @@ -1,9 +1,9 @@ -import { t } from '@lingui/macro'; -import { PostsList as PostsListData } from '@ts/types/blog'; -import styles from './PostsList.module.scss'; import PostPreview from '@components/PostPreview/PostPreview'; -import { ForwardedRef, forwardRef, Fragment } from 'react'; +import { PostsList as PostsListData } from '@ts/types/blog'; import { sortPostsByYear } from '@utils/helpers/sort'; +import { ForwardedRef, forwardRef, Fragment } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './PostsList.module.scss'; const PostsList = ( { @@ -15,6 +15,7 @@ const PostsList = ( }, ref: ForwardedRef<HTMLSpanElement> ) => { + const intl = useIntl(); const titleLevel = showYears ? 3 : 2; const getPostsListByYear = () => { @@ -32,7 +33,12 @@ const PostsList = ( <section key={year} className={styles.section}> {showYears && ( <h2 className={styles.year}> - <span className="screen-reader-text">{t`Published in`} </span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Published on', + description: 'PostsList: published on year label', + })}{' '} + </span> {year} </h2> )} @@ -62,7 +68,14 @@ const PostsList = ( }; if (page.posts.length === 0) { - return <p key="no-result">{t`No results found.`}</p>; + return ( + <p key="no-result"> + {intl.formatMessage({ + defaultMessage: 'No results found.', + description: 'PostsList: no results', + })} + </p> + ); } else { return ( <Fragment key={page.pageInfo.endCursor}> diff --git a/src/components/ProjectPreview/ProjectPreview.tsx b/src/components/ProjectPreview/ProjectPreview.tsx index cba0b02..043d945 100644 --- a/src/components/ProjectPreview/ProjectPreview.tsx +++ b/src/components/ProjectPreview/ProjectPreview.tsx @@ -1,12 +1,13 @@ -import { t } from '@lingui/macro'; import { Project } from '@ts/types/app'; import { slugify } from '@utils/helpers/slugify'; import Image from 'next/image'; import Link from 'next/link'; +import { useIntl } from 'react-intl'; import styles from './ProjectPreview.module.scss'; const ProjectPreview = ({ project }: { project: Project }) => { const { id, meta, tagline, title } = project; + const intl = useIntl(); return ( <Link href={`/projet/${project.slug}`}> @@ -20,7 +21,13 @@ const ProjectPreview = ({ project }: { project: Project }) => { layout="fill" objectFit="contain" objectPosition="center" - alt={t`${title} picture`} + alt={intl.formatMessage( + { + defaultMessage: '{title} picture', + description: 'ProjectPreview: cover alt text', + }, + { title } + )} /> </div> )} @@ -36,7 +43,16 @@ const ProjectPreview = ({ project }: { project: Project }) => { <dl className={styles.meta}> {meta.technologies && ( <div className={styles.meta__item}> - <dt className="screen-reader-text">{t`Technologies:`}</dt> + <dt className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: + '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}', + description: 'ProjectPreview: technologies list label', + }, + { count: meta.technologies.length } + )} + </dt> {meta.technologies.map((techno) => ( <dd key={slugify(techno)} className={styles.techno}> {techno} diff --git a/src/components/ProjectSummary/ProjectSummary.tsx b/src/components/ProjectSummary/ProjectSummary.tsx index b32c11f..f2d73b6 100644 --- a/src/components/ProjectSummary/ProjectSummary.tsx +++ b/src/components/ProjectSummary/ProjectSummary.tsx @@ -1,13 +1,14 @@ import GithubIcon from '@assets/images/social-media/github.svg'; import GitlabIcon from '@assets/images/social-media/gitlab.svg'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { ProjectMeta } from '@ts/types/app'; import { getFormattedDate } from '@utils/helpers/format'; import { slugify } from '@utils/helpers/slugify'; import useGithubApi from '@utils/hooks/useGithubApi'; +import IntlMessageFormat from 'intl-messageformat'; import Image from 'next/image'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import styles from './ProjectSummary.module.scss'; const ProjectSummary = ({ @@ -20,6 +21,7 @@ const ProjectSummary = ({ meta: ProjectMeta; }) => { const { hasCover, license, repos, technologies } = meta; + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; const { data } = useGithubApi(repos?.github ? repos.github : ''); @@ -30,7 +32,10 @@ const ProjectSummary = ({ <div className={styles.cover}> <Image src={`/projects/${id}.jpg`} - alt={t`${title} preview`} + alt={intl.formatMessage({ + defaultMessage: '{title} preview', + description: 'ProjectSummary: cover alt text', + })} layout="fill" objectFit="contain" /> @@ -39,7 +44,12 @@ const ProjectSummary = ({ <dl className={styles.info}> {data && ( <div className={styles.info__item}> - <dt>{t`Created on`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Created on:', + description: 'ProjectSummary: creation date label', + })} + </dt> <dd> <time dateTime={data.created_at}> {getFormattedDate(data.created_at, locale)} @@ -49,7 +59,12 @@ const ProjectSummary = ({ )} {data && ( <div className={styles.info__item}> - <dt>{t`Last updated on`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Last updated on:', + description: 'ProjectSummary: update date label', + })} + </dt> <dd> <time dateTime={data.updated_at}> {getFormattedDate(data.updated_at, locale)} @@ -58,12 +73,26 @@ const ProjectSummary = ({ </div> )} <div className={styles.info__item}> - <dt>{t`License`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'License:', + description: 'ProjectSummary: license label', + })} + </dt> <dd>{license}</dd> </div> {technologies && ( <div className={styles.info__item}> - <dt>{t`Technologies`}</dt> + <dt> + {intl.formatMessage( + { + defaultMessage: + '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}', + description: 'ProjectSummary: technologies list label', + }, + { count: technologies.length } + )} + </dt> {technologies.map((techno) => ( <dd key={slugify(techno)} @@ -76,7 +105,16 @@ const ProjectSummary = ({ )} {repos && ( <div className={styles.info__item}> - <dt>{t`Repositories`}</dt> + <dt> + {intl.formatMessage( + { + defaultMessage: + '{count, plural, =0 {Repositories:} one {Repository:} other {Repositories:}}', + description: 'ProjectSummary: repositories list label', + }, + { count: Object.keys(repos).length } + )} + </dt> {repos.github && ( <dd className={styles['inline-data']}> <a @@ -103,12 +141,24 @@ const ProjectSummary = ({ )} {data && repos && ( <div> - <dt>{t`Popularity`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Popularity:', + description: 'ProjectSummary: popularity label', + })} + </dt> {repos.github && ( <dd> ⭐ <a href={`https://github.com/${repos.github}/stargazers`}> - {t`${data.stargazers_count} stars on Github`} + {intl.formatMessage( + { + defaultMessage: + '{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}', + description: 'ProjectPreview: technologies list label', + }, + { starsCount: data.stargazers_count } + )} </a> </dd> )} diff --git a/src/components/SearchForm/SearchForm.tsx b/src/components/SearchForm/SearchForm.tsx index cefda85..38ae60d 100644 --- a/src/components/SearchForm/SearchForm.tsx +++ b/src/components/SearchForm/SearchForm.tsx @@ -1,12 +1,13 @@ import { ButtonSubmit } from '@components/Buttons'; import { Form, Input } from '@components/Form'; import { SearchIcon } from '@components/Icons'; -import { t } from '@lingui/macro'; import { useRouter } from 'next/router'; import { FormEvent, useEffect, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './SearchForm.module.scss'; const SearchForm = ({ isOpened }: { isOpened: boolean }) => { + const intl = useIntl(); const [query, setQuery] = useState(''); const inputRef = useRef<HTMLInputElement>(null); const router = useRouter(); @@ -27,7 +28,12 @@ const SearchForm = ({ isOpened }: { isOpened: boolean }) => { return ( <> - <div className={styles.title}>{t`Search`}</div> + <div className={styles.title}> + {intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchForm : form title', + })} + </div> <Form submitHandler={launchSearch} modifier="search"> <Input ref={inputRef} @@ -39,7 +45,12 @@ const SearchForm = ({ isOpened }: { isOpened: boolean }) => { /> <ButtonSubmit modifier="search"> <SearchIcon /> - <span className="screen-reader-text">{t`Search`}</span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchForm: search button text', + })} + </span> </ButtonSubmit> </Form> </> diff --git a/src/components/Settings/ReduceMotion/ReduceMotion.tsx b/src/components/Settings/ReduceMotion/ReduceMotion.tsx index 01f8b67..c7b5775 100644 --- a/src/components/Settings/ReduceMotion/ReduceMotion.tsx +++ b/src/components/Settings/ReduceMotion/ReduceMotion.tsx @@ -1,9 +1,10 @@ import { Toggle } from '@components/Form'; -import { t } from '@lingui/macro'; import { LocalStorage } from '@services/local-storage'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; const ReduceMotion = () => { + const intl = useIntl(); const [isDeactivated, setIsDeactivated] = useState<boolean>(false); useEffect(() => { @@ -23,9 +24,18 @@ const ReduceMotion = () => { return ( <Toggle id="reduced-motion" - label={t`Animations:`} - leftChoice={t`On`} - rightChoice={t`Off`} + label={intl.formatMessage({ + defaultMessage: 'Animations:', + description: 'ReduceMotion: toggle label', + })} + leftChoice={intl.formatMessage({ + defaultMessage: 'On', + description: 'ReduceMotion: toggle on label', + })} + rightChoice={intl.formatMessage({ + defaultMessage: 'Off', + description: 'ReduceMotion: toggle off label', + })} value={isDeactivated} changeHandler={updateState} /> diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index bd2f33d..80eb0c3 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1,14 +1,20 @@ import { CogIcon } from '@components/Icons'; import ThemeToggle from '@components/Settings/ThemeToggle/ThemeToggle'; -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import ReduceMotion from './ReduceMotion/ReduceMotion'; import styles from './Settings.module.scss'; const Settings = () => { + const intl = useIntl(); + return ( <> <div className={styles.title}> - <CogIcon /> {t`Settings`} + <CogIcon />{' '} + {intl.formatMessage({ + defaultMessage: 'Settings', + description: 'Settings: modal title', + })} </div> <ThemeToggle /> <ReduceMotion /> diff --git a/src/components/Settings/ThemeToggle/ThemeToggle.tsx b/src/components/Settings/ThemeToggle/ThemeToggle.tsx index e14f39a..5b7a34d 100644 --- a/src/components/Settings/ThemeToggle/ThemeToggle.tsx +++ b/src/components/Settings/ThemeToggle/ThemeToggle.tsx @@ -1,11 +1,12 @@ import { Toggle } from '@components/Form'; import { MoonIcon, SunIcon } from '@components/Icons'; import Spinner from '@components/Spinner/Spinner'; -import { t } from '@lingui/macro'; import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; const ThemeToggle = () => { + const intl = useIntl(); const [isMounted, setIsMounted] = useState<boolean>(false); const { resolvedTheme, setTheme } = useTheme(); @@ -24,7 +25,10 @@ const ThemeToggle = () => { return ( <Toggle id="dark-theme" - label={t`Theme:`} + label={intl.formatMessage({ + defaultMessage: 'Theme:', + description: 'ThemeToggle: toggle label', + })} leftChoice={<SunIcon />} rightChoice={<MoonIcon />} value={isDarkTheme} diff --git a/src/components/Spinner/Spinner.tsx b/src/components/Spinner/Spinner.tsx index cfa5717..381fbb6 100644 --- a/src/components/Spinner/Spinner.tsx +++ b/src/components/Spinner/Spinner.tsx @@ -1,13 +1,20 @@ -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './Spinner.module.scss'; const Spinner = () => { + const intl = useIntl(); + return ( <div className={styles.wrapper}> <div className={styles.ball}></div> <div className={styles.ball}></div> <div className={styles.ball}></div> - <div className={styles.text}>{t`Loading...`}</div> + <div className={styles.text}> + {intl.formatMessage({ + defaultMessage: 'Loading...', + description: 'Spinner: loading text', + })} + </div> </div> ); }; diff --git a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx b/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx index 52b5c06..6a19d92 100644 --- a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx +++ b/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx @@ -1,6 +1,6 @@ -import { t } from '@lingui/macro'; import { TitleLevel } from '@ts/types/app'; import { ReactNode, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './ExpandableWidget.module.scss'; const ExpandableWidget = ({ @@ -16,6 +16,7 @@ const ExpandableWidget = ({ expand?: boolean; withBorders?: boolean; }) => { + const intl = useIntl(); const [isExpanded, setIsExpanded] = useState<boolean>(expand); const handleExpanse = () => setIsExpanded((prev) => !prev); @@ -34,7 +35,15 @@ const ExpandableWidget = ({ <div className={wrapperClasses}> <button type="button" className={styles.header} onClick={handleExpanse}> <span className="screen-reader-text"> - {isExpanded ? t`Collapse` : t`Expand`} + {isExpanded + ? intl.formatMessage({ + defaultMessage: 'Collapse', + description: 'ExpandableWidget: collapse text', + }) + : intl.formatMessage({ + defaultMessage: 'Expand', + description: 'ExpandableWidget: expand text', + })} </span> <TitleTag className={styles.title}>{title}</TitleTag> <span className={styles.icon} aria-hidden={true}></span> diff --git a/src/components/Widgets/CVPreview/CVPreview.tsx b/src/components/Widgets/CVPreview/CVPreview.tsx index e52a9b2..08a4c72 100644 --- a/src/components/Widgets/CVPreview/CVPreview.tsx +++ b/src/components/Widgets/CVPreview/CVPreview.tsx @@ -1,7 +1,7 @@ import { ExpandableWidget } from '@components/WidgetParts'; -import { Trans } from '@lingui/macro'; import Image from 'next/image'; import Link from 'next/link'; +import { FormattedMessage } from 'react-intl'; import styles from './CVPreview.module.scss'; const CVPreview = ({ @@ -25,9 +25,17 @@ const CVPreview = ({ /> </div> <p> - <Trans> - Download <Link href={pdf}>CV in PDF</Link> - </Trans> + <FormattedMessage + defaultMessage="Download <link>CV in PDF</link>" + description="CVPreview: download as PDF link" + values={{ + link: (chunks: string) => ( + <Link href={pdf}> + <a>{chunks}</a> + </Link> + ), + }} + /> </p> </ExpandableWidget> ); diff --git a/src/components/Widgets/RecentPosts/RecentPosts.tsx b/src/components/Widgets/RecentPosts/RecentPosts.tsx index 8022bff..08ce7e4 100644 --- a/src/components/Widgets/RecentPosts/RecentPosts.tsx +++ b/src/components/Widgets/RecentPosts/RecentPosts.tsx @@ -1,16 +1,17 @@ import Spinner from '@components/Spinner/Spinner'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { getPublishedPosts } from '@services/graphql/queries'; import { ArticlePreview } from '@ts/types/articles'; import { getFormattedDate } from '@utils/helpers/format'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import useSWR from 'swr'; import styles from './RecentPosts.module.scss'; const RecentPosts = () => { + const intl = useIntl(); const { data, error } = useSWR('/recent-posts', () => getPublishedPosts({ first: 3 }) ); @@ -36,7 +37,12 @@ const RecentPosts = () => { )} <h3 className={styles.title}>{post.title}</h3> <dl className={styles.meta}> - <dt>{t`Published on:`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'RecentPosts: publication date label', + })} + </dt> <dd> <time dateTime={post.dates.publication}> {getFormattedDate(post.dates.publication, locale)} @@ -51,7 +57,11 @@ const RecentPosts = () => { }; const getPostsItems = () => { - if (error) return t`Failed to load.`; + if (error) + return intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'RecentPosts: failed to load text', + }); if (!data) return <Spinner />; return data.posts.map((post) => getPost(post)); diff --git a/src/components/Widgets/RelatedThematics/RelatedThematics.tsx b/src/components/Widgets/RelatedThematics/RelatedThematics.tsx index afe3460..c6be3ca 100644 --- a/src/components/Widgets/RelatedThematics/RelatedThematics.tsx +++ b/src/components/Widgets/RelatedThematics/RelatedThematics.tsx @@ -1,9 +1,10 @@ import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { ThematicPreview } from '@ts/types/taxonomies'; import Link from 'next/link'; +import { useIntl } from 'react-intl'; const RelatedThematics = ({ thematics }: { thematics: ThematicPreview[] }) => { + const intl = useIntl(); const sortedThematics = [...thematics].sort((a, b) => a.title.localeCompare(b.title) ); @@ -20,7 +21,14 @@ const RelatedThematics = ({ thematics }: { thematics: ThematicPreview[] }) => { return ( <ExpandableWidget - title={thematics.length > 1 ? t`Related thematics` : t`Related thematic`} + title={intl.formatMessage( + { + defaultMessage: + '{thematicsCount, plural, =0 {Related thematics} one {Related thematic} other {Related thematics}}', + description: 'RelatedThematics: widget title', + }, + { thematicsCount: thematics.length } + )} withBorders={true} > <List items={thematicsList} /> diff --git a/src/components/Widgets/RelatedTopics/RelatedTopics.tsx b/src/components/Widgets/RelatedTopics/RelatedTopics.tsx index 178e5bc..b9699e2 100644 --- a/src/components/Widgets/RelatedTopics/RelatedTopics.tsx +++ b/src/components/Widgets/RelatedTopics/RelatedTopics.tsx @@ -1,9 +1,10 @@ import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { TopicPreview } from '@ts/types/taxonomies'; import Link from 'next/link'; +import { useIntl } from 'react-intl'; const RelatedTopics = ({ topics }: { topics: TopicPreview[] }) => { + const intl = useIntl(); const sortedTopics = [...topics].sort((a, b) => a.title.localeCompare(b.title) ); @@ -20,7 +21,14 @@ const RelatedTopics = ({ topics }: { topics: TopicPreview[] }) => { return ( <ExpandableWidget - title={topicsList.length > 1 ? t`Related topics` : t`Related topic`} + title={intl.formatMessage( + { + defaultMessage: + '{topicsCount, plural, =0 {Related topics} one {Related topic} other {Related topics}}', + description: 'RelatedTopics: widget title', + }, + { topicsCount: topicsList.length } + )} withBorders={true} > <List items={topicsList} /> diff --git a/src/components/Widgets/Sharing/Sharing.tsx b/src/components/Widgets/Sharing/Sharing.tsx index 89b48ca..1025717 100644 --- a/src/components/Widgets/Sharing/Sharing.tsx +++ b/src/components/Widgets/Sharing/Sharing.tsx @@ -1,8 +1,8 @@ import { ExpandableWidget } from '@components/WidgetParts'; import sharingMedia from '@config/sharing'; -import { t } from '@lingui/macro'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './Sharing.module.scss'; type Parameters = { @@ -20,6 +20,7 @@ type Website = { }; const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { + const intl = useIntl(); const [pageExcerpt, setPageExcerpt] = useState(''); const [pageUrl, setPageUrl] = useState(''); const [domainName, setDomainName] = useState(''); @@ -54,8 +55,14 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { switch (key) { case 'content': if (id === 'email') { - const intro = t`Introduction:`; - const readMore = t`Read more here:`; + const intro = intl.formatMessage({ + defaultMessage: 'Introduction:', + description: 'Sharing: email content prefix', + }); + const readMore = intl.formatMessage({ + defaultMessage: 'Read more here:', + description: 'Sharing: content link prefix', + }); const body = `${intro}\n\n"${pageExcerpt}"\n\n${readMore} ${pageUrl}`; sharingUrl += encodeURI(body); } else { @@ -63,7 +70,16 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { } break; case 'title': - const prefix = id === 'email' ? t`Seen on ${domainName}:` : ''; + const prefix = + id === 'email' + ? intl.formatMessage( + { + defaultMessage: 'Seen on {domainName}:', + description: 'Sharing: seen on text', + }, + { domainName } + ) + : ''; sharingUrl += encodeURI(`${prefix} ${title}`); break; case 'url': @@ -94,7 +110,15 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { title={name} className={`${styles.link} ${styles[linkModifier]}`} > - <span className="screen-reader-text">{t`Share on ${name}`}</span> + <span className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: 'Share on {name}', + description: 'Sharing: share on social network text', + }, + { name } + )} + </span> </a> </li> ); @@ -102,7 +126,13 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { }; return ( - <ExpandableWidget title={t`Share`} expand={true}> + <ExpandableWidget + title={intl.formatMessage({ + defaultMessage: 'Share', + description: 'Sharing: widget title', + })} + expand={true} + > <ul className={`${styles.list} ${styles['list--sharing']}`}> {getItems()} </ul> diff --git a/src/components/Widgets/ThematicsList/ThematicsList.tsx b/src/components/Widgets/ThematicsList/ThematicsList.tsx index e5162b4..9b1f03a 100644 --- a/src/components/Widgets/ThematicsList/ThematicsList.tsx +++ b/src/components/Widgets/ThematicsList/ThematicsList.tsx @@ -1,10 +1,10 @@ import Spinner from '@components/Spinner/Spinner'; import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { getAllThematics } from '@services/graphql/queries'; import { TitleLevel } from '@ts/types/app'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import useSWR from 'swr'; const ThematicsList = ({ @@ -14,6 +14,7 @@ const ThematicsList = ({ title: string; titleLevel?: TitleLevel; }) => { + const intl = useIntl(); const router = useRouter(); const isThematic = () => router.asPath.includes('/thematique/'); const currentThematicSlug = isThematic() @@ -23,7 +24,15 @@ const ThematicsList = ({ const { data, error } = useSWR('/api/thematics', getAllThematics); const getList = () => { - if (error) return <ul>{t`Failed to load.`}</ul>; + if (error) + return ( + <ul> + {intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'ThematicsList: failed to load text', + })} + </ul> + ); if (!data) return ( <ul> diff --git a/src/components/Widgets/ToC/ToC.tsx b/src/components/Widgets/ToC/ToC.tsx index 6010354..c499478 100644 --- a/src/components/Widgets/ToC/ToC.tsx +++ b/src/components/Widgets/ToC/ToC.tsx @@ -1,11 +1,15 @@ import { ExpandableWidget, OrderedList } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { Heading } from '@ts/types/app'; import useHeadingsTree from '@utils/hooks/useHeadingsTree'; +import { useIntl } from 'react-intl'; const ToC = () => { + const intl = useIntl(); const headingsTree = useHeadingsTree('article'); - const title = t`Table of contents`; + const title = intl.formatMessage({ + defaultMessage: 'Table of contents', + description: 'ToC: widget title', + }); const getItems = (headings: Heading[]) => { return headings.map((heading) => { diff --git a/src/components/Widgets/TopicsList/TopicsList.tsx b/src/components/Widgets/TopicsList/TopicsList.tsx index 5b0c44e..80341c3 100644 --- a/src/components/Widgets/TopicsList/TopicsList.tsx +++ b/src/components/Widgets/TopicsList/TopicsList.tsx @@ -1,10 +1,10 @@ import Spinner from '@components/Spinner/Spinner'; import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { getAllTopics } from '@services/graphql/queries'; import { TitleLevel } from '@ts/types/app'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import useSWR from 'swr'; const TopicsList = ({ @@ -14,6 +14,7 @@ const TopicsList = ({ title: string; titleLevel?: TitleLevel; }) => { + const intl = useIntl(); const router = useRouter(); const isTopic = () => router.asPath.includes('/sujet/'); const currentTopicSlug = isTopic() @@ -23,7 +24,15 @@ const TopicsList = ({ const { data, error } = useSWR('/api/topics', getAllTopics); const getList = () => { - if (error) return <ul>{t`Failed to load.`}</ul>; + if (error) + return ( + <ul> + {intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'TopicsList: failed to load text', + })} + </ul> + ); if (!data) return ( <ul> diff --git a/src/config/nav.ts b/src/config/nav.ts deleted file mode 100644 index ff69a78..0000000 --- a/src/config/nav.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { t } from '@lingui/macro'; -import { NavItem } from '@ts/types/nav'; - -export const mainNav: NavItem[] = [ - { id: 'home', name: t`Home`, slug: '/' }, - { id: 'blog', name: t`Blog`, slug: '/blog' }, - { id: 'projects', name: t`Projects`, slug: '/projets' }, - { id: 'cv', name: t`Resume`, slug: '/cv' }, - { id: 'contact', name: t`Contact`, slug: '/contact' }, -]; - -export const footerNav: NavItem[] = [ - { id: 'legal-notice', name: t`Legal notice`, slug: '/mentions-legales' }, -]; diff --git a/src/config/seo.ts b/src/config/seo.ts deleted file mode 100644 index 3ff08b6..0000000 --- a/src/config/seo.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { t } from '@lingui/macro'; -import { config } from './website'; - -export const seo = { - homepage: { - title: t`${config.name} | Front-end developer: WordPress/React`, - description: t`${config.name} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.`, - }, - blog: { - title: t`Blog: development, open source - ${config.name}`, - description: t`Discover ${config.name}'s writings. He talks about web development, Linux and open source mostly.`, - }, - cv: { - title: t`CV Front-end developer - ${config.name}`, - description: t`Discover the curriculum of ${config.name}, front-end developer located in France: skills, experiences and training.`, - }, - contact: { - title: t`Contact form - ${config.name}`, - description: t`Contact ${config.name} through its website. All you need to do it's to fill the contact form.`, - }, - legalNotice: { - title: t`Legal notice - ${config.name}`, - description: t`Discover the legal notice of ${config.name}'s website.`, - }, - error404: { - title: t`Error 404: Page not found - ${config.name}`, - description: '', - }, - projects: { - title: t`Projects: open-source makings - ${config.name}`, - description: t`Discover ${config.name} projects. Mostly related to web development and open source.`, - }, -}; diff --git a/src/config/sharing.ts b/src/config/sharing.ts index 580145e..9e84801 100644 --- a/src/config/sharing.ts +++ b/src/config/sharing.ts @@ -1,5 +1,3 @@ -import { t } from '@lingui/macro'; - const sharingMedia = [ { id: 'diaspora', @@ -58,7 +56,7 @@ const sharingMedia = [ }, { id: 'email', - name: t`Email`, + name: 'Email', parameters: { content: 'body', image: '', diff --git a/src/config/website.ts b/src/config/website.ts index a359d9a..81c493f 100644 --- a/src/config/website.ts +++ b/src/config/website.ts @@ -1,8 +1,6 @@ -import { t } from '@lingui/macro'; - export const config = { name: 'Armand Philippot', - baseline: t`Front-end developer`, + baseline: 'Front-end developer', copyright: { startYear: '2012', endYear: new Date().getFullYear(), diff --git a/src/i18n/en.json b/src/i18n/en.json new file mode 100644 index 0000000..23f5278 --- /dev/null +++ b/src/i18n/en.json @@ -0,0 +1,586 @@ +{ + "+4tiVb": { + "defaultMessage": "Others topics", + "description": "TopicPage: topics list widget title" + }, + "+COyEW": { + "defaultMessage": "{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}", + "description": "PostMeta: topics list label" + }, + "+Dre5J": { + "defaultMessage": "Open-source projects", + "description": "CVPage: social media widget title" + }, + "+Y+tLK": { + "defaultMessage": "Blog: development, open source - {websiteName}", + "description": "BlogPage: SEO - Page title" + }, + "/IirIt": { + "defaultMessage": "Legal notice", + "description": "LegalNoticePage: page title" + }, + "/ly3AC": { + "defaultMessage": "Copy", + "description": "Prism: copy button text (no clicked)" + }, + "00Pf5p": { + "defaultMessage": "Failed to load.", + "description": "TopicsList: failed to load text" + }, + "16zl9Z": { + "defaultMessage": "You are here:", + "description": "Breadcrumb: You are here prefix" + }, + "18h/t0": { + "defaultMessage": "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.", + "description": "BlogPage: SEO - Meta description" + }, + "1h+N2z": { + "defaultMessage": "Published on:", + "description": "RecentPosts: publication date label" + }, + "2D9tB5": { + "defaultMessage": "Topics", + "description": "BlogPage: topics list widget title" + }, + "2pykor": { + "defaultMessage": "{title} picture", + "description": "ProjectPreview: cover alt text" + }, + "310o3F": { + "defaultMessage": "Error 404: Page not found - {websiteName}", + "description": "404Page: SEO - Page title" + }, + "43OmTY": { + "defaultMessage": "Thanks. Your message was successfully sent. I will answer it as soon as possible.", + "description": "ContactPage: success message" + }, + "48Ww//": { + "defaultMessage": "Page not found.", + "description": "404Page: SEO - Meta description" + }, + "4zAUSu": { + "defaultMessage": "Legal notice - {websiteName}", + "description": "LegalNoticePage: SEO - Page title" + }, + "7TbbIk": { + "defaultMessage": "Blog", + "description": "BlogPage: page title" + }, + "8Ls2mD": { + "defaultMessage": "Please fill the form to contact me.", + "description": "ContactPage: page introduction" + }, + "A4LTGq": { + "defaultMessage": "Discover search results for {query}", + "description": "SearchPage: meta description with query" + }, + "AN9iy7": { + "defaultMessage": "Contact", + "description": "ContactPage: page title" + }, + "AnaPbu": { + "defaultMessage": "Search", + "description": "SearchForm: search button text" + }, + "B9OCyV": { + "defaultMessage": "Others formats", + "description": "CVPage: cv preview widget title" + }, + "C/XGkH": { + "defaultMessage": "Failed to load.", + "description": "BlogPage: failed to load text" + }, + "CSimmh": { + "defaultMessage": "{articlesCount, plural, =0 {# articles} one {# article} other {# articles}} out of a total of {total}", + "description": "PaginationCursor: loaded articles count message" + }, + "CT3ydM": { + "defaultMessage": "{date} at {time}", + "description": "Comment: publication date" + }, + "CWi0go": { + "defaultMessage": "Created on:", + "description": "ProjectSummary: creation date label" + }, + "CzTbM4": { + "defaultMessage": "Contact", + "description": "ContactPage: breadcrumb item" + }, + "Dq6+WH": { + "defaultMessage": "Thematics", + "description": "SearchPage: thematics list widget title" + }, + "Enij19": { + "defaultMessage": "Home", + "description": "Breadcrumb: Home item" + }, + "EvODgw": { + "defaultMessage": "Published on", + "description": "PostsList: published on year label" + }, + "F7QxJH": { + "defaultMessage": "Name", + "description": "CommentForm: Name field label" + }, + "FLkF2R": { + "defaultMessage": "All posts in {name}", + "description": "TopicPage: posts list title" + }, + "Fj8WFC": { + "defaultMessage": "{results, plural, =0 {No articles} one {# article} other {# articles}}", + "description": "PostMeta: total found articles" + }, + "FtokGF": { + "defaultMessage": "Updated on:", + "description": "PostMeta: update date label" + }, + "G/SLvC": { + "defaultMessage": "Thanks for your comment! It is now awaiting moderation.", + "description": "CommentForm: Comment sent success message" + }, + "GUfnQ4": { + "defaultMessage": "Reading time:", + "description": "Article meta" + }, + "HriY57": { + "defaultMessage": "Thematics", + "description": "BlogPage: thematics list widget title" + }, + "Hs+q2V": { + "defaultMessage": "Email", + "description": "ContactPage: email field label" + }, + "ILRLTq": { + "defaultMessage": "{brandingName} picture", + "description": "Branding: branding name picture." + }, + "Igx3qp": { + "defaultMessage": "Projects", + "description": "Breadcrumb: Projects item" + }, + "J4nhm4": { + "defaultMessage": "Comment", + "description": "CommentForm: Comment field label" + }, + "Ji6xwo": { + "defaultMessage": "An error occurred:", + "description": "ContactPage: error message" + }, + "KERk7L": { + "defaultMessage": "Filter by:", + "description": "BlogPage: sidebar title" + }, + "KeRtm/": { + "defaultMessage": "Light theme", + "description": "Icons: Sun icon (light theme)" + }, + "Kqq2cm": { + "defaultMessage": "Load more?", + "description": "BlogPage: load more text" + }, + "Mj2BQf": { + "defaultMessage": "{name}'s CV", + "description": "CVPage: page title" + }, + "N44SOc": { + "defaultMessage": "Projects", + "description": "HomePage: link to projects" + }, + "N7I4lC": { + "defaultMessage": "less than 1 minute", + "description": "PostMeta: Reading time value" + }, + "N804XO": { + "defaultMessage": "Topics", + "description": "SearchPage: topics list widget title" + }, + "Ns8CFb": { + "defaultMessage": "Comments", + "description": "CommentsList: Comments section title" + }, + "O9XLDc": { + "defaultMessage": "Theme:", + "description": "ThemeToggle: toggle label" + }, + "OIffB4": { + "defaultMessage": "Contact {websiteName} through its website. All you need to do it's to fill the contact form.", + "description": "ContactPage: SEO - Meta description" + }, + "OTTv+m": { + "defaultMessage": "{count, plural, =0 {Repositories:} one {Repository:} other {Repositories:}}", + "description": "ProjectSummary: repositories list label" + }, + "OV9r1K": { + "defaultMessage": "Copied!", + "description": "Prism: copy button text (clicked)" + }, + "OccTWi": { + "defaultMessage": "Page not found", + "description": "404Page: page title" + }, + "Oim3rQ": { + "defaultMessage": "Email", + "description": "CommentForm: Email field label" + }, + "Ox/daH": { + "defaultMessage": "{commentCount, plural, =0 {No comments} one {# comment} other {# comments}}", + "description": "PostMeta: comment count value" + }, + "P7fxX2": { + "defaultMessage": "All posts in {name}", + "description": "ThematicPage: posts list title" + }, + "PXp2hv": { + "defaultMessage": "{websiteName} | Front-end developer: WordPress/React", + "description": "HomePage: SEO - Page title" + }, + "PrIz5o": { + "defaultMessage": "Search for a post on {websiteName}", + "description": "SearchPage: meta description without query" + }, + "PxMDzL": { + "defaultMessage": "Failed to load.", + "description": "ThematicsList: failed to load text" + }, + "Qh2CwH": { + "defaultMessage": "Find me elsewhere", + "description": "ContactPage: social media widget title" + }, + "R0eDmw": { + "defaultMessage": "Blog", + "description": "BlogPage: breadcrumb item" + }, + "SWq8a4": { + "defaultMessage": "Close {type}", + "description": "ButtonToolbar: Close button" + }, + "SX1z3t": { + "defaultMessage": "Projects: open-source makings - {websiteName}", + "description": "ProjectsPage: SEO - Page title" + }, + "T4YA64": { + "defaultMessage": "Subscribe", + "description": "HomePage: RSS feed subscription text" + }, + "TfU6Qm": { + "defaultMessage": "Search", + "description": "SearchPage: breadcrumb item" + }, + "U++A+B": { + "defaultMessage": "{readingTime, plural, =0 {# minutes} one {# minute} other {# minutes}}", + "description": "PostMeta: reading time value" + }, + "U+35YD": { + "defaultMessage": "Search", + "description": "SearchPage: page title" + }, + "UsQske": { + "defaultMessage": "Read more here:", + "description": "Sharing: content link prefix" + }, + "VSGuGE": { + "defaultMessage": "Search results for {query}", + "description": "SearchPage: search results text" + }, + "W2G95o": { + "defaultMessage": "Comments:", + "description": "PostMeta: comment count label" + }, + "WGFOmA": { + "defaultMessage": "Send", + "description": "CommentForm: Send button" + }, + "WRkY1/": { + "defaultMessage": "Collapse", + "description": "ExpandableWidget: collapse text" + }, + "X3PDXO": { + "defaultMessage": "Animations:", + "description": "ReduceMotion: toggle label" + }, + "Y1ZdJ6": { + "defaultMessage": "CV Front-end developer - {websiteName}", + "description": "CVPage: SEO - Page title" + }, + "Y3qRib": { + "defaultMessage": "Contact form - {websiteName}", + "description": "ContactPage: SEO - Page title" + }, + "YEudoh": { + "defaultMessage": "Read more articles about:", + "description": "PostFooter: read more posts about given subjects" + }, + "Z1eSIz": { + "defaultMessage": "Open {type}", + "description": "ButtonToolbar: Open button" + }, + "ZJMNRW": { + "defaultMessage": "Home", + "description": "MainNav: home link" + }, + "ZWh78Y": { + "defaultMessage": "Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem.", + "description": "404Page: page body" + }, + "Zg4L7U": { + "defaultMessage": "Table of contents", + "description": "ToC: widget title" + }, + "aQLBE3": { + "defaultMessage": "{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}", + "description": "ProjectPreview: technologies list label" + }, + "agLf5v": { + "defaultMessage": "Website:", + "description": "PostMeta: website label" + }, + "akSutM": { + "defaultMessage": "Projects", + "description": "MainNav: projects link" + }, + "azc1GT": { + "defaultMessage": "Open menu", + "description": "MainNav: open button" + }, + "bBdMGm": { + "defaultMessage": "Discover the curriculum of {websiteName}, front-end developer located in France: skills, experiences and training.", + "description": "CVPage: SEO - Meta description" + }, + "bHEmkY": { + "defaultMessage": "Settings", + "description": "Settings: modal title" + }, + "bkbrN7": { + "defaultMessage": "Read more<a11y> about {title}</a11y>", + "description": "PostPreview: read more link" + }, + "c2NtPj": { + "defaultMessage": "Contact", + "description": "MainNav: contact link" + }, + "cr2fA4": { + "defaultMessage": "Subject", + "description": "ContactPage: subject field label" + }, + "dE8xxV": { + "defaultMessage": "Close menu", + "description": "MainNav: close button" + }, + "dqrd6I": { + "defaultMessage": "Back to top", + "description": "Footer: Back to top button" + }, + "e9L59q": { + "defaultMessage": "No comments yet.", + "description": "CommentsList: No comment message" + }, + "eFMu2E": { + "defaultMessage": "Search", + "description": "SearchForm : form title" + }, + "eUXMG4": { + "defaultMessage": "Seen on {domainName}:", + "description": "Sharing: seen on text" + }, + "ec3m6p": { + "defaultMessage": "Leave a comment", + "description": "CommentForm: form title" + }, + "enwhNm": { + "defaultMessage": "{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}", + "description": "ProjectSummary: technologies list label" + }, + "fGnfqp": { + "defaultMessage": "Published on:", + "description": "PostMeta: publication date label" + }, + "fOe8rH": { + "defaultMessage": "Failed to load.", + "description": "SearchPage: failed to load text" + }, + "hKagVG": { + "defaultMessage": "License:", + "description": "ProjectSummary: license label" + }, + "hV0qHp": { + "defaultMessage": "Expand", + "description": "ExpandableWidget: expand text" + }, + "hzHuCc": { + "defaultMessage": "Reply", + "description": "Comment: reply button" + }, + "iqAbyn": { + "defaultMessage": "Skip to content", + "description": "Layout: Skip to content button" + }, + "iyEh0R": { + "defaultMessage": "Failed to load.", + "description": "RecentPosts: failed to load text" + }, + "jASD7k": { + "defaultMessage": "Linux", + "description": "HomePage: link to Linux thematic" + }, + "jGqV2+": { + "defaultMessage": "Written by:", + "description": "Article meta" + }, + "jN+dY5": { + "defaultMessage": "Website", + "description": "CommentForm: Website field label" + }, + "jpv+Nz": { + "defaultMessage": "Resume", + "description": "MainNav: resume link" + }, + "l0+ROl": { + "defaultMessage": "{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}", + "description": "PostMeta: thematics list label" + }, + "mC21ht": { + "defaultMessage": "Number of articles loaded out of the total available.", + "description": "PaginationCursor: loaded articles count aria-label" + }, + "mJLflX": { + "defaultMessage": "Send", + "description": "ContactPage: send button text" + }, + "mh7tGg": { + "defaultMessage": "{title} preview", + "description": "ProjectSummary: cover alt text" + }, + "norrGp": { + "defaultMessage": "Others thematics", + "description": "ThematicPage: thematics list widget title" + }, + "ode0YK": { + "defaultMessage": "Dark theme", + "description": "Icons: Moon icon (dark theme)" + }, + "okFrAO": { + "defaultMessage": "{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}", + "description": "ProjectPreview: technologies list label" + }, + "pEtJik": { + "defaultMessage": "Load more?", + "description": "SearchPage: load more text" + }, + "q3U6uI": { + "defaultMessage": "Share", + "description": "Sharing: widget title" + }, + "q9cJQe": { + "defaultMessage": "Loading...", + "description": "Spinner: loading text" + }, + "qPU/Qn": { + "defaultMessage": "On", + "description": "ReduceMotion: toggle on label" + }, + "qXQETZ": { + "defaultMessage": "{thematicsCount, plural, =0 {Related thematics} one {Related thematic} other {Related thematics}}", + "description": "RelatedThematics: widget title" + }, + "rXeTkM": { + "defaultMessage": "This comment is awaiting moderation.", + "description": "Comment: awaiting moderation message" + }, + "s6U1Xt": { + "defaultMessage": "Discover {websiteName} projects. Mostly related to web development and open source.", + "description": "ProjectsPage: SEO - Meta description" + }, + "sO/Iwj": { + "defaultMessage": "Contact me", + "description": "HomePage: contact button text" + }, + "soj7do": { + "defaultMessage": "Published on:", + "description": "Comment: publication date label" + }, + "tMuNTy": { + "defaultMessage": "{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.", + "description": "HomePage: SEO - Meta description" + }, + "txusHd": { + "defaultMessage": "All fields marked with * are required.", + "description": "ContactPage: required fields text" + }, + "uHp568": { + "defaultMessage": "Name", + "description": "ContactPage: name field label" + }, + "ureXFw": { + "defaultMessage": "Share on {name}", + "description": "Sharing: share on social network text" + }, + "uvB+32": { + "defaultMessage": "Discover the legal notice of {websiteName}'s website.", + "description": "LegalNoticePage: SEO - Meta description" + }, + "vJ+QDV": { + "defaultMessage": "Last updated on:", + "description": "ProjectSummary: update date label" + }, + "vK7Sxv": { + "defaultMessage": "No results found.", + "description": "PostsList: no results" + }, + "vgMk0q": { + "defaultMessage": "Popularity:", + "description": "ProjectSummary: popularity label" + }, + "vhIggb": { + "defaultMessage": "Total:", + "description": "Article meta" + }, + "vkF/RP": { + "defaultMessage": "Web development", + "description": "HomePage: link to web development thematic" + }, + "w/lPUh": { + "defaultMessage": "{topicsCount, plural, =0 {Related topics} one {Related topic} other {Related topics}}", + "description": "RelatedTopics: widget title" + }, + "w1nIrj": { + "defaultMessage": "Off", + "description": "ReduceMotion: toggle off label" + }, + "w8GrOf": { + "defaultMessage": "Free", + "description": "HomePage: link to free thematic" + }, + "x6PPlk": { + "defaultMessage": "Message", + "description": "ContactPage: message field label" + }, + "xC3Khf": { + "defaultMessage": "Download <link>CV in PDF</link>", + "description": "CVPreview: download as PDF link" + }, + "yWjXRx": { + "defaultMessage": "Legal notice", + "description": "FooterNav: legal notice link" + }, + "yfgMcl": { + "defaultMessage": "Introduction:", + "description": "Sharing: email content prefix" + }, + "ywkCsK": { + "defaultMessage": "Error 404", + "description": "404Page: breadcrumb item" + }, + "z0ic9c": { + "defaultMessage": "Blog", + "description": "Breadcrumb: Blog item" + }, + "z9qkcQ": { + "defaultMessage": "Use Ctrl+c to copy", + "description": "Prism: error text" + }, + "zPJifH": { + "defaultMessage": "Blog", + "description": "MainNav: blog link" + } +} diff --git a/src/i18n/fr.json b/src/i18n/fr.json new file mode 100644 index 0000000..23f5278 --- /dev/null +++ b/src/i18n/fr.json @@ -0,0 +1,586 @@ +{ + "+4tiVb": { + "defaultMessage": "Others topics", + "description": "TopicPage: topics list widget title" + }, + "+COyEW": { + "defaultMessage": "{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}", + "description": "PostMeta: topics list label" + }, + "+Dre5J": { + "defaultMessage": "Open-source projects", + "description": "CVPage: social media widget title" + }, + "+Y+tLK": { + "defaultMessage": "Blog: development, open source - {websiteName}", + "description": "BlogPage: SEO - Page title" + }, + "/IirIt": { + "defaultMessage": "Legal notice", + "description": "LegalNoticePage: page title" + }, + "/ly3AC": { + "defaultMessage": "Copy", + "description": "Prism: copy button text (no clicked)" + }, + "00Pf5p": { + "defaultMessage": "Failed to load.", + "description": "TopicsList: failed to load text" + }, + "16zl9Z": { + "defaultMessage": "You are here:", + "description": "Breadcrumb: You are here prefix" + }, + "18h/t0": { + "defaultMessage": "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.", + "description": "BlogPage: SEO - Meta description" + }, + "1h+N2z": { + "defaultMessage": "Published on:", + "description": "RecentPosts: publication date label" + }, + "2D9tB5": { + "defaultMessage": "Topics", + "description": "BlogPage: topics list widget title" + }, + "2pykor": { + "defaultMessage": "{title} picture", + "description": "ProjectPreview: cover alt text" + }, + "310o3F": { + "defaultMessage": "Error 404: Page not found - {websiteName}", + "description": "404Page: SEO - Page title" + }, + "43OmTY": { + "defaultMessage": "Thanks. Your message was successfully sent. I will answer it as soon as possible.", + "description": "ContactPage: success message" + }, + "48Ww//": { + "defaultMessage": "Page not found.", + "description": "404Page: SEO - Meta description" + }, + "4zAUSu": { + "defaultMessage": "Legal notice - {websiteName}", + "description": "LegalNoticePage: SEO - Page title" + }, + "7TbbIk": { + "defaultMessage": "Blog", + "description": "BlogPage: page title" + }, + "8Ls2mD": { + "defaultMessage": "Please fill the form to contact me.", + "description": "ContactPage: page introduction" + }, + "A4LTGq": { + "defaultMessage": "Discover search results for {query}", + "description": "SearchPage: meta description with query" + }, + "AN9iy7": { + "defaultMessage": "Contact", + "description": "ContactPage: page title" + }, + "AnaPbu": { + "defaultMessage": "Search", + "description": "SearchForm: search button text" + }, + "B9OCyV": { + "defaultMessage": "Others formats", + "description": "CVPage: cv preview widget title" + }, + "C/XGkH": { + "defaultMessage": "Failed to load.", + "description": "BlogPage: failed to load text" + }, + "CSimmh": { + "defaultMessage": "{articlesCount, plural, =0 {# articles} one {# article} other {# articles}} out of a total of {total}", + "description": "PaginationCursor: loaded articles count message" + }, + "CT3ydM": { + "defaultMessage": "{date} at {time}", + "description": "Comment: publication date" + }, + "CWi0go": { + "defaultMessage": "Created on:", + "description": "ProjectSummary: creation date label" + }, + "CzTbM4": { + "defaultMessage": "Contact", + "description": "ContactPage: breadcrumb item" + }, + "Dq6+WH": { + "defaultMessage": "Thematics", + "description": "SearchPage: thematics list widget title" + }, + "Enij19": { + "defaultMessage": "Home", + "description": "Breadcrumb: Home item" + }, + "EvODgw": { + "defaultMessage": "Published on", + "description": "PostsList: published on year label" + }, + "F7QxJH": { + "defaultMessage": "Name", + "description": "CommentForm: Name field label" + }, + "FLkF2R": { + "defaultMessage": "All posts in {name}", + "description": "TopicPage: posts list title" + }, + "Fj8WFC": { + "defaultMessage": "{results, plural, =0 {No articles} one {# article} other {# articles}}", + "description": "PostMeta: total found articles" + }, + "FtokGF": { + "defaultMessage": "Updated on:", + "description": "PostMeta: update date label" + }, + "G/SLvC": { + "defaultMessage": "Thanks for your comment! It is now awaiting moderation.", + "description": "CommentForm: Comment sent success message" + }, + "GUfnQ4": { + "defaultMessage": "Reading time:", + "description": "Article meta" + }, + "HriY57": { + "defaultMessage": "Thematics", + "description": "BlogPage: thematics list widget title" + }, + "Hs+q2V": { + "defaultMessage": "Email", + "description": "ContactPage: email field label" + }, + "ILRLTq": { + "defaultMessage": "{brandingName} picture", + "description": "Branding: branding name picture." + }, + "Igx3qp": { + "defaultMessage": "Projects", + "description": "Breadcrumb: Projects item" + }, + "J4nhm4": { + "defaultMessage": "Comment", + "description": "CommentForm: Comment field label" + }, + "Ji6xwo": { + "defaultMessage": "An error occurred:", + "description": "ContactPage: error message" + }, + "KERk7L": { + "defaultMessage": "Filter by:", + "description": "BlogPage: sidebar title" + }, + "KeRtm/": { + "defaultMessage": "Light theme", + "description": "Icons: Sun icon (light theme)" + }, + "Kqq2cm": { + "defaultMessage": "Load more?", + "description": "BlogPage: load more text" + }, + "Mj2BQf": { + "defaultMessage": "{name}'s CV", + "description": "CVPage: page title" + }, + "N44SOc": { + "defaultMessage": "Projects", + "description": "HomePage: link to projects" + }, + "N7I4lC": { + "defaultMessage": "less than 1 minute", + "description": "PostMeta: Reading time value" + }, + "N804XO": { + "defaultMessage": "Topics", + "description": "SearchPage: topics list widget title" + }, + "Ns8CFb": { + "defaultMessage": "Comments", + "description": "CommentsList: Comments section title" + }, + "O9XLDc": { + "defaultMessage": "Theme:", + "description": "ThemeToggle: toggle label" + }, + "OIffB4": { + "defaultMessage": "Contact {websiteName} through its website. All you need to do it's to fill the contact form.", + "description": "ContactPage: SEO - Meta description" + }, + "OTTv+m": { + "defaultMessage": "{count, plural, =0 {Repositories:} one {Repository:} other {Repositories:}}", + "description": "ProjectSummary: repositories list label" + }, + "OV9r1K": { + "defaultMessage": "Copied!", + "description": "Prism: copy button text (clicked)" + }, + "OccTWi": { + "defaultMessage": "Page not found", + "description": "404Page: page title" + }, + "Oim3rQ": { + "defaultMessage": "Email", + "description": "CommentForm: Email field label" + }, + "Ox/daH": { + "defaultMessage": "{commentCount, plural, =0 {No comments} one {# comment} other {# comments}}", + "description": "PostMeta: comment count value" + }, + "P7fxX2": { + "defaultMessage": "All posts in {name}", + "description": "ThematicPage: posts list title" + }, + "PXp2hv": { + "defaultMessage": "{websiteName} | Front-end developer: WordPress/React", + "description": "HomePage: SEO - Page title" + }, + "PrIz5o": { + "defaultMessage": "Search for a post on {websiteName}", + "description": "SearchPage: meta description without query" + }, + "PxMDzL": { + "defaultMessage": "Failed to load.", + "description": "ThematicsList: failed to load text" + }, + "Qh2CwH": { + "defaultMessage": "Find me elsewhere", + "description": "ContactPage: social media widget title" + }, + "R0eDmw": { + "defaultMessage": "Blog", + "description": "BlogPage: breadcrumb item" + }, + "SWq8a4": { + "defaultMessage": "Close {type}", + "description": "ButtonToolbar: Close button" + }, + "SX1z3t": { + "defaultMessage": "Projects: open-source makings - {websiteName}", + "description": "ProjectsPage: SEO - Page title" + }, + "T4YA64": { + "defaultMessage": "Subscribe", + "description": "HomePage: RSS feed subscription text" + }, + "TfU6Qm": { + "defaultMessage": "Search", + "description": "SearchPage: breadcrumb item" + }, + "U++A+B": { + "defaultMessage": "{readingTime, plural, =0 {# minutes} one {# minute} other {# minutes}}", + "description": "PostMeta: reading time value" + }, + "U+35YD": { + "defaultMessage": "Search", + "description": "SearchPage: page title" + }, + "UsQske": { + "defaultMessage": "Read more here:", + "description": "Sharing: content link prefix" + }, + "VSGuGE": { + "defaultMessage": "Search results for {query}", + "description": "SearchPage: search results text" + }, + "W2G95o": { + "defaultMessage": "Comments:", + "description": "PostMeta: comment count label" + }, + "WGFOmA": { + "defaultMessage": "Send", + "description": "CommentForm: Send button" + }, + "WRkY1/": { + "defaultMessage": "Collapse", + "description": "ExpandableWidget: collapse text" + }, + "X3PDXO": { + "defaultMessage": "Animations:", + "description": "ReduceMotion: toggle label" + }, + "Y1ZdJ6": { + "defaultMessage": "CV Front-end developer - {websiteName}", + "description": "CVPage: SEO - Page title" + }, + "Y3qRib": { + "defaultMessage": "Contact form - {websiteName}", + "description": "ContactPage: SEO - Page title" + }, + "YEudoh": { + "defaultMessage": "Read more articles about:", + "description": "PostFooter: read more posts about given subjects" + }, + "Z1eSIz": { + "defaultMessage": "Open {type}", + "description": "ButtonToolbar: Open button" + }, + "ZJMNRW": { + "defaultMessage": "Home", + "description": "MainNav: home link" + }, + "ZWh78Y": { + "defaultMessage": "Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem.", + "description": "404Page: page body" + }, + "Zg4L7U": { + "defaultMessage": "Table of contents", + "description": "ToC: widget title" + }, + "aQLBE3": { + "defaultMessage": "{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}", + "description": "ProjectPreview: technologies list label" + }, + "agLf5v": { + "defaultMessage": "Website:", + "description": "PostMeta: website label" + }, + "akSutM": { + "defaultMessage": "Projects", + "description": "MainNav: projects link" + }, + "azc1GT": { + "defaultMessage": "Open menu", + "description": "MainNav: open button" + }, + "bBdMGm": { + "defaultMessage": "Discover the curriculum of {websiteName}, front-end developer located in France: skills, experiences and training.", + "description": "CVPage: SEO - Meta description" + }, + "bHEmkY": { + "defaultMessage": "Settings", + "description": "Settings: modal title" + }, + "bkbrN7": { + "defaultMessage": "Read more<a11y> about {title}</a11y>", + "description": "PostPreview: read more link" + }, + "c2NtPj": { + "defaultMessage": "Contact", + "description": "MainNav: contact link" + }, + "cr2fA4": { + "defaultMessage": "Subject", + "description": "ContactPage: subject field label" + }, + "dE8xxV": { + "defaultMessage": "Close menu", + "description": "MainNav: close button" + }, + "dqrd6I": { + "defaultMessage": "Back to top", + "description": "Footer: Back to top button" + }, + "e9L59q": { + "defaultMessage": "No comments yet.", + "description": "CommentsList: No comment message" + }, + "eFMu2E": { + "defaultMessage": "Search", + "description": "SearchForm : form title" + }, + "eUXMG4": { + "defaultMessage": "Seen on {domainName}:", + "description": "Sharing: seen on text" + }, + "ec3m6p": { + "defaultMessage": "Leave a comment", + "description": "CommentForm: form title" + }, + "enwhNm": { + "defaultMessage": "{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}", + "description": "ProjectSummary: technologies list label" + }, + "fGnfqp": { + "defaultMessage": "Published on:", + "description": "PostMeta: publication date label" + }, + "fOe8rH": { + "defaultMessage": "Failed to load.", + "description": "SearchPage: failed to load text" + }, + "hKagVG": { + "defaultMessage": "License:", + "description": "ProjectSummary: license label" + }, + "hV0qHp": { + "defaultMessage": "Expand", + "description": "ExpandableWidget: expand text" + }, + "hzHuCc": { + "defaultMessage": "Reply", + "description": "Comment: reply button" + }, + "iqAbyn": { + "defaultMessage": "Skip to content", + "description": "Layout: Skip to content button" + }, + "iyEh0R": { + "defaultMessage": "Failed to load.", + "description": "RecentPosts: failed to load text" + }, + "jASD7k": { + "defaultMessage": "Linux", + "description": "HomePage: link to Linux thematic" + }, + "jGqV2+": { + "defaultMessage": "Written by:", + "description": "Article meta" + }, + "jN+dY5": { + "defaultMessage": "Website", + "description": "CommentForm: Website field label" + }, + "jpv+Nz": { + "defaultMessage": "Resume", + "description": "MainNav: resume link" + }, + "l0+ROl": { + "defaultMessage": "{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}", + "description": "PostMeta: thematics list label" + }, + "mC21ht": { + "defaultMessage": "Number of articles loaded out of the total available.", + "description": "PaginationCursor: loaded articles count aria-label" + }, + "mJLflX": { + "defaultMessage": "Send", + "description": "ContactPage: send button text" + }, + "mh7tGg": { + "defaultMessage": "{title} preview", + "description": "ProjectSummary: cover alt text" + }, + "norrGp": { + "defaultMessage": "Others thematics", + "description": "ThematicPage: thematics list widget title" + }, + "ode0YK": { + "defaultMessage": "Dark theme", + "description": "Icons: Moon icon (dark theme)" + }, + "okFrAO": { + "defaultMessage": "{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}", + "description": "ProjectPreview: technologies list label" + }, + "pEtJik": { + "defaultMessage": "Load more?", + "description": "SearchPage: load more text" + }, + "q3U6uI": { + "defaultMessage": "Share", + "description": "Sharing: widget title" + }, + "q9cJQe": { + "defaultMessage": "Loading...", + "description": "Spinner: loading text" + }, + "qPU/Qn": { + "defaultMessage": "On", + "description": "ReduceMotion: toggle on label" + }, + "qXQETZ": { + "defaultMessage": "{thematicsCount, plural, =0 {Related thematics} one {Related thematic} other {Related thematics}}", + "description": "RelatedThematics: widget title" + }, + "rXeTkM": { + "defaultMessage": "This comment is awaiting moderation.", + "description": "Comment: awaiting moderation message" + }, + "s6U1Xt": { + "defaultMessage": "Discover {websiteName} projects. Mostly related to web development and open source.", + "description": "ProjectsPage: SEO - Meta description" + }, + "sO/Iwj": { + "defaultMessage": "Contact me", + "description": "HomePage: contact button text" + }, + "soj7do": { + "defaultMessage": "Published on:", + "description": "Comment: publication date label" + }, + "tMuNTy": { + "defaultMessage": "{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.", + "description": "HomePage: SEO - Meta description" + }, + "txusHd": { + "defaultMessage": "All fields marked with * are required.", + "description": "ContactPage: required fields text" + }, + "uHp568": { + "defaultMessage": "Name", + "description": "ContactPage: name field label" + }, + "ureXFw": { + "defaultMessage": "Share on {name}", + "description": "Sharing: share on social network text" + }, + "uvB+32": { + "defaultMessage": "Discover the legal notice of {websiteName}'s website.", + "description": "LegalNoticePage: SEO - Meta description" + }, + "vJ+QDV": { + "defaultMessage": "Last updated on:", + "description": "ProjectSummary: update date label" + }, + "vK7Sxv": { + "defaultMessage": "No results found.", + "description": "PostsList: no results" + }, + "vgMk0q": { + "defaultMessage": "Popularity:", + "description": "ProjectSummary: popularity label" + }, + "vhIggb": { + "defaultMessage": "Total:", + "description": "Article meta" + }, + "vkF/RP": { + "defaultMessage": "Web development", + "description": "HomePage: link to web development thematic" + }, + "w/lPUh": { + "defaultMessage": "{topicsCount, plural, =0 {Related topics} one {Related topic} other {Related topics}}", + "description": "RelatedTopics: widget title" + }, + "w1nIrj": { + "defaultMessage": "Off", + "description": "ReduceMotion: toggle off label" + }, + "w8GrOf": { + "defaultMessage": "Free", + "description": "HomePage: link to free thematic" + }, + "x6PPlk": { + "defaultMessage": "Message", + "description": "ContactPage: message field label" + }, + "xC3Khf": { + "defaultMessage": "Download <link>CV in PDF</link>", + "description": "CVPreview: download as PDF link" + }, + "yWjXRx": { + "defaultMessage": "Legal notice", + "description": "FooterNav: legal notice link" + }, + "yfgMcl": { + "defaultMessage": "Introduction:", + "description": "Sharing: email content prefix" + }, + "ywkCsK": { + "defaultMessage": "Error 404", + "description": "404Page: breadcrumb item" + }, + "z0ic9c": { + "defaultMessage": "Blog", + "description": "Breadcrumb: Blog item" + }, + "z9qkcQ": { + "defaultMessage": "Use Ctrl+c to copy", + "description": "Prism: error text" + }, + "zPJifH": { + "defaultMessage": "Blog", + "description": "MainNav: blog link" + } +} diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 64c74ba..079dead 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,44 +1,70 @@ import { getLayout } from '@components/Layouts/Layout'; import PostHeader from '@components/PostHeader/PostHeader'; -import { seo } from '@config/seo'; -import { t, Trans } from '@lingui/macro'; +import { config } from '@config/website'; import styles from '@styles/pages/Page.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; -import { loadTranslation } from '@utils/helpers/i18n'; +import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import Link from 'next/link'; +import { FormattedMessage, useIntl } from 'react-intl'; + +const Error404: NextPageWithLayout = () => { + const intl = useIntl(); + + const pageTitle = intl.formatMessage( + { + defaultMessage: 'Error 404: Page not found - {websiteName}', + description: '404Page: SEO - Page title', + }, + { websiteName: config.name } + ); + const pageDescription = intl.formatMessage({ + defaultMessage: 'Page not found.', + description: '404Page: SEO - Meta description', + }); -const error404: NextPageWithLayout = () => { return ( <> <Head> - <title>{seo.error404.title}</title> - <meta name="description" content={seo.error404.description} /> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> </Head> <div className={`${styles.article} ${styles['article--no-comments']}`}> - <PostHeader title={t`Page not found`} /> + <PostHeader + title={intl.formatMessage({ + defaultMessage: 'Page not found', + description: '404Page: page title', + })} + /> <div className={styles.body}> - <Trans> - Sorry, it seems that the page you are looking for does not exist. - </Trans>{' '} - <Trans> - If you think this path should work, feel free to{' '} - <Link href="/contact/">contact me</Link> with the necessary - information so that I can fix the problem. - </Trans> + <FormattedMessage + defaultMessage="Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem." + description="404Page: page body" + values={{ + link: (chunks: string) => ( + <Link href="/contact/"> + <a>{chunks}</a> + </Link> + ), + }} + /> </div> </div> </> ); }; -error404.getLayout = getLayout; +Error404.getLayout = getLayout; export const getStaticProps: GetStaticProps = async ( context: GetStaticPropsContext ) => { - const breadcrumbTitle = t`Error`; + const intl = await getIntlInstance(); + const breadcrumbTitle = intl.formatMessage({ + defaultMessage: 'Error 404', + description: '404Page: breadcrumb item', + }); const { locale } = context; const translation = await loadTranslation(locale); @@ -51,4 +77,4 @@ export const getStaticProps: GetStaticProps = async ( }; }; -export default error404; +export default Error404; diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index ce9c22b..8668a66 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -18,6 +18,7 @@ import { useRouter } from 'next/router'; import Prism from 'prismjs'; import { ParsedUrlQuery } from 'querystring'; import { useEffect } from 'react'; +import { useIntl } from 'react-intl'; import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts'; const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => { @@ -45,6 +46,7 @@ const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => { wordsCount: info.wordsCount, }; + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; const articleUrl = `${config.url}${router.asPath}`; @@ -55,8 +57,8 @@ const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => { }); useEffect(() => { - translateCopyButton(locale); - }, [locale]); + translateCopyButton(locale, intl); + }, [intl, locale]); const webpageSchema: WebPage = { '@id': `${articleUrl}`, diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx index 8e42e02..9a86d9f 100644 --- a/src/pages/blog/index.tsx +++ b/src/pages/blog/index.tsx @@ -6,22 +6,22 @@ import PostsList from '@components/PostsList/PostsList'; import Sidebar from '@components/Sidebar/Sidebar'; import Spinner from '@components/Spinner/Spinner'; import { ThematicsList, TopicsList } from '@components/Widgets'; -import { seo } from '@config/seo'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { getPublishedPosts } from '@services/graphql/queries'; import styles from '@styles/pages/Page.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; import { BlogPageProps, PostsList as PostsListData } from '@ts/types/blog'; -import { loadTranslation } from '@utils/helpers/i18n'; +import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import { Blog as BlogSchema, Graph, WebPage } from 'schema-dts'; import useSWRInfinite from 'swr/infinite'; const Blog: NextPageWithLayout<BlogPageProps> = ({ fallback }) => { + const intl = useIntl(); const lastPostRef = useRef<HTMLSpanElement>(null); const router = useRouter(); @@ -76,21 +76,39 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({ fallback }) => { }; const getPostsList = () => { - if (error) return t`Failed to load.`; + if (error) + return intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'BlogPage: failed to load text', + }); if (!data) return <Spinner />; return <PostsList ref={lastPostRef} data={data} showYears={true} />; }; - const title = t`Blog`; + const pageTitle = intl.formatMessage( + { + defaultMessage: 'Blog: development, open source - {websiteName}', + description: 'BlogPage: SEO - Page title', + }, + { websiteName: config.name } + ); + const pageDescription = intl.formatMessage( + { + defaultMessage: + "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.", + description: 'BlogPage: SEO - Meta description', + }, + { websiteName: config.name } + ); const pageUrl = `${config.url}${router.asPath}`; const webpageSchema: WebPage = { '@id': `${pageUrl}`, '@type': 'WebPage', breadcrumb: { '@id': `${config.url}/#breadcrumb` }, - name: seo.blog.title, - description: seo.blog.description, + name: pageTitle, + description: pageDescription, inLanguage: config.locales.defaultLocale, reviewedBy: { '@id': `${config.url}/#branding` }, url: `${config.url}`, @@ -115,15 +133,20 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({ fallback }) => { '@graph': [webpageSchema, blogSchema], }; + const title = intl.formatMessage({ + defaultMessage: 'Blog', + description: 'BlogPage: page title', + }); + return ( <> <Head> - <title>{seo.blog.title}</title> - <meta name="description" content={seo.blog.description} /> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> <meta property="og:url" content={`${pageUrl}`} /> <meta property="og:type" content="website" /> <meta property="og:title" content={title} /> - <meta property="og:description" content={seo.blog.description} /> + <meta property="og:description" content={pageDescription} /> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} @@ -146,13 +169,34 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({ fallback }) => { isDisabled={isLoadingMore} clickHandler={loadMorePosts} position="center" - >{t`Load more?`}</Button> + > + {intl.formatMessage({ + defaultMessage: 'Load more?', + description: 'BlogPage: load more text', + })} + </Button> </> )} </div> - <Sidebar position="right" title={t`Filter by`}> - <ThematicsList title={t`Thematics`} /> - <TopicsList title={t`Topics`} /> + <Sidebar + position="right" + title={intl.formatMessage({ + defaultMessage: 'Filter by:', + description: 'BlogPage: sidebar title', + })} + > + <ThematicsList + title={intl.formatMessage({ + defaultMessage: 'Thematics', + description: 'BlogPage: thematics list widget title', + })} + /> + <TopicsList + title={intl.formatMessage({ + defaultMessage: 'Topics', + description: 'BlogPage: topics list widget title', + })} + /> </Sidebar> </article> </> @@ -164,7 +208,11 @@ Blog.getLayout = getLayout; export const getStaticProps: GetStaticProps = async ( context: GetStaticPropsContext ) => { - const breadcrumbTitle = t`Blog`; + const intl = await getIntlInstance(); + const breadcrumbTitle = intl.formatMessage({ + defaultMessage: 'Blog', + description: 'BlogPage: breadcrumb item', + }); const data = await getPublishedPosts({ first: config.postsPerPage }); const { locale } = context; const translation = await loadTranslation(locale); diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx index 464854d..489135d 100644 --- a/src/pages/contact.tsx +++ b/src/pages/contact.tsx @@ -4,20 +4,20 @@ import { getLayout } from '@components/Layouts/Layout'; import PostHeader from '@components/PostHeader/PostHeader'; import Sidebar from '@components/Sidebar/Sidebar'; import { SocialMedia } from '@components/Widgets'; -import { seo } from '@config/seo'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { sendMail } from '@services/graphql/mutations'; import styles from '@styles/pages/Page.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; -import { loadTranslation } from '@utils/helpers/i18n'; +import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { FormEvent, useState } from 'react'; +import { useIntl } from 'react-intl'; import { ContactPage as ContactPageSchema, Graph, WebPage } from 'schema-dts'; const ContactPage: NextPageWithLayout = () => { + const intl = useIntl(); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [subject, setSubject] = useState(''); @@ -46,26 +46,54 @@ const ContactPage: NextPageWithLayout = () => { if (mail.sent) { setStatus( - t`Thanks. Your message was successfully sent. I will answer it as soon as possible.` + intl.formatMessage({ + defaultMessage: + 'Thanks. Your message was successfully sent. I will answer it as soon as possible.', + description: 'ContactPage: success message', + }) ); resetForm(); } else { - const errorPrefix = t`An error occurred:`; + const errorPrefix = intl.formatMessage({ + defaultMessage: 'An error occurred:', + description: 'ContactPage: error message', + }); const error = `${errorPrefix} ${mail.message}`; setStatus(error); } }; - const title = t`Contact`; - const intro = t`Please fill the form to contact me.`; + const pageTitle = intl.formatMessage( + { + defaultMessage: 'Contact form - {websiteName}', + description: 'ContactPage: SEO - Page title', + }, + { websiteName: config.name } + ); + const pageDescription = intl.formatMessage( + { + defaultMessage: + "Contact {websiteName} through its website. All you need to do it's to fill the contact form.", + description: 'ContactPage: SEO - Meta description', + }, + { websiteName: config.name } + ); const pageUrl = `${config.url}${router.asPath}`; + const title = intl.formatMessage({ + defaultMessage: 'Contact', + description: 'ContactPage: page title', + }); + const intro = intl.formatMessage({ + defaultMessage: 'Please fill the form to contact me.', + description: 'ContactPage: page introduction', + }); const webpageSchema: WebPage = { '@id': `${pageUrl}`, '@type': 'WebPage', breadcrumb: { '@id': `${config.url}/#breadcrumb` }, - name: seo.contact.title, - description: seo.contact.description, + name: pageTitle, + description: pageDescription, reviewedBy: { '@id': `${config.url}/#branding` }, url: `${pageUrl}`, isPartOf: { @@ -94,8 +122,8 @@ const ContactPage: NextPageWithLayout = () => { return ( <> <Head> - <title>{seo.contact.title}</title> - <meta name="description" content={seo.contact.description} /> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> <meta property="og:url" content={`${pageUrl}`} /> <meta property="og:type" content="article" /> <meta property="og:title" content={title} /> @@ -111,7 +139,12 @@ const ContactPage: NextPageWithLayout = () => { > <PostHeader title={title} intro={intro} /> <div className={styles.body}> - <p>{t`All fields marked with * are required.`}</p> + <p> + {intl.formatMessage({ + defaultMessage: 'All fields marked with * are required.', + description: 'ContactPage: required fields text', + })} + </p> {status && <p>{status}</p>} <Form submitHandler={submitHandler}> <FormItem> @@ -120,7 +153,10 @@ const ContactPage: NextPageWithLayout = () => { name="name" value={name} setValue={setName} - label={t`Name`} + label={intl.formatMessage({ + defaultMessage: 'Name', + description: 'ContactPage: name field label', + })} required={true} /> </FormItem> @@ -130,7 +166,10 @@ const ContactPage: NextPageWithLayout = () => { name="email" value={email} setValue={setEmail} - label={t`Email`} + label={intl.formatMessage({ + defaultMessage: 'Email', + description: 'ContactPage: email field label', + })} required={true} /> </FormItem> @@ -140,7 +179,10 @@ const ContactPage: NextPageWithLayout = () => { name="subject" value={subject} setValue={setSubject} - label={t`Subject`} + label={intl.formatMessage({ + defaultMessage: 'Subject', + description: 'ContactPage: subject field label', + })} /> </FormItem> <FormItem> @@ -149,18 +191,29 @@ const ContactPage: NextPageWithLayout = () => { name="message" value={message} setValue={setMessage} - label={t`Message`} + label={intl.formatMessage({ + defaultMessage: 'Message', + description: 'ContactPage: message field label', + })} required={true} /> </FormItem> <FormItem> - <ButtonSubmit>{t`Send`}</ButtonSubmit> + <ButtonSubmit> + {intl.formatMessage({ + defaultMessage: 'Send', + description: 'ContactPage: send button text', + })} + </ButtonSubmit> </FormItem> </Form> </div> <Sidebar position="right"> <SocialMedia - title={t`Find me elsewhere`} + title={intl.formatMessage({ + defaultMessage: 'Find me elsewhere', + description: 'ContactPage: social media widget title', + })} github={true} gitlab={true} linkedin={true} @@ -176,7 +229,11 @@ ContactPage.getLayout = getLayout; export const getStaticProps: GetStaticProps = async ( context: GetStaticPropsContext ) => { - const breadcrumbTitle = t`Contact`; + const intl = await getIntlInstance(); + const breadcrumbTitle = intl.formatMessage({ + defaultMessage: 'Contact', + description: 'ContactPage: breadcrumb item', + }); const { locale } = context; const translation = await loadTranslation(locale); diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx index a851c38..c3686de 100644 --- a/src/pages/cv.tsx +++ b/src/pages/cv.tsx @@ -2,10 +2,8 @@ import { getLayout } from '@components/Layouts/Layout'; import PostHeader from '@components/PostHeader/PostHeader'; import Sidebar from '@components/Sidebar/Sidebar'; import { CVPreview, SocialMedia, ToC } from '@components/Widgets'; -import { seo } from '@config/seo'; import { config } from '@config/website'; import CVContent, { intro, meta, pdf, image } from '@content/pages/cv.mdx'; -import { t } from '@lingui/macro'; import styles from '@styles/pages/Page.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; import { ArticleMeta } from '@ts/types/articles'; @@ -13,9 +11,11 @@ import { loadTranslation } from '@utils/helpers/i18n'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import { AboutPage, Graph, WebPage } from 'schema-dts'; const CV: NextPageWithLayout = () => { + const intl = useIntl(); const router = useRouter(); const dates = { publication: meta.publishedOn, @@ -26,13 +26,28 @@ const CV: NextPageWithLayout = () => { dates, }; const pageUrl = `${config.url}${router.asPath}`; + const pageTitle = intl.formatMessage( + { + defaultMessage: 'CV Front-end developer - {websiteName}', + description: 'CVPage: SEO - Page title', + }, + { websiteName: config.name } + ); + const pageDescription = intl.formatMessage( + { + defaultMessage: + 'Discover the curriculum of {websiteName}, front-end developer located in France: skills, experiences and training.', + description: 'CVPage: SEO - Meta description', + }, + { websiteName: config.name } + ); const webpageSchema: WebPage = { '@id': `${pageUrl}`, '@type': 'WebPage', breadcrumb: { '@id': `${config.url}/#breadcrumb` }, - name: seo.cv.title, - description: seo.cv.description, + name: pageTitle, + description: pageDescription, reviewedBy: { '@id': `${config.url}/#branding` }, url: `${pageUrl}`, isPartOf: { @@ -46,7 +61,7 @@ const CV: NextPageWithLayout = () => { const cvSchema: AboutPage = { '@id': `${config.url}/#cv`, '@type': 'AboutPage', - name: `${config.name} CV`, + name: pageTitle, description: intro, author: { '@id': `${config.url}/#branding` }, creator: { '@id': `${config.url}/#branding` }, @@ -66,17 +81,25 @@ const CV: NextPageWithLayout = () => { '@graph': [webpageSchema, cvSchema], }; + const title = intl.formatMessage( + { + defaultMessage: "{name}'s CV", + description: 'CVPage: page title', + }, + { name: config.name } + ); + return ( <> <Head> - <title>{seo.cv.title}</title> - <meta name="description" content={seo.cv.description} /> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> <meta property="og:url" content={`${pageUrl}`} /> <meta property="og:type" content="article" /> - <meta property="og:title" content={`${config.name} CV`} /> + <meta property="og:title" content={title} /> <meta property="og:description" content={intro} /> <meta property="og:image" content={image} /> - <meta property="og:image:alt" content={`${config.name} CV`} /> + <meta property="og:image:alt" content={title} /> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} @@ -94,9 +117,19 @@ const CV: NextPageWithLayout = () => { <CVContent /> </div> <Sidebar position="right"> - <CVPreview title={t`Other formats`} imgSrc={image} pdf={pdf} /> + <CVPreview + title={intl.formatMessage({ + defaultMessage: 'Others formats', + description: 'CVPage: cv preview widget title', + })} + imgSrc={image} + pdf={pdf} + /> <SocialMedia - title={t`Open-source projects`} + title={intl.formatMessage({ + defaultMessage: 'Open-source projects', + description: 'CVPage: social media widget title', + })} github={true} gitlab={true} /> diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 264c45a..41a4603 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,29 +2,38 @@ import FeedIcon from '@assets/images/icon-feed.svg'; import { ButtonLink } from '@components/Buttons'; import { ContactIcon } from '@components/Icons'; import Layout from '@components/Layouts/Layout'; -import { seo } from '@config/seo'; import { config } from '@config/website'; import HomePageContent from '@content/pages/homepage.mdx'; -import { t } from '@lingui/macro'; import styles from '@styles/pages/Home.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; import { loadTranslation } from '@utils/helpers/i18n'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import type { ReactElement } from 'react'; +import { useIntl } from 'react-intl'; import { Graph, WebPage } from 'schema-dts'; const Home: NextPageWithLayout = () => { + const intl = useIntl(); + const CodingLinks = () => { return ( <ul className={styles['links-list']}> <li> <ButtonLink target="/thematique/developpement-web"> - {t`Web development`} + {intl.formatMessage({ + defaultMessage: 'Web development', + description: 'HomePage: link to web development thematic', + })} </ButtonLink> </li> <li> - <ButtonLink target="/projets">{t`Projects`}</ButtonLink> + <ButtonLink target="/projets"> + {intl.formatMessage({ + defaultMessage: 'Projects', + description: 'HomePage: link to projects', + })} + </ButtonLink> </li> </ul> ); @@ -57,10 +66,20 @@ const Home: NextPageWithLayout = () => { return ( <ul className={styles['links-list']}> <li> - <ButtonLink target="/thematique/libre">{t`Free`}</ButtonLink> + <ButtonLink target="/thematique/libre"> + {intl.formatMessage({ + defaultMessage: 'Free', + description: 'HomePage: link to free thematic', + })} + </ButtonLink> </li> <li> - <ButtonLink target="/thematique/linux">{t`Linux`}</ButtonLink> + <ButtonLink target="/thematique/linux"> + {intl.formatMessage({ + defaultMessage: 'Linux', + description: 'HomePage: link to Linux thematic', + })} + </ButtonLink> </li> </ul> ); @@ -72,13 +91,19 @@ const Home: NextPageWithLayout = () => { <li> <ButtonLink target="/contact"> <ContactIcon /> - {t`Contact me`} + {intl.formatMessage({ + defaultMessage: 'Contact me', + description: 'HomePage: contact button text', + })} </ButtonLink> </li> <li> <ButtonLink target="/feed"> <FeedIcon className={styles['icon--feed']} /> - {t`Subscribe`} + {intl.formatMessage({ + defaultMessage: 'Subscribe', + description: 'HomePage: RSS feed subscription text', + })} </ButtonLink> </li> </ul> @@ -92,12 +117,28 @@ const Home: NextPageWithLayout = () => { MoreLinks: MoreLinks, }; + const pageTitle = intl.formatMessage( + { + defaultMessage: '{websiteName} | Front-end developer: WordPress/React', + description: 'HomePage: SEO - Page title', + }, + { websiteName: config.name } + ); + const pageDescription = intl.formatMessage( + { + defaultMessage: + '{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.', + description: 'HomePage: SEO - Meta description', + }, + { websiteName: config.name } + ); + const webpageSchema: WebPage = { '@id': `${config.url}/#home`, '@type': 'WebPage', breadcrumb: { '@id': `${config.url}/#breadcrumb` }, - name: seo.legalNotice.title, - description: seo.legalNotice.description, + name: pageTitle, + description: pageDescription, author: { '@id': `${config.url}/#branding` }, creator: { '@id': `${config.url}/#branding` }, editor: { '@id': `${config.url}/#branding` }, @@ -115,12 +156,12 @@ const Home: NextPageWithLayout = () => { return ( <> <Head> - <title>{seo.homepage.title}</title> - <meta name="description" content={seo.homepage.description} /> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> <meta property="og:type" content="website" /> <meta property="og:url" content={`${config.url}`} /> - <meta property="og:title" content={seo.homepage.title} /> - <meta property="og:description" content={seo.homepage.description} /> + <meta property="og:title" content={pageTitle} /> + <meta property="og:description" content={pageDescription} /> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx index e13a7e2..0ec92a2 100644 --- a/src/pages/mentions-legales.tsx +++ b/src/pages/mentions-legales.tsx @@ -2,13 +2,11 @@ import { getLayout } from '@components/Layouts/Layout'; import PostHeader from '@components/PostHeader/PostHeader'; import Sidebar from '@components/Sidebar/Sidebar'; import { ToC } from '@components/Widgets'; -import { seo } from '@config/seo'; import { config } from '@config/website'; import LegalNoticeContent, { intro, meta, } from '@content/pages/legal-notice.mdx'; -import { t } from '@lingui/macro'; import styles from '@styles/pages/Page.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; import { ArticleMeta } from '@ts/types/articles'; @@ -16,9 +14,11 @@ import { loadTranslation } from '@utils/helpers/i18n'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import { Article, Graph, WebPage } from 'schema-dts'; const LegalNotice: NextPageWithLayout = () => { + const intl = useIntl(); const router = useRouter(); const dates = { publication: meta.publishedOn, @@ -28,8 +28,25 @@ const LegalNotice: NextPageWithLayout = () => { const pageMeta: ArticleMeta = { dates, }; + const pageTitle = intl.formatMessage( + { + defaultMessage: 'Legal notice - {websiteName}', + description: 'LegalNoticePage: SEO - Page title', + }, + { websiteName: config.name } + ); + const pageDescription = intl.formatMessage( + { + defaultMessage: "Discover the legal notice of {websiteName}'s website.", + description: 'LegalNoticePage: SEO - Meta description', + }, + { websiteName: config.name } + ); const pageUrl = `${config.url}${router.asPath}`; - + const title = intl.formatMessage({ + defaultMessage: 'Legal notice', + description: 'LegalNoticePage: page title', + }); const publicationDate = new Date(dates.publication); const updateDate = new Date(dates.update); @@ -37,8 +54,8 @@ const LegalNotice: NextPageWithLayout = () => { '@id': `${pageUrl}`, '@type': 'WebPage', breadcrumb: { '@id': `${config.url}/#breadcrumb` }, - name: seo.legalNotice.title, - description: seo.legalNotice.description, + name: pageTitle, + description: pageDescription, inLanguage: config.locales.defaultLocale, license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', reviewedBy: { '@id': `${config.url}/#branding` }, @@ -51,7 +68,7 @@ const LegalNotice: NextPageWithLayout = () => { const articleSchema: Article = { '@id': `${config.url}/#legal-notice`, '@type': 'Article', - name: t`Legal notice`, + name: title, description: intro, author: { '@id': `${config.url}/#branding` }, copyrightYear: publicationDate.getFullYear(), @@ -73,11 +90,11 @@ const LegalNotice: NextPageWithLayout = () => { return ( <> <Head> - <title>{seo.legalNotice.title}</title> - <meta name="description" content={seo.legalNotice.description} /> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> <meta property="og:url" content={`${pageUrl}`} /> <meta property="og:type" content="article" /> - <meta property="og:title" content={t`Legal notice`} /> + <meta property="og:title" content={pageTitle} /> <meta property="og:description" content={intro} /> <script type="application/ld+json" diff --git a/src/pages/projets.tsx b/src/pages/projets.tsx index 263973d..da4523c 100644 --- a/src/pages/projets.tsx +++ b/src/pages/projets.tsx @@ -1,7 +1,6 @@ import { getLayout } from '@components/Layouts/Layout'; import PostHeader from '@components/PostHeader/PostHeader'; import ProjectsList from '@components/ProjectsList/ProjectsList'; -import { seo } from '@config/seo'; import { config } from '@config/website'; import PageContent, { meta } from '@content/pages/projects.mdx'; import styles from '@styles/pages/Projects.module.scss'; @@ -11,9 +10,11 @@ import { getSortedProjects } from '@utils/helpers/projects'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import { Article, Graph, WebPage } from 'schema-dts'; const Projects = ({ projects }: { projects: Project[] }) => { + const intl = useIntl(); const dates = { publication: meta.publishedOn, update: meta.updatedOn, @@ -22,13 +23,28 @@ const Projects = ({ projects }: { projects: Project[] }) => { const updateDate = new Date(dates.update); const router = useRouter(); const pageUrl = `${config.url}${router.asPath}`; + const pageTitle = intl.formatMessage( + { + defaultMessage: 'Projects: open-source makings - {websiteName}', + description: 'ProjectsPage: SEO - Page title', + }, + { websiteName: config.name } + ); + const pageDescription = intl.formatMessage( + { + defaultMessage: + 'Discover {websiteName} projects. Mostly related to web development and open source.', + description: 'ProjectsPage: SEO - Meta description', + }, + { websiteName: config.name } + ); const webpageSchema: WebPage = { '@id': `${pageUrl}`, '@type': 'WebPage', breadcrumb: { '@id': `${config.url}/#breadcrumb` }, - name: seo.legalNotice.title, - description: seo.legalNotice.description, + name: pageTitle, + description: pageDescription, inLanguage: config.locales.defaultLocale, license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', reviewedBy: { '@id': `${config.url}/#branding` }, @@ -42,7 +58,7 @@ const Projects = ({ projects }: { projects: Project[] }) => { '@id': `${config.url}/#projects`, '@type': 'Article', name: meta.title, - description: seo.projects.description, + description: pageDescription, author: { '@id': `${config.url}/#branding` }, copyrightYear: publicationDate.getFullYear(), creator: { '@id': `${config.url}/#branding` }, @@ -63,12 +79,12 @@ const Projects = ({ projects }: { projects: Project[] }) => { return ( <> <Head> - <title>{seo.projects.title}</title> - <meta name="description" content={seo.projects.description} /> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> <meta property="og:url" content={`${pageUrl}`} /> <meta property="og:type" content="article" /> <meta property="og:title" content={meta.title} /> - <meta property="og:description" content={seo.projects.description} /> + <meta property="og:description" content={pageDescription} /> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx index c3c71f3..857b114 100644 --- a/src/pages/recherche/index.tsx +++ b/src/pages/recherche/index.tsx @@ -7,19 +7,20 @@ import Sidebar from '@components/Sidebar/Sidebar'; import Spinner from '@components/Spinner/Spinner'; import { ThematicsList, TopicsList } from '@components/Widgets'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { getPublishedPosts } from '@services/graphql/queries'; import styles from '@styles/pages/Page.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; import { PostsList as PostsListData } from '@ts/types/blog'; -import { loadTranslation } from '@utils/helpers/i18n'; +import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n'; import { GetStaticProps, GetStaticPropsContext } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import useSWRInfinite from 'swr/infinite'; const Search: NextPageWithLayout = () => { + const intl = useIntl(); const [query, setQuery] = useState(''); const router = useRouter(); const lastPostRef = useRef<HTMLSpanElement>(null); @@ -76,15 +77,33 @@ const Search: NextPageWithLayout = () => { const hasNextPage = data && data[data.length - 1].pageInfo.hasNextPage; const title = query - ? t`Search results for: ${query}` - : t({ - comment: 'Search page title', - message: 'Search', + ? intl.formatMessage( + { + defaultMessage: 'Search results for {query}', + description: 'SearchPage: search results text', + }, + { query } + ) + : intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchPage: page title', }); const description = query - ? t`Discover search results for: ${query}` - : t`Search for a post on ${config.name}.`; + ? intl.formatMessage( + { + defaultMessage: 'Discover search results for {query}', + description: 'SearchPage: meta description with query', + }, + { query } + ) + : intl.formatMessage( + { + defaultMessage: 'Search for a post on {websiteName}', + description: 'SearchPage: meta description without query', + }, + { websiteName: config.name } + ); const head = { title: `${title} | ${config.name}`, @@ -99,7 +118,11 @@ const Search: NextPageWithLayout = () => { }; const getPostsList = () => { - if (error) return t`Failed to load.`; + if (error) + return intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'SearchPage: failed to load text', + }); if (!data) return <Spinner />; return <PostsList ref={lastPostRef} data={data} showYears={false} />; @@ -127,13 +150,28 @@ const Search: NextPageWithLayout = () => { isDisabled={isLoadingMore} clickHandler={loadMorePosts} position="center" - >{t`Load more?`}</Button> + > + {intl.formatMessage({ + defaultMessage: 'Load more?', + description: 'SearchPage: load more text', + })} + </Button> </> )} </div> <Sidebar position="right"> - <ThematicsList title={t`Thematics`} /> - <TopicsList title={t`Topics`} /> + <ThematicsList + title={intl.formatMessage({ + defaultMessage: 'Thematics', + description: 'SearchPage: thematics list widget title', + })} + /> + <TopicsList + title={intl.formatMessage({ + defaultMessage: 'Topics', + description: 'SearchPage: topics list widget title', + })} + /> </Sidebar> </article> </> @@ -145,7 +183,11 @@ Search.getLayout = getLayout; export const getStaticProps: GetStaticProps = async ( context: GetStaticPropsContext ) => { - const breadcrumbTitle = t`Search`; + const intl = await getIntlInstance(); + const breadcrumbTitle = intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchPage: breadcrumb item', + }); const { locale } = context; const translation = await loadTranslation(locale); diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx index 65ea2fd..87a86a2 100644 --- a/src/pages/sujet/[slug].tsx +++ b/src/pages/sujet/[slug].tsx @@ -4,7 +4,6 @@ import PostPreview from '@components/PostPreview/PostPreview'; import Sidebar from '@components/Sidebar/Sidebar'; import { RelatedThematics, ToC, TopicsList } from '@components/Widgets'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { getAllTopicsSlug, getTopicBySlug } from '@services/graphql/queries'; import styles from '@styles/pages/Page.module.scss'; import { NextPageWithLayout } from '@ts/types/app'; @@ -16,9 +15,11 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { ParsedUrlQuery } from 'querystring'; import { useRef } from 'react'; +import { useIntl } from 'react-intl'; import { Article as Article, Graph, WebPage } from 'schema-dts'; const Topic: NextPageWithLayout<TopicProps> = ({ topic }) => { + const intl = useIntl(); const relatedThematics = useRef<ThematicPreview[]>([]); const router = useRouter(); @@ -128,14 +129,27 @@ const Topic: NextPageWithLayout<TopicProps> = ({ topic }) => { <div dangerouslySetInnerHTML={{ __html: topic.content }}></div> {topic.posts.length > 0 && ( <section className={styles.section}> - <h2>{t`All posts in ${topic.title}`}</h2> + <h2> + {intl.formatMessage( + { + defaultMessage: 'All posts in {name}', + description: 'TopicPage: posts list title', + }, + { name: topic.title } + )} + </h2> <ol className={styles.list}>{getPostsList()}</ol> </section> )} </div> <Sidebar position="right"> <RelatedThematics thematics={relatedThematics.current} /> - <TopicsList title={t`Other topics`} /> + <TopicsList + title={intl.formatMessage({ + defaultMessage: 'Others topics', + description: 'TopicPage: topics list widget title', + })} + /> </Sidebar> </article> </> diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx index d263ee9..61019fd 100644 --- a/src/pages/thematique/[slug].tsx +++ b/src/pages/thematique/[slug].tsx @@ -4,7 +4,6 @@ import PostPreview from '@components/PostPreview/PostPreview'; import Sidebar from '@components/Sidebar/Sidebar'; import { RelatedTopics, ThematicsList, ToC } from '@components/Widgets'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { getAllThematicsSlug, getThematicBySlug, @@ -19,9 +18,11 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { ParsedUrlQuery } from 'querystring'; import { useRef } from 'react'; +import { useIntl } from 'react-intl'; import { Article, Graph, WebPage } from 'schema-dts'; const Thematic: NextPageWithLayout<ThematicProps> = ({ thematic }) => { + const intl = useIntl(); const relatedTopics = useRef<TopicPreview[]>([]); const router = useRouter(); @@ -118,14 +119,27 @@ const Thematic: NextPageWithLayout<ThematicProps> = ({ thematic }) => { <div dangerouslySetInnerHTML={{ __html: thematic.content }}></div> {thematic.posts.length > 0 && ( <section className={styles.section}> - <h2>{t`All posts in ${thematic.title}`}</h2> + <h2> + {intl.formatMessage( + { + defaultMessage: 'All posts in {name}', + description: 'ThematicPage: posts list title', + }, + { name: thematic.title } + )} + </h2> <ol className={styles.list}>{getPostsList()}</ol> </section> )} </div> <Sidebar position="right"> <RelatedTopics topics={relatedTopics.current} /> - <ThematicsList title={t`Other thematics`} /> + <ThematicsList + title={intl.formatMessage({ + defaultMessage: 'Others thematics', + description: 'ThematicPage: thematics list widget title', + })} + /> </Sidebar> </article> </> diff --git a/src/utils/helpers/i18n.ts b/src/utils/helpers/i18n.ts index dd010c4..16c83f4 100644 --- a/src/utils/helpers/i18n.ts +++ b/src/utils/helpers/i18n.ts @@ -1,18 +1,22 @@ import { config } from '@config/website'; +import { createIntl, createIntlCache, IntlShape } from '@formatjs/intl'; import { readFile } from 'fs/promises'; import path from 'path'; type Messages = { [key: string]: string }; +export const defaultLocale = config.locales.defaultLocale; + /** * Load the translation for the provided locale. + * * @param currentLocale - The current locale. * @returns {Promise<Messages>} The translated strings. */ export async function loadTranslation( currentLocale: string | undefined ): Promise<Messages> { - const locale: string = currentLocale || config.locales.defaultLocale; + const locale: string = currentLocale || defaultLocale; const languagePath = path.join(process.cwd(), `lang/${locale}.json`); @@ -26,3 +30,20 @@ export async function loadTranslation( throw error; } } + +/** + * Create an Intl object to be used outside components. + * + * @returns {<Promise<IntlShape<string>>} The Intl object. + */ +export async function getIntlInstance(): Promise<IntlShape<string>> { + try { + const cache = createIntlCache(); + const messages = await loadTranslation(defaultLocale); + + return createIntl({ locale: defaultLocale, messages }, cache); + } catch (error) { + console.error('Error: Could not create an Intl instance.'); + throw error; + } +} diff --git a/src/utils/helpers/prism.ts b/src/utils/helpers/prism.ts index 86c8f7d..7f10dc9 100644 --- a/src/utils/helpers/prism.ts +++ b/src/utils/helpers/prism.ts @@ -1,4 +1,4 @@ -import { t } from '@lingui/macro'; +import { IntlShape } from 'react-intl'; /** * Check if the current block has a defined language. @@ -39,13 +39,25 @@ export const addPrismClasses = () => { /** * Translate the PrismJS Copy to clipboard button. */ -export const translateCopyButton = (locale: string) => { +export const translateCopyButton = (locale: string, intl: IntlShape) => { const articles = document.getElementsByTagName('article'); + const copyText = intl.formatMessage({ + defaultMessage: 'Copy', + description: 'Prism: copy button text (no clicked)', + }); + const copiedText = intl.formatMessage({ + defaultMessage: 'Copied!', + description: 'Prism: copy button text (clicked)', + }); + const errorText = intl.formatMessage({ + defaultMessage: 'Use Ctrl+c to copy', + description: 'Prism: error text', + }); Array.from(articles).forEach((article) => { article.setAttribute('lang', locale); - article.setAttribute('data-prismjs-copy', t`Copy`); - article.setAttribute('data-prismjs-copy-success', t`Copied!`); - article.setAttribute('data-prismjs-copy-error', t`Use Ctrl+c to copy`); + article.setAttribute('data-prismjs-copy', copyText); + article.setAttribute('data-prismjs-copy-success', copiedText); + article.setAttribute('data-prismjs-copy-error', errorText); }); }; |
