From 951e35b56be7fa96e6967d78e472afb81b1b78da Mon Sep 17 00:00:00 2001 From: romrosmay <99548008+romrosmay@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:34:01 +0200 Subject: [PATCH 1/4] feat(lang): adding multilanguage support --- .env.example | 10 + components/BuiltWith.tsx | 7 +- components/DevIcon.tsx | 2 +- components/Footer.tsx | 6 +- components/Header.tsx | 60 +-- components/LanguageSwitcher.tsx | 89 +++++ components/MobileNav.tsx | 22 +- components/PageTitle.tsx | 2 +- components/Pagination.tsx | 12 +- components/PostListItem.tsx | 8 +- components/PostsSearch.tsx | 6 +- components/ProfileCard.tsx | 4 +- components/ProfileInfo.tsx | 7 +- components/ProjectCard.tsx | 6 +- components/SEO.tsx | 11 +- components/SocialButtons.tsx | 71 ++-- components/ToC.tsx | 92 +++++ components/ViewCounter.tsx | 9 +- components/blog/BlogHeader.tsx | 6 +- components/blog/BlogMeta.tsx | 9 +- components/homepage/BlogLinks.tsx | 15 +- components/homepage/FeaturedPosts.tsx | 7 +- components/homepage/Greeting.tsx | 6 +- components/homepage/Heading.tsx | 9 +- components/homepage/ShortDescription.tsx | 12 +- components/homepage/TypedBios.tsx | 43 +-- data/authors/default.mdx | 108 ------ data/ca/authors/default.mdx | 108 ++++++ data/{ => ca}/authors/resume.mdx | 0 data/{ => ca}/blog/DRAFT.mdx | 0 ...image-with-srcset-and-sizes-attributes.mdx | 0 ...nd-config-website-on-name-cheap-part-1.mdx | 0 ...and-config-website-on-namecheap-part-2.mdx | 0 .../blog/drag-n-drop-api-keynotes.mdx | 0 ...embed-script-using-github-and-jsdelivr.mdx | 0 ...claration-vs-function-expression-in-js.mdx | 0 data/{ => ca}/blog/git-notes.mdx | 0 ...from-a-junior-developer-to-a-developer.mdx | 0 ...te-tailwind-css-with-react-application.mdx | 0 ...to-committing-with-conventional-commit.mdx | 0 .../blog/nodejs-fetch-json-with-https.mdx | 0 ...ft-while-show-hide-scrollbar-on-window.mdx | 0 ...locking-css-and-chrome-performance-api.mdx | 68 ++++ .../blog/set-up-path-aliases-in-nodejs.mdx | 0 ...-to-new-store-with-file-api-and-nodejs.mdx | 0 .../shopify-section-rendering-apis-notes.mdx | 0 .../tricky-use-case-of-array-map-in-js.mdx | 0 .../blog/use-https-in-local-development.mdx | 0 .../blog/viec-tim-dev-nhu-the-nao-part-2.mdx | 0 .../blog/viec-tim-dev-nhu-the-nao.mdx | 0 data/ca/projectsData.ts | 101 +++++ data/ca/snippets/SAMPLE.mdx | 19 + .../adding-or-substracting-days-in-liquid.mdx | 43 +++ data/ca/snippets/casing-utils.mdx | 96 +++++ data/ca/snippets/color-validator.mdx | 61 +++ ...eep-remove-falsy-values-from-an-object.mdx | 78 ++++ data/ca/snippets/event-emitter.mdx | 75 ++++ ...nd-kill-process-on-given-port-in-macos.mdx | 56 +++ ...display-nowplaying-tracks-on-your-site.mdx | 159 ++++++++ ...gitignore-ignore-directory-keep-1-file.mdx | 23 ++ .../snippets/markdown-code-block-syntax.mdx | 126 +++++++ .../read-all-file-names-inside-a-folder.mdx | 39 ++ ...me-a-case-sensitive-file-in-a-git-repo.mdx | 34 ++ data/ca/snippets/use-async.mdx | 102 +++++ data/ca/snippets/use-local-storage-state.mdx | 32 ++ data/ca/snippets/useful-array-utilities.mdx | 23 ++ .../verify-github-webhooks-requests.mdx | 72 ++++ data/ca/snippets/vnese-to-plain-english.mdx | 37 ++ data/en/authors/default.mdx | 109 ++++++ data/en/authors/resume.mdx | 199 ++++++++++ data/en/blog/DRAFT.mdx | 17 + ...image-with-srcset-and-sizes-attributes.mdx | 131 +++++++ ...nd-config-website-on-name-cheap-part-1.mdx | 110 ++++++ ...and-config-website-on-namecheap-part-2.mdx | 125 +++++++ data/en/blog/drag-n-drop-api-keynotes.mdx | 136 +++++++ ...embed-script-using-github-and-jsdelivr.mdx | 109 ++++++ ...claration-vs-function-expression-in-js.mdx | 141 +++++++ data/en/blog/git-notes.mdx | 348 ++++++++++++++++++ ...from-a-junior-developer-to-a-developer.mdx | 202 ++++++++++ ...te-tailwind-css-with-react-application.mdx | 197 ++++++++++ ...to-committing-with-conventional-commit.mdx | 170 +++++++++ data/en/blog/nodejs-fetch-json-with-https.mdx | 96 +++++ ...ft-while-show-hide-scrollbar-on-window.mdx | 102 +++++ ...locking-css-and-chrome-performance-api.mdx | 0 .../en/blog/set-up-path-aliases-in-nodejs.mdx | 117 ++++++ ...-to-new-store-with-file-api-and-nodejs.mdx | 205 +++++++++++ .../shopify-section-rendering-apis-notes.mdx | 158 ++++++++ .../tricky-use-case-of-array-map-in-js.mdx | 185 ++++++++++ .../blog/use-https-in-local-development.mdx | 248 +++++++++++++ .../blog/viec-tim-dev-nhu-the-nao-part-2.mdx | 125 +++++++ data/en/blog/viec-tim-dev-nhu-the-nao.mdx | 138 +++++++ data/{ => en}/projectsData.ts | 0 data/en/snippets/SAMPLE.mdx | 19 + .../adding-or-substracting-days-in-liquid.mdx | 43 +++ data/en/snippets/casing-utils.mdx | 96 +++++ data/en/snippets/color-validator.mdx | 61 +++ ...eep-remove-falsy-values-from-an-object.mdx | 78 ++++ data/en/snippets/event-emitter.mdx | 75 ++++ ...nd-kill-process-on-given-port-in-macos.mdx | 56 +++ ...display-nowplaying-tracks-on-your-site.mdx | 159 ++++++++ ...gitignore-ignore-directory-keep-1-file.mdx | 23 ++ .../snippets/markdown-code-block-syntax.mdx | 126 +++++++ .../read-all-file-names-inside-a-folder.mdx | 39 ++ ...me-a-case-sensitive-file-in-a-git-repo.mdx | 34 ++ data/en/snippets/use-async.mdx | 102 +++++ data/en/snippets/use-local-storage-state.mdx | 32 ++ data/en/snippets/useful-array-utilities.mdx | 23 ++ .../verify-github-webhooks-requests.mdx | 72 ++++ data/en/snippets/vnese-to-plain-english.mdx | 37 ++ data/es/authors/default.mdx | 109 ++++++ data/es/authors/resume.mdx | 205 +++++++++++ data/es/blog/DRAFT.mdx | 17 + ...image-with-srcset-and-sizes-attributes.mdx | 131 +++++++ ...nd-config-website-on-name-cheap-part-1.mdx | 110 ++++++ ...and-config-website-on-namecheap-part-2.mdx | 125 +++++++ data/es/blog/drag-n-drop-api-keynotes.mdx | 136 +++++++ ...embed-script-using-github-and-jsdelivr.mdx | 109 ++++++ ...claration-vs-function-expression-in-js.mdx | 141 +++++++ data/es/blog/git-notes.mdx | 348 ++++++++++++++++++ ...from-a-junior-developer-to-a-developer.mdx | 202 ++++++++++ ...te-tailwind-css-with-react-application.mdx | 197 ++++++++++ ...to-committing-with-conventional-commit.mdx | 170 +++++++++ data/es/blog/nodejs-fetch-json-with-https.mdx | 96 +++++ ...ft-while-show-hide-scrollbar-on-window.mdx | 102 +++++ ...locking-css-and-chrome-performance-api.mdx | 174 +++++++++ .../es/blog/set-up-path-aliases-in-nodejs.mdx | 117 ++++++ ...-to-new-store-with-file-api-and-nodejs.mdx | 205 +++++++++++ .../shopify-section-rendering-apis-notes.mdx | 158 ++++++++ .../tricky-use-case-of-array-map-in-js.mdx | 185 ++++++++++ .../blog/use-https-in-local-development.mdx | 248 +++++++++++++ .../blog/viec-tim-dev-nhu-the-nao-part-2.mdx | 125 +++++++ data/es/blog/viec-tim-dev-nhu-the-nao.mdx | 138 +++++++ data/es/projectsData.ts | 101 +++++ data/es/snippets/SAMPLE.mdx | 19 + .../adding-or-substracting-days-in-liquid.mdx | 43 +++ data/es/snippets/casing-utils.mdx | 96 +++++ data/es/snippets/color-validator.mdx | 61 +++ ...eep-remove-falsy-values-from-an-object.mdx | 78 ++++ data/es/snippets/event-emitter.mdx | 75 ++++ ...nd-kill-process-on-given-port-in-macos.mdx | 56 +++ ...display-nowplaying-tracks-on-your-site.mdx | 159 ++++++++ ...gitignore-ignore-directory-keep-1-file.mdx | 23 ++ .../snippets/markdown-code-block-syntax.mdx | 126 +++++++ .../read-all-file-names-inside-a-folder.mdx | 39 ++ ...me-a-case-sensitive-file-in-a-git-repo.mdx | 34 ++ data/es/snippets/use-async.mdx | 102 +++++ data/es/snippets/use-local-storage-state.mdx | 32 ++ data/es/snippets/useful-array-utilities.mdx | 23 ++ .../verify-github-webhooks-requests.mdx | 72 ++++ data/es/snippets/vnese-to-plain-english.mdx | 37 ++ data/headerNavLinks.ts | 10 +- data/siteMetadata.ts | 8 - hooks/useLocale.tsx | 9 + layouts/AuthorLayout.tsx | 11 +- layouts/ListLayout.tsx | 9 +- layouts/PostSimple.tsx | 8 +- layouts/ResumeLayout.module.css | 18 + layouts/ResumeLayout.tsx | 36 +- libs/files.ts | 18 +- libs/generate-rss.ts | 27 +- libs/mdx.ts | 23 +- libs/remark-toc-heading.ts | 60 ++- next-i18next.config.js | 6 + next.config.js | 19 +- package-lock.json | 300 ++++++++++++++- package.json | 6 +- pages/404.tsx | 21 +- pages/_app.tsx | 6 +- pages/_document.tsx | 2 +- pages/about.tsx | 9 +- pages/blog.tsx | 25 +- pages/blog/[...slug].tsx | 62 +++- pages/blog/page/[page].tsx | 41 ++- pages/index.tsx | 26 +- pages/projects.tsx | 34 +- pages/resume.tsx | 12 +- pages/snippets/[...slug].tsx | 35 +- pages/snippets/index.tsx | 16 +- pages/tags.tsx | 15 +- pages/tags/[tag].tsx | 44 ++- public/locales/ca/common.json | 106 ++++++ public/locales/en/common.json | 106 ++++++ public/locales/es/common.json | 106 ++++++ tailwind.config.js | 27 +- types/layout.ts | 3 +- types/mdx.ts | 3 +- types/server.ts | 11 +- utils/date.ts | 6 +- 188 files changed, 12022 insertions(+), 420 deletions(-) create mode 100644 components/LanguageSwitcher.tsx create mode 100644 components/ToC.tsx delete mode 100644 data/authors/default.mdx create mode 100644 data/ca/authors/default.mdx rename data/{ => ca}/authors/resume.mdx (100%) rename data/{ => ca}/blog/DRAFT.mdx (100%) rename data/{ => ca}/blog/better-responsive-image-with-srcset-and-sizes-attributes.mdx (100%) rename data/{ => ca}/blog/deploy-and-config-website-on-name-cheap-part-1.mdx (100%) rename data/{ => ca}/blog/deploy-and-config-website-on-namecheap-part-2.mdx (100%) rename data/{ => ca}/blog/drag-n-drop-api-keynotes.mdx (100%) rename data/{ => ca}/blog/embed-script-using-github-and-jsdelivr.mdx (100%) rename data/{ => ca}/blog/function-declaration-vs-function-expression-in-js.mdx (100%) rename data/{ => ca}/blog/git-notes.mdx (100%) rename data/{ => ca}/blog/goodbye-bravebits-from-a-junior-developer-to-a-developer.mdx (100%) rename data/{ => ca}/blog/integrate-tailwind-css-with-react-application.mdx (100%) rename data/{ => ca}/blog/introduction-to-committing-with-conventional-commit.mdx (100%) rename data/{ => ca}/blog/nodejs-fetch-json-with-https.mdx (100%) rename data/{ => ca}/blog/prevent-layout-shift-while-show-hide-scrollbar-on-window.mdx (100%) create mode 100644 data/ca/blog/render-blocking-css-and-chrome-performance-api.mdx rename data/{ => ca}/blog/set-up-path-aliases-in-nodejs.mdx (100%) rename data/{ => ca}/blog/shopify-migrate-files-to-new-store-with-file-api-and-nodejs.mdx (100%) rename data/{ => ca}/blog/shopify-section-rendering-apis-notes.mdx (100%) rename data/{ => ca}/blog/tricky-use-case-of-array-map-in-js.mdx (100%) rename data/{ => ca}/blog/use-https-in-local-development.mdx (100%) rename data/{ => ca}/blog/viec-tim-dev-nhu-the-nao-part-2.mdx (100%) rename data/{ => ca}/blog/viec-tim-dev-nhu-the-nao.mdx (100%) create mode 100644 data/ca/projectsData.ts create mode 100644 data/ca/snippets/SAMPLE.mdx create mode 100644 data/ca/snippets/adding-or-substracting-days-in-liquid.mdx create mode 100644 data/ca/snippets/casing-utils.mdx create mode 100644 data/ca/snippets/color-validator.mdx create mode 100644 data/ca/snippets/deep-remove-falsy-values-from-an-object.mdx create mode 100644 data/ca/snippets/event-emitter.mdx create mode 100644 data/ca/snippets/find-and-kill-process-on-given-port-in-macos.mdx create mode 100644 data/ca/snippets/getting-spotify-token-to-display-nowplaying-tracks-on-your-site.mdx create mode 100644 data/ca/snippets/gitignore-ignore-directory-keep-1-file.mdx create mode 100644 data/ca/snippets/markdown-code-block-syntax.mdx create mode 100644 data/ca/snippets/read-all-file-names-inside-a-folder.mdx create mode 100644 data/ca/snippets/rename-a-case-sensitive-file-in-a-git-repo.mdx create mode 100644 data/ca/snippets/use-async.mdx create mode 100644 data/ca/snippets/use-local-storage-state.mdx create mode 100644 data/ca/snippets/useful-array-utilities.mdx create mode 100644 data/ca/snippets/verify-github-webhooks-requests.mdx create mode 100644 data/ca/snippets/vnese-to-plain-english.mdx create mode 100644 data/en/authors/default.mdx create mode 100644 data/en/authors/resume.mdx create mode 100644 data/en/blog/DRAFT.mdx create mode 100644 data/en/blog/better-responsive-image-with-srcset-and-sizes-attributes.mdx create mode 100644 data/en/blog/deploy-and-config-website-on-name-cheap-part-1.mdx create mode 100644 data/en/blog/deploy-and-config-website-on-namecheap-part-2.mdx create mode 100644 data/en/blog/drag-n-drop-api-keynotes.mdx create mode 100644 data/en/blog/embed-script-using-github-and-jsdelivr.mdx create mode 100644 data/en/blog/function-declaration-vs-function-expression-in-js.mdx create mode 100644 data/en/blog/git-notes.mdx create mode 100644 data/en/blog/goodbye-bravebits-from-a-junior-developer-to-a-developer.mdx create mode 100644 data/en/blog/integrate-tailwind-css-with-react-application.mdx create mode 100644 data/en/blog/introduction-to-committing-with-conventional-commit.mdx create mode 100644 data/en/blog/nodejs-fetch-json-with-https.mdx create mode 100644 data/en/blog/prevent-layout-shift-while-show-hide-scrollbar-on-window.mdx rename data/{ => en}/blog/render-blocking-css-and-chrome-performance-api.mdx (100%) create mode 100644 data/en/blog/set-up-path-aliases-in-nodejs.mdx create mode 100644 data/en/blog/shopify-migrate-files-to-new-store-with-file-api-and-nodejs.mdx create mode 100644 data/en/blog/shopify-section-rendering-apis-notes.mdx create mode 100644 data/en/blog/tricky-use-case-of-array-map-in-js.mdx create mode 100644 data/en/blog/use-https-in-local-development.mdx create mode 100644 data/en/blog/viec-tim-dev-nhu-the-nao-part-2.mdx create mode 100644 data/en/blog/viec-tim-dev-nhu-the-nao.mdx rename data/{ => en}/projectsData.ts (100%) create mode 100644 data/en/snippets/SAMPLE.mdx create mode 100644 data/en/snippets/adding-or-substracting-days-in-liquid.mdx create mode 100644 data/en/snippets/casing-utils.mdx create mode 100644 data/en/snippets/color-validator.mdx create mode 100644 data/en/snippets/deep-remove-falsy-values-from-an-object.mdx create mode 100644 data/en/snippets/event-emitter.mdx create mode 100644 data/en/snippets/find-and-kill-process-on-given-port-in-macos.mdx create mode 100644 data/en/snippets/getting-spotify-token-to-display-nowplaying-tracks-on-your-site.mdx create mode 100644 data/en/snippets/gitignore-ignore-directory-keep-1-file.mdx create mode 100644 data/en/snippets/markdown-code-block-syntax.mdx create mode 100644 data/en/snippets/read-all-file-names-inside-a-folder.mdx create mode 100644 data/en/snippets/rename-a-case-sensitive-file-in-a-git-repo.mdx create mode 100644 data/en/snippets/use-async.mdx create mode 100644 data/en/snippets/use-local-storage-state.mdx create mode 100644 data/en/snippets/useful-array-utilities.mdx create mode 100644 data/en/snippets/verify-github-webhooks-requests.mdx create mode 100644 data/en/snippets/vnese-to-plain-english.mdx create mode 100644 data/es/authors/default.mdx create mode 100644 data/es/authors/resume.mdx create mode 100644 data/es/blog/DRAFT.mdx create mode 100644 data/es/blog/better-responsive-image-with-srcset-and-sizes-attributes.mdx create mode 100644 data/es/blog/deploy-and-config-website-on-name-cheap-part-1.mdx create mode 100644 data/es/blog/deploy-and-config-website-on-namecheap-part-2.mdx create mode 100644 data/es/blog/drag-n-drop-api-keynotes.mdx create mode 100644 data/es/blog/embed-script-using-github-and-jsdelivr.mdx create mode 100644 data/es/blog/function-declaration-vs-function-expression-in-js.mdx create mode 100644 data/es/blog/git-notes.mdx create mode 100644 data/es/blog/goodbye-bravebits-from-a-junior-developer-to-a-developer.mdx create mode 100644 data/es/blog/integrate-tailwind-css-with-react-application.mdx create mode 100644 data/es/blog/introduction-to-committing-with-conventional-commit.mdx create mode 100644 data/es/blog/nodejs-fetch-json-with-https.mdx create mode 100644 data/es/blog/prevent-layout-shift-while-show-hide-scrollbar-on-window.mdx create mode 100644 data/es/blog/render-blocking-css-and-chrome-performance-api.mdx create mode 100644 data/es/blog/set-up-path-aliases-in-nodejs.mdx create mode 100644 data/es/blog/shopify-migrate-files-to-new-store-with-file-api-and-nodejs.mdx create mode 100644 data/es/blog/shopify-section-rendering-apis-notes.mdx create mode 100644 data/es/blog/tricky-use-case-of-array-map-in-js.mdx create mode 100644 data/es/blog/use-https-in-local-development.mdx create mode 100644 data/es/blog/viec-tim-dev-nhu-the-nao-part-2.mdx create mode 100644 data/es/blog/viec-tim-dev-nhu-the-nao.mdx create mode 100644 data/es/projectsData.ts create mode 100644 data/es/snippets/SAMPLE.mdx create mode 100644 data/es/snippets/adding-or-substracting-days-in-liquid.mdx create mode 100644 data/es/snippets/casing-utils.mdx create mode 100644 data/es/snippets/color-validator.mdx create mode 100644 data/es/snippets/deep-remove-falsy-values-from-an-object.mdx create mode 100644 data/es/snippets/event-emitter.mdx create mode 100644 data/es/snippets/find-and-kill-process-on-given-port-in-macos.mdx create mode 100644 data/es/snippets/getting-spotify-token-to-display-nowplaying-tracks-on-your-site.mdx create mode 100644 data/es/snippets/gitignore-ignore-directory-keep-1-file.mdx create mode 100644 data/es/snippets/markdown-code-block-syntax.mdx create mode 100644 data/es/snippets/read-all-file-names-inside-a-folder.mdx create mode 100644 data/es/snippets/rename-a-case-sensitive-file-in-a-git-repo.mdx create mode 100644 data/es/snippets/use-async.mdx create mode 100644 data/es/snippets/use-local-storage-state.mdx create mode 100644 data/es/snippets/useful-array-utilities.mdx create mode 100644 data/es/snippets/verify-github-webhooks-requests.mdx create mode 100644 data/es/snippets/vnese-to-plain-english.mdx create mode 100644 hooks/useLocale.tsx create mode 100644 layouts/ResumeLayout.module.css create mode 100644 next-i18next.config.js create mode 100644 public/locales/ca/common.json create mode 100644 public/locales/en/common.json create mode 100644 public/locales/es/common.json diff --git a/.env.example b/.env.example index 8e7a5804..12dc11ed 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,11 @@ GISCUS_CATEGORY= GISCUS_CATEGORY_ID= UTTERANCES_REPO= DISQUS_SHORTNAME= +NEXT_PUBLIC_CREATE_DISCUS_ON_TWITTER=FALSE +NEXT_PUBLIC_CREATE_DISCUS_ON_GITHUB=FALSE +NEXT_PUBLIC_SHARE_ON_FACEBOOK=FALSE +NEXT_PUBLIC_SHARE_ON_TWITTER=TRUE + # Spotify (for fetching now playing track) SPOTIFY_CLIENT_ID= @@ -16,3 +21,8 @@ DATABASE_URL= # GitHub (for fetching repository's data in /projects page) GITHUB_API_TOKEN= + + +# Default locale (for i18n) +NEXT_PUBLIC_DEFAULT_LOCALE=en +NEXT_PUBLIC_DEFAULT_AUTHOR= diff --git a/components/BuiltWith.tsx b/components/BuiltWith.tsx index 2e556859..d65b0ffe 100644 --- a/components/BuiltWith.tsx +++ b/components/BuiltWith.tsx @@ -1,11 +1,14 @@ import { siteMetadata } from '~/data/siteMetadata' import { DevIcon } from './DevIcon' import { Link } from './Link' +import { useTranslation } from 'next-i18next' export function BuiltWith() { + const { t } = useTranslation('common') + return (
- Built with + {t('buildWith.built_with')}
@@ -28,7 +31,7 @@ export function BuiltWith() { href={siteMetadata.siteRepo} className="text-gray-500 underline underline-offset-4 dark:text-gray-400" > - View source + {t('buildWith.view_source')}
) diff --git a/components/DevIcon.tsx b/components/DevIcon.tsx index 87393697..448991ec 100644 --- a/components/DevIcon.tsx +++ b/components/DevIcon.tsx @@ -37,7 +37,7 @@ export let DevIconsMap = { export function DevIcon(props: { type: keyof typeof DevIconsMap; className?: string }) { let { type, className } = props let Icon = DevIconsMap[type] - if (!Icon) return
Missing icon
+ if (!Icon) return
Missing icon for {type}
// Add `type` here let defaultClass = 'h-16 w-16 lg:h-14 lg:w-14 xl:h-24 xl:w-24' return diff --git a/components/Footer.tsx b/components/Footer.tsx index 59fcdf49..59903474 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -1,7 +1,9 @@ -import { siteMetadata } from '~/data/siteMetadata' +import { useTranslation } from 'next-i18next' import { BuiltWith } from './BuiltWith' export function Footer() { + const { t } = useTranslation('common') + return (
@@ -9,7 +11,7 @@ export function Footer() {
{`Copyright © ${new Date().getFullYear()}`}
{` • `} - {siteMetadata.footerTitle} + {t('buildWith.copyright_author')}
diff --git a/components/Header.tsx b/components/Header.tsx index 1bc6aa85..947f8393 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,17 +1,20 @@ import clsx from 'clsx' -import { headerNavLinks } from 'data/headerNavLinks' import NextImage from 'next/image' import { useRouter } from 'next/router' import { AnalyticsLink } from './AnalyticsLink' import { Link } from './Link' import { ThemeSwitcher } from './ThemeSwitcher' +import { LanguageSwitcher } from './LanguageSwitcher' +import { useTranslation } from 'next-i18next' +import { headerNavLinks } from '~/data/headerNavLinks' export function Header({ onToggleNav }: { onToggleNav: () => void }) { + const { t } = useTranslation('common') // 'common' fa referència al fitxer common.json let router = useRouter() return ( -
+
-
+
@@ -25,29 +28,28 @@ export function Header({ onToggleNav }: { onToggleNav: () => void }) {
-
-
- {headerNavLinks.map((link) => { - return ( - - - {link.title} - - - ) - })} + {headerNavLinks.map((link) => ( + + + {t(link.titleKey)}{' '} + + + ))}
+
+
+
diff --git a/components/LanguageSwitcher.tsx b/components/LanguageSwitcher.tsx new file mode 100644 index 00000000..eeab3d23 --- /dev/null +++ b/components/LanguageSwitcher.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' + +const localeNames = { + en: 'English', + es: 'Español', + ca: 'Català', + // add other languages as needed +} + +function getLocaleName(localeCode) { + return localeNames[localeCode] || localeCode +} + +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1) +} +export function LanguageSwitcher() { + const router = useRouter() + const [value, setValue] = useState(router.locale) + const [dropdownOpen, setDropdownOpen] = useState(false) + + useEffect(() => { + setValue(router.locale) + }, [router.locale]) + + function translate_to(locale) { + router.push(router.pathname, router.asPath, { locale }) + setDropdownOpen(false) // Tancar el menú desplegable un cop l'usuari hagi seleccionat una opció + } + + return ( +
+ + + {dropdownOpen && ( + + )} +
+ ) +} diff --git a/components/MobileNav.tsx b/components/MobileNav.tsx index d535a67f..6de4549a 100644 --- a/components/MobileNav.tsx +++ b/components/MobileNav.tsx @@ -1,8 +1,10 @@ import { headerNavLinks } from '~/data/headerNavLinks' import { Link } from './Link' import clsx from 'clsx' +import { useTranslation } from 'next-i18next' export function MobileNav({ navShow, onToggleNav }) { + const { t } = useTranslation('common') let className = clsx( `sm:hidden fixed w-full h-screen inset-0 bg-gray-200 dark:bg-gray-800 opacity-95 z-50 transition-transform transform ease-in-out duration-300`, navShow ? 'translate-x-0' : 'translate-x-full' @@ -23,20 +25,30 @@ export function MobileNav({ navShow, onToggleNav }) { > + > + + -
+
+ +
+ {children} +
+
) diff --git a/libs/files.ts b/libs/files.ts index 0feba480..a1455842 100644 --- a/libs/files.ts +++ b/libs/files.ts @@ -29,10 +29,26 @@ export function formatSlug(slug: string) { return slug.replace(/\.(mdx|md)/, '') } +export function getCommon(locale) { + // Resol la ruta al fitxer .json basat en l'idioma + const filePath = path.join(process.cwd(), 'public', 'locales', locale, 'common.json') + + // Llegeix el fitxer .json + const rawData = fs.readFileSync(filePath, 'utf8') + const data = JSON.parse(rawData) + return data +} + export function getFiles(type: string): string[] { let root = process.cwd() let prefixPaths = path.join(root, 'data', type) - let files = getAllFilesRecursively(prefixPaths) + let files + try { + files = getAllFilesRecursively(prefixPaths) + } catch (error) { + // Si no es troben fitxers, retorna una llista buida + return [] + } // Only want to return blog/path and ignore root, replace is needed to work on Windows return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/')) } diff --git a/libs/generate-rss.ts b/libs/generate-rss.ts index b4f73410..10866726 100644 --- a/libs/generate-rss.ts +++ b/libs/generate-rss.ts @@ -1,34 +1,33 @@ import { escape } from '~/utils/html-escaper' -import { siteMetadata } from '~/data/siteMetadata' import type { BlogFrontMatter } from '~/types' -function generateRssItem(post: BlogFrontMatter) { +function generateRssItem(lang_siteMetadata, post: BlogFrontMatter) { return ` - ${siteMetadata.siteUrl}/blog/${post.slug} + ${lang_siteMetadata.siteUrl}/blog/${post.slug} ${escape(post.title)} - ${siteMetadata.siteUrl}/blog/${post.slug} + ${lang_siteMetadata.siteUrl}/blog/${post.slug} ${post.summary && `${escape(post.summary)}`} ${new Date(post.date).toUTCString()} - ${siteMetadata.email} (${siteMetadata.author}) + ${lang_siteMetadata.email} (${lang_siteMetadata.author}) ${post.tags && post.tags.map((t) => `${t}`).join('')} ` } -export function generateRss(posts: BlogFrontMatter[], page = 'feed.xml') { +export function generateRss(lang_siteMetadata, posts: BlogFrontMatter[], page = 'feed.xml') { return ` - ${escape(siteMetadata.title)} - ${siteMetadata.siteUrl}/blog - ${escape(siteMetadata.description)} - ${siteMetadata.language} - ${siteMetadata.email} (${siteMetadata.author}) - ${siteMetadata.email} (${siteMetadata.author}) + ${escape(lang_siteMetadata.title)} + ${lang_siteMetadata.siteUrl}/blog + ${escape(lang_siteMetadata.description)} + ${lang_siteMetadata.language} + ${lang_siteMetadata.email} (${lang_siteMetadata.author}) + ${lang_siteMetadata.email} (${lang_siteMetadata.author}) ${new Date(posts[0].date).toUTCString()} - - ${posts.map(generateRssItem).join('')} + + ${posts.map((post) => generateRssItem(lang_siteMetadata, post)).join('')} ` diff --git a/libs/mdx.ts b/libs/mdx.ts index 50960fe0..9687774c 100644 --- a/libs/mdx.ts +++ b/libs/mdx.ts @@ -17,15 +17,27 @@ import { remarkCodeBlockTitle } from './remark-code-block-title' import { remarkImgToJsx } from './remark-img-to-jsx' import { remarkTocHeading } from './remark-toc-heading' -export async function getFileBySlug(type: string, slug: string): Promise { +export async function getFileBySlug( + type: string, + slug: string, + locale: string = process.env.NEXT_PUBLIC_DEFAULT_LOCALE +): Promise { let root = process.cwd() - let mdxPath = path.join(root, 'data', type, `${slug}.mdx`) - let mdPath = path.join(root, 'data', type, `${slug}.md`) + let mdxPath = path.join(root, 'data', locale, type, `${slug}.mdx`) + let mdPath = path.join(root, 'data', locale, type, `${slug}.md`) + + console.log('mdxPath', mdxPath) + console.log('mdPath', mdPath) + + if (!fs.existsSync(mdxPath) && !fs.existsSync(mdPath)) { + // Si no es troba cap fitxer, retorna null + return null + } + let source = fs.existsSync(mdxPath) ? fs.readFileSync(mdxPath, 'utf8') : fs.readFileSync(mdPath, 'utf8') - - /** + /**F * Point esbuild directly at the correct executable for the current platform * Ref: https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent */ @@ -106,6 +118,7 @@ export async function getFileBySlug(type: string, slug: string): Promise 1 ? `${originalSlug}-${nodes[originalSlug]}` : originalSlug + + node.data = node.data || {} + node.data.hProperties = node.data.hProperties || {} + node.data.hProperties.id = id +} + export function remarkTocHeading(options: RemarkTocHeadingOptions) { - return (tree: UnistTreeType) => + return (tree: UnistTreeType) => { + const nodes = {} + const sluggerInstance = new Slugger() + + if (!options.cleaned) { + options.exportRef.length = 0 + options.cleaned = true + } + visit(tree, 'heading', (node: UnistNodeType) => { - let textContent = toString(node) - options.exportRef.push({ - value: textContent, - url: '#' + slug(textContent), - depth: node.depth, - }) + addID(node, nodes, sluggerInstance) + transformNode(node, options.exportRef, {}, sluggerInstance) }) + } } diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 00000000..31791836 --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,6 @@ +module.exports = { + i18n: { + defaultLocale: 'en', + locales: ['en', 'es', 'ca'], + }, +} diff --git a/next.config.js b/next.config.js index ab0f096d..5c20b30d 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,5 @@ +const path = require('path') + const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) @@ -12,7 +14,16 @@ module.exports = withBundleAnalyzer({ domains: ['i.scdn.co'], }, typescript: { tsconfigPath: './tsconfig.json' }, - webpack: (config, { dev, isServer }) => { + i18n: { + // These are all the locales you want to support in + // your application + locales: ['en', 'es', 'ca'], + // This is the default locale you want to be used when visiting + // a non-locale prefixed path e.g. `/hello` + defaultLocale: process.env.NEXT_PUBLIC_DEFAULT_LOCALE, + }, + trailingSlash: true, + webpack: (config, options) => { config.module.rules.push({ test: /\.(png|jpe?g|gif|mp4)$/i, use: [ @@ -31,6 +42,12 @@ module.exports = withBundleAnalyzer({ use: ['@svgr/webpack'], }) + config.module.rules.push({ + test: /\.json$/, + use: [options.defaultLoaders.babel], + include: path.resolve(__dirname, 'public/locales'), + }) + return config }, }) diff --git a/package-lock.json b/package-lock.json index f8abf569..069ff87b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,11 @@ { - "name": "leo-blog", + "name": "leohuynh.dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "hasInstallScript": true, "dependencies": { - "@esbuild/linux-x64": "*", "@octokit/graphql": "^7.0.0", "@prisma/client": "^4.16.2", "@tailwindcss/forms": "^0.5.3", @@ -14,12 +13,15 @@ "autoprefixer": "^10.4.14", "clsx": "^1.2.1", "esbuild": "^0.18.11", + "flowbite": "^1.8.1", + "flowbite-react": "^0.5.0", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "image-size": "^1.0.2", "isomorphic-unfetch": "^4.0.2", "mdx-bundler": "^9.2.1", "next": "^13.4.9", + "next-i18next": "^14.0.0", "next-themes": "^0.2.1", "postcss": "^8.4.25", "postcss-loader": "^7.3.3", @@ -1782,10 +1784,11 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "license": "MIT", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", + "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -2643,6 +2646,54 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/@floating-ui/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", + "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "dependencies": { + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", + "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "dependencies": { + "@floating-ui/core": "^1.4.1", + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.24.8.tgz", + "integrity": "sha512-AuYeDoaR8jtUlUXtZ1IJ/6jtBkGnSpJXbGNzokBL87VDJ8opMq1Bgrc0szhK482ReQY6KZsMoZCVSb4xwalkBA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.1", + "aria-hidden": "^1.2.3", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", + "integrity": "sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==", + "dependencies": { + "@floating-ui/dom": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", + "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "dev": true, @@ -3084,6 +3135,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@prisma/client": { "version": "4.16.2", "hasInstallScript": true, @@ -3469,6 +3529,15 @@ "@types/unist": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "license": "MIT" @@ -3524,12 +3593,10 @@ }, "node_modules/@types/prop-types": { "version": "15.7.5", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.2.14", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3543,7 +3610,6 @@ }, "node_modules/@types/scheduler": { "version": "0.16.3", - "dev": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -3945,6 +4011,17 @@ "version": "2.0.1", "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.1.3", "dev": true, @@ -4913,6 +4990,16 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.0.tgz", + "integrity": "sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.30.0", "dev": true, @@ -4963,8 +5050,9 @@ }, "node_modules/cross-env": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, @@ -5075,7 +5163,6 @@ }, "node_modules/csstype": { "version": "3.1.2", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -5098,6 +5185,11 @@ "node": ">= 12" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.4", "license": "MIT", @@ -5424,6 +5516,11 @@ "dev": true, "license": "MIT" }, + "node_modules/easy-bem": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/easy-bem/-/easy-bem-1.1.1.tgz", + "integrity": "sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==" + }, "node_modules/ee-first": { "version": "1.1.1", "dev": true, @@ -6879,6 +6976,32 @@ "dev": true, "license": "ISC" }, + "node_modules/flowbite": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-1.8.1.tgz", + "integrity": "sha512-lXTcO8a6dRTPFpINyOLcATCN/pK1Of/jY4PryklPllAiqH64tSDUsOdQpar3TO59ZXWwugm2e92oaqwH6X90Xg==", + "dependencies": { + "@popperjs/core": "^2.9.3", + "mini-svg-data-uri": "^1.4.3" + } + }, + "node_modules/flowbite-react": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/flowbite-react/-/flowbite-react-0.5.0.tgz", + "integrity": "sha512-OoJ0Dbv0ZuunTkPwa802wL3kalUWQ7YBh1t4It5uVfAYs5KUvOqhXIygdlVMawnmq3+xkMPbUZkWh1sDxVb3nw==", + "dependencies": { + "@floating-ui/react": "^0.24.3", + "flowbite": "^1.6.6", + "react-icons": "^4.10.1", + "react-indiana-drag-scroll": "^2.2.0", + "tailwind-merge": "^1.13.2" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18", + "tailwindcss": "^3" + } + }, "node_modules/for-each": { "version": "0.3.3", "dev": true, @@ -7629,6 +7752,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "dev": true, @@ -7664,6 +7795,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "peer": true, + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "2.0.0", "license": "MIT", @@ -7718,6 +7858,34 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next": { + "version": "23.4.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.4.2.tgz", + "integrity": "sha512-hkVPHKFLtn9iewdqHDiU+MGVIBk+bVFn5usw7CIeCn/SBcVKGTItGdjNPm2B8Lnz42CeHUlnSOTgsr5vbITjhA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "peer": true, + "dependencies": { + "@babel/runtime": "^7.22.5" + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.5.tgz", + "integrity": "sha512-7fgSH8nVhXSBYPHR/W3tEXXhcnwHwNiND4Dfx9knzPzdsWTUTL/TdDVV+DY0dL0asHKLbdoJaXS4LdVW6R8MVQ==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "dev": true, @@ -10381,6 +10549,41 @@ } } }, + "node_modules/next-i18next": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-14.0.0.tgz", + "integrity": "sha512-umv8hOZoSoAA+td3ErfemyO/5Ib2pnYCdQ8/Oy+fncS2skFIL3hHKRer3Oa3Nfm4Xbv5p6DHWzm3NhT1j4tWwg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://locize.com" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.13", + "@types/hoist-non-react-statics": "^3.3.1", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.1.5" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "i18next": "^23.0.1", + "next": ">= 12.0.0", + "react": ">= 17.0.2", + "react-i18next": "^13.0.0" + } + }, "node_modules/next-remote-watch": { "version": "2.0.0", "dev": true, @@ -11433,9 +11636,56 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.0.3.tgz", + "integrity": "sha512-/t4kt4Y2o+21hbvx+o9zpVnmoiud7KLDncyZFGN0U6TGAWYaXdTsp/ytAHFcKKSAODg4noIMaOO3X7bMgCqLHw==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.22.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-icons": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-indiana-drag-scroll": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-indiana-drag-scroll/-/react-indiana-drag-scroll-2.2.0.tgz", + "integrity": "sha512-+W/3B2OQV0FrbdnsoIo4dww/xpH0MUQJz6ziQb7H+oBko3OCbXuzDFYnho6v6yhGrYDNWYPuFUewb89IONEl/A==", + "dependencies": { + "classnames": "^2.2.6", + "debounce": "^1.2.0", + "easy-bem": "^1.1.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", - "dev": true, "license": "MIT" }, "node_modules/react-share": { @@ -11647,8 +11897,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "license": "MIT" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regenerator-transform": { "version": "0.15.1", @@ -13574,6 +13825,20 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, + "node_modules/tailwind-merge": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", + "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.3.2", "license": "MIT", @@ -14419,6 +14684,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "license": "MIT", diff --git a/package.json b/package.json index db0e1c0a..2fcc3108 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "private": true, "scripts": { - "start": "PORT=3434 next-remote-watch ./data", + "start": "cross-env NODE_ENV=development && PORT=3434 next-remote-watch ./data", + "extractStrings": "tsx ./scripts/extractStrings.ts", "dev": "PORT=3434 next dev", "postinstall": "prisma generate", "build": "next build && tsx ./scripts/generate-sitemap", @@ -20,12 +21,15 @@ "autoprefixer": "^10.4.14", "clsx": "^1.2.1", "esbuild": "^0.18.11", + "flowbite": "^1.8.1", + "flowbite-react": "^0.5.0", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "image-size": "^1.0.2", "isomorphic-unfetch": "^4.0.2", "mdx-bundler": "^9.2.1", "next": "^13.4.9", + "next-i18next": "^14.0.0", "next-themes": "^0.2.1", "postcss": "^8.4.25", "postcss-loader": "^7.3.3", diff --git a/pages/404.tsx b/pages/404.tsx index fdd16315..73f334d3 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -1,8 +1,21 @@ import Image from 'next/image' import { Link } from '~/components/Link' import { Twemoji } from '~/components/Twemoji' +import { useTranslation } from 'next-i18next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' + +export async function getStaticProps({ locale }) { + // Afegeix la traducció + return { + props: { + ...(await serverSideTranslations(locale, ['common'])), // Aquí estem assumint que el nom del teu fitxer de traducció és 'common' + }, + } +} export default function FourZeroFour() { + const { t } = useTranslation('common') + return (
@@ -11,14 +24,12 @@ export default function FourZeroFour() {

- Hmm.. it seems that you're lost -

-

- But don't worry, you can find plenty of other things on my homepage. + {t('lostMessage')}

+

{t('returnMessage')}

diff --git a/pages/_app.tsx b/pages/_app.tsx index 6140c27e..1d809d0e 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,3 +1,5 @@ +import { appWithTranslation } from 'next-i18next' + import 'css/tailwind.css' import 'css/twemoji.css' @@ -6,7 +8,7 @@ import Head from 'next/head' import { Analytics } from '~/components/analytics' import { LayoutWrapper } from '~/components/LayoutWrapper' -export default function App({ Component, pageProps }) { +function App({ Component, pageProps }) { return ( // @ts-ignore @@ -20,3 +22,5 @@ export default function App({ Component, pageProps }) { ) } + +export default appWithTranslation(App) diff --git a/pages/_document.tsx b/pages/_document.tsx index daa05d10..b8ac7cfb 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -2,7 +2,7 @@ import Document, { Html, Head, Main, NextScript } from 'next/document' class MyDocument extends Document { render() { return ( - + diff --git a/pages/about.tsx b/pages/about.tsx index a7027fa3..5b2ae071 100644 --- a/pages/about.tsx +++ b/pages/about.tsx @@ -1,10 +1,13 @@ import { MDXLayoutRenderer } from '~/components/MDXComponents' import { getFileBySlug } from '~/libs/mdx' import type { MdxFileData } from '~/types' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -export async function getStaticProps() { - let authorData = await getFileBySlug('authors', 'default') - return { props: { authorData } } +export async function getStaticProps({ locale }: { params: { slug: string[] }; locale: string }) { + let authorData = await getFileBySlug('authors', 'default', locale) + return { + props: { authorData, ...(await serverSideTranslations(locale, ['common'])) }, + } } export default function About({ authorData }: { authorData: MdxFileData }) { diff --git a/pages/blog.tsx b/pages/blog.tsx index 48e72224..d1697329 100644 --- a/pages/blog.tsx +++ b/pages/blog.tsx @@ -1,33 +1,42 @@ import { PageSeo } from '~/components/SEO' import { POSTS_PER_PAGE } from '~/constant' -import { siteMetadata } from '~/data/siteMetadata' import { ListLayout } from '~/layouts/ListLayout' import { getAllFilesFrontMatter } from '~/libs/mdx' import type { BlogListProps } from '~/types' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' +import { useTranslation } from 'next-i18next' + +export async function getStaticProps({ locale }) { + let posts = getAllFilesFrontMatter(`${locale}/blog`) -export function getStaticProps() { - let posts = getAllFilesFrontMatter('blog') let initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE) let pagination = { currentPage: 1, totalPages: Math.ceil(posts.length / POSTS_PER_PAGE), } - - return { props: { posts, initialDisplayPosts, pagination } } + return { + props: { + posts, + initialDisplayPosts, + pagination, + ...(await serverSideTranslations(locale, ['common'])), + }, + } } export default function Blog({ posts, initialDisplayPosts, pagination }: BlogListProps) { + const { t } = useTranslation('common') // utilitza 'common' si els teus strings estan a common.ts o canvia-ho pel nom adequat return ( <> ) diff --git a/pages/blog/[...slug].tsx b/pages/blog/[...slug].tsx index 7d8ba608..84e7e175 100644 --- a/pages/blog/[...slug].tsx +++ b/pages/blog/[...slug].tsx @@ -3,53 +3,85 @@ import { MDXLayoutRenderer } from '~/components/MDXComponents' import { PageTitle } from '~/components/PageTitle' import { POSTS_PER_PAGE } from '~/constant' import { getCommentConfigs } from '~/libs/comment' -import { formatSlug, getFiles } from '~/libs/files' +import { formatSlug, getCommon, getFiles } from '~/libs/files' import { generateRss } from '~/libs/generate-rss' import { getAllFilesFrontMatter, getFileBySlug } from '~/libs/mdx' import type { AuthorFrontMatter, BlogProps, MdxPageLayout } from '~/types' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' +import { useTranslation } from 'next-i18next' let DEFAULT_LAYOUT: MdxPageLayout = 'PostSimple' -export async function getStaticPaths() { - let posts = getFiles('blog') +export async function getStaticPaths({ locales }: { locales: string[] }) { + let paths = [] + for (let locale of locales) { + let posts = getFiles(`${locale}/blog`) + for (let post of posts) { + paths.push({ + params: { + slug: formatSlug(post).split('/'), + }, + locale: locale, + }) + } + } return { - paths: posts.map((p: string) => ({ - params: { - slug: formatSlug(p).split('/'), - }, - })), + paths, fallback: false, } } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - let allPosts = getAllFilesFrontMatter('blog') +/// getStaticProps +export async function getStaticProps({ + params, + locale, +}: { + params: { slug: string[] } + locale: string +}) { + let allPosts = getAllFilesFrontMatter(`${locale}/blog`) + let postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === params.slug.join('/')) let prev = allPosts[postIndex + 1] || null let next = allPosts[postIndex - 1] || null let page = Math.ceil((postIndex + 1) / POSTS_PER_PAGE) - let post = await getFileBySlug('blog', params.slug.join('/')) + // obté l'entrada de blog individual per a l'idioma correcte + let post = await getFileBySlug('blog', params.slug.join('/'), locale) let authors = post.frontMatter.authors || ['default'] let authorDetails = await Promise.all( authors.map(async (author) => { - let authorData = await getFileBySlug('authors', author) + let authorData = await getFileBySlug('authors', author, locale) // eslint-disable-next-line return authorData.frontMatter as unknown as AuthorFrontMatter }) ) + // Resol la ruta al fitxer .json basat en l'idioma + const lang_siteMetadata = getCommon(locale).siteMetadata + // rss - let rss = generateRss(allPosts) + let rss = generateRss(lang_siteMetadata, allPosts) fs.writeFileSync('./public/feed.xml', rss) let commentConfig = getCommentConfigs() - return { props: { post, authorDetails, prev, next, page, commentConfig } } + return { + props: { + post, + authorDetails, + prev, + next, + page, + commentConfig, + ...(await serverSideTranslations(locale, ['common'])), + }, + } } export default function Blog(props: BlogProps) { let { post, ...rest } = props let { mdxSource, frontMatter } = post + const { t } = useTranslation('common') return ( <> @@ -64,7 +96,7 @@ export default function Blog(props: BlogProps) { ) : (
- Under letruction{' '} + {t('blog.under_construction')}{' '} 🚧 diff --git a/pages/blog/page/[page].tsx b/pages/blog/page/[page].tsx index e5556412..29e4fb2a 100644 --- a/pages/blog/page/[page].tsx +++ b/pages/blog/page/[page].tsx @@ -1,16 +1,27 @@ import { PageSeo } from 'components/SEO' import ListLayout from 'layouts/ListLayout' import { POSTS_PER_PAGE } from '~/constant' -import { siteMetadata } from '~/data/siteMetadata' + import { getAllFilesFrontMatter } from '~/libs/mdx' import type { BlogListProps } from '~/types' +import type { GetStaticPathsContext } from 'next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' +import { useTranslation } from 'next-i18next' + +export async function getStaticPaths(context: GetStaticPathsContext) { + const locales = context.locales || [] + const paths = [] -export async function getStaticPaths() { - let totalPosts = getAllFilesFrontMatter('blog') - let totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE) - let paths = Array.from({ length: totalPages }, (_, i) => ({ - params: { page: (i + 1).toString() }, - })) + for (const locale of locales) { + let totalPosts = getAllFilesFrontMatter(`${locale}/blog`) // canviat `${locale}/blog` a `blog/${locale}` + let totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE) + let localePaths = Array.from({ length: totalPages }, (_, i) => ({ + params: { page: (i + 1).toString() }, + locale, + })) + + paths.push(...localePaths) + } return { paths, @@ -18,9 +29,15 @@ export async function getStaticPaths() { } } -export async function getStaticProps({ params }: { params: { page: string } }) { +export async function getStaticProps({ + params, + locale, +}: { + params: { page: string } + locale: string +}) { let { page } = params - let posts = getAllFilesFrontMatter('blog') + let posts = getAllFilesFrontMatter(`${locale}/blog`) let pageNumber = parseInt(page) let initialDisplayPosts = posts.slice( POSTS_PER_PAGE * (pageNumber - 1), @@ -36,20 +53,22 @@ export async function getStaticProps({ params }: { params: { page: string } }) { posts, initialDisplayPosts, pagination, + ...(await serverSideTranslations(locale, ['common'])), }, } } export default function PostPage(props: BlogListProps) { let { posts, initialDisplayPosts, pagination } = props + const { t } = useTranslation('common') return ( <> - + ) diff --git a/pages/index.tsx b/pages/index.tsx index 5b6034a3..3869b276 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react' import { PageSeo } from 'components/SEO' import { BlogLinks } from '~/components/homepage/BlogLinks' import { FeaturedPosts } from '~/components/homepage/FeaturedPosts' @@ -7,19 +8,28 @@ import { ShortDescription } from '~/components/homepage/ShortDescription' import { TypedBios } from '~/components/homepage/TypedBios' import { ProfileCard } from '~/components/ProfileCard' import { Twemoji } from '~/components/Twemoji' -import { siteMetadata } from '~/data/siteMetadata' import { getAllFilesFrontMatter } from '~/libs/mdx' -import type { BlogFrontMatter } from '~/types' +import { useTranslation } from 'next-i18next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -export function getStaticProps() { - let posts = getAllFilesFrontMatter('blog') - return { props: { posts } } +export async function getStaticProps({ locale }) { + let posts = getAllFilesFrontMatter(`${locale}/blog`) + + // Afegeix la traducció + return { + props: { + posts, + ...(await serverSideTranslations(locale, ['common'])), // Aquí estem assumint que el nom del teu fitxer de traducció és 'common' + }, + } } -export default function Home({ posts }: { posts: BlogFrontMatter[] }) { +export default function Home({ posts }) { + const { t } = useTranslation('common') // utilitza 'common' si els teus strings estan a common.ts o canvia-ho pel nom adequat + return ( <> - +
@@ -30,7 +40,7 @@ export default function Home({ posts }: { posts: BlogFrontMatter[] }) {

- Happy reading + {t('happyReading')}

diff --git a/pages/projects.tsx b/pages/projects.tsx index 0f78ac5f..10f05df0 100644 --- a/pages/projects.tsx +++ b/pages/projects.tsx @@ -1,26 +1,44 @@ import { PageSeo } from 'components/SEO' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { ProjectCard } from '~/components/ProjectCard' -import { projectsData } from '~/data/projectsData' -import { siteMetadata } from '~/data/siteMetadata' +import { useTranslation } from 'next-i18next' -export default function Projects() { +export async function getStaticProps({ locale }) { + // Importa dinàmicament les dades basades en l'idioma actual + const projectsDataModule = await import(`~/data/${locale}/projectsData.ts`) + const projectsData = projectsDataModule.projectsData + + return { + props: { + projectsData, + ...(await serverSideTranslations(locale, ['common'])), + }, + } +} + +export default function Projects({ projectsData }) { let workProjects = projectsData.filter(({ type }) => type === 'work') let sideProjects = projectsData.filter(({ type }) => type === 'self') - let description = 'My open-source side projects and stuff that I built with my colleagues at work' + const { t } = useTranslation('common') + + let description = t('projects.projects_description') return ( <> - +

- Projects + {t('projects.projects_title')}

{description}

- Work + {t('projects.work_title')}

{workProjects.map((project) => ( @@ -30,7 +48,7 @@ export default function Projects() {

- Side projects + {t('projects.side_title')}

{sideProjects.map((project) => ( diff --git a/pages/resume.tsx b/pages/resume.tsx index cbb89425..cf81d08a 100644 --- a/pages/resume.tsx +++ b/pages/resume.tsx @@ -1,20 +1,24 @@ import { MDXLayoutRenderer } from '~/components/MDXComponents' import { getFileBySlug } from '~/libs/mdx' import type { MdxFileData } from '~/types' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -export async function getStaticProps() { - let resumeData = await getFileBySlug('authors', 'resume') - return { props: { resumeData } } +export async function getStaticProps({ locale }) { + let resumeData = await getFileBySlug('authors', 'resume', locale) + return { + props: { resumeData, ...(await serverSideTranslations(locale, ['common'])) }, + } } export default function About({ resumeData }: { resumeData: MdxFileData }) { - let { mdxSource, frontMatter } = resumeData + let { mdxSource, frontMatter, toc } = resumeData return ( ) } diff --git a/pages/snippets/[...slug].tsx b/pages/snippets/[...slug].tsx index 5b4e7eba..130f1ae4 100644 --- a/pages/snippets/[...slug].tsx +++ b/pages/snippets/[...slug].tsx @@ -1,4 +1,5 @@ import { MDXLayoutRenderer } from 'components/MDXComponents' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { PageTitle } from '~/components/PageTitle' import { getCommentConfigs } from '~/libs/comment' import { formatSlug, getFiles } from '~/libs/files' @@ -7,25 +8,39 @@ import type { MdxPageLayout, SnippetProps } from '~/types' let DEFAULT_LAYOUT: MdxPageLayout = 'PostSimple' -export async function getStaticPaths() { - let snippets = getFiles('snippets') +export async function getStaticPaths({ locales }: { locales: string[] }) { + let paths = [] + for (let locale of locales) { + let snippets = getFiles(`${locale}/snippets`) + for (let snippet of snippets) { + paths.push({ + params: { + slug: formatSlug(snippet).split('/'), + }, + locale: locale, + }) + } + } + return { - paths: snippets.map((p: string) => ({ - params: { - slug: formatSlug(p).split('/'), - }, - })), + paths, fallback: false, } } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - let snippet = await getFileBySlug('snippets', params.slug.join('/')) +export async function getStaticProps({ params, locale }) { + console.log('params slug', params.slug.join('/')) + let snippet = await getFileBySlug('snippets', params.slug.join('/'), locale) + let commentConfig = getCommentConfigs() - return { props: { snippet, commentConfig } } + return { + props: { snippet, commentConfig, ...(await serverSideTranslations(locale, ['common'])) }, + } } export default function Snippet({ snippet, commentConfig }: SnippetProps) { + console.log('snippet', snippet) + let { mdxSource, frontMatter } = snippet return ( diff --git a/pages/snippets/index.tsx b/pages/snippets/index.tsx index 93156906..697edc81 100644 --- a/pages/snippets/index.tsx +++ b/pages/snippets/index.tsx @@ -1,20 +1,24 @@ import { PageSeo } from 'components/SEO' -import { siteMetadata } from '~/data/siteMetadata' import { SnippetLayout } from '~/layouts/SnippetLayout' import { getAllFilesFrontMatter } from '~/libs/mdx' import type { SnippetFrontMatter } from '~/types' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' +import { useTranslation } from 'next-i18next' -export async function getStaticProps() { - let snippets = getAllFilesFrontMatter('snippets') - return { props: { snippets } } +export async function getStaticProps({ locale }: { locale: string }) { + let snippets = getAllFilesFrontMatter(`${locale}/snippets`) + return { + props: { snippets, ...(await serverSideTranslations(locale, ['common'])) }, + } } export default function Snippet({ snippets }: { snippets: SnippetFrontMatter[] }) { - let description = 'Reuseable code snippets collected by me' + const { t } = useTranslation('common') + let description = t('menu_receptes_2') return ( <> diff --git a/pages/tags.tsx b/pages/tags.tsx index e785c985..77b893d2 100644 --- a/pages/tags.tsx +++ b/pages/tags.tsx @@ -1,22 +1,25 @@ import { Link } from '~/components/Link' import { PageSeo } from '~/components/SEO' import { Tag } from '~/components/Tag' -import { siteMetadata } from '~/data/siteMetadata' import { getAllTags } from '~/libs/tags' import type { TagsCount } from '~/types' import { kebabCase } from '~/utils/kebab-case' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' +import { useTranslation } from 'next-i18next' -export function getStaticProps() { +export async function getStaticProps({ locale }) { let tags = getAllTags('blog') - return { props: { tags } } + return { + props: { tags, ...(await serverSideTranslations(locale, ['common'])) }, + } } export default function Tags({ tags }: { tags: TagsCount }) { let sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a]) - + const { t } = useTranslation('common') return ( <> - +

@@ -24,7 +27,7 @@ export default function Tags({ tags }: { tags: TagsCount }) {

- {Object.keys(tags).length === 0 && 'No tags found.'} + {Object.keys(tags).length === 0 && t('tag.noTagsFound')} {sortedTags.map((tag) => { return (
diff --git a/pages/tags/[tag].tsx b/pages/tags/[tag].tsx index e1f497b2..f610f13d 100644 --- a/pages/tags/[tag].tsx +++ b/pages/tags/[tag].tsx @@ -1,17 +1,19 @@ import { PageSeo } from 'components/SEO' import fs from 'fs' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import path from 'path' -import { siteMetadata } from '~/data/siteMetadata' import { ListLayout } from '~/layouts/ListLayout' import { generateRss } from '~/libs/generate-rss' import { getAllFilesFrontMatter } from '~/libs/mdx' import { getAllTags } from '~/libs/tags' import type { BlogFrontMatter } from '~/types' import { kebabCase } from '~/utils/kebab-case' +import { useTranslation } from 'next-i18next' +import { getCommon } from '~/libs/files' -export function getStaticPaths() { - let tags = getAllTags('blog') - +export function getStaticPaths({ locale }) { + let tags = getAllTags(`${locale}/blog`) + console.log(tags) return { paths: Object.keys(tags).map((tag) => ({ params: { @@ -22,30 +24,52 @@ export function getStaticPaths() { } } -export async function getStaticProps({ params }: { params: { tag: string } }) { - let allPosts = getAllFilesFrontMatter('blog') +export async function getStaticProps({ + params, + locale, +}: { + params: { tag: string } + locale: string +}) { + let allPosts = getAllFilesFrontMatter(`${locale}/blog`) let filteredPosts = allPosts.filter( (post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag) ) // rss let root = process.cwd() - let rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`) + const lang_siteMetadata = getCommon(locale).siteMetadata + + console.log('lang_siteMetadata', lang_siteMetadata) + let rss = generateRss(lang_siteMetadata, filteredPosts, `tags/${params.tag}/feed.xml`) let rssPath = path.join(root, 'public', 'tags', params.tag) fs.mkdirSync(rssPath, { recursive: true }) fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss) - return { props: { posts: filteredPosts, tag: params.tag } } + return { + props: { + posts: filteredPosts, + tag: params.tag, + ...(await serverSideTranslations(locale, ['common'])), + }, + } } export default function Tag({ posts, tag }: { posts: BlogFrontMatter[]; tag: string }) { + const { t } = useTranslation('common') // Mueve esto al principio de tu componente + // Capitalize first letter and convert space to dash + if (!tag) { + // gestionar l'error com consideris més adequat + return
${t('tag.noTagsFound')}
+ } + let title = tag[0] + tag.split(' ').join('-').slice(1) return ( <> diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json new file mode 100644 index 00000000..21510d74 --- /dev/null +++ b/public/locales/ca/common.json @@ -0,0 +1,106 @@ +{ + "siteMetadata": { + "title": "El blog del Leo - El viatge de programació del Leo", + "author": "Leo Huynh", + "fullName": "Tuan Anh Huynh", + "headerTitle": "El blog del Leo - El viatge de programació del Leo", + "footerTitle": "El blog del Leo - El viatge de programació del Leo", + "description": "El viatge de programació del Leo - històries de treball i de vida a través del teclat d'un Enginyer de Software amb ment oberta", + "language": "ca" + }, + + "happyReading": "Bona lectura", + "menu_blog": "Bloc", + "menu_receptes": "Fragments", + "menu_projectes": "Projectes", + "menu_sobremi": "Sobre mi", + "menu_curriculum": "Currículum", + "greetingMessage": "Hola, company!", + + "introduction": "Sóc", + "name": "Tuan Anh Huynh", + "description": "un Enginyer de Software dedicat a", + "location": "Ha Noi, VN", + + "bio1": "Sóc conegut com a Leo a la feina.", + "bio2": "Sóc un aprenent, constructor i buscador de llibertat.", + "bio3": "Visc a Ha Noi, Viet Nam.", + "bio4": "Vaig néixer a la bonica plana de Moc Chau.", + "bio5": "El meu primer llenguatge de programació va ser Pascal.", + "bio6": "M'encanta el desenvolupament web.", + "bio7": "Estic centrant-me en construir software d'eCommerce.", + "bio8": "Treballo principalment amb tecnologies JS/TS.", + "bio9": "Sóc el marit de Tu Le.", + "bio10": "Sóc una persona de gossos.", + "bio11": "Sóc un home esportiu. M'encanta", + "bio12": "M'encanta mirar futbol.", + "bio13": "M'encanta tocar el teclat i la guitarra.", + "bio14": "M'encanta la música rock.", + "bio15": "M'encanta jugar a escacs.", + "bio16": "M'encanta jugar a videojocs, PES és el meu preferit.", + + "bio_startCoding": "Vaig començar a aprendre a programar el 2016 i des d'aleshores m'he enganxat.", + "bio_firstJob": "Vaig aconseguir el meu primer treball com a mentor de programació en Python el 2017.", + "bio_passion": "Tinc una passió per JS/TS, desenvolupament web i comerç electrònic.", + "bio_blogPurpose": "Vaig començar aquest blog per documentar i compartir el meu coneixement i experiència.", + + "menu_blog_2": "Els meus escrits", + "menu_receptes_2": "La meva col·lecció de snippets", + "menu_projectes_2": "Què he construït?", + "menu_sobremi_2": "Més sobre mi", + "menu_curriculum_2": "La meva carrera", + "analytics": "Tràfic i segmentació d'aquest lloc", + + "avatarDescription": "avatar", + + "name_position": "Tuan Anh (Leo) Huynh", + "description_position": "Aprenent | Constructor", + + "lostMessage": "Hmm.. sembla que t'has perdut", + "returnMessage": "Però no et preocupis, pots trobar moltes altres coses a la meva pàgina d'inici.", + "backButton": "Tornar a la pàgina d'inici", + + "blog": { + "allPostsTitle": "Tots els Posts", + "intro": "Escric principalment sobre desenvolupament web, tecnologia i alguna vegada sobre la meva vida personal. Utilitza la cerca a continuació per filtrar per títol.", + "noPosts": "No s'han trobat entrades.", + "searchPosts": "Cerca publicacions", + "published_on": "Publicat el", + "under_construction": "En construcció", + "readingTime": "Temps de lectura: {{time}}", + "mins": "mins", + "views": "vistes" + }, + + "about_description": "Més sobre mi i aquest bloc", + "resume_description": "La meva carrera professional, experiència i habilitats.", + + "year": "any", + "month": "mes", + "day": "dia", + + "buildWith": { + "built_with": "Fet amb", + "view_source": "Veure el codi font", + "copyri,ght_author": "El bloc de Leo: el viatge de codificació de Leo" + }, + + "projects": { + "projects_description": "Els meus projectes de codi obert i altres coses que he construït amb els meus companys de feina", + "projects_title": "Projectes", + "work_title": "treball", + "side_title": "Projectes paral·lels", + "learn_more": "Apendre més", + "built_with": "Fet amb" + }, + + "pagination": { + "previous": "Anterior", + "next": "Següent", + "of": "de" + }, + + "tag": { + "noTagsFound": "No s'han trobat l'etiqueta" + } +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 00000000..2835eba1 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,106 @@ +{ + "siteMetadata": { + "title": "Leo's blog - Leo's coding journey", + "author": "Leo Huynh", + "fullName": "Tuan Anh Huynh", + "headerTitle": "Leo's blog - Leo's coding journey", + "footerTitle": "Leo's blog - Leo's coding journey", + "description": "Leo's coding journey - work and life stories through the keyboard of an open-minded Software Engineer", + "language": "en" + }, + + "happyReading": "Happy reading", + "menu_blog": "Blog", + "menu_receptes": "Snippets", + "menu_projectes": "Projects", + "menu_sobremi": "About", + "menu_curriculum": "Resume", + "greetingMessage": "Howdy, fellow!", + + "introduction": "I'm", + "name": "Tuan Anh Huynh", + "description": "a dedicated Software Engineer in", + "location": "Ha Noi, VN", + + "bio1": "I'm aliased as Leo at work.", + "bio2": "I'm a learner, builder, and freedom seeker.", + "bio3": "I live in Ha Noi, Viet Nam.", + "bio4": "I was born in the beautiful Moc Chau plateau.", + "bio5": "My first programming language I learned was Pascal.", + "bio6": "I love web development.", + "bio7": "I'm focusing on building eCommerce software.", + "bio8": "I work mostly with JS/TS technologies.", + "bio9": "I'm Tu Le's husband.", + "bio10": "I'm a dog-person.", + "bio11": "I'm a sport-guy. I love", + "bio12": "I love watching football.", + "bio13": "I love playing keyboard & guitar.", + "bio14": "I love rock music.", + "bio15": "I love playing chess.", + "bio16": "I love playing video games, PES is my favorite one.", + + "bio_startCoding": "I started learning to code in 2016 and have been hooked ever since.", + "bio_firstJob": "I landed my first job as a Python coding mentor in 2017.", + "bio_passion": "I have a passion for JS/TS, web dev, and eCommerce.", + "bio_blogPurpose": "I started this blog to document and share my knowledge & experience.", + + "menu_blog_2": "My writings", + "menu_receptes_2": "My snippets collection", + "menu_projectes_2": "What have I built?", + "menu_sobremi_2": "More about me", + "menu_curriculum_2": "My career", + "analytics": "Traffic & engagement of this site", + + "avatarDescription": "avatar", + + "name_position": "Tuan Anh (Leo) Huynh", + "description_position": "Learner | Builder", + + "lostMessage": "Hmm.. it seems that you're lost", + "returnMessage": "But don't worry, you can find plenty of other things on my homepage.", + "backButton": "Back to homepage", + + "blog": { + "allPostsTitle": "All Posts", + "intro": "I write mostly about web development, tech related, and sometime about my personal life. Use the search below to filter by title.", + "noPosts": "No posts found.", + "searchPosts": "Search posts", + "published_on": "Published on", + "under_construction": "Under construction", + "readingTime": "Reading time: {{time}}", + "mins": "mins", + "views": "views" + }, + + "about_description": "More about me and this blog", + "resume_description": "My professional career, experience, and skills.", + + "year": "year", + "month": "month", + "day": "day", + + "buildWith": { + "built_with": "Build with", + "view_source": "View source", + "copyright_author": "Leo's blog - Leo's coding journey" + }, + + "projects": { + "projects_description": "My open-source side projects and stuff that I built with my colleagues at work", + "projects_title": "Projects", + "work_title": "Work", + "side_title": "Side projects", + "learn_more": "Learn more", + "built_with": "Built with" + }, + + "pagination": { + "previous": "Previous", + "next": "Next", + "of": "of" + }, + + "tag": { + "noTagsFound": "No tags found." + } +} diff --git a/public/locales/es/common.json b/public/locales/es/common.json new file mode 100644 index 00000000..c8366f45 --- /dev/null +++ b/public/locales/es/common.json @@ -0,0 +1,106 @@ +{ + "siteMetadata": { + "title": "El blog de Leo - La aventura de programación de Leo", + "author": "Leo Huynh", + "fullName": "Tuan Anh Huynh", + "headerTitle": "El blog de Leo - La aventura de programación de Leo", + "footerTitle": "El blog de Leo - La aventura de programación de Leo", + "description": "La aventura de programación de Leo - historias de trabajo y vida a través del teclado de un Ingeniero de Software de mente abierta", + "language": "es" + }, + + "happyReading": "Feliz lectura", + "menu_blog": "Blog", + "menu_receptes": "Fragmentos", + "menu_projectes": "Proyectos", + "menu_sobremi": "Sobre mi", + "menu_curriculum": "Currículum", + "greetingMessage": "Hola, amigo!", + + "introduction": "Soy", + "name": "Tuan Anh Huynh", + "description": "un Ingeniero de Software dedicado en", + "location": "Ha Noi, VN", + + "bio1": "Me conocen como Leo en el trabajo.", + "bio2": "Soy un aprendiz, constructor y buscador de libertad.", + "bio3": "Vivo en Ha Noi, Viet Nam.", + "bio4": "Nací en la hermosa meseta de Moc Chau.", + "bio5": "El primer lenguaje de programación que aprendí fue Pascal.", + "bio6": "Me encanta el desarrollo web.", + "bio7": "Me estoy enfocando en construir software de comercio electrónico.", + "bio8": "Trabajo principalmente con tecnologías JS/TS.", + "bio9": "Soy el esposo de Tu Le.", + "bio10": "Soy una persona de perros.", + "bio11": "Soy un hombre deportivo. Me encanta", + "bio12": "Me encanta ver fútbol.", + "bio13": "Me encanta tocar el teclado y la guitarra.", + "bio14": "Me encanta la música rock.", + "bio15": "Me encanta jugar al ajedrez.", + "bio16": "Me encanta jugar videojuegos, PES es mi favorito.", + + "bio_startCoding": "Empecé a aprender a programar en 2016 y desde entonces me enganché.", + "bio_firstJob": "Conseguí mi primer trabajo como mentor de programación en Python en 2017.", + "bio_passion": "Tengo una pasión por JS/TS, desarrollo web y comercio electrónico.", + "bio_blogPurpose": "Empecé este blog para documentar y compartir mi conocimiento y experiencia.", + + "menu_blog_2": "Mis escritos", + "menu_receptes_2": "Mi colección de snippets", + "menu_projectes_2": "¿Qué he construido?", + "menu_sobremi_2": "Más sobre mí", + "menu_curriculum_2": "Mi carrera", + "analytics": "Tráfico y segmentación de este sitio", + + "avatarDescription": "avatar", + + "name_position": "Tuan Anh (Leo) Huynh", + "description_position": "Aprendiz | Constructor", + + "lostMessage": "Hmm.. parece que te has perdido", + "returnMessage": "Pero no te preocupes, puedes encontrar muchas otras cosas en mi página de inicio.", + "backButton": "Volver a la página de inicio", + + "blog": { + "allPostsTitle": "Todos los Posts", + "intro": "Escribo principalmente sobre desarrollo web, tecnología y algunas veces sobre mi vida personal. Usa la búsqueda a continuación para filtrar por título.", + "noPosts": "No se encontraron entradas.", + "searchPosts": "Buscar publicaciones", + "published_on": "Publicado el", + "under_construction": "En construcción", + "readingTime": "Tiempo de lectura: {{time}}", + "mins": "mins", + "views": "vistos" + }, + + "about_description": "Más sobre mi y este blog", + "resume_description": "Mi carrera profesional, experiencia y habilidades.", + + "year": "año", + "month": "mes", + "day": "día", + + "buildWith": { + "built_with": "Hecho con", + "view_source": "Ver código fuente", + "copyright_author": "El blog de Leo: el viaje de codificación de Leo" + }, + + "projects": { + "projects_description": "Mis proyectos de código abierto que he construido con mis compañeros de trabajo.", + "projects_title": "Proyectos", + "work_title": "Trabajo", + "side_title": "Proyectos paralelos", + "learn_more": "Aprende más", + "built_with": "Hecho con" + }, + + "pagination": { + "previous": "Anterior", + "next": "Siguiente", + "of": "de" + }, + + "tag": { + "noTagsFound": "No se han encontrado etiquetas." + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 29f68ee7..a89ee368 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,12 +4,16 @@ const colors = require('tailwindcss/colors') module.exports = { content: [ + './node_modules/flowbite-react/**/*.js', './(components|constant|layouts|pages)/**/*.(ts|tsx)', './data/(blog|snippets|authors)/*.mdx', ], darkMode: 'class', theme: { extend: { + gridTemplateColumns: { + main: '15em 1fr', + }, keyframes: { wiggle: { '0%': { transform: 'rotate(0deg)' }, @@ -84,8 +88,25 @@ module.exports = { 'zoom-out': 'zoom-out', }, typography: (theme) => ({ + extend: { + fontSize: { + xs: '.75rem', // 12px + sm: '.875rem', // 14px + base: '1rem', // 16px + lg: '1.125rem', // 18px + xl: '1.25rem', // 20px + '2xl': '1.5rem', // 24px + '3xl': '1.875rem', // 30px + '4xl': '2.25rem', // 36px + '5xl': '3rem', // 48px + '6xl': '4rem', // 64px + }, + }, DEFAULT: { css: { + html: { + 'scroll-behavior': 'smooth', + }, color: theme('colors.gray.700'), a: { color: theme('colors.primary.500'), @@ -214,5 +235,9 @@ module.exports = { variants: { typography: ['dark'], }, - plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')], + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + require('flowbite/plugin'), + ], } diff --git a/types/layout.ts b/types/layout.ts index 274572d0..a16ee235 100644 --- a/types/layout.ts +++ b/types/layout.ts @@ -1,7 +1,7 @@ import type React from 'react' import type { CommentConfigType } from './components' +import type { PaginationType, TOC } from './server' import type { AuthorFrontMatter, BlogFrontMatter, MdxFrontMatter, SnippetFrontMatter } from './mdx' -import type { PaginationType } from './server' export interface AuthorLayoutProps { children: React.ReactNode @@ -34,4 +34,5 @@ export interface SnippetLayoutProps { export interface ResumeLayoutProps { children: React.ReactNode frontMatter: MdxFrontMatter + toc: TOC } diff --git a/types/mdx.ts b/types/mdx.ts index 5e647754..b6047289 100644 --- a/types/mdx.ts +++ b/types/mdx.ts @@ -1,5 +1,6 @@ import type readingTime from 'reading-time' import type { DevIconsMap } from '~/components/DevIcon' +import type { TOC } from './server' export type MdxPageLayout = | 'AuthorLayout' @@ -43,7 +44,7 @@ export interface AuthorFrontMatter extends MdxFrontMatter { export interface MdxFileData { mdxSource: string frontMatter: BlogFrontMatter - toc: unknown[] + toc: TOC[] } export interface MdxLayoutRendererProps { diff --git a/types/server.ts b/types/server.ts index b5a78443..7d9c0d5d 100644 --- a/types/server.ts +++ b/types/server.ts @@ -55,12 +55,15 @@ export interface UnistImageNode extends UnistNodeType { attributes: unknown[] } -export interface TOC { - value: string - url: string +export type TOC = { + id: string depth: number + data: { hProperties?: { id?: string } } + children: TOC[] + url: string } export interface RemarkTocHeadingOptions { - exportRef: TOC[] + exportRef: Array<{ value: string; url: string; depth: number }> + cleaned?: boolean } diff --git a/utils/date.ts b/utils/date.ts index 90509ee7..8c763604 100644 --- a/utils/date.ts +++ b/utils/date.ts @@ -1,7 +1,5 @@ -import { siteMetadata } from '~/data/siteMetadata' - -export function formatDate(date: string) { - return new Date(date).toLocaleDateString(siteMetadata.locale, { +export function formatDate(date: string, language: string = 'en') { + return new Date(date).toLocaleDateString(language, { year: 'numeric', month: 'long', day: 'numeric', From ea73acdf56539244e1e70237693326db4c7990bf Mon Sep 17 00:00:00 2001 From: romrosmay <99548008+romrosmay@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:54:20 +0200 Subject: [PATCH 2/4] feat(lang): adding multilanguage support --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 12dc11ed..6f3197cd 100644 --- a/.env.example +++ b/.env.example @@ -25,4 +25,4 @@ GITHUB_API_TOKEN= # Default locale (for i18n) NEXT_PUBLIC_DEFAULT_LOCALE=en -NEXT_PUBLIC_DEFAULT_AUTHOR= +NEXT_PUBLIC_DEFAULT_AUTHOR=Tuan Anh Huynh From 6ff971f514382989441bf34c886d8022d12e80a3 Mon Sep 17 00:00:00 2001 From: romrosmay <99548008+romrosmay@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:56:10 +0200 Subject: [PATCH 3/4] feat(lang): adding multilanguage support --- types/components.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/components.ts b/types/components.ts index 6a1d07ff..6343c73b 100644 --- a/types/components.ts +++ b/types/components.ts @@ -1,7 +1,7 @@ import type { ImageProps as NextImageProps } from 'next/image' import type React from 'react' import type { SocialIconsMap } from '~/components/SocialIcon' -import type { projectsData } from '~/data/projectsData' +import type { projectsData } from '~/data/en/projectsData' import type { commentConfig } from '~/data/siteMetadata' import type { MdxFrontMatter, ReadingTime } from './mdx' From 7a675803299949a6c1a637d9431888f3d05b4c73 Mon Sep 17 00:00:00 2001 From: romrosmay <99548008+romrosmay@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:48:47 +0200 Subject: [PATCH 4/4] feat(lang): add translations fo projectsData --- data/ca/projectsData.ts | 2 +- data/es/projectsData.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/ca/projectsData.ts b/data/ca/projectsData.ts index d2d71b74..b9722208 100644 --- a/data/ca/projectsData.ts +++ b/data/ca/projectsData.ts @@ -4,7 +4,7 @@ export let projectsData: Project[] = [ { type: 'work', title: 'Weaverse - Constructor de llocs web universals', - description: `El primer constructor de llocs web impulsat per Hydrogen i potenciat per IA. Weaverse és un canal de vendes de Shopify que et permet crear un lloc web en minuts sense necessitat de programar.`, + description: `El primer constructor de llocs web promogut per Hydrogen i potenciat per IA. Weaverse és un canal de vendes de Shopify que et permet crear un lloc web en minuts sense necessitat de programar.`, imgSrc: '/static/images/weaverse-hydrogen.jpg', url: 'https://www.weaverse.io?ref=leohuynh.dev', builtWith: ['Remix', 'Prisma', 'Tailwind', 'OpenAI'], diff --git a/data/es/projectsData.ts b/data/es/projectsData.ts index fb5cc121..58f6c5be 100644 --- a/data/es/projectsData.ts +++ b/data/es/projectsData.ts @@ -4,7 +4,7 @@ export let projectsData: Project[] = [ { type: 'work', title: 'Weaverse - Constructor de sitios web universal', - description: `El primer constructor de sitios web impulsado por Hydrogen y potenciado por IA. Weaverse es un canal de ventas de Shopify que te permite crear un sitio web en minutos sin necesidad de programación.`, + description: `El primer constructor de sitios web promovido por Hydrogen y potenciado por IA. Weaverse es un canal de ventas de Shopify que te permite crear un sitio web en minutos sin necesidad de programación.`, imgSrc: '/static/images/weaverse-hydrogen.jpg', url: 'https://www.weaverse.io?ref=leohuynh.dev', builtWith: ['Remix', 'Prisma', 'Tailwind', 'OpenAI'],