From 1c7da391e0e84b31f280a22851a520fc934ad52a Mon Sep 17 00:00:00 2001 From: Salvatore Date: Sat, 20 Apr 2024 09:56:45 +0200 Subject: [PATCH] feat(i18n): issue #27 (#191) * checkpoint: issue with localeswitcher component * fix: prettify code for pr * fix: commit pr ready * fix: redirects from pages to next config file * fix: admin link on header component * fix: redirects moved to _app * fix: minor fixations * fix: html lang switch * fix: prettify for pr * fix: automatic redirect to /it * feat: Adds some translations & Salvatore Riccardi as community member --------- Co-authored-by: Coluzzi Andrea --- _community/members/sriccardi.mdx | 6 ++ dictionaries/en.json | 9 +++ dictionaries/it.json | 9 +++ i18n.config.ts | 6 ++ package-lock.json | 11 ++++ package.json | 11 ++-- public/assets/community/sriccardi.jpg | Bin 0 -> 6880 bytes src/components/Header.tsx | 25 ++++++--- src/components/LeaveFeedback.tsx | 5 +- src/components/LocaleSwitcher.tsx | 62 +++++++++++++++++++++ src/components/event/EventWidget.tsx | 5 +- src/globals.css | 39 +++++++++++++ src/pages/{ => [lang]}/admins/team.tsx | 9 ++- src/pages/{ => [lang]}/community/index.tsx | 16 ++++++ src/pages/{ => [lang]}/events/[slug].tsx | 24 ++++++-- src/pages/{ => [lang]}/events/index.tsx | 25 ++++++++- src/pages/{ => [lang]}/feedback/new.tsx | 25 ++++++++- src/pages/{ => [lang]}/index.tsx | 52 +++++++++++++---- src/pages/[lang]/newsletter/index.tsx | 21 +++++++ src/pages/_app.tsx | 31 +++++++++++ src/pages/_document.tsx | 17 +++++- src/pages/newsletter/index.tsx | 12 ---- src/utils/dictionary.ts | 12 ++++ src/utils/locale.ts | 9 +++ tsconfig.json | 33 +++++++++-- 25 files changed, 418 insertions(+), 56 deletions(-) create mode 100644 _community/members/sriccardi.mdx create mode 100644 dictionaries/en.json create mode 100644 dictionaries/it.json create mode 100644 i18n.config.ts create mode 100644 public/assets/community/sriccardi.jpg create mode 100644 src/components/LocaleSwitcher.tsx create mode 100644 src/globals.css rename src/pages/{ => [lang]}/admins/team.tsx (95%) rename src/pages/{ => [lang]}/community/index.tsx (91%) rename src/pages/{ => [lang]}/events/[slug].tsx (94%) rename src/pages/{ => [lang]}/events/index.tsx (64%) rename src/pages/{ => [lang]}/feedback/new.tsx (94%) rename src/pages/{ => [lang]}/index.tsx (67%) create mode 100644 src/pages/[lang]/newsletter/index.tsx delete mode 100644 src/pages/newsletter/index.tsx create mode 100644 src/utils/dictionary.ts create mode 100644 src/utils/locale.ts diff --git a/_community/members/sriccardi.mdx b/_community/members/sriccardi.mdx new file mode 100644 index 00000000..8778c3a6 --- /dev/null +++ b/_community/members/sriccardi.mdx @@ -0,0 +1,6 @@ +--- +fullname: Salvatore Riccardi +bio: Developer +picture: sriccardi.jpg +linkedin: https://www.linkedin.com/in/salvatore-riccardi/ +github: https://github.com/salvatorericcardi diff --git a/dictionaries/en.json b/dictionaries/en.json new file mode 100644 index 00000000..1b645a74 --- /dev/null +++ b/dictionaries/en.json @@ -0,0 +1,9 @@ +{ + "home": { + "nextEventsTitle": "Next Events", + "nextEventsSubtitle": "Save dates and don't make commitments for upcoming community events!", + "previousEventsTitle": "Previous Events", + "previousEventsSubtitle": "Too bad, these events have already taken place! Follow the page to stay updated on upcoming events.", + "andManyOthers": "...and many others!" + } +} \ No newline at end of file diff --git a/dictionaries/it.json b/dictionaries/it.json new file mode 100644 index 00000000..64de7069 --- /dev/null +++ b/dictionaries/it.json @@ -0,0 +1,9 @@ +{ + "home": { + "nextEventsTitle": "Prossimi Eventi", + "nextEventsSubtitle": "Fissa le date e non prendere impegni per i prossimi eventi della community!", + "previousEventsTitle": "Eventi Passati", + "previousEventsSubtitle": "Peccato, questi eventi si sono già svolti! Segui la pagina per rimanere aggiornato sui prossimi appuntamenti.", + "andManyOthers": "...e molti altri!" + } +} \ No newline at end of file diff --git a/i18n.config.ts b/i18n.config.ts new file mode 100644 index 00000000..27143014 --- /dev/null +++ b/i18n.config.ts @@ -0,0 +1,6 @@ +export const i18n = { + defaultLocale: "it", + locales: ["it", "en"], +} as const; + +export type Locale = (typeof i18n)["locales"][number]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fea26a79..3587f5ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "add-to-calendar-button-react": "^2.6.10", "autoprefixer": "^10.4.19", "firebase": "^10.11.0", + "flag-icons": "^7.2.1", "gray-matter": "^4.0.3", "image-size": "^1.1.1", "luxon": "^3.4.4", @@ -5309,6 +5310,11 @@ "@firebase/util": "1.9.5" } }, + "node_modules/flag-icons": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.2.1.tgz", + "integrity": "sha512-EaU4XZmFt1BOilz9nMmJKjma5pOaNjzL7somOhadrrilollh4xj6aaXI2M1sd00VUfVWN0E25Q6xaW3SNt0k/Q==" + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -17236,6 +17242,11 @@ "@firebase/util": "1.9.5" } }, + "flag-icons": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.2.1.tgz", + "integrity": "sha512-EaU4XZmFt1BOilz9nMmJKjma5pOaNjzL7somOhadrrilollh4xj6aaXI2M1sd00VUfVWN0E25Q6xaW3SNt0k/Q==" + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", diff --git a/package.json b/package.json index 8c2ddb4c..25492df9 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "add-to-calendar-button-react": "^2.6.10", "autoprefixer": "^10.4.19", "firebase": "^10.11.0", + "flag-icons": "^7.2.1", "gray-matter": "^4.0.3", "image-size": "^1.1.1", "luxon": "^3.4.4", @@ -32,19 +33,19 @@ "zod": "^3.22.4" }, "devDependencies": { - "@types/image-size": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^5.59.6", - "@typescript-eslint/parser": "^5.59.6", - "@vitejs/plugin-basic-ssl": "^1.1.0", - "@vitejs/plugin-react": "^4.2.1", "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.3", "@tailwindcss/typography": "^0.5.12", + "@types/image-size": "^0.8.0", "@types/luxon": "^3.4.2", "@types/node": "^20.12.7", "@types/react": "18.2.79", "@types/react-dom": "18.2.22", "@types/react-helmet": "^6.1.11", + "@typescript-eslint/eslint-plugin": "^5.59.6", + "@typescript-eslint/parser": "^5.59.6", + "@vitejs/plugin-basic-ssl": "^1.1.0", + "@vitejs/plugin-react": "^4.2.1", "dotenv-cli": "^7.4.1", "eslint": "8.57.0", "eslint-config-prettier": "^9.1.0", diff --git a/public/assets/community/sriccardi.jpg b/public/assets/community/sriccardi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3984aa0cdc8934fb08fc1fcb07fb0aee6d45475e GIT binary patch literal 6880 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 VBU+R!$Q#Ck`K7*%Nmt!X{|{){Uta(K literal 0 HcmV?d00001 diff --git a/src/components/Header.tsx b/src/components/Header.tsx index cefe2017..8aca012c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -8,21 +8,30 @@ import { XMarkIcon, Bars3Icon } from '@heroicons/react/24/outline'; import navigationLinks from '@/model/navigation'; import React, { useMemo } from 'react'; import { usePathname } from 'next/navigation'; +import { Locale } from 'i18n.config'; +import LocaleSwitcher from './LocaleSwitcher'; function classNames(...classes: string[]): string { return classes.filter(Boolean).join(' '); } -const Header: React.FC = () => { +const Header = (props: { lang: Locale }) => { const pathname = usePathname(); const links = useMemo( () => - navigationLinks.map(link => ({ - ...link, - current: link.href === pathname - })), - [pathname] + navigationLinks.map(link => { + if (link.name === 'Admin team') { + link.href = `/${props.lang}/admins/team`; + } + + return { + ...link, + current: link.href === pathname + }; + }), + [pathname, props.lang] ); + return ( {({ open: isOpen }) => ( @@ -48,7 +57,7 @@ const Header: React.FC = () => {
- + Logo LiT {
+
{links.map(item => ( {
+ {links.map(item => ( { + const router = useRouter(); + return (

@@ -13,7 +16,7 @@ export const LeaveFeedback = () => {

Lascia un Feedback! diff --git a/src/components/LocaleSwitcher.tsx b/src/components/LocaleSwitcher.tsx new file mode 100644 index 00000000..874345e8 --- /dev/null +++ b/src/components/LocaleSwitcher.tsx @@ -0,0 +1,62 @@ +'use client'; +import 'flag-icons'; +import { MouseEvent, useState } from 'react'; +import { useRouter } from 'next/router'; +import { Locale, i18n } from 'i18n.config'; +import { usePathname } from 'next/navigation'; + +export default function LocaleSwitcher(props: { + lang: Locale; + mobile: boolean; +}) { + const router = useRouter(); + const pathname = usePathname(); + + const defaultLocale = i18n.defaultLocale; + const [animation, setAnimation] = useState(''); + const [locale, setLocale] = useState(props.lang); + + const toggle = (e: MouseEvent) => { + const container = e.target as HTMLDivElement; + const changeLocale = i18n.locales.filter( + locale => locale !== props.lang + )[0]; + + setLocale(changeLocale); + setAnimation(props.lang === defaultLocale ? 'slide-out' : 'slide-in'); + + container.onanimationend = () => { + const regex = /it|en/; + + if (regex.test(pathname)) { + void router.replace(pathname.replace(regex, changeLocale)); + } else { + void router.replace(`/${changeLocale}${pathname}`); + } + }; + }; + + return ( +
+ +
toggle(e)} + > +
+
+ +
+ ); +} diff --git a/src/components/event/EventWidget.tsx b/src/components/event/EventWidget.tsx index 1add913f..76698a77 100644 --- a/src/components/event/EventWidget.tsx +++ b/src/components/event/EventWidget.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useMemo } from 'react'; import EventActions from './EventActions'; import EventDescription from './EventDescription'; import EventTags from './EventTags'; +import { useRouter } from 'next/router'; const thumbHeight = 400; type Props = { @@ -12,6 +13,8 @@ type Props = { }; const EventWidget: React.FC = ({ event }: Props) => { + const router = useRouter(); + const renderEventImage = useCallback(() => { if (event.thumbnail) { return ( @@ -40,7 +43,7 @@ const EventWidget: React.FC = ({ event }: Props) => { }, [isPast]); return ( - +
{renderEventImage()}
diff --git a/src/globals.css b/src/globals.css new file mode 100644 index 00000000..a04859f3 --- /dev/null +++ b/src/globals.css @@ -0,0 +1,39 @@ +.en { + position: relative; + left: calc(100% - 16px); +} + +.it { + position: relative; + left: 0; +} + +.slide-in { + animation-name: slidein; + animation-duration: 750ms; +} + +.slide-out { + animation-name: slideout; + animation-duration: 750ms; +} + +@keyframes slideout { + from { + left: 0%; + } + + to { + left: calc(100% - 16px); + } +} + +@keyframes slidein { + from { + left: calc(100% - 16px); + } + + to { + left: 0%; + } +} \ No newline at end of file diff --git a/src/pages/admins/team.tsx b/src/pages/[lang]/admins/team.tsx similarity index 95% rename from src/pages/admins/team.tsx rename to src/pages/[lang]/admins/team.tsx index a6af3a61..8a0b7972 100644 --- a/src/pages/admins/team.tsx +++ b/src/pages/[lang]/admins/team.tsx @@ -6,6 +6,8 @@ import { BsTwitter, BsFillHouseDoorFill } from 'react-icons/bs'; +import { i18n } from 'i18n.config'; +import { useRouter } from 'next/router'; type Admin = { name: string; @@ -138,9 +140,14 @@ const AdminCard: React.FC = ({ }; const AdminTeam = () => { + const router = useRouter(); + const locale = i18n.locales.filter( + locale => router?.query.lang === locale + )[0]; + return (
-
+
diff --git a/src/pages/community/index.tsx b/src/pages/[lang]/community/index.tsx similarity index 91% rename from src/pages/community/index.tsx rename to src/pages/[lang]/community/index.tsx index d0a395bb..cccd9069 100644 --- a/src/pages/community/index.tsx +++ b/src/pages/[lang]/community/index.tsx @@ -9,6 +9,7 @@ import { import { getAllCommunityMembers } from '@/utils/community'; import { isDevEnv } from '@/utils/dev'; import CommunityMember from '@/components/CommunityMember'; +import { getAllLocales } from '@/utils/locale'; /** * display the list of community members @@ -87,6 +88,21 @@ const CommunityMemberList: React.FC< ); }; +export const getStaticPaths = async () => { + const locales = getAllLocales(); + + return { + paths: locales.map(locale => { + return { + params: { + lang: locale + } + }; + }), + fallback: false + }; +}; + export const getStaticProps = (() => ({ props: { members: getAllCommunityMembers() } })) satisfies GetStaticProps<{ diff --git a/src/pages/events/[slug].tsx b/src/pages/[lang]/events/[slug].tsx similarity index 94% rename from src/pages/events/[slug].tsx rename to src/pages/[lang]/events/[slug].tsx index 359e99b8..bdb9f16e 100644 --- a/src/pages/events/[slug].tsx +++ b/src/pages/[lang]/events/[slug].tsx @@ -16,6 +16,9 @@ import EventTags from '@/components/event/EventTags'; import EventsSlides from '@/components/event/EventSlides'; import { ZodSchema } from 'zod'; import { isDevEnv } from '@/utils/dev'; +import { i18n } from 'i18n.config'; +import { useRouter } from 'next/router'; +import { getAllLocales } from '@/utils/locale'; type Props = { source: string; @@ -54,6 +57,11 @@ const parseItems = ( const thumbHeight = 250; const EventPage: React.FC = ({ source, frontMatter: event }: Props) => { + const router = useRouter(); + const locale = i18n.locales.filter( + locale => router?.query.lang === locale + )[0]; + const speakersObjects = useMemo( () => parseItems(event.speakers ?? [], speakerSchema), [event.speakers] @@ -108,7 +116,7 @@ const EventPage: React.FC = ({ source, frontMatter: event }: Props) => { LiT - {event.title} -
+

{event.title} @@ -232,12 +240,16 @@ export const getStaticProps: GetStaticProps = async context => { export const getStaticPaths: GetStaticPaths = () => { const events = getAllEvents(['slug']); + const locales = getAllLocales(); - const paths = events.map(event => ({ - params: { - slug: event.slug - } - })); + const paths = locales.flatMap(locale => + events.map(event => ({ + params: { + slug: event.slug, + lang: locale + } + })) + ); return { paths, diff --git a/src/pages/events/index.tsx b/src/pages/[lang]/events/index.tsx similarity index 64% rename from src/pages/events/index.tsx rename to src/pages/[lang]/events/index.tsx index 7507d861..0b294f50 100644 --- a/src/pages/events/index.tsx +++ b/src/pages/[lang]/events/index.tsx @@ -5,19 +5,27 @@ import { GetStaticProps, NextPage } from 'next'; import { getAllEvents } from '@/utils/mdxUtils'; import { IEvent, sortEvents } from '@/model/event'; import Head from 'next/head'; +import { i18n } from 'i18n.config'; +import { useRouter } from 'next/router'; +import { getAllLocales } from '@/utils/locale'; const EventsPage: NextPage<{ events: [IEvent] }> = ({ events }: { events: [IEvent]; }) => { + const router = useRouter(); + const locale = i18n.locales.filter( + locale => router?.query.lang === locale + )[0]; + const sortedEvents = useMemo(() => sortEvents(events), [events]); return ( <> LiT - Eventi -
+
= ({ export default EventsPage; +export const getStaticPaths = async () => { + const locales = getAllLocales(); + + return { + paths: locales.map(locale => { + return { + params: { + lang: locale + } + }; + }), + fallback: false + }; +}; + export const getStaticProps: GetStaticProps = async () => { const events = getAllEvents(); return { props: { events } }; diff --git a/src/pages/feedback/new.tsx b/src/pages/[lang]/feedback/new.tsx similarity index 94% rename from src/pages/feedback/new.tsx rename to src/pages/[lang]/feedback/new.tsx index a0c757b6..b0868df9 100644 --- a/src/pages/feedback/new.tsx +++ b/src/pages/[lang]/feedback/new.tsx @@ -12,6 +12,9 @@ import { TextArea } from '@/components/TextArea'; import { PaperAirplaneIcon, HomeIcon } from '@heroicons/react/24/outline'; import { saveFeedback } from '@/service/feedback/save'; import { Alert } from '@/components/Alert'; +import { i18n } from 'i18n.config'; +import { useRouter } from 'next/router'; +import { getAllLocales } from '@/utils/locale'; type Props = { events: [IEvent]; @@ -27,6 +30,11 @@ const NOT_CAME_REASONS = [ const MAX_RATE = 5; const NewFeedback: NextPage = ({ events: events }: Props) => { + const router = useRouter(); + const locale = i18n.locales.filter( + locale => router?.query.lang === locale + )[0]; + const pastEvents = useMemo(() => filterPastEvents(events), [events]); const lastEvent = sortEvents(pastEvents)[0]; @@ -82,7 +90,7 @@ const NewFeedback: NextPage = ({ events: events }: Props) => { > -
+

@@ -239,6 +247,21 @@ const NewFeedback: NextPage = ({ events: events }: Props) => { export default NewFeedback; +export const getStaticPaths = async () => { + const locales = getAllLocales(); + + return { + paths: locales.map(locale => { + return { + params: { + lang: locale + } + }; + }), + fallback: false + }; +}; + export const getStaticProps: GetStaticProps = async () => { const events = getAllEvents(); return { props: { events } }; diff --git a/src/pages/index.tsx b/src/pages/[lang]/index.tsx similarity index 67% rename from src/pages/index.tsx rename to src/pages/[lang]/index.tsx index 41a7a064..0a12575a 100644 --- a/src/pages/index.tsx +++ b/src/pages/[lang]/index.tsx @@ -1,3 +1,4 @@ +'use client'; import Header from '@/components/Header'; import Hero from '@/components/Hero'; import { @@ -13,15 +14,25 @@ import { Sponsors } from '@/components/Sponsors'; import { LeaveFeedback } from '@/components/LeaveFeedback'; import EventsList from '@/components/event/EventsList'; import { Newsletter } from '@/components/Newsletter'; -import Community from '@/pages/community'; +import Community from '@/pages/[lang]/community'; import { getAllCommunityMembers } from '@/utils/community'; +import { getAllLocales } from '@/utils/locale'; +import { useRouter } from 'next/router'; +import { Locale, i18n } from 'i18n.config'; +import { getDictionary } from '@/utils/dictionary'; const MAX_PAST_EVENTS = 3; const Home: React.FC> = ({ events, - communityMembers + communityMembers, + translations }) => { + const router = useRouter(); + const locale = i18n.locales.filter( + locale => router?.query.lang === locale + )[0]; + const allPastEvents = useMemo( () => sortEvents(filterPastEvents(events)), [events] @@ -49,7 +60,7 @@ const Home: React.FC> = ({ > -
+
@@ -57,25 +68,25 @@ const Home: React.FC> = ({ {nextEvents.length > 0 && ( )} {pastEventsPreview.length > 0 && ( )} {hasMorePastEvents && ( )} @@ -89,10 +100,27 @@ const Home: React.FC> = ({ ); }; -export default Home; +export const getStaticPaths = async () => { + const locales = getAllLocales(); + + return { + paths: locales.map(locale => { + return { + params: { + lang: locale + } + }; + }), + fallback: false + }; +}; -export const getStaticProps: GetStaticProps = async () => { +export const getStaticProps: GetStaticProps = async context => { + const lang = context.params?.lang as string; + const dictionary = await getDictionary(lang as Locale); const events = getAllEvents(); const communityMembers = getAllCommunityMembers(); - return { props: { events, communityMembers } }; + return { props: { events, communityMembers, translations: dictionary.home } }; }; + +export default Home; diff --git a/src/pages/[lang]/newsletter/index.tsx b/src/pages/[lang]/newsletter/index.tsx new file mode 100644 index 00000000..56ff5c18 --- /dev/null +++ b/src/pages/[lang]/newsletter/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Newsletter } from '@/components/Newsletter'; +import Header from '@/components/Header'; +import { useRouter } from 'next/router'; +import { i18n } from 'i18n.config'; + +const NewsletterPage = () => { + const router = useRouter(); + const locale = i18n.locales.filter( + locale => router?.query.lang === locale + )[0]; + + return ( + <> +
+ + + ); +}; + +export default NewsletterPage; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 34bf2752..d9e5ec06 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,9 +1,40 @@ import 'tailwindcss/tailwind.css'; +import '@/globals.css'; import type { AppProps } from 'next/app'; import Head from 'next/head'; import React from 'react'; +import { useRouter } from 'next/router'; +import { i18n } from 'i18n.config'; + +const oldPaths = [ + '/', + '/admins/team', + '/events/\\d+', + '/feedback/new', + '/newsletter', + '/community' +].map(p => { + if (p === '/') { + return new RegExp(`^${p}$`); + } + + return new RegExp(`^${p}`); +}); + +const useBackRouteCompatibility = () => { + const router = useRouter(); + React.useEffect(() => { + if (oldPaths.some(p => p.test(router.asPath))) { + void router.replace( + router.asPath, + `/${i18n.defaultLocale}${router.asPath}` + ); + } + }, [router]); +}; export default function App({ Component, pageProps }: AppProps) { + useBackRouteCompatibility(); return ( <> diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 08fe479b..76297db5 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -1,11 +1,22 @@ +import { Locale, i18n } from 'i18n.config'; import { Html, Head, Main, NextScript } from 'next/document'; -import React from 'react'; +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; export default function Document() { + const [locale, setLocale] = useState(i18n.defaultLocale); + + useEffect(() => { + const router = useRouter(); + if (router.query.lang !== i18n.defaultLocale) { + setLocale('en'); + } + }, [locale]); + return ( - + - +
diff --git a/src/pages/newsletter/index.tsx b/src/pages/newsletter/index.tsx deleted file mode 100644 index 47bd5f80..00000000 --- a/src/pages/newsletter/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { Newsletter } from '@/components/Newsletter'; -import Header from '@/components/Header'; - -const NewsletterPage = () => ( - <> -
- - -); - -export default NewsletterPage; diff --git a/src/utils/dictionary.ts b/src/utils/dictionary.ts new file mode 100644 index 00000000..fc7a4225 --- /dev/null +++ b/src/utils/dictionary.ts @@ -0,0 +1,12 @@ +// We enumerate all dictionaries here for better linting and typescript support + +import { Locale } from 'i18n.config'; + +// We also get the default import for cleaner types +const dictionaries = { + en: () => import('../../dictionaries/en.json').then(module => module.default), + it: () => import('../../dictionaries/it.json').then(module => module.default) +}; + +export const getDictionary = async (locale: Locale) => + dictionaries[locale]?.() ?? dictionaries.it(); diff --git a/src/utils/locale.ts b/src/utils/locale.ts new file mode 100644 index 00000000..a9bd5385 --- /dev/null +++ b/src/utils/locale.ts @@ -0,0 +1,9 @@ +import { i18n } from 'i18n.config'; + +export const getDefaultLocale = () => { + return i18n.defaultLocale; +}; + +export const getAllLocales = () => { + return i18n.locales; +}; diff --git a/tsconfig.json b/tsconfig.json index e771e6a1..3173cee1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -15,11 +19,28 @@ "jsx": "preserve", "incremental": true, "baseUrl": ".", - "typeRoots" : ["node_modules/@types", "src/@types"], + "typeRoots": [ + "node_modules/@types", + "src/@types" + ], "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }