From 47b854de26dea24e7838fd0804df103dee99635f Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 28 Jan 2022 18:19:24 +0100
Subject: chore(i18n): replace i18n provider and helpers

---
 src/pages/404.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'src/pages/404.tsx')

diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index 5ba7b95..64c74ba 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -2,12 +2,12 @@ import { getLayout } from '@components/Layouts/Layout';
 import PostHeader from '@components/PostHeader/PostHeader';
 import { seo } from '@config/seo';
 import { t, Trans } from '@lingui/macro';
+import styles from '@styles/pages/Page.module.scss';
 import { NextPageWithLayout } from '@ts/types/app';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
+import { loadTranslation } from '@utils/helpers/i18n';
 import { GetStaticProps, GetStaticPropsContext } from 'next';
 import Head from 'next/head';
 import Link from 'next/link';
-import styles from '@styles/pages/Page.module.scss';
 
 const error404: NextPageWithLayout = () => {
   return (
@@ -40,7 +40,7 @@ export const getStaticProps: GetStaticProps = async (
 ) => {
   const breadcrumbTitle = t`Error`;
   const { locale } = context;
-  const translation = await loadTranslation(locale || defaultLocale);
+  const translation = await loadTranslation(locale);
 
   return {
     props: {
-- 
cgit v1.2.3


From e4d5b8151802517b2943756fc0d09ffa95e2c4e2 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Sat, 29 Jan 2022 18:21:37 +0100
Subject: chore: replace lingui functions with react-intl

---
 src/components/Branding/Branding.tsx               |  24 +-
 src/components/Breadcrumb/Breadcrumb.tsx           |  46 +-
 .../Buttons/ButtonToolbar/ButtonToolbar.tsx        |  27 +-
 src/components/Comment/Comment.tsx                 |  39 +-
 src/components/CommentForm/CommentForm.tsx         |  45 +-
 src/components/CommentsList/CommentsList.tsx       |  20 +-
 src/components/Footer/Footer.tsx                   |  10 +-
 src/components/FooterNav/FooterNav.tsx             |  18 +-
 src/components/Icons/Copyright/Copyright.tsx       |   3 +-
 src/components/Icons/Moon/Moon.tsx                 |  11 +-
 src/components/Icons/Sun/Sun.tsx                   |  11 +-
 src/components/Layouts/Layout.tsx                  |  14 +-
 src/components/MDX/CodeBlock/CodeBlock.tsx         |   6 +-
 src/components/MainNav/MainNav.tsx                 |  66 ++-
 .../PaginationCursor/PaginationCursor.tsx          |  21 +-
 src/components/PostFooter/PostFooter.tsx           |  11 +-
 src/components/PostMeta/PostMeta.tsx               | 118 ++++-
 src/components/PostPreview/PostPreview.tsx         |  31 +-
 src/components/PostsList/PostsList.tsx             |  25 +-
 src/components/ProjectPreview/ProjectPreview.tsx   |  22 +-
 src/components/ProjectSummary/ProjectSummary.tsx   |  68 ++-
 src/components/SearchForm/SearchForm.tsx           |  17 +-
 .../Settings/ReduceMotion/ReduceMotion.tsx         |  18 +-
 src/components/Settings/Settings.tsx               |  10 +-
 .../Settings/ThemeToggle/ThemeToggle.tsx           |   8 +-
 src/components/Spinner/Spinner.tsx                 |  11 +-
 .../ExpandableWidget/ExpandableWidget.tsx          |  13 +-
 src/components/Widgets/CVPreview/CVPreview.tsx     |  16 +-
 src/components/Widgets/RecentPosts/RecentPosts.tsx |  16 +-
 .../Widgets/RelatedThematics/RelatedThematics.tsx  |  12 +-
 .../Widgets/RelatedTopics/RelatedTopics.tsx        |  12 +-
 src/components/Widgets/Sharing/Sharing.tsx         |  42 +-
 .../Widgets/ThematicsList/ThematicsList.tsx        |  13 +-
 src/components/Widgets/ToC/ToC.tsx                 |   8 +-
 src/components/Widgets/TopicsList/TopicsList.tsx   |  13 +-
 src/config/nav.ts                                  |  14 -
 src/config/seo.ts                                  |  33 --
 src/config/sharing.ts                              |   4 +-
 src/config/website.ts                              |   4 +-
 src/i18n/en.json                                   | 586 +++++++++++++++++++++
 src/i18n/fr.json                                   | 586 +++++++++++++++++++++
 src/pages/404.tsx                                  |  62 ++-
 src/pages/article/[slug].tsx                       |   6 +-
 src/pages/blog/index.tsx                           |  78 ++-
 src/pages/contact.tsx                              |  95 +++-
 src/pages/cv.tsx                                   |  55 +-
 src/pages/index.tsx                                |  69 ++-
 src/pages/mentions-legales.tsx                     |  35 +-
 src/pages/projets.tsx                              |  30 +-
 src/pages/recherche/index.tsx                      |  68 ++-
 src/pages/sujet/[slug].tsx                         |  20 +-
 src/pages/thematique/[slug].tsx                    |  20 +-
 src/utils/helpers/i18n.ts                          |  23 +-
 src/utils/helpers/prism.ts                         |  22 +-
 54 files changed, 2314 insertions(+), 341 deletions(-)
 delete mode 100644 src/config/nav.ts
 delete mode 100644 src/config/seo.ts
 create mode 100644 src/i18n/en.json
 create mode 100644 src/i18n/fr.json

(limited to 'src/pages/404.tsx')

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>
                 ⭐&nbsp;
                 <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);
   });
 };
-- 
cgit v1.2.3

