diff options
62 files changed, 3077 insertions, 529 deletions
@@ -2,6 +2,7 @@ "presets": ["next/babel"], "plugins": [ "macros", + ["formatjs", { "ast": true }], [ "prismjs", { diff --git a/.eslintrc.json b/.eslintrc.json index 4d765f2..46d209f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,7 @@ { - "extends": ["next/core-web-vitals", "prettier"] + "extends": ["next/core-web-vitals", "prettier"], + "plugins": ["formatjs"], + "rules": { + "formatjs/enforce-default-message": ["error", "literal"] + } } @@ -42,3 +42,6 @@ yarn-error.log* # dotenv .env* !.env.example + +# i18n +lang diff --git a/next.config.js b/next.config.js index a26eda6..3583639 100644 --- a/next.config.js +++ b/next.config.js @@ -30,7 +30,12 @@ const nextConfig = { path.join(__dirname, 'node_modules'), ], }, - webpack: (config) => { + webpack: (config, { dev }) => { + if (!dev) { + // https://formatjs.io/docs/guides/advanced-usage#react-intl-without-parser-40-smaller + config.resolve.alias['@formatjs/icu-messageformat-parser'] = + '@formatjs/icu-messageformat-parser/no-parser'; + } config.module.rules.push( { test: /\.pdf/, diff --git a/package.json b/package.json index 3ce04a9..f821652 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ }, "private": true, "scripts": { + "predev": "npm run i18n:compile", "dev": "next dev", + "prebuild": "npm run i18n:compile", "build": "next build", "start": "next start", "lint": "next lint", - "i18n:compile": "lingui compile", - "i18n:extract": "NODE_ENV=development lingui extract", + "i18n:compile": "formatjs compile-folder src/i18n lang/", + "i18n:extract": "formatjs extract 'src/**/*.ts*' --out-file src/i18n/en.json", "release": "standard-version -s", "test": "jest", "test:coverage": "jest --coverage", @@ -36,10 +38,12 @@ "@next/mdx": "^12.0.7", "@types/mdx": "^2.0.1", "@types/prismjs": "^1.16.6", + "babel-plugin-formatjs": "^10.3.17", "babel-plugin-prismjs": "^2.1.0", "feed": "^4.2.2", "graphql": "^16.1.0", "graphql-request": "^3.7.0", + "intl-messageformat": "^9.11.3", "modern-normalize": "^1.1.0", "next": "12.0.7", "next-themes": "^0.0.15", @@ -47,6 +51,7 @@ "prismjs": "^1.25.0", "react": "17.0.2", "react-dom": "17.0.2", + "react-intl": "^5.24.4", "schema-dts": "^1.0.0", "swr": "^1.1.1" }, @@ -56,6 +61,7 @@ "@babel/preset-react": "^7.16.0", "@commitlint/cli": "^15.0.0", "@commitlint/config-conventional": "^15.0.0", + "@formatjs/cli": "^4.8.1", "@lingui/cli": "^3.13.0", "@lingui/loader": "^3.13.0", "@lingui/macro": "^3.13.0", @@ -68,6 +74,7 @@ "eslint": "^8.4.1", "eslint-config-next": "^12.0.7", "eslint-config-prettier": "^8.3.0", + "eslint-plugin-formatjs": "^2.20.5", "husky": "^7.0.4", "jest": "^27.4.4", "lint-staged": "^12.1.2", diff --git a/src/components/Branding/Branding.tsx b/src/components/Branding/Branding.tsx index 01948e9..efb3a49 100644 --- a/src/components/Branding/Branding.tsx +++ b/src/components/Branding/Branding.tsx @@ -1,17 +1,18 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { ReactElement } from 'react'; -import { t } from '@lingui/macro'; import photo from '@assets/images/armand-philippot.jpg'; import { config } from '@config/website'; -import styles from './Branding.module.scss'; import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import { ReactElement } from 'react'; +import { useIntl } from 'react-intl'; import { Person, WithContext } from 'schema-dts'; +import styles from './Branding.module.scss'; import Logo from './Logo/Logo'; type BrandingReturn = ({ isHome }: { isHome: boolean }) => ReactElement; const Branding: BrandingReturn = ({ isHome = false }) => { + const intl = useIntl(); const TitleTag = isHome ? 'h1' : 'p'; const schemaJsonLd: WithContext<Person> = { @@ -38,10 +39,15 @@ const Branding: BrandingReturn = ({ isHome = false }) => { <div className={styles.logo__front}> <Image src={photo} - alt={t({ - message: `${config.name} picture`, - comment: 'Branding logo.', - })} + alt={intl.formatMessage( + { + defaultMessage: '{brandingName} picture', + description: 'Branding: branding name picture.', + }, + { + brandingName: config.name, + } + )} layout="responsive" /> </div> diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 7c8eb5c..30179be 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -1,12 +1,13 @@ import { config } from '@config/website'; -import { t } from '@lingui/macro'; import Head from 'next/head'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import { BreadcrumbList, WithContext } from 'schema-dts'; import styles from './Breadcrumb.module.scss'; const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { + const intl = useIntl(); const router = useRouter(); const isHome = router.pathname === '/'; @@ -20,14 +21,24 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { <> <li className={styles.item}> <Link href="/"> - <a>{t`Home`}</a> + <a> + {intl.formatMessage({ + defaultMessage: 'Home', + description: 'Breadcrumb: Home item', + })} + </a> </Link> </li> {(isArticle || isThematic || isSubject) && ( <> <li className={styles.item}> <Link href="/blog"> - <a>{t`Blog`}</a> + <a> + {intl.formatMessage({ + defaultMessage: 'Blog', + description: 'Breadcrumb: Blog item', + })} + </a> </Link> </li> </> @@ -36,7 +47,12 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { <> <li className={styles.item}> <Link href="/projets"> - <a>{t`Projects`}</a> + <a> + {intl.formatMessage({ + defaultMessage: 'Projects', + description: 'Breadcrumb: Projects item', + })} + </a> </Link> </li> </> @@ -51,7 +67,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { const homepage: BreadcrumbList['itemListElement'] = { '@type': 'ListItem', position: 1, - name: t`Home`, + name: intl.formatMessage({ + defaultMessage: 'Home', + description: 'Breadcrumb: Home item', + }), item: config.url, }; @@ -61,7 +80,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { const blog: BreadcrumbList['itemListElement'] = { '@type': 'ListItem', position: 2, - name: t`Blog`, + name: intl.formatMessage({ + defaultMessage: 'Blog', + description: 'Breadcrumb: Blog item', + }), item: `${config.url}/blog`, }; @@ -72,7 +94,10 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { const blog: BreadcrumbList['itemListElement'] = { '@type': 'ListItem', position: 2, - name: t`Projects`, + name: intl.formatMessage({ + defaultMessage: 'Projects', + description: 'Breadcrumb: Projects item', + }), item: `${config.url}/projets`, }; @@ -108,7 +133,12 @@ const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => { </Head> {!isHome && ( <nav id="breadcrumb" className={styles.wrapper}> - <span className="screen-reader-text">{t`You are here:`}</span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'You are here:', + description: 'Breadcrumb: You are here prefix', + })} + </span> <ol className={styles.list}>{getItems()}</ol> </nav> )} diff --git a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx b/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx index 246ad80..e9f6079 100644 --- a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx +++ b/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx @@ -1,6 +1,6 @@ import { CloseIcon, CogIcon, SearchIcon } from '@components/Icons'; -import { t } from '@lingui/macro'; import { ForwardedRef, forwardRef, SetStateAction } from 'react'; +import { useIntl } from 'react-intl'; import styles from '../Buttons.module.scss'; type ButtonType = 'search' | 'settings'; @@ -17,6 +17,7 @@ const ButtonToolbar = ( }, ref: ForwardedRef<HTMLButtonElement> ) => { + const intl = useIntl(); const ButtonIcon = () => (type === 'search' ? <SearchIcon /> : <CogIcon />); const btnClasses = isActivated ? `${styles.toolbar} ${styles['toolbar--activated']}` @@ -38,9 +39,29 @@ const ButtonToolbar = ( </span> </span> {isActivated ? ( - <span className="screen-reader-text">{t`Close ${type}`}</span> + <span className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: 'Close {type}', + description: 'ButtonToolbar: Close button', + }, + { + type, + } + )} + </span> ) : ( - <span className="screen-reader-text">{t`Open ${type}`}</span> + <span className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: 'Open {type}', + description: 'ButtonToolbar: Open button', + }, + { + type, + } + )} + </span> )} </button> ); diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx index 6eb0184..e95a378 100644 --- a/src/components/Comment/Comment.tsx +++ b/src/components/Comment/Comment.tsx @@ -1,7 +1,6 @@ import { Button } from '@components/Buttons'; import CommentForm from '@components/CommentForm/CommentForm'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { Comment as CommentData } from '@ts/types/comments'; import { getFormattedDate } from '@utils/helpers/format'; import Head from 'next/head'; @@ -9,6 +8,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import { Comment as CommentSchema, WithContext } from 'schema-dts'; import styles from './Comment.module.scss'; @@ -21,6 +21,7 @@ const Comment = ({ comment: CommentData; isNested?: boolean; }) => { + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; const [isReply, setIsReply] = useState<boolean>(false); @@ -48,7 +49,16 @@ const Comment = ({ minute: 'numeric', }) .replace(':', 'h'); - return t`${date} at ${time}`; + return intl.formatMessage( + { + defaultMessage: '{date} at {time}', + description: 'Comment: publication date', + }, + { + date, + time, + } + ); }; const getApprovedComment = () => { @@ -68,7 +78,12 @@ const Comment = ({ {getCommentAuthor()} </header> <dl className={styles.date}> - <dt>{t`Published on:`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'Comment: publication date label', + })} + </dt> <dd> <time dateTime={comment.date}> <Link href={`#comment-${comment.commentId}`}> @@ -83,9 +98,12 @@ const Comment = ({ ></div> {!isNested && ( <footer className={styles.footer}> - <Button - clickHandler={() => setIsReply((prev) => !prev)} - >{t`Reply`}</Button> + <Button clickHandler={() => setIsReply((prev) => !prev)}> + {intl.formatMessage({ + defaultMessage: 'Reply', + description: 'Comment: reply button', + })} + </Button> </footer> )} </article> @@ -116,7 +134,14 @@ const Comment = ({ }; const getCommentStatus = () => { - return <p>{t`This comment is awaiting moderation.`}</p>; + return ( + <p> + {intl.formatMessage({ + defaultMessage: 'This comment is awaiting moderation.', + description: 'Comment: awaiting moderation message', + })} + </p> + ); }; const schemaJsonLd: WithContext<CommentSchema> = { diff --git a/src/components/CommentForm/CommentForm.tsx b/src/components/CommentForm/CommentForm.tsx index 1ed219c..0ea3276 100644 --- a/src/components/CommentForm/CommentForm.tsx +++ b/src/components/CommentForm/CommentForm.tsx @@ -1,9 +1,9 @@ import { ButtonSubmit } from '@components/Buttons'; import { Form, FormItem, Input, TextArea } from '@components/Form'; import Notice from '@components/Notice/Notice'; -import { t } from '@lingui/macro'; import { createComment } from '@services/graphql/mutations'; import { ForwardedRef, forwardRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './CommentForm.module.scss'; const CommentForm = ( @@ -18,6 +18,7 @@ const CommentForm = ( }, ref: ForwardedRef<HTMLInputElement> ) => { + const intl = useIntl(); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [website, setWebsite] = useState(''); @@ -68,7 +69,12 @@ const CommentForm = ( return ( <div className={wrapperClasses}> - <h2 className={styles.title}>{t`Leave a comment`}</h2> + <h2 className={styles.title}> + {intl.formatMessage({ + defaultMessage: 'Leave a comment', + description: 'CommentForm: form title', + })} + </h2> <Form submitHandler={submitHandler} modifier={isReply ? 'centered' : undefined} @@ -77,7 +83,10 @@ const CommentForm = ( <Input id="commenter-name" name="commenter-name" - label={t`Name`} + label={intl.formatMessage({ + defaultMessage: 'Name', + description: 'CommentForm: Name field label', + })} required={true} value={name} setValue={setName} @@ -88,7 +97,10 @@ const CommentForm = ( <Input id="commenter-email" name="commenter-email" - label={t`Email`} + label={intl.formatMessage({ + defaultMessage: 'Email', + description: 'CommentForm: Email field label', + })} required={true} value={email} setValue={setEmail} @@ -98,7 +110,10 @@ const CommentForm = ( <Input id="commenter-website" name="commenter-website" - label={t`Website`} + label={intl.formatMessage({ + defaultMessage: 'Website', + description: 'CommentForm: Website field label', + })} value={website} setValue={setWebsite} /> @@ -107,17 +122,31 @@ const CommentForm = ( <TextArea id="commenter-message" name="commenter-message" - label={t`Comment`} + label={intl.formatMessage({ + defaultMessage: 'Comment', + description: 'CommentForm: Comment field label', + })} value={message} setValue={setMessage} required={true} /> </FormItem> <FormItem> - <ButtonSubmit>{t`Send`}</ButtonSubmit> + <ButtonSubmit> + {intl.formatMessage({ + defaultMessage: 'Send', + description: 'CommentForm: Send button', + })} + </ButtonSubmit> </FormItem> {isSuccess && !isApproved && ( - <Notice type="success">{t`Thanks for your comment! It is now awaiting moderation.`}</Notice> + <Notice type="success"> + {intl.formatMessage({ + defaultMessage: + 'Thanks for your comment! It is now awaiting moderation.', + description: 'CommentForm: Comment sent success message', + })} + </Notice> )} </Form> </div> diff --git a/src/components/CommentsList/CommentsList.tsx b/src/components/CommentsList/CommentsList.tsx index bdca00b..6630a03 100644 --- a/src/components/CommentsList/CommentsList.tsx +++ b/src/components/CommentsList/CommentsList.tsx @@ -1,6 +1,6 @@ -import { Comment as CommentData } from '@ts/types/comments'; import Comment from '@components/Comment/Comment'; -import { t } from '@lingui/macro'; +import { Comment as CommentData } from '@ts/types/comments'; +import { useIntl } from 'react-intl'; import styles from './CommentsList.module.scss'; const CommentsList = ({ @@ -10,6 +10,8 @@ const CommentsList = ({ articleId: number; comments: CommentData[]; }) => { + const intl = useIntl(); + const getCommentsList = () => { return comments.map((comment) => { return ( @@ -20,11 +22,21 @@ const CommentsList = ({ return ( <> - <h2 className={styles.title}>{t`Comments`}</h2> + <h2 className={styles.title}> + {intl.formatMessage({ + defaultMessage: 'Comments', + description: 'CommentsList: Comments section title', + })} + </h2> {comments.length > 0 ? ( <ol className={styles.list}>{getCommentsList()}</ol> ) : ( - <p className={styles['no-comments']}>{t`No comments yet.`}</p> + <p className={styles['no-comments']}> + {intl.formatMessage({ + defaultMessage: 'No comments yet.', + description: 'CommentsList: No comment message', + })} + </p> )} </> ); diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 15a4660..4aa980d 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -2,11 +2,12 @@ import { ButtonLink } from '@components/Buttons'; import Copyright from '@components/Copyright/Copyright'; import FooterNav from '@components/FooterNav/FooterNav'; import { ArrowIcon } from '@components/Icons'; -import { t } from '@lingui/macro'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './Footer.module.scss'; const Footer = () => { + const intl = useIntl(); const [backToTopClasses, setBackToTopClasses] = useState( `${styles['back-to-top']} ${styles['back-to-top--hidden']}` ); @@ -36,7 +37,12 @@ const Footer = () => { <FooterNav /> <div className={backToTopClasses}> <ButtonLink target="#top" position="center"> - <span className="screen-reader-text">{t`Back to top`}</span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Back to top', + description: 'Footer: Back to top button', + })} + </span> <ArrowIcon direction="top" /> </ButtonLink> </div> diff --git a/src/components/FooterNav/FooterNav.tsx b/src/components/FooterNav/FooterNav.tsx index 7266e7e..918fed7 100644 --- a/src/components/FooterNav/FooterNav.tsx +++ b/src/components/FooterNav/FooterNav.tsx @@ -1,9 +1,23 @@ import Link from 'next/link'; import styles from './FooterNav.module.scss'; -import { footerNav } from '@config/nav'; +import { NavItem } from '@ts/types/nav'; +import { useIntl } from 'react-intl'; const FooterNav = () => { - const navItems = footerNav.map((item) => { + const intl = useIntl(); + + const footerNavConfig: NavItem[] = [ + { + id: 'legal-notice', + name: intl.formatMessage({ + defaultMessage: 'Legal notice', + description: 'FooterNav: legal notice link', + }), + slug: '/mentions-legales', + }, + ]; + + const navItems = footerNavConfig.map((item) => { return ( <li key={item.id} className={styles.item}> <Link href={item.slug}> diff --git a/src/components/Icons/Copyright/Copyright.tsx b/src/components/Icons/Copyright/Copyright.tsx index 396c127..d27c042 100644 --- a/src/components/Icons/Copyright/Copyright.tsx +++ b/src/components/Icons/Copyright/Copyright.tsx @@ -1,4 +1,3 @@ -import { t } from '@lingui/macro'; import styles from './Copyright.module.scss'; const CopyrightIcon = () => { @@ -8,7 +7,7 @@ const CopyrightIcon = () => { viewBox="0 0 211.99811 63.999996" xmlns="http://www.w3.org/2000/svg" > - <title>{t`CC BY SA`}</title> + <title>CC BY SA</title> <path d="m 175.53911,15.829498 c 0,-3.008 1.485,-4.514 4.458,-4.514 2.973,0 4.457,1.504 4.457,4.514 0,2.971 -1.486,4.457 -4.457,4.457 -2.971,0 -4.458,-1.486 -4.458,-4.457 z" /> <path d="m 188.62611,24.057498 v 13.085 h -3.656 v 15.542 h -9.944 v -15.541 h -3.656 v -13.086 c 0,-0.572 0.2,-1.057 0.599,-1.457 0.401,-0.399 0.887,-0.6 1.457,-0.6 h 13.144 c 0.533,0 1.01,0.2 1.428,0.6 0.417,0.4 0.628,0.886 0.628,1.457 z" /> <path d="m 179.94147,-1.9073486e-6 c -8.839,0 -16.34167,3.0848125073486 -22.51367,9.2578125073486 -6.285,6.4000004 -9.42969,13.9811874 -9.42969,22.7421874 0,8.762 3.14469,16.284312 9.42969,22.570312 6.361,6.286 13.86467,9.429688 22.51367,9.429688 8.799,0 16.43611,-3.181922 22.91211,-9.544922 6.096,-5.98 9.14453,-13.464078 9.14453,-22.455078 0,-8.952 -3.10646,-16.532188 -9.31446,-22.7421874 -6.172,-6.172 -13.75418,-9.2578125073486 -22.74218,-9.2578125073486 z M 180.05475,5.7714825 c 7.238,0 13.40967,2.55225 18.51367,7.6562495 5.103,5.106 7.65625,11.294313 7.65625,18.570313 0,7.391 -2.51397,13.50575 -7.54297,18.34375 -5.295,5.221 -11.50591,7.828125 -18.6289,7.828125 -7.162,0 -13.33268,-2.589484 -18.51368,-7.771484 -5.18,-5.178001 -7.76953,-11.310485 -7.76953,-18.396485 0,-7.047 2.60813,-13.238266 7.82813,-18.572265 5.029,-5.1040004 11.18103,-7.6582035 18.45703,-7.6582035 z" /> diff --git a/src/components/Icons/Moon/Moon.tsx b/src/components/Icons/Moon/Moon.tsx index 62e7203..acdf6ae 100644 --- a/src/components/Icons/Moon/Moon.tsx +++ b/src/components/Icons/Moon/Moon.tsx @@ -1,14 +1,21 @@ -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './Moon.module.scss'; const MoonIcon = () => { + const intl = useIntl(); + return ( <svg className={styles.moon} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" > - <title>{t`Dark theme`}</title> + <title> + {intl.formatMessage({ + defaultMessage: 'Dark theme', + description: 'Icons: Moon icon (dark theme)', + })} + </title> <path d="M 51.077315,1.9893942 A 43.319985,43.319985 0 0 1 72.840039,39.563145 43.319985,43.319985 0 0 1 29.520053,82.88313 43.319985,43.319985 0 0 1 5.4309911,75.569042 48.132997,48.132997 0 0 0 46.126047,98 48.132997,48.132997 0 0 0 94.260004,49.867002 48.132997,48.132997 0 0 0 51.077315,1.9893942 Z" /> </svg> ); diff --git a/src/components/Icons/Sun/Sun.tsx b/src/components/Icons/Sun/Sun.tsx index 612d3fa..44945c2 100644 --- a/src/components/Icons/Sun/Sun.tsx +++ b/src/components/Icons/Sun/Sun.tsx @@ -1,14 +1,21 @@ -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './Sun.module.scss'; const SunIcon = () => { + const intl = useIntl(); + return ( <svg className={styles.sun} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" > - <title>{t`Light theme`}</title> + <title> + {intl.formatMessage({ + defaultMessage: 'Light theme', + description: 'Icons: Sun icon (light theme)', + })} + </title> <path d="M 69.398043,50.000437 A 19.399259,19.399204 0 0 1 49.998784,69.399641 19.399259,19.399204 0 0 1 30.599525,50.000437 19.399259,19.399204 0 0 1 49.998784,30.601234 19.399259,19.399204 0 0 1 69.398043,50.000437 Z m 27.699233,1.125154 c 2.657696,0.0679 1.156196,12.061455 -1.435545,11.463959 L 80.113224,59.000697 c -2.589801,-0.597494 -1.625657,-8.345536 1.032041,-8.278609 z m -18.06653,37.251321 c 1.644087,2.091234 -9.030355,8.610337 -10.126414,6.188346 L 62.331863,80.024585 c -1.096058,-2.423931 5.197062,-6.285342 6.839209,-4.194107 z M 38.611418,97.594444 C 38.02653,100.18909 26.24148,95.916413 27.436475,93.54001 l 7.168026,-14.256474 c 1.194024,-2.376403 8.102101,0.151313 7.517214,2.744986 z M 6.1661563,71.834242 C 3.7916868,73.028262 -0.25499873,61.16274 2.3386824,60.577853 L 17.905618,57.067567 c 2.593681,-0.584886 4.894434,6.403678 2.518995,7.598668 z M 6.146757,30.055146 c -2.3764094,-1.194991 4.46571,-11.714209 6.479353,-9.97798 l 12.090589,10.414462 c 2.014613,1.736229 -1.937017,7.926514 -4.314396,6.731524 z M 38.56777,4.2639045 C 37.982883,1.6682911 50.480855,0.41801247 50.415868,3.0766733 L 50.020123,19.028638 c -0.06596,2.657691 -7.357169,3.394862 -7.943027,0.800218 z m 40.403808,9.1622435 c 1.635357,-2.098023 10.437771,6.872168 8.339742,8.506552 l -12.58818,9.805327 c -2.099,1.634383 -7.192276,-3.626682 -5.557888,-5.724706 z M 97.096306,50.69105 c 2.657696,-0.06596 1.164926,12.462047 -1.425846,11.863582 L 80.122924,58.96578 c -2.590771,-0.597496 -1.636327,-7.814 1.021371,-7.879957 z" /> </svg> ); diff --git a/src/components/Layouts/Layout.tsx b/src/components/Layouts/Layout.tsx index 599cfe2..b479ef3 100644 --- a/src/components/Layouts/Layout.tsx +++ b/src/components/Layouts/Layout.tsx @@ -1,12 +1,12 @@ -import { ReactElement, ReactNode, useEffect, useRef } from 'react'; import Footer from '@components/Footer/Footer'; import Header from '@components/Header/Header'; import Main from '@components/Main/Main'; import Breadcrumb from '@components/Breadcrumb/Breadcrumb'; -import { t } from '@lingui/macro'; -import Head from 'next/head'; import { config } from '@config/website'; +import Head from 'next/head'; import { useRouter } from 'next/router'; +import { ReactElement, ReactNode, useEffect, useRef } from 'react'; +import { useIntl } from 'react-intl'; import { WebSite, WithContext } from 'schema-dts'; const Layout = ({ @@ -16,6 +16,7 @@ const Layout = ({ children: ReactNode; isHome?: boolean; }) => { + const intl = useIntl(); const ref = useRef<HTMLSpanElement>(null); const { asPath } = useRouter(); @@ -91,7 +92,12 @@ const Layout = ({ ></script> </Head> <span ref={ref} tabIndex={-1} /> - <a href="#main" className="screen-reader-text">{t`Skip to content`}</a> + <a href="#main" className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Skip to content', + description: 'Layout: Skip to content button', + })} + </a> <Header isHome={isHome} /> <Main>{children}</Main> <Footer /> diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx index ef8b587..59386af 100644 --- a/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/src/components/MDX/CodeBlock/CodeBlock.tsx @@ -3,6 +3,7 @@ import { translateCopyButton } from '@utils/helpers/prism'; import { useRouter } from 'next/router'; import Prism from 'prismjs'; import { ReactChildren, useEffect } from 'react'; +import { useIntl } from 'react-intl'; const CodeBlock = ({ className, @@ -15,6 +16,7 @@ const CodeBlock = ({ const languageClass = classNames.find((name: string) => name.startsWith('language-') ); + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; @@ -23,8 +25,8 @@ const CodeBlock = ({ }); useEffect(() => { - translateCopyButton(locale); - }, [locale]); + translateCopyButton(locale, intl); + }, [intl, locale]); return ( <div> diff --git a/src/components/MainNav/MainNav.tsx b/src/components/MainNav/MainNav.tsx index afc4193..a866b9c 100644 --- a/src/components/MainNav/MainNav.tsx +++ b/src/components/MainNav/MainNav.tsx @@ -1,6 +1,3 @@ -import { SetStateAction } from 'react'; -import Link from 'next/link'; -import { t } from '@lingui/macro'; import { BlogIcon, ContactIcon, @@ -9,9 +6,12 @@ import { HomeIcon, ProjectsIcon, } from '@components/Icons'; -import { mainNav } from '@config/nav'; -import styles from './MainNav.module.scss'; +import { NavItem } from '@ts/types/nav'; +import Link from 'next/link'; import { useRouter } from 'next/router'; +import { SetStateAction } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './MainNav.module.scss'; const MainNav = ({ isOpened, @@ -20,8 +20,52 @@ const MainNav = ({ isOpened: boolean; setIsOpened: (value: SetStateAction<boolean>) => void; }) => { + const intl = useIntl(); const router = useRouter(); + const mainNavConfig: NavItem[] = [ + { + id: 'home', + name: intl.formatMessage({ + defaultMessage: 'Home', + description: 'MainNav: home link', + }), + slug: '/', + }, + { + id: 'blog', + name: intl.formatMessage({ + defaultMessage: 'Blog', + description: 'MainNav: blog link', + }), + slug: '/blog', + }, + { + id: 'projects', + name: intl.formatMessage({ + defaultMessage: 'Projects', + description: 'MainNav: projects link', + }), + slug: '/projets', + }, + { + id: 'cv', + name: intl.formatMessage({ + defaultMessage: 'Resume', + description: 'MainNav: resume link', + }), + slug: '/cv', + }, + { + id: 'contact', + name: intl.formatMessage({ + defaultMessage: 'Contact', + description: 'MainNav: contact link', + }), + slug: '/contact', + }, + ]; + const getIcon = (id: string) => { switch (id) { case 'home': @@ -39,7 +83,7 @@ const MainNav = ({ } }; - const navItems = mainNav.map((item) => { + const navItems = mainNavConfig.map((item) => { const currentClass = router.asPath === item.slug ? styles.current : ''; return ( @@ -73,7 +117,15 @@ const MainNav = ({ > <HamburgerIcon isActive={isOpened} /> <span className="screen-reader-text"> - {isOpened ? t`Close menu` : t`Open menu`} + {isOpened + ? intl.formatMessage({ + defaultMessage: 'Close menu', + description: 'MainNav: close button', + }) + : intl.formatMessage({ + defaultMessage: 'Open menu', + description: 'MainNav: open button', + })} </span> </label> <nav className={styles.nav}> diff --git a/src/components/PaginationCursor/PaginationCursor.tsx b/src/components/PaginationCursor/PaginationCursor.tsx index bcbb555..a8c6265 100644 --- a/src/components/PaginationCursor/PaginationCursor.tsx +++ b/src/components/PaginationCursor/PaginationCursor.tsx @@ -1,4 +1,4 @@ -import { plural, t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './PaginationCursor.module.scss'; const PaginationCursor = ({ @@ -8,6 +8,8 @@ const PaginationCursor = ({ current: number; total: number; }) => { + const intl = useIntl(); + return ( <div className={styles.wrapper}> <progress @@ -16,12 +18,19 @@ const PaginationCursor = ({ value={current} aria-valuemin={0} aria-valuemax={total} - aria-label={t`Number of articles loaded out of the total available.`} - title={plural(current, { - zero: `# articles out of a total of ${total}`, - one: `# article out of a total of ${total}`, - other: `# articles out of a total of ${total}`, + aria-label={intl.formatMessage({ + defaultMessage: + 'Number of articles loaded out of the total available.', + description: 'PaginationCursor: loaded articles count aria-label', })} + title={intl.formatMessage( + { + defaultMessage: + '{articlesCount, plural, =0 {# articles} one {# article} other {# articles}} out of a total of {total}', + description: 'PaginationCursor: loaded articles count message', + }, + { articlesCount: current, total } + )} ></progress> </div> ); diff --git a/src/components/PostFooter/PostFooter.tsx b/src/components/PostFooter/PostFooter.tsx index ad471eb..6c97ec2 100644 --- a/src/components/PostFooter/PostFooter.tsx +++ b/src/components/PostFooter/PostFooter.tsx @@ -1,10 +1,12 @@ import { ButtonLink } from '@components/Buttons'; -import { t } from '@lingui/macro'; import { TopicPreview } from '@ts/types/taxonomies'; import Image from 'next/image'; +import { useIntl } from 'react-intl'; import styles from './PostFooter.module.scss'; const PostFooter = ({ topics }: { topics: TopicPreview[] }) => { + const intl = useIntl(); + const getTopics = () => { return topics.map((topic) => { return ( @@ -31,7 +33,12 @@ const PostFooter = ({ topics }: { topics: TopicPreview[] }) => { {topics.length > 0 && ( <> <dl className={styles.meta}> - <dt>{t`Read more articles about:`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Read more articles about:', + description: 'PostFooter: read more posts about given subjects', + })} + </dt> <dd> <ul className={styles.list}>{getTopics()}</ul> </dd> diff --git a/src/components/PostMeta/PostMeta.tsx b/src/components/PostMeta/PostMeta.tsx index f95707a..86e4e71 100644 --- a/src/components/PostMeta/PostMeta.tsx +++ b/src/components/PostMeta/PostMeta.tsx @@ -1,9 +1,9 @@ import { config } from '@config/website'; -import { plural, t } from '@lingui/macro'; import { ArticleMeta } from '@ts/types/articles'; import { getFormattedDate } from '@utils/helpers/format'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import styles from './PostMeta.module.scss'; type PostMetaMode = 'list' | 'single'; @@ -26,6 +26,7 @@ const PostMeta = ({ website, wordsCount, } = meta; + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; const isThematic = () => router.asPath.includes('/thematique/'); @@ -62,24 +63,31 @@ const PostMeta = ({ }; const getCommentsCount = () => { - switch (commentCount) { - case 0: - return t`No comments`; - case 1: - return t`1 comment`; - default: - return t`${commentCount} comments`; - } + return intl.formatMessage( + { + defaultMessage: + '{commentCount, plural, =0 {No comments} one {# comment} other {# comments}}', + description: 'PostMeta: comment count value', + }, + { commentCount } + ); }; const getReadingTime = () => { if (!readingTime) return; - if (readingTime < 0) return t`less than 1 minute`; - return plural(readingTime, { - zero: '# minutes', - one: '# minute', - other: '# minutes', - }); + if (readingTime < 0) + return intl.formatMessage({ + defaultMessage: 'less than 1 minute', + description: 'PostMeta: Reading time value', + }); + return intl.formatMessage( + { + defaultMessage: + '{readingTime, plural, =0 {# minutes} one {# minute} other {# minutes}}', + description: 'PostMeta: reading time value', + }, + { readingTime } + ); }; const getDates = () => { @@ -91,14 +99,24 @@ const PostMeta = ({ return ( <> <div className={styles.item}> - <dt className={styles.term}>{t`Published on:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'PostMeta: publication date label', + })} + </dt> <dd className={styles.description}> <time dateTime={dates.publication}>{publicationDate}</time> </dd> </div> {publicationDate !== updateDate && ( <div className={styles.item}> - <dt className={styles.term}>{t`Updated on:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Updated on:', + description: 'PostMeta: update date label', + })} + </dt> <dd className={styles.description}> <time dateTime={dates.update}>{updateDate}</time> </dd> @@ -114,14 +132,24 @@ const PostMeta = ({ <dl className={wrapperClass}> {author && ( <div className={styles.item}> - <dt className={styles.term}>{t`Written by:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Written by:', + description: 'Article meta', + })} + </dt> <dd className={styles.description}>{author.name}</dd> </div> )} {getDates()} {readingTime !== undefined && wordsCount !== undefined && ( <div className={styles.item}> - <dt className={styles.term}>{t`Reading time:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Reading time:', + description: 'Article meta', + })} + </dt> <dd className={styles.description} title={`Approximately ${wordsCount.toLocaleString(locale)} words`} @@ -132,20 +160,35 @@ const PostMeta = ({ )} {results && ( <div className={styles.item}> - <dt className={styles.term}>{t`Total: `}</dt> - <dd className={styles.description}> - {plural(results, { - zero: '# articles', - one: '# article', - other: '# articles', + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Total:', + description: 'Article meta', })} + </dt> + <dd className={styles.description}> + {intl.formatMessage( + { + defaultMessage: + '{results, plural, =0 {No articles} one {# article} other {# articles}}', + description: 'PostMeta: total found articles', + }, + { results } + )} </dd> </div> )} {!isThematic() && thematics && thematics.length > 0 && ( <div className={styles.item}> <dt className={styles.term}> - {thematics.length > 1 ? t`Thematics:` : t`Thematic:`} + {intl.formatMessage( + { + defaultMessage: + '{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}', + description: 'PostMeta: thematics list label', + }, + { thematicsCount: thematics.length } + )} </dt> {getThematics()} </div> @@ -153,14 +196,26 @@ const PostMeta = ({ {isThematic() && topics && topics.length > 0 && ( <div className={styles.item}> <dt className={styles.term}> - {topics.length > 1 ? t`Topics:` : t`Topic:`} + {intl.formatMessage( + { + defaultMessage: + '{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}', + description: 'PostMeta: topics list label', + }, + { topicsCount: topics.length } + )} </dt> {getTopics()} </div> )} {website && ( <div className={styles.item}> - <dt className={styles.term}>{t`Website:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Website:', + description: 'PostMeta: website label', + })} + </dt> <dd className={styles.description}> <a href={website}>{website}</a> </dd> @@ -168,7 +223,12 @@ const PostMeta = ({ )} {commentCount !== undefined && ( <div className={styles.item}> - <dt className={styles.term}>{t`Comments:`}</dt> + <dt className={styles.term}> + {intl.formatMessage({ + defaultMessage: 'Comments:', + description: 'PostMeta: comment count label', + })} + </dt> <dd className={styles.description}> {isArticle() ? ( <a href="#comments">{getCommentsCount()}</a> diff --git a/src/components/PostPreview/PostPreview.tsx b/src/components/PostPreview/PostPreview.tsx index b084ca1..72ba638 100644 --- a/src/components/PostPreview/PostPreview.tsx +++ b/src/components/PostPreview/PostPreview.tsx @@ -1,15 +1,15 @@ -import PostMeta from '@components/PostMeta/PostMeta'; -import { t } from '@lingui/macro'; -import { ArticleMeta, ArticlePreview } from '@ts/types/articles'; -import Link from 'next/link'; -import styles from './PostPreview.module.scss'; -import Image from 'next/image'; import { ButtonLink } from '@components/Buttons'; import { ArrowIcon } from '@components/Icons'; +import PostMeta from '@components/PostMeta/PostMeta'; +import { config } from '@config/website'; import { TitleLevel } from '@ts/types/app'; -import { BlogPosting, WithContext } from 'schema-dts'; +import { ArticleMeta, ArticlePreview } from '@ts/types/articles'; +import Image from 'next/image'; import Head from 'next/head'; -import { config } from '@config/website'; +import Link from 'next/link'; +import { FormattedMessage } from 'react-intl'; +import { BlogPosting, WithContext } from 'schema-dts'; +import styles from './PostPreview.module.scss'; const PostPreview = ({ post, @@ -97,11 +97,16 @@ const PostPreview = ({ ></div> <footer className={styles.footer}> <ButtonLink target={`/article/${slug}`} position="left"> - {t`Read more`} - <span className="screen-reader-text"> - {' '} - {t({ message: `about ${title}`, comment: 'Post title' })} - </span> + <FormattedMessage + defaultMessage="Read more<a11y> about {title}</a11y>" + description="PostPreview: read more link" + values={{ + title, + a11y: (chunks: string) => ( + <span className="screen-reader-text">{chunks}</span> + ), + }} + /> <ArrowIcon /> </ButtonLink> </footer> diff --git a/src/components/PostsList/PostsList.tsx b/src/components/PostsList/PostsList.tsx index df9dfe4..16deee3 100644 --- a/src/components/PostsList/PostsList.tsx +++ b/src/components/PostsList/PostsList.tsx @@ -1,9 +1,9 @@ -import { t } from '@lingui/macro'; -import { PostsList as PostsListData } from '@ts/types/blog'; -import styles from './PostsList.module.scss'; import PostPreview from '@components/PostPreview/PostPreview'; -import { ForwardedRef, forwardRef, Fragment } from 'react'; +import { PostsList as PostsListData } from '@ts/types/blog'; import { sortPostsByYear } from '@utils/helpers/sort'; +import { ForwardedRef, forwardRef, Fragment } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './PostsList.module.scss'; const PostsList = ( { @@ -15,6 +15,7 @@ const PostsList = ( }, ref: ForwardedRef<HTMLSpanElement> ) => { + const intl = useIntl(); const titleLevel = showYears ? 3 : 2; const getPostsListByYear = () => { @@ -32,7 +33,12 @@ const PostsList = ( <section key={year} className={styles.section}> {showYears && ( <h2 className={styles.year}> - <span className="screen-reader-text">{t`Published in`} </span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Published on', + description: 'PostsList: published on year label', + })}{' '} + </span> {year} </h2> )} @@ -62,7 +68,14 @@ const PostsList = ( }; if (page.posts.length === 0) { - return <p key="no-result">{t`No results found.`}</p>; + return ( + <p key="no-result"> + {intl.formatMessage({ + defaultMessage: 'No results found.', + description: 'PostsList: no results', + })} + </p> + ); } else { return ( <Fragment key={page.pageInfo.endCursor}> diff --git a/src/components/ProjectPreview/ProjectPreview.tsx b/src/components/ProjectPreview/ProjectPreview.tsx index cba0b02..043d945 100644 --- a/src/components/ProjectPreview/ProjectPreview.tsx +++ b/src/components/ProjectPreview/ProjectPreview.tsx @@ -1,12 +1,13 @@ -import { t } from '@lingui/macro'; import { Project } from '@ts/types/app'; import { slugify } from '@utils/helpers/slugify'; import Image from 'next/image'; import Link from 'next/link'; +import { useIntl } from 'react-intl'; import styles from './ProjectPreview.module.scss'; const ProjectPreview = ({ project }: { project: Project }) => { const { id, meta, tagline, title } = project; + const intl = useIntl(); return ( <Link href={`/projet/${project.slug}`}> @@ -20,7 +21,13 @@ const ProjectPreview = ({ project }: { project: Project }) => { layout="fill" objectFit="contain" objectPosition="center" - alt={t`${title} picture`} + alt={intl.formatMessage( + { + defaultMessage: '{title} picture', + description: 'ProjectPreview: cover alt text', + }, + { title } + )} /> </div> )} @@ -36,7 +43,16 @@ const ProjectPreview = ({ project }: { project: Project }) => { <dl className={styles.meta}> {meta.technologies && ( <div className={styles.meta__item}> - <dt className="screen-reader-text">{t`Technologies:`}</dt> + <dt className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: + '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}', + description: 'ProjectPreview: technologies list label', + }, + { count: meta.technologies.length } + )} + </dt> {meta.technologies.map((techno) => ( <dd key={slugify(techno)} className={styles.techno}> {techno} diff --git a/src/components/ProjectSummary/ProjectSummary.tsx b/src/components/ProjectSummary/ProjectSummary.tsx index b32c11f..f2d73b6 100644 --- a/src/components/ProjectSummary/ProjectSummary.tsx +++ b/src/components/ProjectSummary/ProjectSummary.tsx @@ -1,13 +1,14 @@ import GithubIcon from '@assets/images/social-media/github.svg'; import GitlabIcon from '@assets/images/social-media/gitlab.svg'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { ProjectMeta } from '@ts/types/app'; import { getFormattedDate } from '@utils/helpers/format'; import { slugify } from '@utils/helpers/slugify'; import useGithubApi from '@utils/hooks/useGithubApi'; +import IntlMessageFormat from 'intl-messageformat'; import Image from 'next/image'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import styles from './ProjectSummary.module.scss'; const ProjectSummary = ({ @@ -20,6 +21,7 @@ const ProjectSummary = ({ meta: ProjectMeta; }) => { const { hasCover, license, repos, technologies } = meta; + const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : config.locales.defaultLocale; const { data } = useGithubApi(repos?.github ? repos.github : ''); @@ -30,7 +32,10 @@ const ProjectSummary = ({ <div className={styles.cover}> <Image src={`/projects/${id}.jpg`} - alt={t`${title} preview`} + alt={intl.formatMessage({ + defaultMessage: '{title} preview', + description: 'ProjectSummary: cover alt text', + })} layout="fill" objectFit="contain" /> @@ -39,7 +44,12 @@ const ProjectSummary = ({ <dl className={styles.info}> {data && ( <div className={styles.info__item}> - <dt>{t`Created on`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Created on:', + description: 'ProjectSummary: creation date label', + })} + </dt> <dd> <time dateTime={data.created_at}> {getFormattedDate(data.created_at, locale)} @@ -49,7 +59,12 @@ const ProjectSummary = ({ )} {data && ( <div className={styles.info__item}> - <dt>{t`Last updated on`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Last updated on:', + description: 'ProjectSummary: update date label', + })} + </dt> <dd> <time dateTime={data.updated_at}> {getFormattedDate(data.updated_at, locale)} @@ -58,12 +73,26 @@ const ProjectSummary = ({ </div> )} <div className={styles.info__item}> - <dt>{t`License`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'License:', + description: 'ProjectSummary: license label', + })} + </dt> <dd>{license}</dd> </div> {technologies && ( <div className={styles.info__item}> - <dt>{t`Technologies`}</dt> + <dt> + {intl.formatMessage( + { + defaultMessage: + '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}', + description: 'ProjectSummary: technologies list label', + }, + { count: technologies.length } + )} + </dt> {technologies.map((techno) => ( <dd key={slugify(techno)} @@ -76,7 +105,16 @@ const ProjectSummary = ({ )} {repos && ( <div className={styles.info__item}> - <dt>{t`Repositories`}</dt> + <dt> + {intl.formatMessage( + { + defaultMessage: + '{count, plural, =0 {Repositories:} one {Repository:} other {Repositories:}}', + description: 'ProjectSummary: repositories list label', + }, + { count: Object.keys(repos).length } + )} + </dt> {repos.github && ( <dd className={styles['inline-data']}> <a @@ -103,12 +141,24 @@ const ProjectSummary = ({ )} {data && repos && ( <div> - <dt>{t`Popularity`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Popularity:', + description: 'ProjectSummary: popularity label', + })} + </dt> {repos.github && ( <dd> ⭐ <a href={`https://github.com/${repos.github}/stargazers`}> - {t`${data.stargazers_count} stars on Github`} + {intl.formatMessage( + { + defaultMessage: + '{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}', + description: 'ProjectPreview: technologies list label', + }, + { starsCount: data.stargazers_count } + )} </a> </dd> )} diff --git a/src/components/SearchForm/SearchForm.tsx b/src/components/SearchForm/SearchForm.tsx index cefda85..38ae60d 100644 --- a/src/components/SearchForm/SearchForm.tsx +++ b/src/components/SearchForm/SearchForm.tsx @@ -1,12 +1,13 @@ import { ButtonSubmit } from '@components/Buttons'; import { Form, Input } from '@components/Form'; import { SearchIcon } from '@components/Icons'; -import { t } from '@lingui/macro'; import { useRouter } from 'next/router'; import { FormEvent, useEffect, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './SearchForm.module.scss'; const SearchForm = ({ isOpened }: { isOpened: boolean }) => { + const intl = useIntl(); const [query, setQuery] = useState(''); const inputRef = useRef<HTMLInputElement>(null); const router = useRouter(); @@ -27,7 +28,12 @@ const SearchForm = ({ isOpened }: { isOpened: boolean }) => { return ( <> - <div className={styles.title}>{t`Search`}</div> + <div className={styles.title}> + {intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchForm : form title', + })} + </div> <Form submitHandler={launchSearch} modifier="search"> <Input ref={inputRef} @@ -39,7 +45,12 @@ const SearchForm = ({ isOpened }: { isOpened: boolean }) => { /> <ButtonSubmit modifier="search"> <SearchIcon /> - <span className="screen-reader-text">{t`Search`}</span> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchForm: search button text', + })} + </span> </ButtonSubmit> </Form> </> diff --git a/src/components/Settings/ReduceMotion/ReduceMotion.tsx b/src/components/Settings/ReduceMotion/ReduceMotion.tsx index 01f8b67..c7b5775 100644 --- a/src/components/Settings/ReduceMotion/ReduceMotion.tsx +++ b/src/components/Settings/ReduceMotion/ReduceMotion.tsx @@ -1,9 +1,10 @@ import { Toggle } from '@components/Form'; -import { t } from '@lingui/macro'; import { LocalStorage } from '@services/local-storage'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; const ReduceMotion = () => { + const intl = useIntl(); const [isDeactivated, setIsDeactivated] = useState<boolean>(false); useEffect(() => { @@ -23,9 +24,18 @@ const ReduceMotion = () => { return ( <Toggle id="reduced-motion" - label={t`Animations:`} - leftChoice={t`On`} - rightChoice={t`Off`} + label={intl.formatMessage({ + defaultMessage: 'Animations:', + description: 'ReduceMotion: toggle label', + })} + leftChoice={intl.formatMessage({ + defaultMessage: 'On', + description: 'ReduceMotion: toggle on label', + })} + rightChoice={intl.formatMessage({ + defaultMessage: 'Off', + description: 'ReduceMotion: toggle off label', + })} value={isDeactivated} changeHandler={updateState} /> diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index bd2f33d..80eb0c3 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1,14 +1,20 @@ import { CogIcon } from '@components/Icons'; import ThemeToggle from '@components/Settings/ThemeToggle/ThemeToggle'; -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import ReduceMotion from './ReduceMotion/ReduceMotion'; import styles from './Settings.module.scss'; const Settings = () => { + const intl = useIntl(); + return ( <> <div className={styles.title}> - <CogIcon /> {t`Settings`} + <CogIcon />{' '} + {intl.formatMessage({ + defaultMessage: 'Settings', + description: 'Settings: modal title', + })} </div> <ThemeToggle /> <ReduceMotion /> diff --git a/src/components/Settings/ThemeToggle/ThemeToggle.tsx b/src/components/Settings/ThemeToggle/ThemeToggle.tsx index e14f39a..5b7a34d 100644 --- a/src/components/Settings/ThemeToggle/ThemeToggle.tsx +++ b/src/components/Settings/ThemeToggle/ThemeToggle.tsx @@ -1,11 +1,12 @@ import { Toggle } from '@components/Form'; import { MoonIcon, SunIcon } from '@components/Icons'; import Spinner from '@components/Spinner/Spinner'; -import { t } from '@lingui/macro'; import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; const ThemeToggle = () => { + const intl = useIntl(); const [isMounted, setIsMounted] = useState<boolean>(false); const { resolvedTheme, setTheme } = useTheme(); @@ -24,7 +25,10 @@ const ThemeToggle = () => { return ( <Toggle id="dark-theme" - label={t`Theme:`} + label={intl.formatMessage({ + defaultMessage: 'Theme:', + description: 'ThemeToggle: toggle label', + })} leftChoice={<SunIcon />} rightChoice={<MoonIcon />} value={isDarkTheme} diff --git a/src/components/Spinner/Spinner.tsx b/src/components/Spinner/Spinner.tsx index cfa5717..381fbb6 100644 --- a/src/components/Spinner/Spinner.tsx +++ b/src/components/Spinner/Spinner.tsx @@ -1,13 +1,20 @@ -import { t } from '@lingui/macro'; +import { useIntl } from 'react-intl'; import styles from './Spinner.module.scss'; const Spinner = () => { + const intl = useIntl(); + return ( <div className={styles.wrapper}> <div className={styles.ball}></div> <div className={styles.ball}></div> <div className={styles.ball}></div> - <div className={styles.text}>{t`Loading...`}</div> + <div className={styles.text}> + {intl.formatMessage({ + defaultMessage: 'Loading...', + description: 'Spinner: loading text', + })} + </div> </div> ); }; diff --git a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx b/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx index 52b5c06..6a19d92 100644 --- a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx +++ b/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx @@ -1,6 +1,6 @@ -import { t } from '@lingui/macro'; import { TitleLevel } from '@ts/types/app'; import { ReactNode, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './ExpandableWidget.module.scss'; const ExpandableWidget = ({ @@ -16,6 +16,7 @@ const ExpandableWidget = ({ expand?: boolean; withBorders?: boolean; }) => { + const intl = useIntl(); const [isExpanded, setIsExpanded] = useState<boolean>(expand); const handleExpanse = () => setIsExpanded((prev) => !prev); @@ -34,7 +35,15 @@ const ExpandableWidget = ({ <div className={wrapperClasses}> <button type="button" className={styles.header} onClick={handleExpanse}> <span className="screen-reader-text"> - {isExpanded ? t`Collapse` : t`Expand`} + {isExpanded + ? intl.formatMessage({ + defaultMessage: 'Collapse', + description: 'ExpandableWidget: collapse text', + }) + : intl.formatMessage({ + defaultMessage: 'Expand', + description: 'ExpandableWidget: expand text', + })} </span> <TitleTag className={styles.title}>{title}</TitleTag> <span className={styles.icon} aria-hidden={true}></span> diff --git a/src/components/Widgets/CVPreview/CVPreview.tsx b/src/components/Widgets/CVPreview/CVPreview.tsx index e52a9b2..08a4c72 100644 --- a/src/components/Widgets/CVPreview/CVPreview.tsx +++ b/src/components/Widgets/CVPreview/CVPreview.tsx @@ -1,7 +1,7 @@ import { ExpandableWidget } from '@components/WidgetParts'; -import { Trans } from '@lingui/macro'; import Image from 'next/image'; import Link from 'next/link'; +import { FormattedMessage } from 'react-intl'; import styles from './CVPreview.module.scss'; const CVPreview = ({ @@ -25,9 +25,17 @@ const CVPreview = ({ /> </div> <p> - <Trans> - Download <Link href={pdf}>CV in PDF</Link> - </Trans> + <FormattedMessage + defaultMessage="Download <link>CV in PDF</link>" + description="CVPreview: download as PDF link" + values={{ + link: (chunks: string) => ( + <Link href={pdf}> + <a>{chunks}</a> + </Link> + ), + }} + /> </p> </ExpandableWidget> ); diff --git a/src/components/Widgets/RecentPosts/RecentPosts.tsx b/src/components/Widgets/RecentPosts/RecentPosts.tsx index 8022bff..08ce7e4 100644 --- a/src/components/Widgets/RecentPosts/RecentPosts.tsx +++ b/src/components/Widgets/RecentPosts/RecentPosts.tsx @@ -1,16 +1,17 @@ import Spinner from '@components/Spinner/Spinner'; import { config } from '@config/website'; -import { t } from '@lingui/macro'; import { getPublishedPosts } from '@services/graphql/queries'; import { ArticlePreview } from '@ts/types/articles'; import { getFormattedDate } from '@utils/helpers/format'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import useSWR from 'swr'; import styles from './RecentPosts.module.scss'; const RecentPosts = () => { + const intl = useIntl(); const { data, error } = useSWR('/recent-posts', () => getPublishedPosts({ first: 3 }) ); @@ -36,7 +37,12 @@ const RecentPosts = () => { )} <h3 className={styles.title}>{post.title}</h3> <dl className={styles.meta}> - <dt>{t`Published on:`}</dt> + <dt> + {intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'RecentPosts: publication date label', + })} + </dt> <dd> <time dateTime={post.dates.publication}> {getFormattedDate(post.dates.publication, locale)} @@ -51,7 +57,11 @@ const RecentPosts = () => { }; const getPostsItems = () => { - if (error) return t`Failed to load.`; + if (error) + return intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'RecentPosts: failed to load text', + }); if (!data) return <Spinner />; return data.posts.map((post) => getPost(post)); diff --git a/src/components/Widgets/RelatedThematics/RelatedThematics.tsx b/src/components/Widgets/RelatedThematics/RelatedThematics.tsx index afe3460..c6be3ca 100644 --- a/src/components/Widgets/RelatedThematics/RelatedThematics.tsx +++ b/src/components/Widgets/RelatedThematics/RelatedThematics.tsx @@ -1,9 +1,10 @@ import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { ThematicPreview } from '@ts/types/taxonomies'; import Link from 'next/link'; +import { useIntl } from 'react-intl'; const RelatedThematics = ({ thematics }: { thematics: ThematicPreview[] }) => { + const intl = useIntl(); const sortedThematics = [...thematics].sort((a, b) => a.title.localeCompare(b.title) ); @@ -20,7 +21,14 @@ const RelatedThematics = ({ thematics }: { thematics: ThematicPreview[] }) => { return ( <ExpandableWidget - title={thematics.length > 1 ? t`Related thematics` : t`Related thematic`} + title={intl.formatMessage( + { + defaultMessage: + '{thematicsCount, plural, =0 {Related thematics} one {Related thematic} other {Related thematics}}', + description: 'RelatedThematics: widget title', + }, + { thematicsCount: thematics.length } + )} withBorders={true} > <List items={thematicsList} /> diff --git a/src/components/Widgets/RelatedTopics/RelatedTopics.tsx b/src/components/Widgets/RelatedTopics/RelatedTopics.tsx index 178e5bc..b9699e2 100644 --- a/src/components/Widgets/RelatedTopics/RelatedTopics.tsx +++ b/src/components/Widgets/RelatedTopics/RelatedTopics.tsx @@ -1,9 +1,10 @@ import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { TopicPreview } from '@ts/types/taxonomies'; import Link from 'next/link'; +import { useIntl } from 'react-intl'; const RelatedTopics = ({ topics }: { topics: TopicPreview[] }) => { + const intl = useIntl(); const sortedTopics = [...topics].sort((a, b) => a.title.localeCompare(b.title) ); @@ -20,7 +21,14 @@ const RelatedTopics = ({ topics }: { topics: TopicPreview[] }) => { return ( <ExpandableWidget - title={topicsList.length > 1 ? t`Related topics` : t`Related topic`} + title={intl.formatMessage( + { + defaultMessage: + '{topicsCount, plural, =0 {Related topics} one {Related topic} other {Related topics}}', + description: 'RelatedTopics: widget title', + }, + { topicsCount: topicsList.length } + )} withBorders={true} > <List items={topicsList} /> diff --git a/src/components/Widgets/Sharing/Sharing.tsx b/src/components/Widgets/Sharing/Sharing.tsx index 89b48ca..1025717 100644 --- a/src/components/Widgets/Sharing/Sharing.tsx +++ b/src/components/Widgets/Sharing/Sharing.tsx @@ -1,8 +1,8 @@ import { ExpandableWidget } from '@components/WidgetParts'; import sharingMedia from '@config/sharing'; -import { t } from '@lingui/macro'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import styles from './Sharing.module.scss'; type Parameters = { @@ -20,6 +20,7 @@ type Website = { }; const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { + const intl = useIntl(); const [pageExcerpt, setPageExcerpt] = useState(''); const [pageUrl, setPageUrl] = useState(''); const [domainName, setDomainName] = useState(''); @@ -54,8 +55,14 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { switch (key) { case 'content': if (id === 'email') { - const intro = t`Introduction:`; - const readMore = t`Read more here:`; + const intro = intl.formatMessage({ + defaultMessage: 'Introduction:', + description: 'Sharing: email content prefix', + }); + const readMore = intl.formatMessage({ + defaultMessage: 'Read more here:', + description: 'Sharing: content link prefix', + }); const body = `${intro}\n\n"${pageExcerpt}"\n\n${readMore} ${pageUrl}`; sharingUrl += encodeURI(body); } else { @@ -63,7 +70,16 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { } break; case 'title': - const prefix = id === 'email' ? t`Seen on ${domainName}:` : ''; + const prefix = + id === 'email' + ? intl.formatMessage( + { + defaultMessage: 'Seen on {domainName}:', + description: 'Sharing: seen on text', + }, + { domainName } + ) + : ''; sharingUrl += encodeURI(`${prefix} ${title}`); break; case 'url': @@ -94,7 +110,15 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { title={name} className={`${styles.link} ${styles[linkModifier]}`} > - <span className="screen-reader-text">{t`Share on ${name}`}</span> + <span className="screen-reader-text"> + {intl.formatMessage( + { + defaultMessage: 'Share on {name}', + description: 'Sharing: share on social network text', + }, + { name } + )} + </span> </a> </li> ); @@ -102,7 +126,13 @@ const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => { }; return ( - <ExpandableWidget title={t`Share`} expand={true}> + <ExpandableWidget + title={intl.formatMessage({ + defaultMessage: 'Share', + description: 'Sharing: widget title', + })} + expand={true} + > <ul className={`${styles.list} ${styles['list--sharing']}`}> {getItems()} </ul> diff --git a/src/components/Widgets/ThematicsList/ThematicsList.tsx b/src/components/Widgets/ThematicsList/ThematicsList.tsx index e5162b4..9b1f03a 100644 --- a/src/components/Widgets/ThematicsList/ThematicsList.tsx +++ b/src/components/Widgets/ThematicsList/ThematicsList.tsx @@ -1,10 +1,10 @@ import Spinner from '@components/Spinner/Spinner'; import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { getAllThematics } from '@services/graphql/queries'; import { TitleLevel } from '@ts/types/app'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import useSWR from 'swr'; const ThematicsList = ({ @@ -14,6 +14,7 @@ const ThematicsList = ({ title: string; titleLevel?: TitleLevel; }) => { + const intl = useIntl(); const router = useRouter(); const isThematic = () => router.asPath.includes('/thematique/'); const currentThematicSlug = isThematic() @@ -23,7 +24,15 @@ const ThematicsList = ({ const { data, error } = useSWR('/api/thematics', getAllThematics); const getList = () => { - if (error) return <ul>{t`Failed to load.`}</ul>; + if (error) + return ( + <ul> + {intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'ThematicsList: failed to load text', + })} + </ul> + ); if (!data) return ( <ul> diff --git a/src/components/Widgets/ToC/ToC.tsx b/src/components/Widgets/ToC/ToC.tsx index 6010354..c499478 100644 --- a/src/components/Widgets/ToC/ToC.tsx +++ b/src/components/Widgets/ToC/ToC.tsx @@ -1,11 +1,15 @@ import { ExpandableWidget, OrderedList } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { Heading } from '@ts/types/app'; import useHeadingsTree from '@utils/hooks/useHeadingsTree'; +import { useIntl } from 'react-intl'; const ToC = () => { + const intl = useIntl(); const headingsTree = useHeadingsTree('article'); - const title = t`Table of contents`; + const title = intl.formatMessage({ + defaultMessage: 'Table of contents', + description: 'ToC: widget title', + }); const getItems = (headings: Heading[]) => { return headings.map((heading) => { diff --git a/src/components/Widgets/TopicsList/TopicsList.tsx b/src/components/Widgets/TopicsList/TopicsList.tsx index 5b0c44e..80341c3 100644 --- a/src/components/Widgets/TopicsList/TopicsList.tsx +++ b/src/components/Widgets/TopicsList/TopicsList.tsx @@ -1,10 +1,10 @@ import Spinner from '@components/Spinner/Spinner'; import { ExpandableWidget, List } from '@components/WidgetParts'; -import { t } from '@lingui/macro'; import { getAllTopics } from '@services/graphql/queries'; import { TitleLevel } from '@ts/types/app'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; import useSWR from 'swr'; const TopicsList = ({ @@ -14,6 +14,7 @@ const TopicsList = ({ title: string; titleLevel?: TitleLevel; }) => { + const intl = useIntl(); const router = useRouter(); const isTopic = () => router.asPath.includes('/sujet/'); const currentTopicSlug = isTopic() @@ -23,7 +24,15 @@ const TopicsList = ({ const { data, error } = useSWR('/api/topics', getAllTopics); const getList = () => { - if (error) return <ul>{t`Failed to load.`}</ul>; + if (error) + return ( + <ul> + {intl.formatMessage({ + defaultMessage: 'Failed to load.', + description: 'TopicsList: failed to load text', + })} + </ul> + ); if (!data) return ( <ul> diff --git a/src/config/nav.ts b/src/config/nav.ts deleted file mode 100644 index ff69a78..0000000 --- a/src/config/nav.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { t } from '@lingui/macro'; -import { NavItem } from '@ts/types/nav'; - -export const mainNav: NavItem[] = [ - { id: 'home', name: t`Home`, slug: '/' }, - { id: 'blog', name: t`Blog`, slug: '/blog' }, - { id: 'projects', name: t`Projects`, slug: '/projets' }, - { id: 'cv', name: t`Resume`, slug: '/cv' }, - { id: 'contact', name: t`Contact`, slug: '/contact' }, -]; - -export const footerNav: NavItem[] = [ - { id: 'legal-notice', name: t`Legal notice`, slug: '/mentions-legales' }, -]; diff --git a/src/config/seo.ts b/src/config/seo.ts deleted file mode 100644 index 3ff08b6..0000000 --- a/src/config/seo.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { t } from '@lingui/macro'; -import { config } from './website'; - -export const seo = { - homepage: { - title: t`${config.name} | Front-end developer: WordPress/React`, - description: t`${config.name} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.`, - }, - blog: { - title: t`Blog: development, open source - ${config.name}`, - description: t`Discover ${config.name}'s writings. He talks about web development, Linux and open source mostly.`, - }, - cv: { - title: t`CV Front-end developer - ${config.name}`, - description: t`Discover the curriculum of ${config.name}, front-end developer located in France: skills, experiences and training.`, - }, - contact: { - title: t`Contact form - ${config.name}`, - description: t`Contact ${config.name} through its website. All you need to do it's to fill the contact form.`, - }, - legalNotice: { - title: t`Legal notice - ${config.name}`, - description: t`Discover the legal notice of ${config.name}'s website.`, - }, - error404: { - title: t`Error 404: Page not found - ${config.name}`, - description: '', - }, - projects: { - title: t`Projects: open-source makings - ${config.name}`, - description: t`Discover ${config.name} projects. Mostly related to web development and open source.`, - }, -}; diff --git a/src/config/sharing.ts b/src/config/sharing.ts index 580145e..9e84801 100644 --- a/src/config/sharing.ts +++ b/src/config/sharing.ts @@ -1,5 +1,3 @@ -import { t } from '@lingui/macro'; - const sharingMedia = [ { id: 'diaspora', @@ -58,7 +56,7 @@ const sharingMedia = [ }, { id: 'email', - name: t`Email`, + name: 'Email', parameters: { content: 'body', image: '', diff --git a/src/config/website.ts b/src/config/website.ts index 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); }); }; @@ -16,6 +16,13 @@ dependencies: "@babel/highlight" "^7.16.0" +"@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.0", "@babel/compat-data@^7.16.4": version "7.16.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" @@ -42,6 +49,27 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/core@^7.10.4": + version "7.16.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.12.tgz#5edc53c1b71e54881315923ae2aedea2522bb784" + integrity sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.16.7" + "@babel/parser" "^7.16.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.10" + "@babel/types" "^7.16.8" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + "@babel/generator@^7.11.6", "@babel/generator@^7.16.0", "@babel/generator@^7.7.2": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" @@ -51,6 +79,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe" + integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw== + dependencies: + "@babel/types" "^7.16.8" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz#9a1f0ebcda53d9a2d00108c4ceace6a5d5f1f08d" @@ -76,6 +113,16 @@ browserslist "^4.17.5" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" + integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== + dependencies: + "@babel/compat-data" "^7.16.4" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.0.tgz#090d4d166b342a03a9fec37ef4fd5aeb9c7c6a4b" @@ -110,6 +157,13 @@ resolve "^1.14.2" semver "^6.1.2" +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-explode-assignable-expression@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778" @@ -126,6 +180,15 @@ "@babel/template" "^7.16.0" "@babel/types" "^7.16.0" +"@babel/helper-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" + integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== + dependencies: + "@babel/helper-get-function-arity" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/types" "^7.16.7" + "@babel/helper-get-function-arity@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa" @@ -133,6 +196,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-get-function-arity@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" + integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-hoist-variables@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a" @@ -140,6 +210,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-member-expression-to-functions@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4" @@ -154,6 +231,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-module-transforms@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" @@ -168,6 +252,20 @@ "@babel/traverse" "^7.16.0" "@babel/types" "^7.16.0" +"@babel/helper-module-transforms@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41" + integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + "@babel/helper-optimise-call-expression@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz#cecdb145d70c54096b1564f8e9f10cd7d193b338" @@ -180,6 +278,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== +"@babel/helper-plugin-utils@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== + "@babel/helper-remap-async-to-generator@^7.16.0", "@babel/helper-remap-async-to-generator@^7.16.4": version "7.16.4" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz#5d7902f61349ff6b963e07f06a389ce139fbfe6e" @@ -206,6 +309,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-simple-access@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" + integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" @@ -220,16 +330,33 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.15.7": version "7.15.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + "@babel/helper-wrap-function@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz#b3cf318afce774dfe75b86767cd6d68f3482e57c" @@ -249,6 +376,15 @@ "@babel/traverse" "^7.16.3" "@babel/types" "^7.16.0" +"@babel/helpers@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.7.tgz#7e3504d708d50344112767c3542fc5e357fffefc" + integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" @@ -258,11 +394,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.11.5", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.3", "@babel/parser@^7.7.2": version "7.16.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e" integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng== +"@babel/parser@^7.16.10", "@babel/parser@^7.16.12", "@babel/parser@^7.16.4", "@babel/parser@^7.16.7": + version "7.16.12" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.12.tgz#9474794f9a650cf5e2f892444227f98e28cdf8b6" + integrity sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.2": version "7.16.2" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.2.tgz#2977fca9b212db153c195674e57cfab807733183" @@ -463,6 +613,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-jsx@7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" + integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-jsx@7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz#000e2e25d8673cce49300517a3eda44c263e4201" @@ -980,6 +1137,31 @@ "@babel/parser" "^7.16.0" "@babel/types" "^7.16.0" +"@babel/template@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@7", "@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.10.tgz#448f940defbe95b5a8029975b051f75993e8239f" + integrity sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.16.10" + "@babel/types" "^7.16.8" + debug "^4.1.0" + globals "^11.1.0" + "@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3", "@babel/traverse@^7.7.2": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" @@ -1011,6 +1193,14 @@ "@babel/helper-validator-identifier" "^7.15.7" to-fast-properties "^2.0.0" +"@babel/types@^7.12.11", "@babel/types@^7.16.7", "@babel/types@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" + integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1181,6 +1371,109 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@formatjs/cli@^4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-4.8.1.tgz#d2fa5308961254f8a575ca976ac279e8e12cf206" + integrity sha512-cXA1ir9DEHJu2Ilc964NL5cs5ndwiA8TqSrSNrgjBRuPqQzzo6XE9dgwUk7PQoCA50LRtHpEtfGbf9P7veZqmw== + dependencies: + "@formatjs/icu-messageformat-parser" "2.0.17" + "@formatjs/ts-transformer" "3.9.1" + "@types/estree" "^0.0.50" + "@types/fs-extra" "^9.0.1" + "@types/json-stable-stringify" "^1.0.32" + "@types/node" "14" + "@vue/compiler-core" "^3.2.23" + chalk "^4.0.0" + commander "8" + fast-glob "^3.2.7" + fs-extra "10" + json-stable-stringify "^1.0.1" + loud-rejection "^2.2.0" + tslib "^2.1.0" + typescript "^4.5" + vue "^3.2.23" + +"@formatjs/ecma402-abstract@1.11.2": + version "1.11.2" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.2.tgz#7f01595e6985a28983aae26bede9b78b273fee3d" + integrity sha512-qDgOL0vtfJ51cc0pRbFB/oXc4qDbamG22Z6h/QWy6FBxaQgppiy8JF0iYbmNO35cC8r88bQGsgfd/eM6/eTEQQ== + dependencies: + "@formatjs/intl-localematcher" "0.2.23" + tslib "^2.1.0" + +"@formatjs/fast-memoize@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz#e6f5aee2e4fd0ca5edba6eba7668e2d855e0fc21" + integrity sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg== + dependencies: + tslib "^2.1.0" + +"@formatjs/icu-messageformat-parser@2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.17.tgz#0f817aa06d3b9f23ae0a8bd667b5d7785df5017c" + integrity sha512-GO4DzmyiDUyT4p9UxSlOcdnRL1CCt43oHBBGe21s5043UjP6dwMbOotugKs1bRiN+FrNrRUSW+TLdT3+4CBI5A== + dependencies: + "@formatjs/ecma402-abstract" "1.11.2" + "@formatjs/icu-skeleton-parser" "1.3.4" + tslib "^2.1.0" + +"@formatjs/icu-skeleton-parser@1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.4.tgz#5508ff60cce4eb4698917cb50cb9ff576dde6e5b" + integrity sha512-BbKjX3rF3hq2bRjI9NjnSPUrNqI1TwwbMomOBamWfAkpOEf4LYEezPL9tHEds/+sN2/82Z+qEmK7s/l9G2J+qA== + dependencies: + "@formatjs/ecma402-abstract" "1.11.2" + tslib "^2.1.0" + +"@formatjs/intl-displaynames@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-5.4.1.tgz#09a4b956468a3a1ed332b93f380546ed02dac431" + integrity sha512-a95nwJcTM5xRsdwC1Y4msjXPINA6dbDsI043VPlSJRpUtBHWcvdSKvPDZP+KgB9RmR3zYfbJof5BSyPsAHK65w== + dependencies: + "@formatjs/ecma402-abstract" "1.11.2" + "@formatjs/intl-localematcher" "0.2.23" + tslib "^2.1.0" + +"@formatjs/intl-listformat@6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-6.5.1.tgz#609ebba0cf7301989a261f8c239ec3e46f02ffca" + integrity sha512-ijsOM7J7aNnGx+1JYUGWgMAcisnK0CxdlPx7KJpUXKj9Mf2Ph28H2WMTL1h1xv9T7SSvH0Nd6asI0Qw4ffw17w== + dependencies: + "@formatjs/ecma402-abstract" "1.11.2" + "@formatjs/intl-localematcher" "0.2.23" + tslib "^2.1.0" + +"@formatjs/intl-localematcher@0.2.23": + version "0.2.23" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.23.tgz#5a0b1d81df1f392ecf37e556ca7040a7ec9f72e8" + integrity sha512-oCe2TOciTtB1bEbJ85EvYrXQxD0epusmVJfJ7AduO0tlbXP42CmDIYIH2CZ+kP2GE+PTLQD1Hbt9kpOpl939MQ== + dependencies: + tslib "^2.1.0" + +"@formatjs/intl@1.18.4": + version "1.18.4" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.18.4.tgz#034b99949a1bf18ac7d8d1dff2b5fb3cdbca6c48" + integrity sha512-1l93aCrAWRoK8KPD6W5Re9f3XUuNwMuxP12ZFebiG/Wb3eqTASIl9yTUoHwa/FJlNTL1JBRs4PYGCxKeqOod2w== + dependencies: + "@formatjs/ecma402-abstract" "1.11.2" + "@formatjs/fast-memoize" "1.2.1" + "@formatjs/icu-messageformat-parser" "2.0.17" + "@formatjs/intl-displaynames" "5.4.1" + "@formatjs/intl-listformat" "6.5.1" + intl-messageformat "9.11.3" + tslib "^2.1.0" + +"@formatjs/ts-transformer@3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.9.1.tgz#92d7d54debf7f427dcc8c9bc57c8813560553708" + integrity sha512-FY31pBrqIO8AeL6+vFFCSqBXe4NZyxCfIb1jRColBXiQHbUlmfaoTFu19BXibqbU5CxFd+wG2LhDLZuitGhDBA== + dependencies: + "@formatjs/icu-messageformat-parser" "2.0.17" + "@types/node" "14 || 16 || 17" + chalk "^4.0.0" + tslib "^2.1.0" + typescript "^4.5" + "@hapi/accept@5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523" @@ -1843,6 +2136,17 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== +"@types/babel__core@*", "@types/babel__core@^7.1.7": + version "7.1.18" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" + integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.17" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.17.tgz#f50ac9d20d64153b510578d84f9643f9a3afbe64" @@ -1861,6 +2165,13 @@ dependencies: "@babel/types" "^7.0.0" +"@types/babel__helper-plugin-utils@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@types/babel__helper-plugin-utils/-/babel__helper-plugin-utils-7.10.0.tgz#dcd2416f9c189d5837ab2a276368cf67134efe78" + integrity sha512-60YtHzhQ9HAkToHVV+TB4VLzBn9lrfgrsOjiJMtbv/c1jPdekBxaByd6DMsGBzROXWoIL6U3lEFvvbu69RkUoA== + dependencies: + "@types/babel__core" "*" + "@types/babel__template@*": version "7.4.1" resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" @@ -1883,6 +2194,14 @@ dependencies: "@types/ms" "*" +"@types/eslint@8": + version "8.4.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.1.tgz#c48251553e8759db9e656de3efc846954ac32304" + integrity sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + "@types/estree-jsx@^0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-0.0.1.tgz#c36d7a1afeb47a95a8ee0b7bc8bc705db38f919d" @@ -1900,6 +2219,13 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe" integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== +"@types/fs-extra@^9.0.1": + version "9.0.13" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" + integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== + dependencies: + "@types/node" "*" + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -1914,6 +2240,14 @@ dependencies: "@types/unist" "*" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1933,6 +2267,16 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/json-schema@*": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + +"@types/json-stable-stringify@^1.0.32": + version "1.0.33" + resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.33.tgz#099b0712d824d15e2660c20e1c16e6a8381f308c" + integrity sha512-qEWiQff6q2tA5gcJGWwzplQcXdJtm+0oy6IHGHzlOf3eFAkGE/FIPXZK9ofWgNSHVp8AFFI33PJJshS0ei3Gvw== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1970,6 +2314,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10" integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw== +"@types/node@14": + version "14.18.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.9.tgz#0e5944eefe2b287391279a19b407aa98bd14436d" + integrity sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q== + +"@types/node@14 || 16 || 17": + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.13.tgz#5ed7ed7c662948335fcad6c412bb42d99ea754e3" + integrity sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -1995,19 +2349,19 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== -"@types/react@17.0.37": - version "17.0.37" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" - integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== +"@types/react@*", "@types/react@16 || 17", "@types/react@>=16": + version "17.0.38" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" + integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@>=16": - version "17.0.38" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" - integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== +"@types/react@17.0.37": + version "17.0.37" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" + integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2065,6 +2419,11 @@ "@typescript-eslint/types" "5.6.0" "@typescript-eslint/visitor-keys" "5.6.0" +"@typescript-eslint/types@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.1.tgz#dca9bd4cb8c067fc85304a31f38ec4766ba2d1ea" + integrity sha512-ZvxQ2QMy49bIIBpTqFiOenucqUyjTQ0WNLhBM6X1fh1NNlYAC6Kxsx8bRTY3jdYsYg44a0Z/uEgQkohbR0H87Q== + "@typescript-eslint/types@5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.6.0.tgz#745cb1b59daadcc1f32f7be95f0f68accf38afdd" @@ -2083,6 +2442,27 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@^5.9.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.1.tgz#b268e67be0553f8790ba3fe87113282977adda15" + integrity sha512-PwIGnH7jIueXv4opcwEbVGDATjGPO1dx9RkUl5LlHDSe+FXxPwFL5W/qYd5/NHr7f6lo/vvTrAzd0KlQtRusJQ== + dependencies: + "@typescript-eslint/types" "5.10.1" + "@typescript-eslint/visitor-keys" "5.10.1" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.1.tgz#29102de692f59d7d34ecc457ed59ab5fc558010b" + integrity sha512-NjQ0Xinhy9IL979tpoTRuLKxMc0zJC7QVSdeerXs2/QvOy2yRkzX5dRb10X5woNUdJgU8G3nYRDlI33sq1K4YQ== + dependencies: + "@typescript-eslint/types" "5.10.1" + eslint-visitor-keys "^3.0.0" + "@typescript-eslint/visitor-keys@5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.6.0.tgz#3e36509e103fe9713d8f035ac977235fd63cb6e6" @@ -2091,6 +2471,96 @@ "@typescript-eslint/types" "5.6.0" eslint-visitor-keys "^3.0.0" +"@vue/compiler-core@3.2.29", "@vue/compiler-core@^3.2.23": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.29.tgz#b06097ab8ff0493177c68c5ea5b63d379a061097" + integrity sha512-RePZ/J4Ub3sb7atQw6V6Rez+/5LCRHGFlSetT3N4VMrejqJnNPXKUt5AVm/9F5MJriy2w/VudEIvgscCfCWqxw== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/shared" "3.2.29" + estree-walker "^2.0.2" + source-map "^0.6.1" + +"@vue/compiler-dom@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.29.tgz#ad0ead405bd2f2754161335aad9758aa12430715" + integrity sha512-y26vK5khdNS9L3ckvkqJk/78qXwWb75Ci8iYLb67AkJuIgyKhIOcR1E8RIt4mswlVCIeI9gQ+fmtdhaiTAtrBQ== + dependencies: + "@vue/compiler-core" "3.2.29" + "@vue/shared" "3.2.29" + +"@vue/compiler-sfc@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz#f76d556cd5fca6a55a3ea84c88db1a2a53a36ead" + integrity sha512-X9+0dwsag2u6hSOP/XsMYqFti/edvYvxamgBgCcbSYuXx1xLZN+dS/GvQKM4AgGS4djqo0jQvWfIXdfZ2ET68g== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.29" + "@vue/compiler-dom" "3.2.29" + "@vue/compiler-ssr" "3.2.29" + "@vue/reactivity-transform" "3.2.29" + "@vue/shared" "3.2.29" + estree-walker "^2.0.2" + magic-string "^0.25.7" + postcss "^8.1.10" + source-map "^0.6.1" + +"@vue/compiler-ssr@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.29.tgz#37b15b32dcd2f6b410bb61fca3f37b1a92b7eb1e" + integrity sha512-LrvQwXlx66uWsB9/VydaaqEpae9xtmlUkeSKF6aPDbzx8M1h7ukxaPjNCAXuFd3fUHblcri8k42lfimHfzMICA== + dependencies: + "@vue/compiler-dom" "3.2.29" + "@vue/shared" "3.2.29" + +"@vue/reactivity-transform@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.29.tgz#a08d606e10016b7cf588d1a43dae4db2953f9354" + integrity sha512-YF6HdOuhdOw6KyRm59+3rML8USb9o8mYM1q+SH0G41K3/q/G7uhPnHGKvspzceD7h9J3VR1waOQ93CUZj7J7OA== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.29" + "@vue/shared" "3.2.29" + estree-walker "^2.0.2" + magic-string "^0.25.7" + +"@vue/reactivity@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.29.tgz#afdc9c111d4139b14600be17ad80267212af6052" + integrity sha512-Ryhb6Gy62YolKXH1gv42pEqwx7zs3n8gacRVZICSgjQz8Qr8QeCcFygBKYfJm3o1SccR7U+bVBQDWZGOyG1k4g== + dependencies: + "@vue/shared" "3.2.29" + +"@vue/runtime-core@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.29.tgz#fb8577b2fcf52e8d967bd91cdf49ab9fb91f9417" + integrity sha512-VMvQuLdzoTGmCwIKTKVwKmIL0qcODIqe74JtK1pVr5lnaE0l25hopodmPag3RcnIcIXe+Ye3B2olRCn7fTCgig== + dependencies: + "@vue/reactivity" "3.2.29" + "@vue/shared" "3.2.29" + +"@vue/runtime-dom@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.29.tgz#35e9a2bf04ef80b86ac2ca0e7b2ceaccf1e18f01" + integrity sha512-YJgLQLwr+SQyORzTsBQLL5TT/5UiV83tEotqjL7F9aFDIQdFBTCwpkCFvX9jqwHoyi9sJqM9XtTrMcc8z/OjPA== + dependencies: + "@vue/runtime-core" "3.2.29" + "@vue/shared" "3.2.29" + csstype "^2.6.8" + +"@vue/server-renderer@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.29.tgz#ea6afa361b9c781a868c8da18c761f9b7bc89102" + integrity sha512-lpiYx7ciV7rWfJ0tPkoSOlLmwqBZ9FTmQm33S+T4g0j1fO/LmhJ9b9Ctl1o5xvIFVDk9QkSUWANZn7H2pXuxVw== + dependencies: + "@vue/compiler-ssr" "3.2.29" + "@vue/shared" "3.2.29" + +"@vue/shared@3.2.29": + version "3.2.29" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925" + integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw== + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -2266,6 +2736,11 @@ aria-query@^5.0.0: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -2391,6 +2866,22 @@ babel-plugin-dynamic-import-node@^2.3.3: dependencies: object.assign "^4.1.0" +babel-plugin-formatjs@^10.3.17: + version "10.3.17" + resolved "https://registry.yarnpkg.com/babel-plugin-formatjs/-/babel-plugin-formatjs-10.3.17.tgz#5c93b479cd1dfae2adb0b4d6e7987d2d834abe4d" + integrity sha512-VQFCGOrJbZ7kZ6B0KYFlsBeNG3eXH3946e3vWxH5VXLR91slKQlohGPP9F4J63bqIxqcfZWJXBYq4Gb8z3uhPw== + dependencies: + "@babel/core" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "7" + "@babel/traverse" "7" + "@babel/types" "^7.12.11" + "@formatjs/icu-messageformat-parser" "2.0.17" + "@formatjs/ts-transformer" "3.9.1" + "@types/babel__core" "^7.1.7" + "@types/babel__helper-plugin-utils" "^7.10.0" + tslib "^2.1.0" + babel-plugin-istanbul@^6.0.0: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -2962,7 +3453,7 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98" integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg== -commander@*, commander@^8.3.0: +commander@*, commander@8, commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== @@ -3357,11 +3848,23 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +csstype@^2.6.8: + version "2.6.19" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa" + integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ== + csstype@^3.0.2: version "3.0.10" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + damerau-levenshtein@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" @@ -3634,6 +4137,11 @@ emittery@^0.8.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +emoji-regex@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.0.tgz#96559e19f82231b436403e059571241d627c42b8" + integrity sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3805,6 +4313,19 @@ eslint-module-utils@^2.7.1: find-up "^2.1.0" pkg-dir "^2.0.0" +eslint-plugin-formatjs@^2.20.5: + version "2.20.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-2.20.5.tgz#bed01b2586fead3e7b34e40333c49d9f07eb74c3" + integrity sha512-VxqoeThPaMMFpAjeGkoGNNFbmUFkLnY1J5m1I2b2yZzjpNF0+FeBykDUxbtGb569TwZRI3qxt5Zn1yXjCU9RjQ== + dependencies: + "@formatjs/icu-messageformat-parser" "2.0.17" + "@formatjs/ts-transformer" "3.9.1" + "@types/eslint" "8" + "@typescript-eslint/typescript-estree" "^5.9.1" + emoji-regex "^10.0.0" + tslib "^2.1.0" + typescript "^4.5" + eslint-plugin-import@^2.25.2: version "2.25.3" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766" @@ -3998,6 +4519,11 @@ estree-util-visit@^1.0.0: "@types/estree-jsx" "^0.0.1" "@types/unist" "^2.0.0" +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + estree-walker@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.1.tgz#c2a9fb4a30232f5039b7c030b37ead691932debd" @@ -4230,7 +4756,7 @@ fs-access@^1.0.1: dependencies: null-check "^1.0.0" -fs-extra@^10.0.0: +fs-extra@10, fs-extra@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== @@ -4588,6 +5114,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -4783,6 +5316,16 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +intl-messageformat@9.11.3, intl-messageformat@^9.11.3: + version "9.11.3" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.11.3.tgz#e9b26b582891ff0fca327a9ddcb2caf6d26c84e7" + integrity sha512-sFOaEw2cytBASTsJkfVod8IJzTx9oOPdU0C7jzprfGATn22FjQGJ60UCyCkKJo6UW+NnpKpwBjO73Pnhvv6HHg== + dependencies: + "@formatjs/ecma402-abstract" "1.11.2" + "@formatjs/fast-memoize" "1.2.1" + "@formatjs/icu-messageformat-parser" "2.0.17" + tslib "^2.1.0" + is-alphabetical@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" @@ -5638,6 +6181,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= + dependencies: + jsonify "~0.0.0" + json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -5666,6 +6216,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= + jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -5899,6 +6454,14 @@ loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +loud-rejection@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-2.2.0.tgz#4255eb6e9c74045b0edc021fa7397ab655a8517c" + integrity sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ== + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -5911,6 +6474,13 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -7127,6 +7697,15 @@ postcss@8.2.15: nanoid "^3.1.23" source-map "^0.6.1" +postcss@^8.1.10: + version "8.4.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" + postcss@^8.3.11: version "8.4.4" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.4.tgz#d53d4ec6a75fd62557a66bb41978bf47ff0c2869" @@ -7313,12 +7892,28 @@ react-dom@17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-intl@^5.24.4: + version "5.24.4" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.24.4.tgz#1c9dbc5b4e33b068e8c73a2b919af6e681fee5b2" + integrity sha512-c3OaJNZUt8CqqjVge+YPof76xRp6HrxmfKtiEB3LOBu466ISliGLPiy3goOdNs9Vj/0+jGagcAk8jqh/pAscAw== + dependencies: + "@formatjs/ecma402-abstract" "1.11.2" + "@formatjs/icu-messageformat-parser" "2.0.17" + "@formatjs/intl" "1.18.4" + "@formatjs/intl-displaynames" "5.4.1" + "@formatjs/intl-listformat" "6.5.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/react" "16 || 17" + hoist-non-react-statics "^3.3.2" + intl-messageformat "9.11.3" + tslib "^2.1.0" + react-is@17.0.2, "react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.8.1: +react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7839,6 +8434,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + space-separated-tokens@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz#43193cec4fb858a2ce934b7f98b7f2c18107098b" @@ -8449,7 +9049,7 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2: +tslib@^2, tslib@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -8537,6 +9137,11 @@ typescript@4.5.3, typescript@^4.4.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.3.tgz#afaa858e68c7103317d89eb90c5d8906268d353c" integrity sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ== +typescript@^4.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + uglify-js@^3.1.4: version "3.14.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.4.tgz#68756f17d1b90b9d289341736cb9a567d6882f90" @@ -8768,6 +9373,17 @@ vm-browserify@1.1.2: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vue@^3.2.23: + version "3.2.29" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.29.tgz#3571b65dbd796d3a6347e2fd45a8e6e11c13d56a" + integrity sha512-cFIwr7LkbtCRanjNvh6r7wp2yUxfxeM2yPpDQpAfaaLIGZSrUmLbNiSze9nhBJt5MrZ68Iqt0O5scwAMEVxF+Q== + dependencies: + "@vue/compiler-dom" "3.2.29" + "@vue/compiler-sfc" "3.2.29" + "@vue/runtime-dom" "3.2.29" + "@vue/server-renderer" "3.2.29" + "@vue/shared" "3.2.29" + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" |
