summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-01-29 19:03:59 +0100
committerArmand Philippot <git@armandphilippot.com>2022-01-29 19:03:59 +0100
commit8fb5e4ef3ae925ebc6622711fb5c8c6147642cbc (patch)
tree9e99137a7b64ea7993a8311a7162336a551be8b2 /src
parent2bae7c43764df5678fe2fc2e68be11ae95d85a41 (diff)
parente4d5b8151802517b2943756fc0d09ffa95e2c4e2 (diff)
feat(i18n): replace linguijs with formatjs
Diffstat (limited to 'src')
-rw-r--r--src/components/Branding/Branding.tsx24
-rw-r--r--src/components/Breadcrumb/Breadcrumb.tsx46
-rw-r--r--src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx27
-rw-r--r--src/components/Comment/Comment.tsx39
-rw-r--r--src/components/CommentForm/CommentForm.tsx45
-rw-r--r--src/components/CommentsList/CommentsList.tsx20
-rw-r--r--src/components/Footer/Footer.tsx10
-rw-r--r--src/components/FooterNav/FooterNav.tsx18
-rw-r--r--src/components/Icons/Copyright/Copyright.tsx3
-rw-r--r--src/components/Icons/Moon/Moon.tsx11
-rw-r--r--src/components/Icons/Sun/Sun.tsx11
-rw-r--r--src/components/Layouts/Layout.tsx14
-rw-r--r--src/components/MDX/CodeBlock/CodeBlock.tsx6
-rw-r--r--src/components/MainNav/MainNav.tsx66
-rw-r--r--src/components/PaginationCursor/PaginationCursor.tsx21
-rw-r--r--src/components/PostFooter/PostFooter.tsx11
-rw-r--r--src/components/PostMeta/PostMeta.tsx118
-rw-r--r--src/components/PostPreview/PostPreview.tsx31
-rw-r--r--src/components/PostsList/PostsList.tsx25
-rw-r--r--src/components/ProjectPreview/ProjectPreview.tsx22
-rw-r--r--src/components/ProjectSummary/ProjectSummary.tsx68
-rw-r--r--src/components/SearchForm/SearchForm.tsx17
-rw-r--r--src/components/Settings/ReduceMotion/ReduceMotion.tsx18
-rw-r--r--src/components/Settings/Settings.tsx10
-rw-r--r--src/components/Settings/ThemeToggle/ThemeToggle.tsx8
-rw-r--r--src/components/Spinner/Spinner.tsx11
-rw-r--r--src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx13
-rw-r--r--src/components/Widgets/CVPreview/CVPreview.tsx16
-rw-r--r--src/components/Widgets/RecentPosts/RecentPosts.tsx16
-rw-r--r--src/components/Widgets/RelatedThematics/RelatedThematics.tsx12
-rw-r--r--src/components/Widgets/RelatedTopics/RelatedTopics.tsx12
-rw-r--r--src/components/Widgets/Sharing/Sharing.tsx42
-rw-r--r--src/components/Widgets/ThematicsList/ThematicsList.tsx13
-rw-r--r--src/components/Widgets/ToC/ToC.tsx8
-rw-r--r--src/components/Widgets/TopicsList/TopicsList.tsx13
-rw-r--r--src/config/nav.ts14
-rw-r--r--src/config/seo.ts33
-rw-r--r--src/config/sharing.ts4
-rw-r--r--src/config/website.ts5
-rw-r--r--src/i18n/en.json586
-rw-r--r--src/i18n/fr.json586
-rw-r--r--src/pages/404.tsx66
-rw-r--r--src/pages/_app.tsx26
-rw-r--r--src/pages/article/[slug].tsx17
-rw-r--r--src/pages/blog/index.tsx106
-rw-r--r--src/pages/contact.tsx109
-rw-r--r--src/pages/cv.tsx75
-rw-r--r--src/pages/index.tsx90
-rw-r--r--src/pages/mentions-legales.tsx57
-rw-r--r--src/pages/projet/[slug].tsx4
-rw-r--r--src/pages/projets.tsx34
-rw-r--r--src/pages/recherche/index.tsx80
-rw-r--r--src/pages/sujet/[slug].tsx42
-rw-r--r--src/pages/thematique/[slug].tsx46
-rw-r--r--src/utils/helpers/i18n.ts91
-rw-r--r--src/utils/helpers/prism.ts22
56 files changed, 2425 insertions, 513 deletions
diff --git a/src/components/Branding/Branding.tsx b/src/components/Branding/Branding.tsx
index 01948e9..efb3a49 100644
--- a/src/components/Branding/Branding.tsx
+++ b/src/components/Branding/Branding.tsx
@@ -1,17 +1,18 @@
-import Image from 'next/image';
-import Link from 'next/link';
-import { ReactElement } from 'react';
-import { t } from '@lingui/macro';
import photo from '@assets/images/armand-philippot.jpg';
import { config } from '@config/website';
-import styles from './Branding.module.scss';
import Head from 'next/head';
+import Image from 'next/image';
+import Link from 'next/link';
+import { ReactElement } from 'react';
+import { useIntl } from 'react-intl';
import { Person, WithContext } from 'schema-dts';
+import styles from './Branding.module.scss';
import Logo from './Logo/Logo';
type BrandingReturn = ({ isHome }: { isHome: boolean }) => ReactElement;
const Branding: BrandingReturn = ({ isHome = false }) => {
+ const intl = useIntl();
const TitleTag = isHome ? 'h1' : 'p';
const schemaJsonLd: WithContext<Person> = {
@@ -38,10 +39,15 @@ const Branding: BrandingReturn = ({ isHome = false }) => {
<div className={styles.logo__front}>
<Image
src={photo}
- alt={t({
- message: `${config.name} picture`,
- comment: 'Branding logo.',
- })}
+ alt={intl.formatMessage(
+ {
+ defaultMessage: '{brandingName} picture',
+ description: 'Branding: branding name picture.',
+ },
+ {
+ brandingName: config.name,
+ }
+ )}
layout="responsive"
/>
</div>
diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx
index 7c8eb5c..30179be 100644
--- a/src/components/Breadcrumb/Breadcrumb.tsx
+++ b/src/components/Breadcrumb/Breadcrumb.tsx
@@ -1,12 +1,13 @@
import { config } from '@config/website';
-import { t } from '@lingui/macro';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
+import { useIntl } from 'react-intl';
import { BreadcrumbList, WithContext } from 'schema-dts';
import styles from './Breadcrumb.module.scss';
const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
+ const intl = useIntl();
const router = useRouter();
const isHome = router.pathname === '/';
@@ -20,14 +21,24 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
<>
<li className={styles.item}>
<Link href="/">
- <a>{t`Home`}</a>
+ <a>
+ {intl.formatMessage({
+ defaultMessage: 'Home',
+ description: 'Breadcrumb: Home item',
+ })}
+ </a>
</Link>
</li>
{(isArticle || isThematic || isSubject) && (
<>
<li className={styles.item}>
<Link href="/blog">
- <a>{t`Blog`}</a>
+ <a>
+ {intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'Breadcrumb: Blog item',
+ })}
+ </a>
</Link>
</li>
</>
@@ -36,7 +47,12 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
<>
<li className={styles.item}>
<Link href="/projets">
- <a>{t`Projects`}</a>
+ <a>
+ {intl.formatMessage({
+ defaultMessage: 'Projects',
+ description: 'Breadcrumb: Projects item',
+ })}
+ </a>
</Link>
</li>
</>
@@ -51,7 +67,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
const homepage: BreadcrumbList['itemListElement'] = {
'@type': 'ListItem',
position: 1,
- name: t`Home`,
+ name: intl.formatMessage({
+ defaultMessage: 'Home',
+ description: 'Breadcrumb: Home item',
+ }),
item: config.url,
};
@@ -61,7 +80,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
const blog: BreadcrumbList['itemListElement'] = {
'@type': 'ListItem',
position: 2,
- name: t`Blog`,
+ name: intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'Breadcrumb: Blog item',
+ }),
item: `${config.url}/blog`,
};
@@ -72,7 +94,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
const blog: BreadcrumbList['itemListElement'] = {
'@type': 'ListItem',
position: 2,
- name: t`Projects`,
+ name: intl.formatMessage({
+ defaultMessage: 'Projects',
+ description: 'Breadcrumb: Projects item',
+ }),
item: `${config.url}/projets`,
};
@@ -108,7 +133,12 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
</Head>
{!isHome && (
<nav id="breadcrumb" className={styles.wrapper}>
- <span className="screen-reader-text">{t`You are here:`}</span>
+ <span className="screen-reader-text">
+ {intl.formatMessage({
+ defaultMessage: 'You are here:',
+ description: 'Breadcrumb: You are here prefix',
+ })}
+ </span>
<ol className={styles.list}>{getItems()}</ol>
</nav>
)}
diff --git a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx b/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx
index 246ad80..e9f6079 100644
--- a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx
+++ b/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx
@@ -1,6 +1,6 @@
import { CloseIcon, CogIcon, SearchIcon } from '@components/Icons';
-import { t } from '@lingui/macro';
import { ForwardedRef, forwardRef, SetStateAction } from 'react';
+import { useIntl } from 'react-intl';
import styles from '../Buttons.module.scss';
type ButtonType = 'search' | 'settings';
@@ -17,6 +17,7 @@ const ButtonToolbar = (
},
ref: ForwardedRef<HTMLButtonElement>
) => {
+ const intl = useIntl();
const ButtonIcon = () => (type === 'search' ? <SearchIcon /> : <CogIcon />);
const btnClasses = isActivated
? `${styles.toolbar} ${styles['toolbar--activated']}`
@@ -38,9 +39,29 @@ const ButtonToolbar = (
</span>
</span>
{isActivated ? (
- <span className="screen-reader-text">{t`Close ${type}`}</span>
+ <span className="screen-reader-text">
+ {intl.formatMessage(
+ {
+ defaultMessage: 'Close {type}',
+ description: 'ButtonToolbar: Close button',
+ },
+ {
+ type,
+ }
+ )}
+ </span>
) : (
- <span className="screen-reader-text">{t`Open ${type}`}</span>
+ <span className="screen-reader-text">
+ {intl.formatMessage(
+ {
+ defaultMessage: 'Open {type}',
+ description: 'ButtonToolbar: Open button',
+ },
+ {
+ type,
+ }
+ )}
+ </span>
)}
</button>
);
diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx
index 6eb0184..e95a378 100644
--- a/src/components/Comment/Comment.tsx
+++ b/src/components/Comment/Comment.tsx
@@ -1,7 +1,6 @@
import { Button } from '@components/Buttons';
import CommentForm from '@components/CommentForm/CommentForm';
import { config } from '@config/website';
-import { t } from '@lingui/macro';
import { Comment as CommentData } from '@ts/types/comments';
import { getFormattedDate } from '@utils/helpers/format';
import Head from 'next/head';
@@ -9,6 +8,7 @@ import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
+import { useIntl } from 'react-intl';
import { Comment as CommentSchema, WithContext } from 'schema-dts';
import styles from './Comment.module.scss';
@@ -21,6 +21,7 @@ const Comment = ({
comment: CommentData;
isNested?: boolean;
}) => {
+ const intl = useIntl();
const router = useRouter();
const locale = router.locale ? router.locale : config.locales.defaultLocale;
const [isReply, setIsReply] = useState<boolean>(false);
@@ -48,7 +49,16 @@ const Comment = ({
minute: 'numeric',
})
.replace(':', 'h');
- return t`${date} at ${time}`;
+ return intl.formatMessage(
+ {
+ defaultMessage: '{date} at {time}',
+ description: 'Comment: publication date',
+ },
+ {
+ date,
+ time,
+ }
+ );
};
const getApprovedComment = () => {
@@ -68,7 +78,12 @@ const Comment = ({
{getCommentAuthor()}
</header>
<dl className={styles.date}>
- <dt>{t`Published on:`}</dt>
+ <dt>
+ {intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Comment: publication date label',
+ })}
+ </dt>
<dd>
<time dateTime={comment.date}>
<Link href={`#comment-${comment.commentId}`}>
@@ -83,9 +98,12 @@ const Comment = ({
></div>
{!isNested && (
<footer className={styles.footer}>
- <Button
- clickHandler={() => setIsReply((prev) => !prev)}
- >{t`Reply`}</Button>
+ <Button clickHandler={() => setIsReply((prev) => !prev)}>
+ {intl.formatMessage({
+ defaultMessage: 'Reply',
+ description: 'Comment: reply button',
+ })}
+ </Button>
</footer>
)}
</article>
@@ -116,7 +134,14 @@ const Comment = ({
};
const getCommentStatus = () => {
- return <p>{t`This comment is awaiting moderation.`}</p>;
+ return (
+ <p>
+ {intl.formatMessage({
+ defaultMessage: 'This comment is awaiting moderation.',
+ description: 'Comment: awaiting moderation message',
+ })}
+ </p>
+ );
};
const schemaJsonLd: WithContext<CommentSchema> = {
diff --git a/src/components/CommentForm/CommentForm.tsx b/src/components/CommentForm/CommentForm.tsx
index 1ed219c..0ea3276 100644
--- a/src/components/CommentForm/CommentForm.tsx
+++ b/src/components/CommentForm/CommentForm.tsx
@@ -1,9 +1,9 @@
import { ButtonSubmit } from '@components/Buttons';
import { Form, FormItem, Input, TextArea } from '@components/Form';
import Notice from '@components/Notice/Notice';
-import { t } from '@lingui/macro';
import { createComment } from '@services/graphql/mutations';
import { ForwardedRef, forwardRef, useState } from 'react';
+import { useIntl } from 'react-intl';
import styles from './CommentForm.module.scss';
const CommentForm = (
@@ -18,6 +18,7 @@ const CommentForm = (
},
ref: ForwardedRef<HTMLInputElement>
) => {
+ const intl = useIntl();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [website, setWebsite] = useState('');
@@ -68,7 +69,12 @@ const CommentForm = (
return (
<div className={wrapperClasses}>
- <h2 className={styles.title}>{t`Leave a comment`}</h2>
+ <h2 className={styles.title}>
+ {intl.formatMessage({
+ defaultMessage: 'Leave a comment',
+ description: 'CommentForm: form title',
+ })}
+ </h2>
<Form
submitHandler={submitHandler}
modifier={isReply ? 'centered' : undefined}
@@ -77,7 +83,10 @@ const CommentForm = (
<Input
id="commenter-name"
name="commenter-name"
- label={t`Name`}
+ label={intl.formatMessage({
+ defaultMessage: 'Name',
+ description: 'CommentForm: Name field label',
+ })}
required={true}
value={name}
setValue={setName}
@@ -88,7 +97,10 @@ const CommentForm = (
<Input
id="commenter-email"
name="commenter-email"
- label={t`Email`}
+ label={intl.formatMessage({
+ defaultMessage: 'Email',
+ description: 'CommentForm: Email field label',
+ })}
required={true}
value={email}
setValue={setEmail}
@@ -98,7 +110,10 @@ const CommentForm = (
<Input
id="commenter-website"
name="commenter-website"
- label={t`Website`}
+ label={intl.formatMessage({
+ defaultMessage: 'Website',
+ description: 'CommentForm: Website field label',
+ })}
value={website}
setValue={setWebsite}
/>
@@ -107,17 +122,31 @@ const CommentForm = (
<TextArea
id="commenter-message"
name="commenter-message"
- label={t`Comment`}
+ label={intl.formatMessage({
+ defaultMessage: 'Comment',
+ description: 'CommentForm: Comment field label',
+ })}
value={message}
setValue={setMessage}
required={true}
/>
</FormItem>
<FormItem>
- <ButtonSubmit>{t`Send`}</ButtonSubmit>
+ <ButtonSubmit>
+ {intl.formatMessage({
+ defaultMessage: 'Send',
+ description: 'CommentForm: Send button',
+ })}
+ </ButtonSubmit>
</FormItem>
{isSuccess && !isApproved && (
- <Notice type="success">{t`Thanks for your comment! It is now awaiting moderation.`}</Notice>
+ <Notice type="success">
+ {intl.formatMessage({
+ defaultMessage:
+ 'Thanks for your comment! It is now awaiting moderation.',
+ description: 'CommentForm: Comment sent success message',
+ })}
+ </Notice>
)}
</Form>
</div>
diff --git a/src/components/CommentsList/CommentsList.tsx b/src/components/CommentsList/CommentsList.tsx
index bdca00b..6630a03 100644
--- a/src/components/CommentsList/CommentsList.tsx
+++ b/src/components/CommentsList/CommentsList.tsx
@@ -1,6 +1,6 @@
-import { Comment as CommentData } from '@ts/types/comments';
import Comment from '@components/Comment/Comment';
-import { t } from '@lingui/macro';
+import { Comment as CommentData } from '@ts/types/comments';
+import { useIntl } from 'react-intl';
import styles from './CommentsList.module.scss';
const CommentsList = ({
@@ -10,6 +10,8 @@ const CommentsList = ({
articleId: number;
comments: CommentData[];
}) => {
+ const intl = useIntl();
+
const getCommentsList = () => {
return comments.map((comment) => {
return (
@@ -20,11 +22,21 @@ const CommentsList = ({
return (
<>
- <h2 className={styles.title}>{t`Comments`}</h2>
+ <h2 className={styles.title}>
+ {intl.formatMessage({
+ defaultMessage: 'Comments',
+ description: 'CommentsList: Comments section title',
+ })}
+ </h2>
{comments.length > 0 ? (
<ol className={styles.list}>{getCommentsList()}</ol>
) : (
- <p className={styles['no-comments']}>{t`No comments yet.`}</p>
+ <p className={styles['no-comments']}>
+ {intl.formatMessage({
+ defaultMessage: 'No comments yet.',
+ description: 'CommentsList: No comment message',
+ })}
+ </p>
)}
</>
);
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
index 15a4660..4aa980d 100644
--- a/src/components/Footer/Footer.tsx
+++ b/src/components/Footer/Footer.tsx
@@ -2,11 +2,12 @@ import { ButtonLink } from '@components/Buttons';
import Copyright from '@components/Copyright/Copyright';
import FooterNav from '@components/FooterNav/FooterNav';
import { ArrowIcon } from '@components/Icons';
-import { t } from '@lingui/macro';
import { useEffect, useState } from 'react';
+import { useIntl } from 'react-intl';
import styles from './Footer.module.scss';
const Footer = () => {
+ const intl = useIntl();
const [backToTopClasses, setBackToTopClasses] = useState(
`${styles['back-to-top']} ${styles['back-to-top--hidden']}`
);
@@ -36,7 +37,12 @@ const Footer = () => {
<FooterNav />
<div className={backToTopClasses}>
<ButtonLink target="#top" position="center">
- <span className="screen-reader-text">{t`Back to top`}</span>
+ <span className="screen-reader-text">
+ {intl.formatMessage({
+ defaultMessage: 'Back to top',
+ description: 'Footer: Back to top button',
+ })}
+ </span>
<ArrowIcon direction="top" />
</ButtonLink>
</div>
diff --git a/src/components/FooterNav/FooterNav.tsx b/src/components/FooterNav/FooterNav.tsx
index 7266e7e..918fed7 100644
--- a/src/components/FooterNav/FooterNav.tsx
+++ b/src/components/FooterNav/FooterNav.tsx
@@ -1,9 +1,23 @@
import Link from 'next/link';
import styles from './FooterNav.module.scss';
-import { footerNav } from '@config/nav';
+import { NavItem } from '@ts/types/nav';
+import { useIntl } from 'react-intl';
const FooterNav = () => {
- const navItems = footerNav.map((item) => {
+ const intl = useIntl();
+
+ const footerNavConfig: NavItem[] = [
+ {
+ id: 'legal-notice',
+ name: intl.formatMessage({
+ defaultMessage: 'Legal notice',
+ description: 'FooterNav: legal notice link',
+ }),
+ slug: '/mentions-legales',
+ },
+ ];
+
+ const navItems = footerNavConfig.map((item) => {
return (
<li key={item.id} className={styles.item}>
<Link href={item.slug}>
diff --git a/src/components/Icons/Copyright/Copyright.tsx b/src/components/Icons/Copyright/Copyright.tsx
index 396c127..d27c042 100644
--- a/src/components/Icons/Copyright/Copyright.tsx
+++ b/src/components/Icons/Copyright/Copyright.tsx
@@ -1,4 +1,3 @@
-import { t } from '@lingui/macro';
import styles from './Copyright.module.scss';
const CopyrightIcon = () => {
@@ -8,7 +7,7 @@ const CopyrightIcon = () => {
viewBox="0 0 211.99811 63.999996"
xmlns="http://www.w3.org/2000/svg"
>
- <title>{t`CC BY SA`}</title>
+ <title>CC BY SA</title>
<path d="m 175.53911,15.829498 c 0,-3.008 1.485,-4.514 4.458,-4.514 2.973,0 4.457,1.504 4.457,4.514 0,2.971 -1.486,4.457 -4.457,4.457 -2.971,0 -4.458,-1.486 -4.458,-4.457 z" />
<path d="m 188.62611,24.057498 v 13.085 h -3.656 v 15.542 h -9.944 v -15.541 h -3.656 v -13.086 c 0,-0.572 0.2,-1.057 0.599,-1.457 0.401,-0.399 0.887,-0.6 1.457,-0.6 h 13.144 c 0.533,0 1.01,0.2 1.428,0.6 0.417,0.4 0.628,0.886 0.628,1.457 z" />
<path d="m 179.94147,-1.9073486e-6 c -8.839,0 -16.34167,3.0848125073486 -22.51367,9.2578125073486 -6.285,6.4000004 -9.42969,13.9811874 -9.42969,22.7421874 0,8.762 3.14469,16.284312 9.42969,22.570312 6.361,6.286 13.86467,9.429688 22.51367,9.429688 8.799,0 16.43611,-3.181922 22.91211,-9.544922 6.096,-5.98 9.14453,-13.464078 9.14453,-22.455078 0,-8.952 -3.10646,-16.532188 -9.31446,-22.7421874 -6.172,-6.172 -13.75418,-9.2578125073486 -22.74218,-9.2578125073486 z M 180.05475,5.7714825 c 7.238,0 13.40967,2.55225 18.51367,7.6562495 5.103,5.106 7.65625,11.294313 7.65625,18.570313 0,7.391 -2.51397,13.50575 -7.54297,18.34375 -5.295,5.221 -11.50591,7.828125 -18.6289,7.828125 -7.162,0 -13.33268,-2.589484 -18.51368,-7.771484 -5.18,-5.178001 -7.76953,-11.310485 -7.76953,-18.396485 0,-7.047 2.60813,-13.238266 7.82813,-18.572265 5.029,-5.1040004 11.18103,-7.6582035 18.45703,-7.6582035 z" />
diff --git a/src/components/Icons/Moon/Moon.tsx b/src/components/Icons/Moon/Moon.tsx
index 62e7203..acdf6ae 100644
--- a/src/components/Icons/Moon/Moon.tsx
+++ b/src/components/Icons/Moon/Moon.tsx
@@ -1,14 +1,21 @@
-import { t } from '@lingui/macro';
+import { useIntl } from 'react-intl';
import styles from './Moon.module.scss';
const MoonIcon = () => {
+ const intl = useIntl();
+
return (
<svg
className={styles.moon}
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
- <title>{t`Dark theme`}</title>
+ <title>
+ {intl.formatMessage({
+ defaultMessage: 'Dark theme',
+ description: 'Icons: Moon icon (dark theme)',
+ })}
+ </title>
<path d="M 51.077315,1.9893942 A 43.319985,43.319985 0 0 1 72.840039,39.563145 43.319985,43.319985 0 0 1 29.520053,82.88313 43.319985,43.319985 0 0 1 5.4309911,75.569042 48.132997,48.132997 0 0 0 46.126047,98 48.132997,48.132997 0 0 0 94.260004,49.867002 48.132997,48.132997 0 0 0 51.077315,1.9893942 Z" />
</svg>
);
diff --git a/src/components/Icons/Sun/Sun.tsx b/src/components/Icons/Sun/Sun.tsx
index 612d3fa..44945c2 100644
--- a/src/components/Icons/Sun/Sun.tsx
+++ b/src/components/Icons/Sun/Sun.tsx
@@ -1,14 +1,21 @@
-import { t } from '@lingui/macro';
+import { useIntl } from 'react-intl';
import styles from './Sun.module.scss';
const SunIcon = () => {
+ const intl = useIntl();
+
return (
<svg
className={styles.sun}
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
- <title>{t`Light theme`}</title>
+ <title>
+ {intl.formatMessage({
+ defaultMessage: 'Light theme',
+ description: 'Icons: Sun icon (light theme)',
+ })}
+ </title>
<path d="M 69.398043,50.000437 A 19.399259,19.399204 0 0 1 49.998784,69.399641 19.399259,19.399204 0 0 1 30.599525,50.000437 19.399259,19.399204 0 0 1 49.998784,30.601234 19.399259,19.399204 0 0 1 69.398043,50.000437 Z m 27.699233,1.125154 c 2.657696,0.0679 1.156196,12.061455 -1.435545,11.463959 L 80.113224,59.000697 c -2.589801,-0.597494 -1.625657,-8.345536 1.032041,-8.278609 z m -18.06653,37.251321 c 1.644087,2.091234 -9.030355,8.610337 -10.126414,6.188346 L 62.331863,80.024585 c -1.096058,-2.423931 5.197062,-6.285342 6.839209,-4.194107 z M 38.611418,97.594444 C 38.02653,100.18909 26.24148,95.916413 27.436475,93.54001 l 7.168026,-14.256474 c 1.194024,-2.376403 8.102101,0.151313 7.517214,2.744986 z M 6.1661563,71.834242 C 3.7916868,73.028262 -0.25499873,61.16274 2.3386824,60.577853 L 17.905618,57.067567 c 2.593681,-0.584886 4.894434,6.403678 2.518995,7.598668 z M 6.146757,30.055146 c -2.3764094,-1.194991 4.46571,-11.714209 6.479353,-9.97798 l 12.090589,10.414462 c 2.014613,1.736229 -1.937017,7.926514 -4.314396,6.731524 z M 38.56777,4.2639045 C 37.982883,1.6682911 50.480855,0.41801247 50.415868,3.0766733 L 50.020123,19.028638 c -0.06596,2.657691 -7.357169,3.394862 -7.943027,0.800218 z m 40.403808,9.1622435 c 1.635357,-2.098023 10.437771,6.872168 8.339742,8.506552 l -12.58818,9.805327 c -2.099,1.634383 -7.192276,-3.626682 -5.557888,-5.724706 z M 97.096306,50.69105 c 2.657696,-0.06596 1.164926,12.462047 -1.425846,11.863582 L 80.122924,58.96578 c -2.590771,-0.597496 -1.636327,-7.814 1.021371,-7.879957 z" />
</svg>
);
diff --git a/src/components/Layouts/Layout.tsx b/src/components/Layouts/Layout.tsx
index 599cfe2..b479ef3 100644
--- a/src/components/Layouts/Layout.tsx
+++ b/src/components/Layouts/Layout.tsx
@@ -1,12 +1,12 @@
-import { ReactElement, ReactNode, useEffect, useRef } from 'react';
import Footer from '@components/Footer/Footer';
import Header from '@components/Header/Header';
import Main from '@components/Main/Main';
import Breadcrumb from '@components/Breadcrumb/Breadcrumb';
-import { t } from '@lingui/macro';
-import Head from 'next/head';
import { config } from '@config/website';
+import Head from 'next/head';
import { useRouter } from 'next/router';
+import { ReactElement, ReactNode, useEffect, useRef } from 'react';
+import { useIntl } from 'react-intl';
import { WebSite, WithContext } from 'schema-dts';
const Layout = ({
@@ -16,6 +16,7 @@ const Layout = ({
children: ReactNode;
isHome?: boolean;
}) => {
+ const intl = useIntl();
const ref = useRef<HTMLSpanElement>(null);
const { asPath } = useRouter();
@@ -91,7 +92,12 @@ const Layout = ({
></script>
</Head>
<span ref={ref} tabIndex={-1} />
- <a href="#main" className="screen-reader-text">{t`Skip to content`}</a>
+ <a href="#main" className="screen-reader-text">
+ {intl.formatMessage({
+ defaultMessage: 'Skip to content',
+ description: 'Layout: Skip to content button',
+ })}
+ </a>
<Header isHome={isHome} />
<Main>{children}</Main>
<Footer />
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx
index ef8b587..59386af 100644
--- a/src/components/MDX/CodeBlock/CodeBlock.tsx
+++ b/src/components/MDX/CodeBlock/CodeBlock.tsx
@@ -3,6 +3,7 @@ import { translateCopyButton } from '@utils/helpers/prism';
import { useRouter } from 'next/router';
import Prism from 'prismjs';
import { ReactChildren, useEffect } from 'react';
+import { useIntl } from 'react-intl';
const CodeBlock = ({
className,
@@ -15,6 +16,7 @@ const CodeBlock = ({
const languageClass = classNames.find((name: string) =>
name.startsWith('language-')
);
+ const intl = useIntl();
const router = useRouter();
const locale = router.locale ? router.locale : config.locales.defaultLocale;
@@ -23,8 +25,8 @@ const CodeBlock = ({
});
useEffect(() => {
- translateCopyButton(locale);
- }, [locale]);
+ translateCopyButton(locale, intl);
+ }, [intl, locale]);
return (
<div>
diff --git a/src/components/MainNav/MainNav.tsx b/src/components/MainNav/MainNav.tsx
index afc4193..a866b9c 100644
--- a/src/components/MainNav/MainNav.tsx
+++ b/src/components/MainNav/MainNav.tsx
@@ -1,6 +1,3 @@
-import { SetStateAction } from 'react';
-import Link from 'next/link';
-import { t } from '@lingui/macro';
import {
BlogIcon,
ContactIcon,
@@ -9,9 +6,12 @@ import {
HomeIcon,
ProjectsIcon,
} from '@components/Icons';
-import { mainNav } from '@config/nav';
-import styles from './MainNav.module.scss';
+import { NavItem } from '@ts/types/nav';
+import Link from 'next/link';
import { useRouter } from 'next/router';
+import { SetStateAction } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './MainNav.module.scss';
const MainNav = ({
isOpened,
@@ -20,8 +20,52 @@ const MainNav = ({
isOpened: boolean;
setIsOpened: (value: SetStateAction<boolean>) => void;
}) => {
+ const intl = useIntl();
const router = useRouter();
+ const mainNavConfig: NavItem[] = [
+ {
+ id: 'home',
+ name: intl.formatMessage({
+ defaultMessage: 'Home',
+ description: 'MainNav: home link',
+ }),
+ slug: '/',
+ },
+ {
+ id: 'blog',
+ name: intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'MainNav: blog link',
+ }),
+ slug: '/blog',
+ },
+ {
+ id: 'projects',
+ name: intl.formatMessage({
+ defaultMessage: 'Projects',
+ description: 'MainNav: projects link',
+ }),
+ slug: '/projets',
+ },
+ {
+ id: 'cv',
+ name: intl.formatMessage({
+ defaultMessage: 'Resume',
+ description: 'MainNav: resume link',
+ }),
+ slug: '/cv',
+ },
+ {
+ id: 'contact',
+ name: intl.formatMessage({
+ defaultMessage: 'Contact',
+ description: 'MainNav: contact link',
+ }),
+ slug: '/contact',
+ },
+ ];
+
const getIcon = (id: string) => {
switch (id) {
case 'home':
@@ -39,7 +83,7 @@ const MainNav = ({
}
};
- const navItems = mainNav.map((item) => {
+ const navItems = mainNavConfig.map((item) => {
const currentClass = router.asPath === item.slug ? styles.current : '';
return (
@@ -73,7 +117,15 @@ const MainNav = ({
>
<HamburgerIcon isActive={isOpened} />
<span className="screen-reader-text">
- {isOpened ? t`Close menu` : t`Open menu`}
+ {isOpened
+ ? intl.formatMessage({
+ defaultMessage: 'Close menu',
+ description: 'MainNav: close button',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'Open menu',
+ description: 'MainNav: open button',
+ })}
</span>
</label>
<nav className={styles.nav}>
diff --git a/src/components/PaginationCursor/PaginationCursor.tsx b/src/components/PaginationCursor/PaginationCursor.tsx
index bcbb555..a8c6265 100644
--- a/src/components/PaginationCursor/PaginationCursor.tsx
+++ b/src/components/PaginationCursor/PaginationCursor.tsx
@@ -1,4 +1,4 @@
-import { plural, t } from '@lingui/macro';
+import { useIntl } from 'react-intl';
import styles from './PaginationCursor.module.scss';
const PaginationCursor = ({
@@ -8,6 +8,8 @@ const PaginationCursor = ({
current: number;
total: number;
}) => {
+ const intl = useIntl();
+
return (
<div className={styles.wrapper}>
<progress
@@ -16,12 +18,19 @@ const PaginationCursor = ({
value={current}
aria-valuemin={0}
aria-valuemax={total}
- aria-label={t`Number of articles loaded out of the total available.`}
- title={plural(current, {
- zero: `# articles out of a total of ${total}`,
- one: `# article out of a total of ${total}`,
- other: `# articles out of a total of ${total}`,
+ aria-label={intl.formatMessage({
+ defaultMessage:
+ 'Number of articles loaded out of the total available.',
+ description: 'PaginationCursor: loaded articles count aria-label',
})}
+ title={intl.formatMessage(
+ {
+ defaultMessage:
+ '{articlesCount, plural, =0 {# articles} one {# article} other {# articles}} out of a total of {total}',
+ description: 'PaginationCursor: loaded articles count message',
+ },
+ { articlesCount: current, total }
+ )}
></progress>
</div>
);
diff --git a/src/components/PostFooter/PostFooter.tsx b/src/components/PostFooter/PostFooter.tsx
index ad471eb..6c97ec2 100644
--- a/src/components/PostFooter/PostFooter.tsx
+++ b/src/components/PostFooter/PostFooter.tsx
@@ -1,10 +1,12 @@
import { ButtonLink } from '@components/Buttons';
-import { t } from '@lingui/macro';
import { TopicPreview } from '@ts/types/taxonomies';
import Image from 'next/image';
+import { useIntl } from 'react-intl';
import styles from './PostFooter.module.scss';
const PostFooter = ({ topics }: { topics: TopicPreview[] }) => {
+ const intl = useIntl();
+
const getTopics = () => {
return topics.map((topic) => {
return (
@@ -31,7 +33,12 @@ const PostFooter = ({ topics }: { topics: TopicPreview[] }) => {
{topics.length > 0 && (
<>
<dl className={styles.meta}>
- <dt>{t`Read more articles about:`}</dt>
+ <dt>
+ {intl.formatMessage({
+ defaultMessage: 'Read more articles about:',
+ description: 'PostFooter: read more posts about given subjects',
+ })}
+ </dt>
<dd>
<ul className={styles.list}>{getTopics()}</ul>
</dd>
diff --git a/src/components/PostMeta/PostMeta.tsx b/src/components/PostMeta/PostMeta.tsx
index f95707a..86e4e71 100644
--- a/src/components/PostMeta/PostMeta.tsx
+++ b/src/components/PostMeta/PostMeta.tsx
@@ -1,9 +1,9 @@
import { config } from '@config/website';
-import { plural, t } from '@lingui/macro';
import { ArticleMeta } from '@ts/types/articles';
import { getFormattedDate } from '@utils/helpers/format';
import Link from 'next/link';
import { useRouter } from 'next/router';
+import { useIntl } from 'react-intl';
import styles from './PostMeta.module.scss';
type PostMetaMode = 'list' | 'single';
@@ -26,6 +26,7 @@ const PostMeta = ({
website,
wordsCount,
} = meta;
+ const intl = useIntl();
const router = useRouter();
const locale = router.locale ? router.locale : config.locales.defaultLocale;
const isThematic = () => router.asPath.includes('/thematique/');
@@ -62,24 +63,31 @@ const PostMeta = ({
};
const getCommentsCount = () => {
- switch (commentCount) {
- case 0:
- return t`No comments`;
- case 1:
- return t`1 comment`;
- default:
- return t`${commentCount} comments`;
- }
+ return intl.formatMessage(
+ {
+ defaultMessage:
+ '{commentCount, plural, =0 {No comments} one {# comment} other {# comments}}',
+ description: 'PostMeta: comment count value',
+ },
+ { commentCount }
+ );
};
const getReadingTime = () => {
if (!readingTime) return;
- if (readingTime < 0) return t`less than 1 minute`;
- return plural(readingTime, {
- zero: '# minutes',
- one: '# minute',
- other: '# minutes',
- });
+ if (readingTime < 0)
+ return intl.formatMessage({
+ defaultMessage: 'less than 1 minute',
+ description: 'PostMeta: Reading time value',
+ });
+ return intl.formatMessage(
+ {
+ defaultMessage:
+ '{readingTime, plural, =0 {# minutes} one {# minute} other {# minutes}}',
+ description: 'PostMeta: reading time value',
+ },
+ { readingTime }
+ );
};
const getDates = () => {
@@ -91,14 +99,24 @@ const PostMeta = ({
return (
<>
<div className={styles.item}>
- <dt className={styles.term}>{t`Published on:`}</dt>
+ <dt className={styles.term}>
+ {intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'PostMeta: publication date label',
+ })}
+ </dt>
<dd className={styles.description}>
<time dateTime={dates.publication}>{publicationDate}</time>
</dd>
</div>
{publicationDate !== updateDate && (
<div className={styles.item}>
- <dt className={styles.term}>{t`Updated on:`}</dt>
+ <dt className={styles.term}>
+ {intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'PostMeta: update date label',
+ })}
+ </dt>
<dd className={styles.description}>
<time dateTime={dates.update}>{updateDate}</time>
</dd>
@@ -114,14 +132,24 @@ const PostMeta = ({
<dl className={wrapperClass}>
{author && (
<div className={styles.item}>
- <dt className={styles.term}>{t`Written by:`}</dt>
+ <dt className={styles.term}>
+ {intl.formatMessage({
+ defaultMessage: 'Written by:',
+ description: 'Article meta',
+ })}
+ </dt>
<dd className={styles.description}>{author.name}</dd>
</div>
)}
{getDates()}
{readingTime !== undefined && wordsCount !== undefined && (
<div className={styles.item}>
- <dt className={styles.term}>{t`Reading time:`}</dt>
+ <dt className={styles.term}>
+ {intl.formatMessage({
+ defaultMessage: 'Reading time:',
+ description: 'Article meta',
+ })}
+ </dt>
<dd
className={styles.description}
title={`Approximately ${wordsCount.toLocaleString(locale)} words`}
@@ -132,20 +160,35 @@ const PostMeta = ({
)}
{results && (
<div className={styles.item}>
- <dt className={styles.term}>{t`Total: `}</dt>
- <dd className={styles.description}>
- {plural(results, {
- zero: '# articles',
- one: '# article',
- other: '# articles',
+ <dt className={styles.term}>
+ {intl.formatMessage({
+ defaultMessage: 'Total:',
+ description: 'Article meta',
})}
+ </dt>
+ <dd className={styles.description}>
+ {intl.formatMessage(
+ {
+ defaultMessage:
+ '{results, plural, =0 {No articles} one {# article} other {# articles}}',
+ description: 'PostMeta: total found articles',
+ },
+ { results }
+ )}
</dd>
</div>
)}
{!isThematic() && thematics && thematics.length > 0 && (
<div className={styles.item}>
<dt className={styles.term}>
- {thematics.length > 1 ? t`Thematics:` : t`Thematic:`}
+ {intl.formatMessage(
+ {
+ defaultMessage:
+ '{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}',
+ description: 'PostMeta: thematics list label',
+ },
+ { thematicsCount: thematics.length }
+ )}
</dt>
{getThematics()}
</div>
@@ -153,14 +196,26 @@ const PostMeta = ({
{isThematic() && topics && topics.length > 0 && (
<div className={styles.item}>
<dt className={styles.term}>
- {topics.length > 1 ? t`Topics:` : t`Topic:`}
+ {intl.formatMessage(
+ {
+ defaultMessage:
+ '{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}',
+ description: 'PostMeta: topics list label',
+ },
+ { topicsCount: topics.length }
+ )}
</dt>
{getTopics()}
</div>
)}
{website && (
<div className={styles.item}>
- <dt className={styles.term}>{t`Website:`}</dt>
+ <dt className={styles.term}>
+ {intl.formatMessage({
+ defaultMessage: 'Website:',
+ description: 'PostMeta: website label',
+ })}
+ </dt>
<dd className={styles.description}>
<a href={website}>{website}</a>
</dd>
@@ -168,7 +223,12 @@ const PostMeta = ({
)}
{commentCount !== undefined && (
<div className={styles.item}>
- <dt className={styles.term}>{t`Comments:`}</dt>
+ <dt className={styles.term}>
+ {intl.formatMessage({
+ defaultMessage: 'Comments:',
+ description: 'PostMeta: comment count label',
+ })}
+ </dt>
<dd className={styles.description}>
{isArticle() ? (
<a href="#comments">{getCommentsCount()}</a>
diff --git a/src/components/PostPreview/PostPreview.tsx b/src/components/PostPreview/PostPreview.tsx
index b084ca1..72ba638 100644
--- a/src/components/PostPreview/PostPreview.tsx
+++ b/src/components/PostPreview/PostPreview.tsx
@@ -1,15 +1,15 @@
-import PostMeta from '@components/PostMeta/PostMeta';
-import { t } from '@lingui/macro';
-import { ArticleMeta, ArticlePreview } from '@ts/types/articles';
-import Link from 'next/link';
-import styles from './PostPreview.module.scss';
-import Image from 'next/image';
import { ButtonLink } from '@components/Buttons';
import { ArrowIcon } from '@components/Icons';
+import PostMeta from '@components/PostMeta/PostMeta';
+import { config } from '@config/website';
import { TitleLevel } from '@ts/types/app';
-import { BlogPosting, WithContext } from 'schema-dts';
+import { ArticleMeta, ArticlePreview } from '@ts/types/articles';
+import Image from 'next/image';
import Head from 'next/head';
-import { config } from '@config/website';
+import Link from 'next/link';
+import { FormattedMessage } from 'react-intl';
+import { BlogPosting, WithContext } from 'schema-dts';
+import styles from './PostPreview.module.scss';
const PostPreview = ({
post,
@@ -97,11 +97,16 @@ const PostPreview = ({
></div>
<footer className={styles.footer}>
<ButtonLink target={`/article/${slug}`} position="left">
- {t`Read more`}
- <span className="screen-reader-text">
- {' '}
- {t({ message: `about ${title}`, comment: 'Post title' })}
- </span>
+ <FormattedMessage
+ defaultMessage="Read more<a11y> about {title}</a11y>"
+ description="PostPreview: read more link"
+ values={{
+ title,
+ a11y: (chunks: string) => (
+ <span className="screen-reader-text">{chunks}</span>
+ ),
+ }}
+ />
<ArrowIcon />
</ButtonLink>
</footer>
diff --git a/src/components/PostsList/PostsList.tsx b/src/components/PostsList/PostsList.tsx
index df9dfe4..16deee3 100644
--- a/src/components/PostsList/PostsList.tsx
+++ b/src/components/PostsList/PostsList.tsx
@@ -1,9 +1,9 @@
-import { t } from '@lingui/macro';
-import { PostsList as PostsListData } from '@ts/types/blog';
-import styles from './PostsList.module.scss';
import PostPreview from '@components/PostPreview/PostPreview';
-import { ForwardedRef, forwardRef, Fragment } from 'react';
+import { PostsList as PostsListData } from '@ts/types/blog';
import { sortPostsByYear } from '@utils/helpers/sort';
+import { ForwardedRef, forwardRef, Fragment } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './PostsList.module.scss';
const PostsList = (
{
@@ -15,6 +15,7 @@ const PostsList = (
},
ref: ForwardedRef<HTMLSpanElement>
) => {
+ const intl = useIntl();
const titleLevel = showYears ? 3 : 2;
const getPostsListByYear = () => {
@@ -32,7 +33,12 @@ const PostsList = (
<section key={year} className={styles.section}>
{showYears && (
<h2 className={styles.year}>
- <span className="screen-reader-text">{t`Published in`} </span>
+ <span className="screen-reader-text">
+ {intl.formatMessage({
+ defaultMessage: 'Published on',
+ description: 'PostsList: published on year label',
+ })}{' '}
+ </span>
{year}
</h2>
)}
@@ -62,7 +68,14 @@ const PostsList = (
};
if (page.posts.length === 0) {
- return <p key="no-result">{t`No results found.`}</p>;
+ return (
+ <p key="no-result">
+ {intl.formatMessage({
+ defaultMessage: 'No results found.',
+ description: 'PostsList: no results',
+ })}
+ </p>
+ );
} else {
return (
<Fragment key={page.pageInfo.endCursor}>
diff --git a/src/components/ProjectPreview/ProjectPreview.tsx b/src/components/ProjectPreview/ProjectPreview.tsx
index cba0b02..043d945 100644
--- a/src/components/ProjectPreview/ProjectPreview.tsx
+++ b/src/components/ProjectPreview/ProjectPreview.tsx
@@ -1,12 +1,13 @@
-import { t } from '@lingui/macro';
import { Project } from '@ts/types/app';
import { slugify } from '@utils/helpers/slugify';
import Image from 'next/image';
import Link from 'next/link';
+import { useIntl } from 'react-intl';
import styles from './ProjectPreview.module.scss';
const ProjectPreview = ({ project }: { project: Project }) => {
const { id, meta, tagline, title } = project;
+ const intl = useIntl();
return (
<Link href={`/projet/${project.slug}`}>
@@ -20,7 +21,13 @@ const ProjectPreview = ({ project }: { project: Project }) => {
layout="fill"
objectFit="contain"
objectPosition="center"
- alt={t`${title} picture`}
+ alt={intl.formatMessage(
+ {
+ defaultMessage: '{title} picture',
+ description: 'ProjectPreview: cover alt text',
+ },
+ { title }
+ )}
/>
</div>
)}
@@ -36,7 +43,16 @@ const ProjectPreview = ({ project }: { project: Project }) => {
<dl className={styles.meta}>
{meta.technologies && (
<div className={styles.meta__item}>
- <dt className="screen-reader-text">{t`Technologies:`}</dt>
+ <dt className="screen-reader-text">
+ {intl.formatMessage(
+ {
+ defaultMessage:
+ '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}',
+ description: 'ProjectPreview: technologies list label',
+ },
+ { count: meta.technologies.length }
+ )}
+ </dt>
{meta.technologies.map((techno) => (
<dd key={slugify(techno)} className={styles.techno}>
{techno}
diff --git a/src/components/ProjectSummary/ProjectSummary.tsx b/src/components/ProjectSummary/ProjectSummary.tsx
index b32c11f..f2d73b6 100644
--- a/src/components/ProjectSummary/ProjectSummary.tsx
+++ b/src/components/ProjectSummary/ProjectSummary.tsx
@@ -1,13 +1,14 @@
import GithubIcon from '@assets/images/social-media/github.svg';
import GitlabIcon from '@assets/images/social-media/gitlab.svg';
import { config } from '@config/website';
-import { t } from '@lingui/macro';
import { ProjectMeta } from '@ts/types/app';
import { getFormattedDate } from '@utils/helpers/format';
import { slugify } from '@utils/helpers/slugify';
import useGithubApi from '@utils/hooks/useGithubApi';
+import IntlMessageFormat from 'intl-messageformat';
import Image from 'next/image';
import { useRouter } from 'next/router';
+import { useIntl } from 'react-intl';
import styles from './ProjectSummary.module.scss';
const ProjectSummary = ({
@@ -20,6 +21,7 @@ const ProjectSummary = ({
meta: ProjectMeta;
}) => {
const { hasCover, license, repos, technologies } = meta;
+ const intl = useIntl();
const router = useRouter();
const locale = router.locale ? router.locale : config.locales.defaultLocale;
const { data } = useGithubApi(repos?.github ? repos.github : '');
@@ -30,7 +32,10 @@ const ProjectSummary = ({
<div className={styles.cover}>
<Image
src={`/projects/${id}.jpg`}
- alt={t`${title} preview`}
+ alt={intl.formatMessage({
+ defaultMessage: '{title} preview',
+ description: 'ProjectSummary: cover alt text',
+ })}
layout="fill"
objectFit="contain"
/>
@@ -39,7 +44,12 @@ const ProjectSummary = ({
<dl className={styles.info}>
{data && (
<div className={styles.info__item}>
- <dt>{t`Created on`}</dt>
+ <dt>
+ {intl.formatMessage({
+ defaultMessage: 'Created on:',
+ description: 'ProjectSummary: creation date label',
+ })}
+ </dt>
<dd>
<time dateTime={data.created_at}>
{getFormattedDate(data.created_at, locale)}
@@ -49,7 +59,12 @@ const ProjectSummary = ({
)}
{data && (
<div className={styles.info__item}>
- <dt>{t`Last updated on`}</dt>
+ <dt>
+ {intl.formatMessage({
+ defaultMessage: 'Last updated on:',
+ description: 'ProjectSummary: update date label',
+ })}
+ </dt>
<dd>
<time dateTime={data.updated_at}>
{getFormattedDate(data.updated_at, locale)}
@@ -58,12 +73,26 @@ const ProjectSummary = ({
</div>
)}
<div className={styles.info__item}>
- <dt>{t`License`}</dt>
+ <dt>
+ {intl.formatMessage({
+ defaultMessage: 'License:',
+ description: 'ProjectSummary: license label',
+ })}
+ </dt>
<dd>{license}</dd>
</div>
{technologies && (
<div className={styles.info__item}>
- <dt>{t`Technologies`}</dt>
+ <dt>
+ {intl.formatMessage(
+ {
+ defaultMessage:
+ '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}',
+ description: 'ProjectSummary: technologies list label',
+ },
+ { count: technologies.length }
+ )}
+ </dt>
{technologies.map((techno) => (
<dd
key={slugify(techno)}
@@ -76,7 +105,16 @@ const ProjectSummary = ({
)}
{repos && (
<div className={styles.info__item}>
- <dt>{t`Repositories`}</dt>
+ <dt>
+ {intl.formatMessage(
+ {
+ defaultMessage:
+ '{count, plural, =0 {Repositories:} one {Repository:} other {Repositories:}}',
+ description: 'ProjectSummary: repositories list label',
+ },
+ { count: Object.keys(repos).length }
+ )}
+ </dt>
{repos.github && (
<dd className={styles['inline-data']}>
<a
@@ -103,12 +141,24 @@ const ProjectSummary = ({
)}
{data && repos && (
<div>
- <dt>{t`Popularity`}</dt>
+ <dt>
+ {intl.formatMessage({
+ defaultMessage: 'Popularity:',
+ description: 'ProjectSummary: popularity label',
+ })}
+ </dt>
{repos.github && (
<dd>
⭐&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 1b0e2d3..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(),
@@ -10,6 +8,7 @@ export const config = {
locales: {
defaultLocale: 'fr',
defaultCountry: 'FR',
+ supported: ['en', 'fr'],
},
postsPerPage: 10,
twitterId: '@ArmandPhilippot',
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 5ba7b95..079dead 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,46 +1,72 @@
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 { defaultLocale, 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 styles from '@styles/pages/Page.module.scss';
+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 || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
@@ -51,4 +77,4 @@ export const getStaticProps: GetStaticProps = async (
};
};
-export default error404;
+export default Error404;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index db021f9..ec97ff7 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,23 +1,21 @@
-import { useEffect } from 'react';
-import { i18n } from '@lingui/core';
-import { I18nProvider } from '@lingui/react';
+import { config } from '@config/website';
import { AppPropsWithLayout } from '@ts/types/app';
-import { activateLocale, defaultLocale, initLingui } from '@utils/helpers/i18n';
-import '../styles/globals.scss';
import { ThemeProvider } from 'next-themes';
-
-initLingui(defaultLocale);
+import { useRouter } from 'next/router';
+import { IntlProvider } from 'react-intl';
+import '../styles/globals.scss';
const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
- const locale: string = pageProps.locale || defaultLocale;
-
- useEffect(() => {
- activateLocale(locale, pageProps.translation);
- });
+ const { locale, defaultLocale } = useRouter();
+ const appLocale: string = locale || config.locales.defaultLocale;
const getLayout = Component.getLayout ?? ((page) => page);
return (
- <I18nProvider i18n={i18n}>
+ <IntlProvider
+ locale={appLocale}
+ defaultLocale={defaultLocale}
+ messages={pageProps.translation}
+ >
<ThemeProvider
defaultTheme="system"
enableColorScheme={true}
@@ -25,7 +23,7 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
>
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
- </I18nProvider>
+ </IntlProvider>
);
};
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index d38ff63..8668a66 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -3,11 +3,14 @@ import CommentsList from '@components/CommentsList/CommentsList';
import { getLayout } from '@components/Layouts/Layout';
import PostFooter from '@components/PostFooter/PostFooter';
import PostHeader from '@components/PostHeader/PostHeader';
+import Sidebar from '@components/Sidebar/Sidebar';
+import { Sharing, ToC } from '@components/Widgets';
import { config } from '@config/website';
import { getAllPostsSlug, getPostBySlug } from '@services/graphql/queries';
+import styles from '@styles/pages/Page.module.scss';
import { NextPageWithLayout } from '@ts/types/app';
import { ArticleMeta, ArticleProps } from '@ts/types/articles';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
+import { loadTranslation } from '@utils/helpers/i18n';
import { addPrismClasses, translateCopyButton } from '@utils/helpers/prism';
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
import Head from 'next/head';
@@ -15,9 +18,7 @@ import { useRouter } from 'next/router';
import Prism from 'prismjs';
import { ParsedUrlQuery } from 'querystring';
import { useEffect } from 'react';
-import styles from '@styles/pages/Page.module.scss';
-import { Sharing, ToC } from '@components/Widgets';
-import Sidebar from '@components/Sidebar/Sidebar';
+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}`,
@@ -163,7 +165,7 @@ export const getStaticProps: GetStaticProps = async (
context: GetStaticPropsContext
) => {
const { locale } = context;
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
const { slug } = context.params as PostParams;
const post = await getPostBySlug(slug);
const breadcrumbTitle = post.title;
@@ -171,7 +173,6 @@ export const getStaticProps: GetStaticProps = async (
return {
props: {
breadcrumbTitle,
- locale,
post,
translation,
},
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index 0650cfb..9a86d9f 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -1,27 +1,27 @@
-import { GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import { t } from '@lingui/macro';
-import { getLayout } from '@components/Layouts/Layout';
-import { seo } from '@config/seo';
-import { config } from '@config/website';
-import { NextPageWithLayout } from '@ts/types/app';
-import { BlogPageProps, PostsList as PostsListData } from '@ts/types/blog';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
-import PostsList from '@components/PostsList/PostsList';
-import useSWRInfinite from 'swr/infinite';
import { Button } from '@components/Buttons';
-import { getPublishedPosts } from '@services/graphql/queries';
+import { getLayout } from '@components/Layouts/Layout';
+import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
import PostHeader from '@components/PostHeader/PostHeader';
-import { ThematicsList, TopicsList } from '@components/Widgets';
+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 { config } from '@config/website';
+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 { 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 Spinner from '@components/Spinner/Spinner';
+import { useIntl } from 'react-intl';
import { Blog as BlogSchema, Graph, WebPage } from 'schema-dts';
-import { useRouter } from 'next/router';
-import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
+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,10 +208,14 @@ 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 || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx
index cb88b7d..489135d 100644
--- a/src/pages/contact.tsx
+++ b/src/pages/contact.tsx
@@ -1,23 +1,23 @@
import { ButtonSubmit } from '@components/Buttons';
import { Form, FormItem, Input, TextArea } from '@components/Form';
import { getLayout } from '@components/Layouts/Layout';
-import { seo } from '@config/seo';
-import { t } from '@lingui/macro';
+import PostHeader from '@components/PostHeader/PostHeader';
+import Sidebar from '@components/Sidebar/Sidebar';
+import { SocialMedia } from '@components/Widgets';
+import { config } from '@config/website';
import { sendMail } from '@services/graphql/mutations';
+import styles from '@styles/pages/Page.module.scss';
import { NextPageWithLayout } from '@ts/types/app';
-import { defaultLocale, 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 PostHeader from '@components/PostHeader/PostHeader';
-import styles from '@styles/pages/Page.module.scss';
-import { SocialMedia } from '@components/Widgets';
-import Sidebar from '@components/Sidebar/Sidebar';
+import { useIntl } from 'react-intl';
import { ContactPage as ContactPageSchema, Graph, WebPage } from 'schema-dts';
-import { config } from '@config/website';
-import { useRouter } from 'next/router';
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,9 +229,13 @@ 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 || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index 85bddd6..c3686de 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -1,21 +1,21 @@
import { getLayout } from '@components/Layouts/Layout';
-import { seo } from '@config/seo';
-import { NextPageWithLayout } from '@ts/types/app';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import CVContent, { intro, meta, pdf, image } from '@content/pages/cv.mdx';
import PostHeader from '@components/PostHeader/PostHeader';
-import { ArticleMeta } from '@ts/types/articles';
-import styles from '@styles/pages/Page.module.scss';
-import { CVPreview, SocialMedia, ToC } from '@components/Widgets';
-import { t } from '@lingui/macro';
import Sidebar from '@components/Sidebar/Sidebar';
-import { AboutPage, Graph, WebPage } from 'schema-dts';
+import { CVPreview, SocialMedia, ToC } from '@components/Widgets';
import { config } from '@config/website';
+import CVContent, { intro, meta, pdf, image } from '@content/pages/cv.mdx';
+import styles from '@styles/pages/Page.module.scss';
+import { NextPageWithLayout } from '@ts/types/app';
+import { ArticleMeta } from '@ts/types/articles';
+import { loadTranslation } from '@utils/helpers/i18n';
+import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Head from 'next/head';
import { useRouter } from 'next/router';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
+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}
/>
@@ -113,7 +146,7 @@ export const getStaticProps: GetStaticProps = async (
) => {
const breadcrumbTitle = meta.title;
const { locale } = context;
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ae5fe4b..41a4603 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,30 +1,39 @@
-import type { ReactElement } from 'react';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
+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 { NextPageWithLayout } from '@ts/types/app';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
+import { config } from '@config/website';
import HomePageContent from '@content/pages/homepage.mdx';
-import { ButtonLink } from '@components/Buttons';
import styles from '@styles/pages/Home.module.scss';
-import { t } from '@lingui/macro';
-import FeedIcon from '@assets/images/icon-feed.svg';
-import { ContactIcon } from '@components/Icons';
+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';
-import { config } from '@config/website';
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) }}
@@ -141,11 +182,10 @@ export const getStaticProps: GetStaticProps = async (
context: GetStaticPropsContext
) => {
const { locale } = context;
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
- locale,
translation,
},
};
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
index c9d2ccd..0ec92a2 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -1,24 +1,24 @@
import { getLayout } from '@components/Layouts/Layout';
-import { seo } from '@config/seo';
-import { NextPageWithLayout } from '@ts/types/app';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
+import PostHeader from '@components/PostHeader/PostHeader';
+import Sidebar from '@components/Sidebar/Sidebar';
+import { ToC } from '@components/Widgets';
+import { config } from '@config/website';
import LegalNoticeContent, {
intro,
meta,
} from '@content/pages/legal-notice.mdx';
-import PostHeader from '@components/PostHeader/PostHeader';
-import { ArticleMeta } from '@ts/types/articles';
import styles from '@styles/pages/Page.module.scss';
-import { ToC } from '@components/Widgets';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { Article, Graph, WebPage } from 'schema-dts';
-import { config } from '@config/website';
+import { NextPageWithLayout } from '@ts/types/app';
+import { ArticleMeta } from '@ts/types/articles';
+import { loadTranslation } from '@utils/helpers/i18n';
+import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Head from 'next/head';
import { useRouter } from 'next/router';
-import { t } from '@lingui/macro';
+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"
@@ -107,7 +124,7 @@ export const getStaticProps: GetStaticProps = async (
) => {
const breadcrumbTitle = meta.title;
const { locale } = context;
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
diff --git a/src/pages/projet/[slug].tsx b/src/pages/projet/[slug].tsx
index 847f84c..14ddf9c 100644
--- a/src/pages/projet/[slug].tsx
+++ b/src/pages/projet/[slug].tsx
@@ -11,7 +11,7 @@ import {
Project as ProjectData,
ProjectProps,
} from '@ts/types/app';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
+import { loadTranslation } from '@utils/helpers/i18n';
import {
getAllProjectsFilename,
getProjectData,
@@ -133,7 +133,7 @@ export const getStaticProps: GetStaticProps = async (
) => {
const breadcrumbTitle = '';
const { locale } = context;
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
const { slug } = context.params as ProjectParams;
const project = await getProjectData(slug);
diff --git a/src/pages/projets.tsx b/src/pages/projets.tsx
index 4359721..da4523c 100644
--- a/src/pages/projets.tsx
+++ b/src/pages/projets.tsx
@@ -1,19 +1,20 @@
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';
import { Project } from '@ts/types/app';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
+import { loadTranslation } from '@utils/helpers/i18n';
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) }}
@@ -92,7 +108,7 @@ export const getStaticProps: GetStaticProps = async (
const breadcrumbTitle = meta.title;
const { locale } = context;
const projects: Project[] = await getSortedProjects();
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index 7f410e8..857b114 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -1,25 +1,26 @@
import { Button } from '@components/Buttons';
import { getLayout } from '@components/Layouts/Layout';
+import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
import PostHeader from '@components/PostHeader/PostHeader';
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 { 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 { 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';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { ThematicsList, TopicsList } from '@components/Widgets';
-import styles from '@styles/pages/Page.module.scss';
-import Spinner from '@components/Spinner/Spinner';
-import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
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,9 +183,13 @@ 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 || defaultLocale);
+ const translation = await loadTranslation(locale);
return {
props: {
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 9947758..87a86a2 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -1,24 +1,25 @@
import { getLayout } from '@components/Layouts/Layout';
+import PostHeader from '@components/PostHeader/PostHeader';
import PostPreview from '@components/PostPreview/PostPreview';
-import { t } from '@lingui/macro';
+import Sidebar from '@components/Sidebar/Sidebar';
+import { RelatedThematics, ToC, TopicsList } from '@components/Widgets';
+import { config } from '@config/website';
+import { getAllTopicsSlug, getTopicBySlug } from '@services/graphql/queries';
+import styles from '@styles/pages/Page.module.scss';
import { NextPageWithLayout } from '@ts/types/app';
+import { ArticleMeta } from '@ts/types/articles';
import { TopicProps, ThematicPreview } from '@ts/types/taxonomies';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
+import { loadTranslation } from '@utils/helpers/i18n';
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
import { ParsedUrlQuery } from 'querystring';
-import styles from '@styles/pages/Page.module.scss';
-import { getAllTopicsSlug, getTopicBySlug } from '@services/graphql/queries';
-import PostHeader from '@components/PostHeader/PostHeader';
-import { ArticleMeta } from '@ts/types/articles';
-import { RelatedThematics, ToC, TopicsList } from '@components/Widgets';
import { useRef } from 'react';
-import Head from 'next/head';
-import Sidebar from '@components/Sidebar/Sidebar';
+import { useIntl } from 'react-intl';
import { Article as Article, Graph, WebPage } from 'schema-dts';
-import { config } from '@config/website';
-import { useRouter } from 'next/router';
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>
</>
@@ -152,7 +166,7 @@ export const getStaticProps: GetStaticProps = async (
context: GetStaticPropsContext
) => {
const { locale } = context;
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
const { slug } = context.params as PostParams;
const topic = await getTopicBySlug(slug);
const breadcrumbTitle = topic.title;
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index 9955089..61019fd 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -1,27 +1,28 @@
import { getLayout } from '@components/Layouts/Layout';
+import PostHeader from '@components/PostHeader/PostHeader';
import PostPreview from '@components/PostPreview/PostPreview';
-import { t } from '@lingui/macro';
-import { NextPageWithLayout } from '@ts/types/app';
-import { TopicPreview, ThematicProps } from '@ts/types/taxonomies';
-import { defaultLocale, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
-import { ParsedUrlQuery } from 'querystring';
-import styles from '@styles/pages/Page.module.scss';
+import Sidebar from '@components/Sidebar/Sidebar';
+import { RelatedTopics, ThematicsList, ToC } from '@components/Widgets';
+import { config } from '@config/website';
import {
getAllThematicsSlug,
getThematicBySlug,
} from '@services/graphql/queries';
-import PostHeader from '@components/PostHeader/PostHeader';
-import { RelatedTopics, ThematicsList, ToC } from '@components/Widgets';
-import { useRef } from 'react';
+import styles from '@styles/pages/Page.module.scss';
+import { NextPageWithLayout } from '@ts/types/app';
import { ArticleMeta } from '@ts/types/articles';
+import { TopicPreview, ThematicProps } from '@ts/types/taxonomies';
+import { loadTranslation } from '@utils/helpers/i18n';
+import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
import Head from 'next/head';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { Article, Graph, WebPage } from 'schema-dts';
-import { config } from '@config/website';
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>
</>
@@ -142,7 +156,7 @@ export const getStaticProps: GetStaticProps = async (
context: GetStaticPropsContext
) => {
const { locale } = context;
- const translation = await loadTranslation(locale || defaultLocale);
+ const translation = await loadTranslation(locale);
const { slug } = context.params as PostParams;
const thematic = await getThematicBySlug(slug);
const breadcrumbTitle = thematic.title;
diff --git a/src/utils/helpers/i18n.ts b/src/utils/helpers/i18n.ts
index 4439906..16c83f4 100644
--- a/src/utils/helpers/i18n.ts
+++ b/src/utils/helpers/i18n.ts
@@ -1,86 +1,49 @@
-import { messages as messagesEn } from '@i18n/en/messages.js';
-import { messages as messagesFr } from '@i18n/fr/messages.js';
-import { i18n, Messages } from '@lingui/core';
-import { en, fr } from 'make-plural/plurals';
+import { config } from '@config/website';
+import { createIntl, createIntlCache, IntlShape } from '@formatjs/intl';
+import { readFile } from 'fs/promises';
+import path from 'path';
-type Catalog = {
- messages: Messages;
-};
+type Messages = { [key: string]: string };
-export const locales = {
- en: 'English',
- fr: 'Français',
-};
-
-export const defaultLocale = 'fr';
+export const defaultLocale = config.locales.defaultLocale;
/**
- * Load the translation with the correct method depending on environment.
+ * Load the translation for the provided locale.
*
- * @param {string} locale - The current locale.
- * @returns {Promise<Messages>} The translated messages.
+ * @param currentLocale - The current locale.
+ * @returns {Promise<Messages>} The translated strings.
*/
-export async function loadTranslation(locale: string): Promise<Messages> {
- let catalog: Catalog;
-
- try {
- if (process.env.NODE_ENV === 'production') {
- catalog = await import(`src/i18n/${locale}/messages`);
- } else {
- catalog = await import(`@lingui/loader!src/i18n/${locale}/messages.po`);
- }
+export async function loadTranslation(
+ currentLocale: string | undefined
+): Promise<Messages> {
+ const locale: string = currentLocale || defaultLocale;
- return catalog.messages;
- } catch (error) {
- console.error('Error while loading translation.');
- throw error;
- }
-}
+ const languagePath = path.join(process.cwd(), `lang/${locale}.json`);
-/**
- * Init lingui.
- *
- * @param {string} locale - The locale to activate.
- * @param {Messages} [messages] - The compiled translation.
- */
-export function initLingui(locale: string, messages?: Messages) {
try {
- i18n.loadLocaleData({
- en: { plurals: en },
- fr: { plurals: fr },
- });
-
- if (messages) {
- i18n.load(locale, messages);
- } else {
- i18n.load({
- en: messagesEn,
- fr: messagesFr,
- });
- }
-
- i18n.activate(locale, Object.keys(locales));
+ const contents = await readFile(languagePath, 'utf8');
+ return JSON.parse(contents);
} catch (error) {
- console.error('Error while Lingui init.');
+ console.error(
+ 'Error: Could not load compiled language files. Please run `yarn run i18n:compile` first."'
+ );
throw error;
}
}
/**
- * Activate the given locale.
+ * Create an Intl object to be used outside components.
*
- * @param {string} locale - The locale to activate.
- * @param {Messages} messages - The compiled translation.
+ * @returns {<Promise<IntlShape<string>>} The Intl object.
*/
-export function activateLocale(currentLocale: string, messages: Messages) {
- const locale: string = Object.keys(locales).includes(currentLocale)
- ? currentLocale
- : defaultLocale;
-
+export async function getIntlInstance(): Promise<IntlShape<string>> {
try {
- initLingui(locale, messages);
+ const cache = createIntlCache();
+ const messages = await loadTranslation(defaultLocale);
+
+ return createIntl({ locale: defaultLocale, messages }, cache);
} catch (error) {
- console.error(`Error while activating ${currentLocale}`);
+ 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);
});
};