From f53753487f8c393046926896a6f9395b917743f3 Mon Sep 17 00:00:00 2001 From: Atash03 Date: Thu, 12 Feb 2026 01:45:37 +0500 Subject: [PATCH] feat: migrate i18n to next-intl with locale-based URL routing Replace manual cookie-based language system with next-intl v4. All routes now use /ru and /en URL prefixes, translation strings live in JSON files, and components use useTranslations/getTranslations instead of useLang/Redux. --- .gitignore | 3 + .../(mainMenu)/company/aboutus/loading.tsx | 0 .../(mainMenu)/company/aboutus/page.tsx | 9 +- .../(root)/(mainMenu)/layout.tsx | 0 .../(root)/(mainMenu)/news/[id]/page.tsx | 18 +- .../(root)/(mainMenu)/news/page.tsx | 29 +- .../(mainMenu)/services/additional/page.tsx | 4 +- .../(mainMenu)/services/advertising/page.tsx | 6 +- .../services/business-tours/page.tsx | 4 +- .../services/certification/page.tsx | 6 +- .../(mainMenu)/services/forwarding/page.tsx | 6 +- .../services/hybrid-organizations/page.tsx | 6 +- .../(root)/(mainMenu)/services/loading.tsx | 0 .../(mainMenu)/services/meetings/page.tsx | 6 +- .../(mainMenu)/services/missions/page.tsx | 6 +- .../(mainMenu)/services/organization/page.tsx | 4 +- .../(mainMenu)/services/outsourcing/page.tsx | 6 +- .../services/personnel-training/page.tsx | 6 +- .../(root)/(mainMenu)/visitors/page.tsx | 19 +- .../visitors/rules-for-visitors/page.tsx | 25 +- .../(root)/calendar/[id]/page.tsx | 17 +- app/{ => [locale]}/(root)/calendar/page.tsx | 10 +- app/{ => [locale]}/(root)/contacts/page.tsx | 18 +- app/{ => [locale]}/(root)/layout.tsx | 0 app/{ => [locale]}/(root)/page.tsx | 5 +- app/[locale]/layout.tsx | 45 ++ app/layout.tsx | 21 +- app/middleware.ts | 31 - components/page/LayoutWithSidebar.tsx | 14 +- components/page/Pathnames.tsx | 19 - components/shared/Card.tsx | 2 +- components/shared/Events.tsx | 36 +- components/shared/Footer.tsx | 59 +- components/shared/Header.tsx | 55 +- components/shared/News.tsx | 19 +- components/shared/Partners.tsx | 15 +- components/shared/Reviews.tsx | 17 +- components/shared/Slider.tsx | 2 +- components/shared/contacts-form.tsx | 42 +- components/shared/event-card.tsx | 18 +- components/shared/event-page-buttons.tsx | 14 +- components/shared/events-mobile.tsx | 15 +- components/shared/news-card.tsx | 2 +- components/shared/search-input.tsx | 42 +- components/shared/services.tsx | 15 +- components/shared/slider-client.tsx | 10 +- components/ui/LangMenu.tsx | 72 +- components/ui/Pagination.tsx | 7 +- components/ui/Sidebar.tsx | 14 +- components/ui/bread-crumbs.tsx | 10 +- components/ui/burger-drop.tsx | 10 +- components/ui/burger-menu.tsx | 89 +-- hooks/useFetch.ts | 17 +- i18n/config.ts | 3 + i18n/navigation.ts | 7 + i18n/request.ts | 15 + lib/database/header.ts | 129 ++-- lib/database/homeInfoData.ts | 72 +- lib/database/pathnames.ts | 441 +++-------- lib/get-lang.ts | 12 - lib/types.ts | 3 +- messages/en.json | 144 ++++ messages/ru.json | 144 ++++ middleware.ts | 13 + next.config.mjs | 6 +- package-lock.json | 717 +++++++++++++++++- package.json | 1 + redux/slices/aboutus.ts | 10 +- redux/slices/headerSlice.ts | 15 +- services/services.ts | 11 +- utils/useLang.ts | 9 - 71 files changed, 1576 insertions(+), 1101 deletions(-) rename app/{ => [locale]}/(root)/(mainMenu)/company/aboutus/loading.tsx (100%) rename app/{ => [locale]}/(root)/(mainMenu)/company/aboutus/page.tsx (65%) rename app/{ => [locale]}/(root)/(mainMenu)/layout.tsx (100%) rename app/{ => [locale]}/(root)/(mainMenu)/news/[id]/page.tsx (82%) rename app/{ => [locale]}/(root)/(mainMenu)/news/page.tsx (77%) rename app/{ => [locale]}/(root)/(mainMenu)/services/additional/page.tsx (79%) rename app/{ => [locale]}/(root)/(mainMenu)/services/advertising/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/services/business-tours/page.tsx (79%) rename app/{ => [locale]}/(root)/(mainMenu)/services/certification/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/services/forwarding/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/services/hybrid-organizations/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/services/loading.tsx (100%) rename app/{ => [locale]}/(root)/(mainMenu)/services/meetings/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/services/missions/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/services/organization/page.tsx (80%) rename app/{ => [locale]}/(root)/(mainMenu)/services/outsourcing/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/services/personnel-training/page.tsx (71%) rename app/{ => [locale]}/(root)/(mainMenu)/visitors/page.tsx (63%) rename app/{ => [locale]}/(root)/(mainMenu)/visitors/rules-for-visitors/page.tsx (58%) rename app/{ => [locale]}/(root)/calendar/[id]/page.tsx (83%) rename app/{ => [locale]}/(root)/calendar/page.tsx (75%) rename app/{ => [locale]}/(root)/contacts/page.tsx (82%) rename app/{ => [locale]}/(root)/layout.tsx (100%) rename app/{ => [locale]}/(root)/page.tsx (85%) create mode 100644 app/[locale]/layout.tsx delete mode 100644 app/middleware.ts delete mode 100644 components/page/Pathnames.tsx create mode 100644 i18n/config.ts create mode 100644 i18n/navigation.ts create mode 100644 i18n/request.ts delete mode 100644 lib/get-lang.ts create mode 100644 messages/en.json create mode 100644 messages/ru.json create mode 100644 middleware.ts delete mode 100644 utils/useLang.ts diff --git a/.gitignore b/.gitignore index 341eba7..7ca65bf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.claude +.agents \ No newline at end of file diff --git a/app/(root)/(mainMenu)/company/aboutus/loading.tsx b/app/[locale]/(root)/(mainMenu)/company/aboutus/loading.tsx similarity index 100% rename from app/(root)/(mainMenu)/company/aboutus/loading.tsx rename to app/[locale]/(root)/(mainMenu)/company/aboutus/loading.tsx diff --git a/app/(root)/(mainMenu)/company/aboutus/page.tsx b/app/[locale]/(root)/(mainMenu)/company/aboutus/page.tsx similarity index 65% rename from app/(root)/(mainMenu)/company/aboutus/page.tsx rename to app/[locale]/(root)/(mainMenu)/company/aboutus/page.tsx index d8b3b64..0c560b2 100644 --- a/app/(root)/(mainMenu)/company/aboutus/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/company/aboutus/page.tsx @@ -1,16 +1,15 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getAbout } from "@/services/about"; -import { cookies } from "next/headers"; +import { getLocale, getTranslations } from "next-intl/server"; export default async function AboutPage() { - const lang = cookies().get("lang")?.value ?? "ru"; + const lang = await getLocale(); + const t = await getTranslations("about"); const data = await getAbout(lang); - const aboutText = lang === "en" ? "About us" : "Коротко о нас"; - return ( - +
(); const fetchData = async () => { try { const response = await fetch(`${baseAPI}news/${params.id}`, { headers: { - "Accept-Language": activeLang.localization, + "Accept-Language": locale, }, }); @@ -43,11 +45,11 @@ export default function SingleNewsPage({ params }: { params: { id: string } }) { React.useEffect(() => { fetchData(); dispatch(setShowInput(false)); - }, [activeLang.localization]); + }, [locale]); return (
- + {newsItemData ? (
@@ -84,7 +86,7 @@ export default function SingleNewsPage({ params }: { params: { id: string } }) { </div> <Link href="/news" className="flex justify-center"> - <GreenBtn text="Все новости" px /> + <GreenBtn text={t("allNews")} px /> </Link> </div> ); diff --git a/app/(root)/(mainMenu)/news/page.tsx b/app/[locale]/(root)/(mainMenu)/news/page.tsx similarity index 77% rename from app/(root)/(mainMenu)/news/page.tsx rename to app/[locale]/(root)/(mainMenu)/news/page.tsx index 7e051b2..d396230 100644 --- a/app/(root)/(mainMenu)/news/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/news/page.tsx @@ -2,9 +2,7 @@ import React, { useEffect, useState } from "react"; -import { useLang } from "@/utils/useLang"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; +import { useLocale, useTranslations } from "next-intl"; import { NewsData } from "@/lib/types/NewsData.type"; import { baseAPI } from "@/lib/API"; import Loader from "@/components/ui/Loader"; @@ -15,9 +13,10 @@ import { Pagination } from "@/components/ui/Pagination"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; const News = () => { - const { activeLang } = useAppSelector(selectHeader); + const locale = useLocale(); + const t = useTranslations("news"); + const tCommon = useTranslations("common"); - const menu = ["Новости", "СМИ о нас"]; const [current, setCurrent] = React.useState<number>(1); const [perPage, setPerPage] = React.useState<number>(6); const [totalNews, setTotalNews] = React.useState<number>(0); @@ -36,7 +35,7 @@ const News = () => { `${baseAPI}news?page=${current}&per_page=${perPage}`, { headers: { - "Accept-Language": activeLang.localization, + "Accept-Language": locale, }, } ); @@ -57,7 +56,7 @@ const News = () => { React.useEffect(() => { fetchNews(); - }, [current, perPage, activeLang.localization]); + }, [current, perPage, locale]); const handleOnClickButton = () => { setPerPage((prev) => prev + 6); @@ -70,23 +69,13 @@ const News = () => { return ( <div> <LayoutWithSidebar - title={useLang("News", "Новости", activeLang.localization)} - second={useLang("News", "Новости", activeLang.localization)} + title={t("title")} + second={t("title")} cursor={false} > <div> <div className="flex flex-col"> <div className="flex flex-col"> - <div className="hidden sm:flex justify-between items-center pb-[5px] "> - <div className="pointer-events-none opacity-0 flex items-center gap-5"> - {menu.map((item, i) => ( - <p key={i} className="cursor-pointer leading-[130%]"> - {item} - </p> - ))} - </div> - </div> - <div className={clsx( "mb-[48px] lg:mb-[108px]", @@ -112,7 +101,7 @@ const News = () => { <div className="hidden sm:flex flex-col gap-6 w-full max-w-[180px] mx-auto justify-center items-center"> {newsData && totalNews > perPage && perPage >= totalNews && ( <div onClick={handleOnClickButton}> - <BorderBtn px text={"Показать ещё"} /> + <BorderBtn px text={tCommon("showMore")} /> </div> )} diff --git a/app/(root)/(mainMenu)/services/additional/page.tsx b/app/[locale]/(root)/(mainMenu)/services/additional/page.tsx similarity index 79% rename from app/(root)/(mainMenu)/services/additional/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/additional/page.tsx index a6c13b7..6ee3ca3 100644 --- a/app/(root)/(mainMenu)/services/additional/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/additional/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function AdvertisingPage() { - const { data } = await getServices(); + const lang = await getLocale(); + const { data } = await getServices(lang); return ( <LayoutWithSidebar diff --git a/app/(root)/(mainMenu)/services/advertising/page.tsx b/app/[locale]/(root)/(mainMenu)/services/advertising/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/advertising/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/advertising/page.tsx index dfe87d6..92b6ee6 100644 --- a/app/(root)/(mainMenu)/services/advertising/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/advertising/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function AdvertisingPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function AdvertisingPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[1].content : "", + __html: data?.data ? data.data[1].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/services/business-tours/page.tsx b/app/[locale]/(root)/(mainMenu)/services/business-tours/page.tsx similarity index 79% rename from app/(root)/(mainMenu)/services/business-tours/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/business-tours/page.tsx index 972af0f..d7f550a 100644 --- a/app/(root)/(mainMenu)/services/business-tours/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/business-tours/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function BusinessTours() { - const { data } = await getServices(); + const lang = await getLocale(); + const { data } = await getServices(lang); return ( <LayoutWithSidebar diff --git a/app/(root)/(mainMenu)/services/certification/page.tsx b/app/[locale]/(root)/(mainMenu)/services/certification/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/certification/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/certification/page.tsx index 98c2394..e4c3b59 100644 --- a/app/(root)/(mainMenu)/services/certification/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/certification/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function CertificationPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function CertificationPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[3].content : "", + __html: data?.data ? data.data[3].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/services/forwarding/page.tsx b/app/[locale]/(root)/(mainMenu)/services/forwarding/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/forwarding/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/forwarding/page.tsx index 0c4c6f8..271519e 100644 --- a/app/(root)/(mainMenu)/services/forwarding/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/forwarding/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function ForwardingPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function ForwardingPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[4].content : "", + __html: data?.data ? data.data[4].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/services/hybrid-organizations/page.tsx b/app/[locale]/(root)/(mainMenu)/services/hybrid-organizations/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/hybrid-organizations/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/hybrid-organizations/page.tsx index 9f52436..278ed46 100644 --- a/app/(root)/(mainMenu)/services/hybrid-organizations/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/hybrid-organizations/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function HybridsPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function HybridsPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[2].content : "", + __html: data?.data ? data.data[2].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/services/loading.tsx b/app/[locale]/(root)/(mainMenu)/services/loading.tsx similarity index 100% rename from app/(root)/(mainMenu)/services/loading.tsx rename to app/[locale]/(root)/(mainMenu)/services/loading.tsx diff --git a/app/(root)/(mainMenu)/services/meetings/page.tsx b/app/[locale]/(root)/(mainMenu)/services/meetings/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/meetings/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/meetings/page.tsx index 1fa8f70..abf0ba2 100644 --- a/app/(root)/(mainMenu)/services/meetings/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/meetings/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function MeetingsPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function MeetingsPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[9].content : "", + __html: data?.data ? data.data[9].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/services/missions/page.tsx b/app/[locale]/(root)/(mainMenu)/services/missions/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/missions/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/missions/page.tsx index 68b74f8..47b7131 100644 --- a/app/(root)/(mainMenu)/services/missions/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/missions/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function MissionsPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function MissionsPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[10].content : "", + __html: data?.data ? data.data[10].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/services/organization/page.tsx b/app/[locale]/(root)/(mainMenu)/services/organization/page.tsx similarity index 80% rename from app/(root)/(mainMenu)/services/organization/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/organization/page.tsx index 89beb10..31657fd 100644 --- a/app/(root)/(mainMenu)/services/organization/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/organization/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function OrganizationPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar diff --git a/app/(root)/(mainMenu)/services/outsourcing/page.tsx b/app/[locale]/(root)/(mainMenu)/services/outsourcing/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/outsourcing/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/outsourcing/page.tsx index b524d0b..c9c12e5 100644 --- a/app/(root)/(mainMenu)/services/outsourcing/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/outsourcing/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function OutsourcingPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function OutsourcingPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[5].content : "", + __html: data?.data ? data.data[5].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/services/personnel-training/page.tsx b/app/[locale]/(root)/(mainMenu)/services/personnel-training/page.tsx similarity index 71% rename from app/(root)/(mainMenu)/services/personnel-training/page.tsx rename to app/[locale]/(root)/(mainMenu)/services/personnel-training/page.tsx index 4616313..59dc16c 100644 --- a/app/(root)/(mainMenu)/services/personnel-training/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/services/personnel-training/page.tsx @@ -1,8 +1,10 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { getServices } from "@/services/services"; +import { getLocale } from "next-intl/server"; export default async function PersonalTrainingPage() { - const data = await getServices(); + const lang = await getLocale(); + const data = await getServices(lang); return ( <LayoutWithSidebar @@ -12,7 +14,7 @@ export default async function PersonalTrainingPage() { <div className="select-inner" dangerouslySetInnerHTML={{ - __html: data ? data.data[6].content : "", + __html: data?.data ? data.data[6].content : "", }} /> </LayoutWithSidebar> diff --git a/app/(root)/(mainMenu)/visitors/page.tsx b/app/[locale]/(root)/(mainMenu)/visitors/page.tsx similarity index 63% rename from app/(root)/(mainMenu)/visitors/page.tsx rename to app/[locale]/(root)/(mainMenu)/visitors/page.tsx index 3fa60d9..fe01515 100644 --- a/app/(root)/(mainMenu)/visitors/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/visitors/page.tsx @@ -2,20 +2,19 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { baseAPI } from "@/lib/API"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; -import { useLang } from "@/utils/useLang"; +import { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; const Visitors = () => { const [visitorsData, setVisitorsData] = useState<string>(); - const { activeLang } = useAppSelector(selectHeader); + const locale = useLocale(); + const t = useTranslations("visitors"); const fetchVisitors = async () => { try { const res = await fetch(`${baseAPI}settings/visitors_page`, { headers: { - "Accept-Language": activeLang.localization, + "Accept-Language": locale, }, }); @@ -33,17 +32,13 @@ const Visitors = () => { useEffect(() => { fetchVisitors(); - }, []); + }, [locale]); return ( <div> <LayoutWithSidebar - title={useLang( - "Information for visitors", - "Информация для посетителей", - activeLang.localization - )} - second={useLang("For visitors", "Посетителям", activeLang.localization)} + title={t("information")} + second={t("title")} > <div dangerouslySetInnerHTML={{ __html: visitorsData ? visitorsData : "" }} diff --git a/app/(root)/(mainMenu)/visitors/rules-for-visitors/page.tsx b/app/[locale]/(root)/(mainMenu)/visitors/rules-for-visitors/page.tsx similarity index 58% rename from app/(root)/(mainMenu)/visitors/rules-for-visitors/page.tsx rename to app/[locale]/(root)/(mainMenu)/visitors/rules-for-visitors/page.tsx index dce7035..448200b 100644 --- a/app/(root)/(mainMenu)/visitors/rules-for-visitors/page.tsx +++ b/app/[locale]/(root)/(mainMenu)/visitors/rules-for-visitors/page.tsx @@ -2,20 +2,19 @@ import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { baseAPI } from "@/lib/API"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; -import { useLang } from "@/utils/useLang"; +import { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; const RulesForVisitors = () => { const [visitorsData, setVisitorsData] = useState<string>(); - const { activeLang } = useAppSelector(selectHeader); + const locale = useLocale(); + const t = useTranslations("visitors"); const fetchVisitors = async () => { try { const res = await fetch(`${baseAPI}settings/visiting_rules`, { headers: { - "Accept-Language": activeLang.localization, + "Accept-Language": locale, }, }); @@ -33,23 +32,15 @@ const RulesForVisitors = () => { useEffect(() => { fetchVisitors(); - }, []); + }, [locale]); return ( <div> <LayoutWithSidebar - title={useLang( - "Entrance rules", - "Порядок регистрации посетителей", - activeLang.localization - )} - second={useLang("Visitors", "Посетителям", activeLang.localization)} + title={t("rules")} + second={t("title")} path="/visitors" - third={useLang( - "Entrance rules", - "Порядок регистрации посетителей", - activeLang.localization - )} + third={t("rules")} > <div dangerouslySetInnerHTML={{ __html: visitorsData ? visitorsData : "" }} diff --git a/app/(root)/calendar/[id]/page.tsx b/app/[locale]/(root)/calendar/[id]/page.tsx similarity index 83% rename from app/(root)/calendar/[id]/page.tsx rename to app/[locale]/(root)/calendar/[id]/page.tsx index f731a37..75d9c3a 100644 --- a/app/(root)/calendar/[id]/page.tsx +++ b/app/[locale]/(root)/calendar/[id]/page.tsx @@ -1,6 +1,6 @@ import { EventPageButtons } from "@/components/shared/event-page-buttons"; import { getEventPage } from "@/services/calendar"; -import { cookies } from "next/headers"; +import { getLocale, getTranslations } from "next-intl/server"; import Image from "next/image"; export default async function EventPage({ @@ -10,7 +10,8 @@ export default async function EventPage({ }) { const { id } = params; - const lang = cookies().get("lang")?.value ?? "ru"; + const lang = await getLocale(); + const t = await getTranslations("events"); const data = await getEventPage(id, lang); @@ -24,7 +25,7 @@ export default async function EventPage({ <div className="flex flex-col gap-20 w-full"> <div className="flex flex-col gap-8 event-block "> <div className="flex justify-between items-center"> - <h4>{lang === "en" ? "Date" : "Дата"}</h4> + <h4>{t("date")}</h4> <h5>{data?.date}</h5> </div> @@ -32,7 +33,7 @@ export default async function EventPage({ {data?.location && ( <> <div className="flex justify-between items-center"> - <h4>{lang === "en" ? "Venue" : "Место"}</h4> + <h4>{t("venue")}</h4> <h5>{data?.location}</h5> </div> <hr /> @@ -42,7 +43,7 @@ export default async function EventPage({ {data?.organizers.length > 0 && ( <> <div className="flex justify-between items-center"> - <h4>{lang === "en" ? "Organiser" : "Организатор"}</h4> + <h4>{t("organiser")}</h4> <h5>{data?.organizers[0]?.name}</h5> </div> </> @@ -53,7 +54,7 @@ export default async function EventPage({ <hr /> <div className="flex justify-between items-center"> - <h4>{lang === "en" ? "Co-organiser" : "Со-организатор"}</h4> + <h4>{t("coOrganiser")}</h4> <h5>{data?.coorganizers[0]?.name}</h5> </div> @@ -81,7 +82,7 @@ export default async function EventPage({ <div className="flex flex-col gap-8"> <div className="flex flex-col lg:flex-row gap-6"> <h4 className="text_24 lg:flex-[0_0_392px]"> - {lang === "en" ? "Description" : "Описание"} + {t("description")} </h4> <p className="flex-1 text_16">{data?.description}</p> </div> @@ -92,7 +93,7 @@ export default async function EventPage({ <div className="flex flex-col lg:flex-row gap-6"> <h4 className="text_24 lg:flex-[0_0_392px]"> - {lang === "en" ? "Theme of event" : "Тематика мероприятия"} + {t("theme")} </h4> <div className="flex-1 text_16" diff --git a/app/(root)/calendar/page.tsx b/app/[locale]/(root)/calendar/page.tsx similarity index 75% rename from app/(root)/calendar/page.tsx rename to app/[locale]/(root)/calendar/page.tsx index 27c7d2a..7df663d 100644 --- a/app/(root)/calendar/page.tsx +++ b/app/[locale]/(root)/calendar/page.tsx @@ -3,26 +3,26 @@ import { BreadCrumbs } from "@/components/ui/bread-crumbs"; import { Title } from "@/components/ui/title"; import { getCalendar } from "@/services/calendar"; import { Metadata } from "next"; -import { cookies } from "next/headers"; +import { getLocale, getTranslations } from "next-intl/server"; export const metadata: Metadata = { title: "TurkmenExpo | Calendar", }; export default async function CalendarPage() { - const lang = cookies().get("lang")?.value ?? "ru"; + const lang = await getLocale(); + const t = await getTranslations("calendar"); const data = await getCalendar(lang); - const title = lang === "en" ? "Calendar of events" : "Календарь мероприятий"; return ( <div className="section-mb pt-10"> <div className="container flex flex-col items-start pt-5 gap-10 md:gap-12"> <div> <div className="mb-[24px]"> - <BreadCrumbs second={title} /> + <BreadCrumbs second={t("title")} /> </div> - <Title text={title} /> + <Title text={t("title")} /> </div> <div className="flex flex-col gap-6 w-full"> {data.data.map((item, i) => ( diff --git a/app/(root)/contacts/page.tsx b/app/[locale]/(root)/contacts/page.tsx similarity index 82% rename from app/(root)/contacts/page.tsx rename to app/[locale]/(root)/contacts/page.tsx index 3d256ea..d9a51bc 100644 --- a/app/(root)/contacts/page.tsx +++ b/app/[locale]/(root)/contacts/page.tsx @@ -2,32 +2,34 @@ import React from "react"; import { BreadCrumbs } from "@/components/ui/bread-crumbs"; import { ContactsForm } from "@/components/shared/contacts-form"; -import { cookies } from "next/headers"; +import { getTranslations } from "next-intl/server"; export default async function ContactsPage() { - const lang = cookies().get("lang")?.value; + const t = await getTranslations("contacts"); return ( <main className="bg-blueBg h-full w-full pt-12"> <div className="container flex flex-col items-start"> <div className="mt-5"> - <BreadCrumbs second={lang === "ru" ? "Контакты" : "Contacts"} /> + <BreadCrumbs second={t("title")} /> </div> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full"> <ContactsForm /> <div className="p-6 bg-bg_surface_container rounded-[8px]"> - <h2 className="h2 mb-10 xl:mb-8 text-3xl font-normal">Контакты</h2> + <h2 className="h2 mb-10 xl:mb-8 text-3xl font-normal"> + {t("title")} + </h2> <div className="flex flex-col gap-20"> <div className="flex items-center gap-6"> <img src="/assets/icons/contacts/address.svg" alt="address" /> <div> - <h3 className="text-xl mb-2">Адрес:</h3> + <h3 className="text-xl mb-2">{t("address")}</h3> <address className="text-base normal not-italic"> - 744000, г. Ашхабад, просп. Битарап Туркменистан, 183 + {t("addressValue")} </address> </div> </div> @@ -36,9 +38,7 @@ export default async function ContactsPage() { <div> <h3 className="text-xl mb-2"></h3> - <h4 className="text-base normal"> - +99371871814; 99363719588 - </h4> + <h4 className="text-base normal">{t("phoneNumbers")}</h4> </div> </div> diff --git a/app/(root)/layout.tsx b/app/[locale]/(root)/layout.tsx similarity index 100% rename from app/(root)/layout.tsx rename to app/[locale]/(root)/layout.tsx diff --git a/app/(root)/page.tsx b/app/[locale]/(root)/page.tsx similarity index 85% rename from app/(root)/page.tsx rename to app/[locale]/(root)/page.tsx index 47a29e6..a1877d4 100644 --- a/app/(root)/page.tsx +++ b/app/[locale]/(root)/page.tsx @@ -4,11 +4,11 @@ import { Partners } from "@/components/shared/Partners"; import { Reviews } from "@/components/shared/Reviews"; import { Slider } from "@/components/shared/Slider"; import Loader from "@/components/ui/Loader"; -import { cookies } from "next/headers"; +import { getLocale } from "next-intl/server"; import { Suspense } from "react"; export default async function HomePage() { - const lang = cookies().get("lang")?.value ?? "ru"; + const lang = await getLocale(); return ( <div className="bg-blueBg flex flex-col gap-[60px] md:gap-20 pb-20 mt-[70px]"> @@ -19,7 +19,6 @@ export default async function HomePage() { </Suspense> <News /> <Reviews /> - {/* <Video /> */} <Partners /> </div> ); diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..f500ded --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,45 @@ +import { Roboto } from "next/font/google"; +import clsx from "clsx"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages, setRequestLocale } from "next-intl/server"; +import { notFound } from "next/navigation"; +import { locales } from "@/i18n/config"; +import StoreProvider from "../StoreProvider"; +import "../globals.css"; + +const roboto = Roboto({ + subsets: ["latin"], + weight: ["300", "400", "500", "700"], + variable: "--font-roboto", +}); + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })); +} + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode; + params: { locale: string }; +}) { + const { locale } = params; + + if (!locales.includes(locale as any)) notFound(); + + setRequestLocale(locale); + const messages = await getMessages(); + + return ( + <html lang={locale}> + <StoreProvider> + <body className={clsx("antialiased font-roboto", roboto.variable)}> + <NextIntlClientProvider locale={locale} messages={messages}> + {children} + </NextIntlClientProvider> + </body> + </StoreProvider> + </html> + ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 6a1ed55..f758967 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,4 @@ import type { Metadata } from "next"; -import { Roboto } from "next/font/google"; -import StoreProvider from "./StoreProvider"; - -import "./globals.css"; -import clsx from "clsx"; - -const roboto = Roboto({ - subsets: ["latin"], - weight: ["300", "400", "500", "700"], - variable: "--font-roboto", -}); export const metadata: Metadata = { title: "TurkmenExpo", @@ -29,13 +18,5 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return ( - <html> - <StoreProvider> - <body className={clsx("antialiased font-roboto", roboto.variable)}> - {children} - </body> - </StoreProvider> - </html> - ); + return children; } diff --git a/app/middleware.ts b/app/middleware.ts deleted file mode 100644 index b446eca..0000000 --- a/app/middleware.ts +++ /dev/null @@ -1,31 +0,0 @@ -// middleware.ts -import { NextRequest, NextResponse } from "next/server"; - -export function middleware(request: NextRequest) { - // 1. Проверяем существующую куку - let lang = request.cookies.get("lang")?.value; - - // 2. Если куки нет, определяем язык из браузера - if (!lang) { - lang = - request.headers - .get("Accept-Language") - ?.split(",")[0] // Берём первый язык из списка - .split("-")[0] // Убираем регион (en-US → en) - .toLowerCase() || "en"; // Fallback на английский - } - - // 3. Сохраняем язык в куки - const response = NextResponse.next(); - if (!request.cookies.has("lang")) { - response.cookies.set("lang", lang, { - maxAge: 60 * 60 * 24 * 365, // 1 год - httpOnly: true, - }); - } - - // 4. Добавляем язык в заголовки ВСЕХ исходящих запросов - response.headers.set("Accept-Language", lang); - - return response; -} diff --git a/components/page/LayoutWithSidebar.tsx b/components/page/LayoutWithSidebar.tsx index 7675aa5..ca634be 100644 --- a/components/page/LayoutWithSidebar.tsx +++ b/components/page/LayoutWithSidebar.tsx @@ -2,9 +2,9 @@ import React from "react"; import { BreadCrumbs } from "../ui/bread-crumbs"; -import { usePathname } from "next/navigation"; -import { useAppSelector } from "@/redux/hooks"; +import { useTranslations } from "next-intl"; import { Title } from "../ui/title"; +import { usePathname } from "@/i18n/navigation"; interface Props { title: string; @@ -26,20 +26,14 @@ export const LayoutWithSidebar = ({ cursor = false, }: Props) => { const pathname = usePathname(); - const lang = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const t = useTranslations("services"); return ( <div className="flex bg-white px-6 py-4 rounded-lg flex-col md:gap-6 gap-10 section-mb w-full"> <div> <BreadCrumbs second={ - pathname.includes("/services") - ? lang === "en" - ? "Services" - : "Сервисы" - : second ?? "" + pathname.includes("/services") ? t("breadcrumb") : second ?? "" } path={path} path2={path2} diff --git a/components/page/Pathnames.tsx b/components/page/Pathnames.tsx deleted file mode 100644 index 60114f7..0000000 --- a/components/page/Pathnames.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -import React from "react"; -import Link from "next/link"; - -import { topMenu } from "@/lib/database/pathnames"; - -export const Pathnames = ({ sort }: { sort: string }) => { - return ( - <div className="text-[12px] text-gray4 flex items-center mob:mb-6 mb-5"> - <Link href={"/"}>{"Главная "}</Link> - {topMenu - .filter((item) => item.path === sort) - .map((obj, i) => - obj.links.map((item) => <p key={i}>{item.default}</p>) - )} - </div> - ); -}; diff --git a/components/shared/Card.tsx b/components/shared/Card.tsx index 139bc1f..d62b1eb 100644 --- a/components/shared/Card.tsx +++ b/components/shared/Card.tsx @@ -1,6 +1,6 @@ import React from "react"; import Image from "next/image"; -import Link from "next/link"; +import { Link } from "@/i18n/navigation"; interface Props { img: string; diff --git a/components/shared/Events.tsx b/components/shared/Events.tsx index 0432b29..08c547c 100644 --- a/components/shared/Events.tsx +++ b/components/shared/Events.tsx @@ -1,4 +1,4 @@ -import { getEvents } from "@/services/home"; +import { getTranslations } from "next-intl/server"; const events = [ { @@ -22,44 +22,18 @@ const events = [ ]; export const Events = async ({ lang }: { lang: string }) => { - // const data = await getEvents(lang); - - const btnText = - lang === "en" ? "Show more" : lang === "ru" ? "Показать еще" : "Show more"; - - const title = lang === "en" ? "Exhibitions" : "Выставки"; - - const linkText = lang === "en" ? "Go to website" : "Перейти на сайт"; + const t = await getTranslations("home"); return ( <section> <div className="container md:block"> <div className="sm:mb-10"> <h2 className="text-center font-semibold text-3xl mb-8"> - {title} <span className="text-PRIMARY">Turkmen</span> + {t("exhibitions")} <span className="text-PRIMARY">Turkmen</span> <span className="text-red">Expo 2025</span> </h2> - {/* <Title - text={ - lang === "en" - ? "Upcoming exhibitions and events" - : lang === "ru" - ? "Ближайшие выставки и мероприятия" - : "Upcoming exhibitions and events" - } - /> */} </div> - {/* <div className="w-full flex flex-col items-center gap-8"> */} <div className="w-full flex flex-col items-center gap-8 lg:flex-row"> - {/* {data.data.slice(0, 2).map((item, i) => ( - <EventCard - coorganizers={item.coorganizers} - dark - key={i} - {...item} - /> - ))} - <LinkButton href="/calendar">{btnText}</LinkButton> */} {events.map((item, i) => ( <article key={i} @@ -87,15 +61,13 @@ export const Events = async ({ lang }: { lang: string }) => { target="_blank" className="text-PRIMARY underline" > - {linkText} + {t("goToWebsite")} </a> </div> </article> ))} </div> </div> - - {/* <EventsMobile data={data.data.slice(0, 5)} /> */} </section> ); }; diff --git a/components/shared/Footer.tsx b/components/shared/Footer.tsx index 52135d7..ec70a4d 100644 --- a/components/shared/Footer.tsx +++ b/components/shared/Footer.tsx @@ -1,11 +1,10 @@ "use client"; import React from "react"; -import Link from "next/link"; +import { useTranslations } from "next-intl"; -import { footerInfo, headerMenu2 } from "@/lib/database/pathnames"; -import { useAppSelector } from "@/redux/hooks"; -import clsx from "clsx"; +import { headerMenu2 } from "@/lib/database/pathnames"; +import { Link } from "@/i18n/navigation"; export const socials = [ { href: "https://www.linkedin.com/company/turkmen-expo", icon: "linkedin" }, @@ -24,9 +23,7 @@ export const socials = [ ]; export const Footer = () => { - const localization = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const t = useTranslations(); return ( <footer className="bg-PRIMARY text-white pt-6 pb-5 mob:py-[40px]"> @@ -36,40 +33,30 @@ export const Footer = () => { <img className="h-12" src="/assets/icons/white-logo.svg" /> <div className="flex flex-col mt-8 lg:flex-row items-center font-medium gap-12"> - {headerMenu2 - .filter((item) => (localization === "en" ? item.en : !item.en)) - .map((item, i) => ( - <Link key={i} href={item.link} className="cursor-pointer"> - {item.title} - </Link> - ))} + {headerMenu2.map((item, i) => ( + <Link key={i} href={item.link} className="cursor-pointer"> + {t(item.titleKey)} + </Link> + ))} </div> </div> <div className="flex flex-col lg:flex-row lg:justify-between justify-center text-center lg:text-left lg:items-end items-center gap-6"> <div className="flex flex-col justify-end w-full"> - {localization === "ru" ? ( - <div className="flex flex-col md:gap-y-[10px] gap-2"> - {footerInfo.slice(0, 4).map((item, i) => ( - <p - className={clsx("text-[12px] leading-[130%]", { - "ml-8": i === 2, - })} - key={i} - > - {item} - </p> - ))} - </div> - ) : localization === "en" ? ( - <div className="flex flex-col md:gap-y-[10px] gap-0"> - {footerInfo.slice(3, 7).map((item, i) => ( - <p className="text-[12px] leading-[130%]" key={i}> - {item} - </p> - ))} - </div> - ) : null} + <div className="flex flex-col md:gap-y-[10px] gap-2"> + <p className="text-[12px] leading-[130%]"> + {t("footer.address")} + </p> + <p className="text-[12px] leading-[130%]"> + {t("footer.phone")} + </p> + <p className="text-[12px] leading-[130%] ml-8"> + {t("footer.phone2")} + </p> + <p className="text-[12px] leading-[130%]"> + {t("footer.email")} + </p> + </div> </div> <div className="flex items-center gap-6"> diff --git a/components/shared/Header.tsx b/components/shared/Header.tsx index a124617..6f7464c 100644 --- a/components/shared/Header.tsx +++ b/components/shared/Header.tsx @@ -1,10 +1,10 @@ "use client"; -import React, { Suspense, useEffect, useState } from "react"; +import React, { Suspense, useState } from "react"; import clsx from "clsx"; -import Link from "next/link"; import Image from "next/image"; import { AnimatePresence } from "framer-motion"; +import { useTranslations } from "next-intl"; import logo from "@/public/assets/icons/logo.svg"; import search from "@/public/assets/icons/header/search.svg"; @@ -17,14 +17,14 @@ import { BurgerMenu } from "../ui/burger-menu"; import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { selectHeader, setShowInput } from "@/redux/slices/headerSlice"; import { selectBurger, setBurgerOpen } from "@/redux/slices/burgerSlice"; -import { useStorage } from "@/hooks/useStorage"; +import { Link } from "@/i18n/navigation"; export const Header = () => { const dispatch = useAppDispatch(); const { showInput } = useAppSelector(selectHeader); const { burgerOpen } = useAppSelector(selectBurger); - const { activeLang } = useAppSelector(selectHeader); const [activeLink, setActiveLink] = useState(""); + const t = useTranslations(); const toggleMenu = () => { dispatch(setBurgerOpen(!burgerOpen)); @@ -36,12 +36,6 @@ export const Header = () => { dispatch(setBurgerOpen(false)); }; - const { setItem } = useStorage("language"); - - useEffect(() => { - setItem(activeLang); - }, [activeLang]); - return ( <> <AnimatePresence> @@ -52,23 +46,17 @@ export const Header = () => { )} </AnimatePresence> - {/* Mobile */} - - {/* <div className="h-[74px] tab:hidden"> */} <div className="h-[70px]"> <header className={clsx( - "bg-[#EEF1F0] border-b z-30 border-[#DFE2E1] tab:hidden fixed top-0 left-0 right-0 flex items-center justify-between px-4 py-6", - { - // 'fixed w-full top-0': burgerOpen, - } + "bg-[#EEF1F0] border-b z-30 border-[#DFE2E1] tab:hidden fixed top-0 left-0 right-0 flex items-center justify-between px-4 py-6" )} > <Image src={searchMob} height={32} width={32} - alt="поиск" + alt="search" className="cursor-pointer" onClick={onSearch} /> @@ -84,7 +72,7 @@ export const Header = () => { src={logo} height={24} width={160} - alt="лого" + alt="logo" className="cursor-pointer" /> </Link> @@ -121,11 +109,7 @@ export const Header = () => { <AnimatePresence>{burgerOpen && <BurgerMenu />}</AnimatePresence> </header> - {/* </div> */} - {/* Desktop */} - - {/* <div className="hidden h-[74px] tab:block"> */} <header className="hidden border-b border-[#DFE2E1] fixed top-0 left-0 right-0 z-30 tab:flex flex-col"> <div className="bg-[#EEF1F0] text-black"> <div className="container py-[17px] flex items-center justify-between"> @@ -139,7 +123,7 @@ export const Header = () => { </Suspense> <Image src={search} - alt="поиск" + alt="search" onClick={() => dispatch(setShowInput(true))} className="cursor-pointer" /> @@ -147,25 +131,20 @@ export const Header = () => { </div> <nav className="flex items-center gap-x-5 font-medium"> - {headerMenu2 - .filter((item) => - activeLang.localization === "en" ? item.en : !item.en - ) - .map((item, i) => ( - <Link - key={i} - href={item.link} - onClick={() => setActiveLink(item.link)} - > - {item.title} - </Link> - ))} + {headerMenu2.map((item, i) => ( + <Link + key={i} + href={item.link} + onClick={() => setActiveLink(item.link)} + > + {t(item.titleKey)} + </Link> + ))} </nav> </div> </div> </header> </div> - {/* </div> */} </> ); }; diff --git a/components/shared/News.tsx b/components/shared/News.tsx index 76214b4..b79ca7d 100644 --- a/components/shared/News.tsx +++ b/components/shared/News.tsx @@ -7,8 +7,6 @@ import { Navigation } from "swiper/modules"; import { NewsCard } from "./news-card"; import { NavBtn } from "../home/ui/NavBtn"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; import { NewsData } from "@/lib/types/NewsData.type"; import "swiper/css/bundle"; @@ -17,17 +15,18 @@ import "swiper/css"; import "swiper/css/navigation"; import "swiper/css/pagination"; import "swiper/css/scrollbar"; -import Link from "next/link"; -import { useLang } from "@/utils/useLang"; +import { useTranslations, useLocale } from "next-intl"; import { GreenBtn } from "../ui/Buttons"; import { Title } from "../ui/title"; import Loader from "@/components/ui/Loader"; +import { Link } from "@/i18n/navigation"; export const News = () => { const [newsData, setNewsData] = useState<NewsData>(); const [loading, setLoading] = useState(true); - const { activeLang } = useAppSelector(selectHeader); + const t = useTranslations("home"); + const locale = useLocale(); const fetchNews = async () => { try { @@ -35,7 +34,7 @@ export const News = () => { const response = await fetch(`https://turkmenexpo.com/app/api/v1/news`, { next: { revalidate: 1800 }, headers: { - "Accept-Language": activeLang.localization, + "Accept-Language": locale, }, }); @@ -54,7 +53,7 @@ export const News = () => { useEffect(() => { fetchNews(); - }, [activeLang.localization]); + }, [locale]); if (loading) { <Loader className="h-[300px] w-full mx-auto" />; @@ -64,7 +63,7 @@ export const News = () => { <section> <div className="container w-full"> <header className="flex items-center mb-5 sm:mb-[43px] justify-between"> - <Title text={useLang("News", "Новости", activeLang.localization)} /> + <Title text={t("news")} /> <div className="hidden sm:flex items-center gap-5"> <NavBtn left /> <NavBtn /> @@ -105,9 +104,7 @@ export const News = () => { </div> <Link href={"/news"} className="hidden sm:flex justify-center"> - <GreenBtn - text={useLang("All news", "Все новости", activeLang.localization)} - /> + <GreenBtn text={t("allNews")} /> </Link> </div> </section> diff --git a/components/shared/Partners.tsx b/components/shared/Partners.tsx index 454c56d..30f36b5 100644 --- a/components/shared/Partners.tsx +++ b/components/shared/Partners.tsx @@ -5,22 +5,21 @@ import Image from "next/image"; import { Swiper, SwiperSlide } from "swiper/react"; import { Autoplay, Pagination } from "swiper/modules"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; import { PartnersType } from "@/lib/types/PartnersData.type"; import { baseAPI } from "@/lib/API"; -import { useLang } from "@/utils/useLang"; +import { useTranslations, useLocale } from "next-intl"; import { Title } from "../ui/title"; export const Partners = () => { - const { activeLang } = useAppSelector(selectHeader); + const locale = useLocale(); + const t = useTranslations("home"); const [partnersData, setPartnersData] = useState<PartnersType>(); const fetchPartners = async () => { try { const res = await fetch(`${baseAPI}partners`, { headers: { - "Accept-Language": activeLang.localization, + "Accept-Language": locale, }, }); @@ -34,14 +33,12 @@ export const Partners = () => { useEffect(() => { fetchPartners(); - }, [activeLang.localization]); + }, [locale]); return ( <section className="container"> <div className="mb-[40px]"> - <Title - text={useLang("Partners", "Партнёры", activeLang.localization)} - /> + <Title text={t("partners")} /> </div> <div className="flex items-center"> diff --git a/components/shared/Reviews.tsx b/components/shared/Reviews.tsx index aefec7f..84b766a 100644 --- a/components/shared/Reviews.tsx +++ b/components/shared/Reviews.tsx @@ -7,31 +7,21 @@ import "swiper/css"; import "swiper/css/pagination"; import { useFetch } from "@/hooks/useFetch"; import { ReviewsType } from "@/lib/types/Reviews.type"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; +import { useTranslations } from "next-intl"; import { Title } from "../ui/title"; import Image from "next/image"; export const Reviews = () => { const { data } = useFetch<ReviewsType>("reviews"); - - const { - activeLang: { localization }, - } = useAppSelector(selectHeader); - - const title = localization === "en" ? "Feedback" : "Обратная связь"; + const t = useTranslations("home"); return ( <section className="container my-20 pb-20 overflow-hidden"> - <Title text={title} className="!text-center mb-10" /> + <Title text={t("feedback")} className="!text-center mb-10" /> <div className="max-w-[1200px] mx-auto px-4 md:px-0"> <Swiper modules={[Autoplay, Pagination]} - // autoplay={{ - // delay: 6000, - // disableOnInteraction: false, - // }} style={{ overflow: "visible" }} freeMode speed={1000} @@ -87,7 +77,6 @@ export const Reviews = () => { ))} </Swiper> - {/* Кастомная пагинация */} <div className="custom-pagination flex justify-center items-center mt-60"></div> </div> </section> diff --git a/components/shared/Slider.tsx b/components/shared/Slider.tsx index 32e9ade..7b5c979 100644 --- a/components/shared/Slider.tsx +++ b/components/shared/Slider.tsx @@ -14,7 +14,7 @@ import { useMediaQuery } from "usehooks-ts"; import { Autoplay, Pagination } from "swiper/modules"; import Loader from "../ui/Loader"; import { baseAPI } from "@/lib/API"; -import Link from "next/link"; +import { Link } from "@/i18n/navigation"; import clsx from "clsx"; export const Slider = ({ lang }: { lang: string }) => { diff --git a/components/shared/contacts-form.tsx b/components/shared/contacts-form.tsx index c19abc0..537a4f5 100644 --- a/components/shared/contacts-form.tsx +++ b/components/shared/contacts-form.tsx @@ -1,28 +1,36 @@ "use client"; import { FC, useState } from "react"; -import { Form, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import clsx from "clsx"; import { postContacts } from "@/services/contacts"; +import { useTranslations } from "next-intl"; interface Props { className?: string; } -const formSchema = z.object({ - name: z.string().min(2, "Имя необходимо"), - email: z.string().email("Email необходим"), - phone: z.string().min(8, "Номер телефона необходим"), - company: z.string().optional(), - msg: z.string().min(5, "Сообщение необходимо"), -}); - -export type FormType = z.infer<typeof formSchema>; +export type FormType = { + name: string; + email: string; + phone: string; + company?: string; + msg: string; +}; export const ContactsForm: FC<Props> = ({ className }) => { const [status, setStatus] = useState(false); + const t = useTranslations(); + + const formSchema = z.object({ + name: z.string().min(2, t("validation.nameRequired")), + email: z.string().email(t("validation.emailRequired")), + phone: z.string().min(8, t("validation.phoneRequired")), + company: z.string().optional(), + msg: z.string().min(5, t("validation.messageRequired")), + }); const { register, handleSubmit, formState, reset } = useForm({ resolver: zodResolver(formSchema), @@ -50,13 +58,13 @@ export const ContactsForm: FC<Props> = ({ className }) => { <div className={clsx("bg-PRIMARY rounded-[8px] py-8 px-6", className)}> <form onSubmit={handleSubmit(onSubmit)}> <h2 className="h2 !text-white text-3xl font-medium lg:mb-8 mb-6"> - Связаться с нами + {t("contacts.contactUs")} </h2> <div className="flex flex-col gap-8"> <div className="flex flex-col relative"> <label htmlFor="name" className="label"> - Имя + {t("contacts.name")} </label> <input type="text" @@ -70,7 +78,7 @@ export const ContactsForm: FC<Props> = ({ className }) => { <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-center"> <div className="flex flex-col relative"> <label htmlFor="email" className="label"> - E-mail + {t("contacts.email")} </label> <input type="text" @@ -83,7 +91,7 @@ export const ContactsForm: FC<Props> = ({ className }) => { <div className="flex flex-col relative"> <label htmlFor="phone" className="label"> - Номер + {t("contacts.phone")} </label> <input type="text" @@ -97,7 +105,7 @@ export const ContactsForm: FC<Props> = ({ className }) => { <div className="flex flex-col relative"> <label htmlFor="company" className="label"> - Название компании + {t("contacts.companyName")} </label> <input type="text" @@ -110,7 +118,7 @@ export const ContactsForm: FC<Props> = ({ className }) => { <div className="flex flex-col relative"> <label htmlFor="msg" className="label"> - Сообщение + {t("contacts.message")} </label> <textarea rows={3} @@ -124,7 +132,7 @@ export const ContactsForm: FC<Props> = ({ className }) => { disabled={formState.isSubmitting || status} className="bg-[#A4FFF3] text-sm text-ON_PRIMARY_CONTAINER h-10 rounded-[2px]" > - {!status ? "Отправить" : "Форма отправлена"} + {!status ? t("common.send") : t("common.formSubmitted")} </button> </div> </form> diff --git a/components/shared/event-card.tsx b/components/shared/event-card.tsx index fe3c0b9..32e9dca 100644 --- a/components/shared/event-card.tsx +++ b/components/shared/event-card.tsx @@ -10,9 +10,8 @@ import { Organizer, Timing, } from "@/lib/types/Calendar.type"; -import { useAppSelector } from "@/redux/hooks"; -import { useLang } from "@/utils/useLang"; -import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { Link } from "@/i18n/navigation"; interface Props { id: number; @@ -54,9 +53,7 @@ export const EventCard = ({ return formattedDate; }; - const lang = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const t = useTranslations("events"); return ( <Link className="w-full cursor-default" href={`/calendar/${id}`}> @@ -97,7 +94,6 @@ export const EventCard = ({ className="lg:size-[228px] size-[100px] object-cover rounded-l-[4px]" /> - {/* CENTER */} <div className="flex flex-col gap-[10px] md:gap-4 max-w-[683px] py-6 w-full"> <p className="text-[12px] text-[#6B7674] font-normal leading-none"> {category} @@ -110,13 +106,10 @@ export const EventCard = ({ > {title} </h3> - {/* <p className={clsx('text-base line-clamp-2 leading-[130%] md:leading-[150%]', {})}> - {description} - </p> */} </div> <div className="flex flex-col gap-[10px]"> <p className="text-[#6B7674] uppercase font-normal leading-none text-[10px]"> - {useLang("Organiser:", "Организатор:", lang)} + {t("organiserLabel")} </p> <p className="text-[#6B7674] font-normal text-[13px] leading-[125%]"> {organizers ? organizers[0]?.name : null} @@ -125,7 +118,7 @@ export const EventCard = ({ {coorganizers && coorganizers?.length > 0 && ( <div className="flex flex-col gap-[10px]"> <p className="text-[#6B7674] uppercase font-normal leading-none text-[10px]"> - {useLang("Co-organiser:", "Со-организатор:", lang)} + {t("coOrganiserLabel")} </p> <p className="text-[#6B7674] font-normal text-[13px] leading-[125%]"> {coorganizers ? coorganizers[0]?.name : null} @@ -133,7 +126,6 @@ export const EventCard = ({ </div> )} </div> - {/* CENTER */} <div className="hidden md:flex text-2xl flex-col text-white leading- none h-[228px] min-w-[245px] items-center justify-center bg-[#31A898] px-12 text-center"> <p className="">{formatDate(starts_at)}</p>{" "} diff --git a/components/shared/event-page-buttons.tsx b/components/shared/event-page-buttons.tsx index 276a667..04b8191 100644 --- a/components/shared/event-page-buttons.tsx +++ b/components/shared/event-page-buttons.tsx @@ -1,7 +1,7 @@ "use client"; -import { useAppSelector } from "@/redux/hooks"; -import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { Link } from "@/i18n/navigation"; import { FC } from "react"; interface Props { @@ -18,9 +18,7 @@ export const EventPageButtons: FC<Props> = ({ visit, details, }) => { - const lang = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const t = useTranslations("events"); return ( <div className="flex items-center gap-6"> @@ -28,19 +26,19 @@ export const EventPageButtons: FC<Props> = ({ <> <Link href={register || ""} className="w-full" target="_blank"> <button className="full-btn bg-PRIMARY py-2.5 text-white"> - {lang === "en" ? "Registration" : "Зарегистрироваться"} + {t("registration")} </button> </Link> <Link href={visit || ""} className="w-full" target="_blank"> <button className="full-btn bg-SECONDARY_CONTAINER py-2.5 text-ON_SECONDARY_CONTAINER"> - {lang === "en" ? "Visit site" : "Посетить сайт"} + {t("visitSite")} </button> </Link> </> ) : ( <Link href={details || ""} className="w-full" target="_blank"> <button className="full-btn w-full bg-PRIMARY py-2.5 text-white"> - {lang === "en" ? "More" : "Подробнее"} + {t("more")} </button> </Link> )} diff --git a/components/shared/events-mobile.tsx b/components/shared/events-mobile.tsx index 67f6c35..84257df 100644 --- a/components/shared/events-mobile.tsx +++ b/components/shared/events-mobile.tsx @@ -2,14 +2,12 @@ import "swiper/css"; import "swiper/css/pagination"; -// import "./styles/events.css"; -import { useLang } from "@/utils/useLang"; +import { useTranslations } from "next-intl"; import { FC } from "react"; import { Swiper, SwiperSlide } from "swiper/react"; import { EventCard } from "./event-card"; import { CalendarType } from "@/lib/types/Calendar.type"; -import { useAppSelector } from "@/redux/hooks"; interface Props { data: CalendarType["data"]; @@ -17,17 +15,12 @@ interface Props { } export const EventsMobile: FC<Props> = ({ data }) => { - const lang = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const t = useTranslations("home"); + return ( <div className="md:hidden container"> <h2 className="text-[26px] mb-5 sm:mb-10 font-semibold leading-[115%]"> - {useLang( - "Upcoming exhibitions and events", - "Ближайшие выставки и мероприятия", - lang - )} + {t("upcomingExhibitions")} </h2> <div className="flex flex-col"> <div className="flex items-center gap-y-[10px]"> diff --git a/components/shared/news-card.tsx b/components/shared/news-card.tsx index 026d9dc..a54a03e 100644 --- a/components/shared/news-card.tsx +++ b/components/shared/news-card.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Image from 'next/image'; -import Link from 'next/link'; +import { Link } from '@/i18n/navigation'; interface Props { img: string; diff --git a/components/shared/search-input.tsx b/components/shared/search-input.tsx index c6ac046..bc2caae 100644 --- a/components/shared/search-input.tsx +++ b/components/shared/search-input.tsx @@ -1,10 +1,8 @@ "use client"; import React, { useEffect, useState } from "react"; -import Image from "next/image"; import { motion } from "framer-motion"; import { useAppDispatch, useAppSelector } from "@/redux/hooks"; -import close from "@/public/assets/icons/home/close-input.svg"; import { selectInput, setInputStatus } from "@/redux/slices/inputSlice"; import { v4 } from "uuid"; @@ -12,20 +10,14 @@ import { selectHeader, setShowInput } from "@/redux/slices/headerSlice"; import clsx from "clsx"; import { selectBurger } from "@/redux/slices/burgerSlice"; import { baseAPI } from "@/lib/API"; -import Link from "next/link"; import { SearchTypes } from "@/lib/types/Search.type"; import { useMediaQuery } from "usehooks-ts"; - -export const inputRadio = [ - { name: "Везде", id: "all" }, - { name: "В событиях", id: "events" }, - { name: "В новостях", id: "news" }, -]; +import { useTranslations, useLocale } from "next-intl"; +import { Link } from "@/i18n/navigation"; export const SearchInput = ({ mob = false }: { mob?: boolean }) => { - const localization = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const locale = useLocale(); + const t = useTranslations("search"); const wrapper = document.querySelector(".wrapper"); const tab = useMediaQuery("(min-width: 980px)"); @@ -38,12 +30,17 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { const { showInput } = useAppSelector(selectHeader); + const inputRadio = [ + { name: t("all"), id: "all" }, + { name: t("inEvents"), id: "events" }, + { name: t("inNews"), id: "news" }, + ]; + useEffect(() => { wrapper?.classList.remove("overflow-hidden"); wrapper?.classList.add("overflow-hidden"); setStatus(inputRadio[0].id); - inputRadio; return () => { wrapper?.classList.remove("overflow-hidden"); @@ -56,7 +53,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { try { const res = await fetch(`${baseAPI}search?search=${value}`, { headers: { - "Accept-Language": localization, + "Accept-Language": locale, "Content-Type": "application/json", }, }); @@ -135,7 +132,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { } value={value} type="text" - placeholder="Что найти?" + placeholder={t("placeholder")} className="p-3 w-full leading-[150%] placeholder:leading-[150%] placeholder:text-gray focus:outline-none rounded-sm bg-transparent border-[1px] border-[#BCC4CC]" /> </div> @@ -167,7 +164,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { </div> ) : ( <div className="bg-green py-[17px] px-[70px] rounded-sm"> - Найти + {t("findButton")} </div> )} </button> @@ -177,11 +174,12 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { searchData.data.posts.length ? ( <> <div className="mb-12 font-light"> - По запросу « <span className="font-bold">{searchValue}</span>{" "} - » нашлось + {t("byRequest")} + <span className="font-bold">{searchValue}</span> + {t("found")}{" "} {searchData.data.expo_events.length + searchData.data.posts.length}{" "} - результатов + {t("results")} </div> <div className="flex flex-col gap-9 w-full tab:mb-0 mb-20"> {inputStatus === "all" || inputStatus === "events" @@ -191,7 +189,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { {item.title} </h3> <Link href={`/calendar/${item.id}`}> - Перейти на страницу + {t("goToPage")} </Link> <hr className="mt-9 border-OUTLINE_VAR" /> </div> @@ -204,7 +202,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { {item.title} </h3> <Link href={`/news/${item.id}`}> - Перейти на страницу + {t("goToPage")} </Link> <hr className="mt-9 border-OUTLINE_VAR" /> </div> @@ -213,7 +211,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => { </div> </> ) : ( - <h3>По вашему запросу ничего не найдено</h3> + <h3>{t("noResults")}</h3> ) ) : null} </div> diff --git a/components/shared/services.tsx b/components/shared/services.tsx index 10b0e2a..b20f598 100644 --- a/components/shared/services.tsx +++ b/components/shared/services.tsx @@ -4,13 +4,10 @@ import React from "react"; import Image from "next/image"; import { contactCardData, roadCardData } from "@/lib/database/homeInfoData"; -import { useLang } from "@/utils/useLang"; -import { useAppSelector } from "@/redux/hooks"; +import { useTranslations } from "next-intl"; export const Services = () => { - const localization = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const t = useTranslations(); return ( <div @@ -18,7 +15,7 @@ export const Services = () => { > <div className="flex flex-col items-start gap-x-5 w-full"> <h3 className="text-[21px] leading-[100%] font-bold text-bgWhite md:text-[#B0E6A1] uppercase md:mb-10 mb-5"> - {useLang("Routes", "как добраться", localization)} + {t("services.routes")} </h3> <div className="flex flex-col gap-y-5 w-full"> {roadCardData.map((item, i) => ( @@ -29,7 +26,7 @@ export const Services = () => { <div className="flex items-center gap-10 md:gap-5"> <Image className="img-auto" src={item.icon} alt="icon" /> <p className="text-[16px] leading-[120%] md:leading-[100%]"> - {localization === "ru" ? item.text : item.enText} + {t(item.textKey)} </p> </div> </div> @@ -38,7 +35,7 @@ export const Services = () => { </div> <div className="flex flex-col items-start md:mt-0 mt-10 gap-x-5 w-full"> <h3 className="text-[21px] leading-[100%] text-bgWhite md:text-[#FFD288] uppercase font-bold md:mb-10 mb-5"> - {useLang("Contacts", "контакты", localization)} + {t("contacts.title")} </h3> <div className="flex flex-col gap-y-5 w-full"> {contactCardData.map((item, i) => ( @@ -49,7 +46,7 @@ export const Services = () => { <div className="flex items-center md:gap-5 gap-10"> <Image src={item.icon} alt="icon" /> <p className="text-[16px] leading-[120%] md:leading-[100%]"> - {localization === "ru" ? item.text : item.enText} + {t(item.textKey)} </p> </div> </div> diff --git a/components/shared/slider-client.tsx b/components/shared/slider-client.tsx index 6e3acb9..82a12e0 100644 --- a/components/shared/slider-client.tsx +++ b/components/shared/slider-client.tsx @@ -3,9 +3,9 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import { useMediaQuery } from "usehooks-ts"; -import { useAppSelector } from "@/redux/hooks"; import { useSliderBanner } from "@/hooks/use-slider"; import Loader from "../ui/Loader"; +import { useLocale } from "next-intl"; export const SliderClient = () => { const isTab = useMediaQuery("(min-width: 1024px)"); @@ -14,16 +14,14 @@ export const SliderClient = () => { const [data, setData] = useState<any>(null); const [loading, setLoading] = useState(true); const bannerType = useSliderBanner(isTab, isMd); - const lang = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const locale = useLocale(); const fetchData = async () => { try { setLoading(true); const res = await fetch(`/api/banners?bannerType=${bannerType}`, { headers: { - "Accept-Language": lang, + "Accept-Language": locale, }, }); const json = await res.json(); @@ -36,7 +34,7 @@ export const SliderClient = () => { useEffect(() => { fetchData(); - }, [bannerType, lang]); + }, [bannerType, locale]); if (loading) return <Loader className="h-[600px] min-h-[320px]" />; diff --git a/components/ui/LangMenu.tsx b/components/ui/LangMenu.tsx index 9af3b16..b1238ca 100644 --- a/components/ui/LangMenu.tsx +++ b/components/ui/LangMenu.tsx @@ -6,51 +6,27 @@ import clsx from "clsx"; import { motion, AnimatePresence } from "framer-motion"; import triangle from "@/public/assets/icons/drop-icon.svg"; -import { useAppSelector, useAppDispatch } from "@/redux/hooks"; -import { activeLangType, selectHeader } from "@/redux/slices/headerSlice"; -import { setActiveLang } from "@/redux/slices/headerSlice"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useLocale } from "next-intl"; +import { useRouter, usePathname } from "@/i18n/navigation"; +import { locales } from "@/i18n/config"; -export const lang = [ - { - title: "Русский", - localization: "ru", - }, - // { - // title: 'Tm', - // localization: 'tm', - // }, - { - title: "English", - localization: "en", - }, -]; +const langNames: Record<string, string> = { + ru: "Русский", + en: "English", +}; export const LangMenu = () => { - const { activeLang } = useAppSelector(selectHeader); - const { refresh } = useRouter(); + const locale = useLocale(); + const router = useRouter(); + const pathname = usePathname(); const [active, setActive] = useState(false); - const dispatch = useAppDispatch(); const menuRef = useRef<HTMLDivElement>(null); - const setLang = (lang: activeLangType) => { - // Устанавливаем cookie через document.cookie - document.cookie = `lang=${lang.localization}; path=/; max-age=31536000`; - dispatch(setActiveLang(lang)); - refresh(); + + const switchLocale = (newLocale: string) => { + router.replace(pathname, { locale: newLocale }); + setActive(false); }; - useEffect(() => { - const cookieLang = document.cookie - .split("; ") - .find((row) => row.startsWith("lang=")) - ?.split("=")[1]; - - if (cookieLang) { - const foundLang = lang.find((l) => l.localization === cookieLang); - if (foundLang) dispatch(setActiveLang(foundLang)); - } - }, [dispatch]); - useEffect(() => { const handleClick = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { @@ -72,7 +48,7 @@ export const LangMenu = () => { }} > <div className="flex items-center px-[12px]"> - <p>{activeLang.title}</p> + <p>{langNames[locale]}</p> <Image src={triangle} alt="arrow" @@ -98,21 +74,15 @@ export const LangMenu = () => { }} className="absolute rounded-[4px] w-[112px] overflow-hidden custom-shadow z-10 flex flex-col top-[27px] bg-[#EEF1F0] transition-all duration-300" > - {lang - .filter((item) => item.title !== activeLang.title) - .map((item, i) => ( + {locales + .filter((l) => l !== locale) + .map((l, i) => ( <div key={i} - onClick={() => setLang(item)} - className={clsx( - "p-3 pr-[22px] py-4 text-extraSm transition-all", - { - "hover:bg-ON_SECONDARY_CONTAINER/[8%]": - item.title === item.title, - } - )} + onClick={() => switchLocale(l)} + className="p-3 pr-[22px] py-4 text-extraSm transition-all hover:bg-ON_SECONDARY_CONTAINER/[8%]" > - {item.title} + {langNames[l]} </div> ))} </motion.div> diff --git a/components/ui/Pagination.tsx b/components/ui/Pagination.tsx index b0c5528..4982fb2 100644 --- a/components/ui/Pagination.tsx +++ b/components/ui/Pagination.tsx @@ -1,6 +1,7 @@ "use client"; import React, { Dispatch, SetStateAction } from "react"; +import { useTranslations } from "next-intl"; interface IProps { lastPage?: number; @@ -11,6 +12,8 @@ interface IProps { } export const Pagination = ({ current, setCurrent, lastPage = 3 }: IProps) => { + const t = useTranslations("common"); + const onNext = () => { setCurrent(current + 1); }; @@ -39,7 +42,9 @@ export const Pagination = ({ current, setCurrent, lastPage = 3 }: IProps) => { <div className="border-[1px] border-OUTLINE_VAR rounded-sm px-3 py-[9px]"> {current} </div> - <p>из {lastPage}</p> + <p> + {t("of")} {lastPage} + </p> <button onClick={onNext} disabled={current >= lastPage} type="button"> <svg width="32" diff --git a/components/ui/Sidebar.tsx b/components/ui/Sidebar.tsx index a1866b0..2779027 100644 --- a/components/ui/Sidebar.tsx +++ b/components/ui/Sidebar.tsx @@ -1,19 +1,15 @@ "use client"; import React from "react"; -import Link from "next/link"; import clsx from "clsx"; -import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { sidebarData } from "@/lib/database/pathnames"; -import { useAppSelector } from "@/redux/hooks"; +import { Link, usePathname } from "@/i18n/navigation"; export const Sidebar = () => { const pathname = usePathname(); - - const lang = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const t = useTranslations(); return ( <div className="flex flex-col bg-white rounded-lg px-6 py-4 items-start gap-y-[12px] pb-5 mt-12 sticky top-0 left-0 overflow-hidden"> @@ -31,7 +27,7 @@ export const Sidebar = () => { <p className={clsx("mb-[16px] text-[16px] font-bold leading-[1.5]")} > - {lang === "ru" ? item.pathname : item.pathnameEn} + {t(item.pathnameKey)} </p> <div className="flex flex-col items-start gap-y-[8px]"> <div className="flex flex-col gap-[10px] pl-4 pr-10"> @@ -47,7 +43,7 @@ export const Sidebar = () => { )} key={i} > - {lang === "ru" ? obj.title : obj.titleEn} + {t(obj.titleKey)} </Link> ))} </div> diff --git a/components/ui/bread-crumbs.tsx b/components/ui/bread-crumbs.tsx index 01b28a8..e6ebfe4 100644 --- a/components/ui/bread-crumbs.tsx +++ b/components/ui/bread-crumbs.tsx @@ -1,9 +1,7 @@ "use client"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; -import { useLang } from "@/utils/useLang"; -import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { Link } from "@/i18n/navigation"; export const BreadCrumbs = ({ second, @@ -18,12 +16,12 @@ export const BreadCrumbs = ({ path2?: string; cursor?: boolean; }) => { - const { activeLang } = useAppSelector(selectHeader); + const t = useTranslations("common"); return ( <div className="text-[12px] text-[#6B7674] flex items-center mob:mb-6 mb-5"> <Link href={"/"}> - {useLang("Home", "Главная", activeLang.localization)} + {t("home")} </Link> <p className="px-1">/</p> diff --git a/components/ui/burger-drop.tsx b/components/ui/burger-drop.tsx index de1497e..b69a91b 100644 --- a/components/ui/burger-drop.tsx +++ b/components/ui/burger-drop.tsx @@ -1,11 +1,12 @@ "use client"; import { burgerMenuData } from "@/lib/database/pathnames"; -import { useAppDispatch, useAppSelector } from "@/redux/hooks"; -import Link from "next/link"; +import { useAppDispatch } from "@/redux/hooks"; +import { Link } from "@/i18n/navigation"; import React from "react"; import Image from "next/image"; import { v4 } from "uuid"; +import { useTranslations } from "next-intl"; import arrow from "@/public/assets/icons/header/burger-arrow.svg"; import { @@ -22,6 +23,7 @@ export const BurgerDrop = ({ setDrop: (name: boolean) => void; }) => { const dispatch = useAppDispatch(); + const t = useTranslations(); return burgerMenuData .filter((obj) => !obj.only) @@ -38,7 +40,7 @@ export const BurgerDrop = ({ className="cursor-pointer flex items-center gap-[10px] mb-[10px]" > <Image src={arrow} alt="стрелка" className="rotate-180" /> - <h3 className="leading-[135%] text-[18px]">{item.title}</h3> + <h3 className="leading-[135%] text-[18px]">{t(item.titleKey)}</h3> </div> <hr className="border-bgWhite mb-5" /> @@ -53,7 +55,7 @@ export const BurgerDrop = ({ setDrop(false); }} > - {subj.title} + {t(subj.titleKey)} </Link> ))} </div> diff --git a/components/ui/burger-menu.tsx b/components/ui/burger-menu.tsx index 9b6d76e..90cbe76 100644 --- a/components/ui/burger-menu.tsx +++ b/components/ui/burger-menu.tsx @@ -1,88 +1,43 @@ "use client"; -import Link from "next/link"; import { useEffect, useState } from "react"; import { v4 } from "uuid"; import { motion } from "framer-motion"; +import { useLocale, useTranslations } from "next-intl"; -import { useAppDispatch, useAppSelector } from "@/redux/hooks"; +import { useAppDispatch } from "@/redux/hooks"; import { setBurgerOpen } from "@/redux/slices/burgerSlice"; -import { activeLangType, setActiveLang } from "@/redux/slices/headerSlice"; import clsx from "clsx"; import { burgerMenu, burgerMenu2 } from "@/lib/database/header"; -import { useRouter } from "next/navigation"; -import { lang } from "./LangMenu"; +import { useRouter, usePathname } from "@/i18n/navigation"; +import { Link } from "@/i18n/navigation"; +import { locales } from "@/i18n/config"; -interface flagTypes { - // title: 'Ру' | 'En' | 'Tm'; - title: "Ру" | "En"; - localization: "ru" | "en"; - // localization: 'ru' | 'en' | 'tm'; -} - -const burgerLangs: flagTypes[] = [ - // { - // title: 'Tm', - // localization: 'tm', - // }, - { - title: "Ру", - localization: "ru", - }, - { - title: "En", - localization: "en", - }, +const burgerLangs = [ + { title: "Ру", localization: "ru" as const }, + { title: "En", localization: "en" as const }, ]; export const BurgerMenu = () => { const dispatch = useAppDispatch(); - const wrapper = document.querySelector(".wrapper"); - const { refresh } = useRouter(); - - const localization = useAppSelector( - (state) => state.headerSlice.activeLang.localization - ); + const locale = useLocale(); + const t = useTranslations(); + const router = useRouter(); + const pathname = usePathname(); const [activeMenu, setActiveMenu] = useState<string>(""); const [activeMenu2, setActiveMenu2] = useState<string>(""); const setActiveTitle = () => { - if (activeMenu.includes("/ser")) - return ( - (localization === "ru" && "Услуги") || - (localization === "en" && "Services") - ); + if (activeMenu.includes("/ser")) return t("nav.services"); }; const setActiveTitle2 = () => { - if (activeMenu2.includes("/company")) - return ( - (localization === "ru" && "О компании") || - (localization === "en" && "About company") - ); - }; - - const setLang = (lang: activeLangType) => { - // Устанавливаем cookie через document.cookie - document.cookie = `lang=${lang.localization}; path=/; max-age=31536000`; - dispatch(setActiveLang(lang)); - refresh(); + if (activeMenu2.includes("/company")) return t("nav.about"); }; useEffect(() => { - const cookieLang = document.cookie - .split("; ") - .find((row) => row.startsWith("lang=")) - ?.split("=")[1]; - - if (cookieLang) { - const foundLang = lang.find((l) => l.localization === cookieLang); - if (foundLang) dispatch(setActiveLang(foundLang)); - } - }, [dispatch]); - - useEffect(() => { + const wrapper = document.querySelector(".wrapper"); wrapper?.classList.remove("overflow-hidden"); wrapper?.classList.add("overflow-hidden"); @@ -145,7 +100,7 @@ export const BurgerMenu = () => { onClick={() => dispatch(setBurgerOpen(false))} href={obj.link} > - {localization === "en" ? obj.titleEn : obj.title} + {t(obj.titleKey)} </Link> )) )} @@ -163,8 +118,7 @@ export const BurgerMenu = () => { }} href={item.link} > - {(localization === "en" && item.titleEn) || - (localization === "ru" && item.title)} + {t(item.titleKey)} </Link> ) : ( <div @@ -174,8 +128,7 @@ export const BurgerMenu = () => { setActiveMenu(item.link); }} > - {(localization === "en" && item.titleEn) || - (localization === "ru" && item.title)} + {t(item.titleKey)} {item.drop && ( <svg width="24" @@ -234,8 +187,7 @@ export const BurgerMenu = () => { dispatch(setBurgerOpen(false)); }} > - {(localization === "en" && item.titleEn) || - (localization === "ru" && item.title)} + {t(item.titleKey)} </Link> )) )} @@ -246,9 +198,8 @@ export const BurgerMenu = () => { <div key={i} onClick={() => { - dispatch(setActiveLang(item)); + router.replace(pathname, { locale: item.localization }); dispatch(setBurgerOpen(false)); - setLang(item); }} className="flex cursor-pointer items-center gap-[10px]" > diff --git a/hooks/useFetch.ts b/hooks/useFetch.ts index cc814b0..4540f98 100644 --- a/hooks/useFetch.ts +++ b/hooks/useFetch.ts @@ -1,8 +1,7 @@ "use client"; import { baseAPI } from "@/lib/API"; -import { useAppSelector } from "@/redux/hooks"; -import { selectHeader } from "@/redux/slices/headerSlice"; +import { useLocale } from "next-intl"; import { useState, useEffect } from "react"; interface FetchState<T> { @@ -21,9 +20,7 @@ export const useFetch = <T>( error: null, }); - const { - activeLang: { localization }, - } = useAppSelector(selectHeader); + const locale = useLocale(); useEffect(() => { const fetchData = async () => { @@ -32,7 +29,7 @@ export const useFetch = <T>( const response = await fetch(baseAPI + url, { headers: { - "Accept-Language": localization, + "Accept-Language": locale, }, }); @@ -57,13 +54,7 @@ export const useFetch = <T>( }; fetchData(); - }, [url, localization]); // Добавьте options в зависимости, если они динамические + }, [url, locale]); return state; }; - -// Пример использования -interface User { - id: number; - name: string; -} diff --git a/i18n/config.ts b/i18n/config.ts new file mode 100644 index 0000000..28586f0 --- /dev/null +++ b/i18n/config.ts @@ -0,0 +1,3 @@ +export const locales = ["ru", "en"] as const; +export type Locale = (typeof locales)[number]; +export const defaultLocale: Locale = "ru"; diff --git a/i18n/navigation.ts b/i18n/navigation.ts new file mode 100644 index 0000000..ee2c3f9 --- /dev/null +++ b/i18n/navigation.ts @@ -0,0 +1,7 @@ +import { createNavigation } from "next-intl/navigation"; +import { locales, defaultLocale } from "./config"; + +export const { Link, redirect, usePathname, useRouter } = createNavigation({ + locales, + defaultLocale, +}); diff --git a/i18n/request.ts b/i18n/request.ts new file mode 100644 index 0000000..1490ab2 --- /dev/null +++ b/i18n/request.ts @@ -0,0 +1,15 @@ +import { getRequestConfig } from "next-intl/server"; +import { locales, type Locale } from "./config"; + +export default getRequestConfig(async ({ requestLocale }) => { + let locale = await requestLocale; + + if (!locale || !locales.includes(locale as Locale)) { + locale = "ru"; + } + + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/lib/database/header.ts b/lib/database/header.ts index 8507e6f..3eec446 100644 --- a/lib/database/header.ts +++ b/lib/database/header.ts @@ -1,123 +1,82 @@ -export const burgerMenu = [ +interface BurgerMenuItem { + titleKey: string; + link: string; + services?: boolean; + drop?: boolean; + dropDown?: { + titleKey: string; + link: string; + }[]; +} + +export const burgerMenu: BurgerMenuItem[] = [ { - title: "Календарь мероприятий", - titleEn: "Calendar of events", + titleKey: "nav.calendar", link: "/calendar", }, { services: true, - title: "Услуги", + titleKey: "nav.services", drop: true, - titleEn: "Services", link: "/services/organization", - dropDown: [ - { - title: "Организация выставок", - titleEn: "Organization of exhibitions", - link: "/services/organization", - }, - { - title: "B2B Встречи и Матчмейкинг", - titleEn: "B2B Meetings & Business Matchmaking", - link: "/services/meetings", - }, - { - title: "Бизнес-Миссии", - titleEn: "Business Missions", - link: "/services/missions", - }, - - { - title: "Рекламная деятельность", - titleEn: "Advertising activity", - link: "/services/advertising", - }, - { - title: "Организация гибридных мероприятий", - titleEn: "Organization of hybrid events", - link: "/services/hybrid-organizations", - }, - { - title: "Сертификация", - titleEn: "Certification", - link: "/services/certification", - }, - { - title: "Экспедирование грузов и таможенное оформление", - titleEn: "Freight forwarding and customs clearance", - link: "/services/forwarding", - }, - { - title: "Аутсорсинг бух.учета и аудита", - titleEn: "Outsourcing of accounting and auditing", - link: "/services/outsourcing", - }, - { - title: "Обучения персонала", - titleEn: "Personnel-training", - link: "/services/personnel-training", - }, - { - title: "Бизнес - туры", - titleEn: "Business - tours", - link: "/services/business-tours", - }, + { titleKey: "sidebar.organizeExhibitions", link: "/services/organization" }, + { titleKey: "sidebar.b2bMeetings", link: "/services/meetings" }, + { titleKey: "sidebar.businessMissions", link: "/services/missions" }, + { titleKey: "burger.advertisingActivity", link: "/services/advertising" }, + { titleKey: "sidebar.hybridEvents", link: "/services/hybrid-organizations" }, + { titleKey: "sidebar.certification", link: "/services/certification" }, + { titleKey: "burger.freightForwarding", link: "/services/forwarding" }, + { titleKey: "burger.accountingOutsourcing", link: "/services/outsourcing" }, + { titleKey: "burger.personnelTraining", link: "/services/personnel-training" }, + { titleKey: "sidebar.businessTours", link: "/services/business-tours" }, ], }, { - title: "О компании", - titleEn: "Calendar of events", + titleKey: "nav.about", link: "/company/aboutus", }, { - title: "Новости", - titleEn: "News", + titleKey: "nav.news", link: "/news", }, { - title: "Контакты", - titleEn: "Contacts", + titleKey: "nav.contacts", link: "/contacts", }, - - // { en: true, title: "Exhibition", link: "/exhibition-about" }, - // { en: true, title: "Participants", link: "/participants-info" }, - // { en: true, title: "For visitors", link: "/visitors-info" }, ]; -export const burgerMenu2 = [ +interface BurgerMenu2Item { + titleKey: string; + link: string; + company?: boolean; + drop?: boolean; + dropDown?: { + titleKey: string; + link: string; + }[]; +} + +export const burgerMenu2: BurgerMenu2Item[] = [ { company: true, - title: "О компании", - - titleEn: "About company", + titleKey: "nav.about", link: "/company/aboutus", drop: true, dropDown: [ - { - title: "О нас", - titleEn: "About us", - link: "/company/aboutus", - }, + { titleKey: "burger.aboutUs", link: "/company/aboutus" }, ], }, { - title: "Новости", - - titleEn: "News", + titleKey: "nav.news", link: "/news", }, { - title: "FAQ", - - titleEn: "FAQ", + titleKey: "nav.faq", link: "/faq", }, { - title: "Контакты", - - titleEn: "Контакты", + titleKey: "nav.contacts", link: "/contacts", }, ]; diff --git a/lib/database/homeInfoData.ts b/lib/database/homeInfoData.ts index b0791fd..9aeea93 100644 --- a/lib/database/homeInfoData.ts +++ b/lib/database/homeInfoData.ts @@ -1,56 +1,36 @@ -import car from '@/public/assets/icons/service/car.svg'; -import bus from '@/public/assets/icons/service/bus.svg'; -import track from '@/public/assets/icons/service/track.svg'; +import car from "@/public/assets/icons/service/car.svg"; +import bus from "@/public/assets/icons/service/bus.svg"; +import track from "@/public/assets/icons/service/track.svg"; -import { RoadCardProps } from '../types'; +import { StaticImageData } from "next/image"; + +export interface RoadCardProps { + icon: StaticImageData; + textKey: string; +} export const roadCardData: RoadCardProps[] = [ - { - icon: car, - text: 'На машине', - - enText: 'Car', - }, - { - icon: bus, - text: 'На автобусе', - - enText: 'Public transport', - }, - { - icon: track, - text: 'Freight transport', - - enText: 'Car', - }, + { icon: car, textKey: "services.byCar" }, + { icon: bus, textKey: "services.byBus" }, + { icon: track, textKey: "services.freight" }, ]; -import call from '@/public/assets/icons/service/call.svg'; -import megaphone from '@/public/assets/icons/service/megaphone.svg'; -import bag from '@/public/assets/icons/service/bag.svg'; +import call from "@/public/assets/icons/service/call.svg"; +import megaphone from "@/public/assets/icons/service/megaphone.svg"; +import bag from "@/public/assets/icons/service/bag.svg"; export const contactCardData: RoadCardProps[] = [ - { - icon: call, - text: 'Справочный центр', - - enText: 'Call centre', - }, - { - icon: bag, - text: 'Услуги', - - enText: 'Servise bureau', - }, - { - icon: megaphone, - text: 'Реклама', - - enText: 'Advertising', - }, + { icon: call, textKey: "services.callCentre" }, + { icon: bag, textKey: "services.serviceBureau" }, + { icon: megaphone, textKey: "services.advertising" }, ]; -import partner from '@/public/assets/icons/home/partner.svg'; -import { StaticImageData } from 'next/image'; +import partner from "@/public/assets/icons/home/partner.svg"; -export const partnersData: StaticImageData[] = [partner, partner, partner, partner, partner]; +export const partnersData: StaticImageData[] = [ + partner, + partner, + partner, + partner, + partner, +]; diff --git a/lib/database/pathnames.ts b/lib/database/pathnames.ts index 60a9838..8d539ac 100644 --- a/lib/database/pathnames.ts +++ b/lib/database/pathnames.ts @@ -1,148 +1,110 @@ +interface MenuItemType { + titleKey: string; + link: string; +} + interface MenuType { - pathname: string; - pathnameEn: string; + pathnameKey: string; company?: boolean; members?: boolean; visitors?: boolean; news?: boolean; services?: boolean; - info: { - titleEn: string; - title: string; - link: string; - }[]; + info: MenuItemType[]; } export const sidebarData: MenuType[] = [ { company: true, - pathname: 'О компании', - pathnameEn: 'About company', + pathnameKey: "sidebar.company", info: [ - { title: 'Коротко о нас', titleEn: 'About us', link: '/company/aboutus' }, - // { title: 'Выставочная деятельность', link: '' }, - // { title: 'История и награды', link: '' }, - // { title: 'Партнеры', link: '' }, - // { title: 'Работы в компании', link: '' }, - // { title: 'Наши издания', link: '' }, + { titleKey: "sidebar.aboutUs", link: "/company/aboutus" }, ], }, { members: true, - pathname: 'Участникам', - pathnameEn: 'Participants', - + pathnameKey: "sidebar.participants", info: [ - { - title: 'Информация для участников', - titleEn: 'Information for participants', - link: '/members', - }, - { - title: 'Онлайн заявка для участников', - titleEn: 'Online application', - link: '/members/bid', - }, - { - title: 'Правила для участников', - titleEn: 'Rules for participants', - link: '/members/members-rules', - }, + { titleKey: "sidebar.participantsInfo", link: "/members" }, + { titleKey: "sidebar.participantsApplication", link: "/members/bid" }, + { titleKey: "sidebar.participantsRules", link: "/members/members-rules" }, ], }, { news: true, - pathname: 'Новости', - pathnameEn: 'News', - - info: [ - { title: 'Новости', titleEn: 'News', link: '/news' }, - // { title: 'Пресс-релизы', link: '' }, - ], + pathnameKey: "sidebar.news", + info: [{ titleKey: "sidebar.news", link: "/news" }], }, { visitors: true, - pathname: 'Посетителям', - pathnameEn: 'Visitors', - + pathnameKey: "sidebar.visitors", info: [ - { - title: 'Информация для посетителей', - titleEn: 'Information for visitors', - link: '/visitors', - }, - { - title: 'Порядок регистрации посетителей', - titleEn: 'Entrance rules', - link: '/visitors/rules-for-visitors', - }, + { titleKey: "sidebar.visitorsInfo", link: "/visitors" }, + { titleKey: "sidebar.visitorsRules", link: "/visitors/rules-for-visitors" }, ], }, { services: true, - pathname: 'Услуги', - pathnameEn: 'Services', - + pathnameKey: "sidebar.services", info: [ - { - title: 'Организация выставок', - titleEn: 'Organization of exhibitions', - link: '/services/organization', - }, - { - title: 'B2B Встречи и Матчмейкинг', - titleEn: 'B2B Meetings & Business Matchmaking', - link: '/services/meetings', - }, - { - title: 'Бизнес-Миссии', - titleEn: 'Business Missions', - link: '/services/missions', - }, - { - title: 'Маркетинговые услуги', - titleEn: 'Advertising activity', - link: '/services/advertising', - }, - { - title: 'Организация гибридных мероприятий', - titleEn: 'Organization of hybrid events', - link: '/services/hybrid-organizations', - }, - { - title: 'Сертификация', - titleEn: 'Certification', - link: '/services/certification', - }, - { - title: 'Организация перевозки и таможенное оформление', - titleEn: 'Freight forwarding and customs clearance', - link: '/services/forwarding', - }, - { - title: 'Аутсорсинг бухгалтерского учета и аудита', - titleEn: 'Outsourcing of accounting and auditing', - link: '/services/outsourcing', - }, - { - title: 'Тренинг для персонала', - titleEn: 'Personnel-training', - link: '/services/personnel-training', - }, - { - title: 'Бизнес - туры', - titleEn: 'Business - tours', - link: '/services/business-tours', - }, - { - title: 'Дополнительные услуги', - titleEn: 'Additional servies', - link: '/services/additional', - }, + { titleKey: "sidebar.organizeExhibitions", link: "/services/organization" }, + { titleKey: "sidebar.b2bMeetings", link: "/services/meetings" }, + { titleKey: "sidebar.businessMissions", link: "/services/missions" }, + { titleKey: "sidebar.marketingServices", link: "/services/advertising" }, + { titleKey: "sidebar.hybridEvents", link: "/services/hybrid-organizations" }, + { titleKey: "sidebar.certification", link: "/services/certification" }, + { titleKey: "sidebar.forwarding", link: "/services/forwarding" }, + { titleKey: "sidebar.outsourcing", link: "/services/outsourcing" }, + { titleKey: "sidebar.personalTraining", link: "/services/personnel-training" }, + { titleKey: "sidebar.businessTours", link: "/services/business-tours" }, + { titleKey: "sidebar.additionalServices", link: "/services/additional" }, ], }, ]; +interface HeaderType { + titleKey: string; + link: string; + id: number; +} + +export const headerMenu: HeaderType[] = [ + { titleKey: "nav.about", link: "/company/aboutus", id: 1 }, + { titleKey: "nav.news", link: "/news", id: 2 }, + { titleKey: "nav.faq", link: "/faq", id: 3 }, + { titleKey: "nav.contacts", link: "/contacts", id: 4 }, +]; + +export const headerMenu2: HeaderType[] = [ + { titleKey: "nav.calendar", link: "/calendar", id: 1 }, + { titleKey: "nav.services", link: "/services/organization", id: 2 }, + { titleKey: "nav.about", link: "/company/aboutus", id: 3 }, + { titleKey: "nav.news", link: "/news", id: 4 }, + { titleKey: "nav.contacts", link: "/contacts", id: 5 }, +]; + +interface FooterMenuType { + titleKey: string; + link: string; + one?: boolean; +} + +export const footerMenu: FooterMenuType[] = [ + { titleKey: "nav.calendar", link: "/calendar", one: true }, + { titleKey: "nav.participants", link: "/members" }, + { titleKey: "nav.visitors", link: "" }, + { titleKey: "nav.services", link: "/services/organization" }, +]; + +export const footerMenu2: FooterMenuType[] = [ + { titleKey: "footer.territory", link: "" }, + { titleKey: "nav.about", link: "/company/aboutus" }, + { titleKey: "footer.pressCenter", link: "" }, + { titleKey: "nav.faq", link: "/faq" }, + { titleKey: "nav.contacts", link: "/contacts" }, + { titleKey: "footer.callCentre", link: "" }, +]; + interface BurgerDataTypes { pathname: string; company?: boolean; @@ -155,278 +117,71 @@ interface BurgerDataTypes { visitors?: boolean; contacts?: boolean; first?: boolean; - title: string; + titleKey: string; drop?: string; info?: { - title: string; + titleKey: string; link: string; }[]; } export const burgerMenuData: BurgerDataTypes[] = [ { - pathname: '/calendar', + pathname: "/calendar", calendar: true, only: true, - title: 'Календарь мероприятий', + titleKey: "nav.calendar", first: true, }, { members: true, - title: 'Участникам', - pathname: '/members', - drop: 'members', + titleKey: "nav.participants", + pathname: "/members", + drop: "members", first: true, info: [ - { title: 'Информация для участников', link: '/members' }, - { title: 'Онлайн заявка для участников', link: '/members/bid' }, + { titleKey: "sidebar.participantsInfo", link: "/members" }, + { titleKey: "sidebar.participantsApplication", link: "/members/bid" }, ], }, { - pathname: '/visitors', + pathname: "/visitors", visitors: true, only: true, - title: 'Посетителям', + titleKey: "nav.visitors", first: true, }, { - pathname: '/services', + pathname: "/services", services: true, only: true, - title: 'Услуги', + titleKey: "nav.services", first: true, }, - { - pathname: '/faq', + pathname: "/faq", faq: true, only: true, - title: 'FAQ', + titleKey: "nav.faq", }, { - pathname: '/contacts', + pathname: "/contacts", contacts: true, only: true, - title: 'Контакты', + titleKey: "nav.contacts", }, { company: true, - title: 'О компании', - pathname: '/company/aboutus', - drop: 'company', - info: [ - { title: 'Коротко о нас', link: '/company/aboutus' }, - // { title: "Выставочная деятельность", link: "" }, - // { title: "История и награды", link: "" }, - // { title: "Партнеры", link: "" }, - // { title: "Работы в компании", link: "" }, - // { title: "Наши издания", link: "" }, - ], + titleKey: "nav.about", + pathname: "/company/aboutus", + drop: "company", + info: [{ titleKey: "sidebar.aboutUs", link: "/company/aboutus" }], }, { news: true, - title: 'Новости', - pathname: '/news', - drop: 'news', - info: [ - { title: 'Новости', link: '/news' }, - // { title: "Пресс-релизы", link: "/news" }, - ], - }, -]; - -interface HeaderType { - title: string; - link: string; - id: number; - one?: boolean; - en?: boolean; -} - -export const headerMenu: HeaderType[] = [ - { title: 'О компании', link: '/company/aboutus', id: 1 }, - { title: 'Новости', link: '/news', id: 2 }, - { title: 'FAQ', link: '/faq', id: 3 }, - { title: 'Контакты', link: '/contacts', id: 4 }, - - { en: true, title: 'About company', link: '/company/aboutus', id: 1 }, - { en: true, title: 'News', link: '/news', id: 2 }, - { en: true, title: 'FAQ', link: '/faq', id: 3 }, - { en: true, title: 'Contacts', link: '/contacts', id: 4 }, -]; - -export const headerMenu2: HeaderType[] = [ - { title: 'Календарь мероприятий', link: '/calendar', id: 1 }, - { title: 'Услуги', link: '/services/organization', id: 2 }, - { title: 'О компании', link: '/company/aboutus', id: 3 }, - { title: 'Новости', link: '/news', id: 4 }, - { title: 'Контакты', link: '/contacts', id: 5 }, - - // { title: 'Участникам', link: '/members', id: 2 }, - // { title: 'Посетителям', link: '/visitors', id: 3 }, - - { en: true, title: 'Calendar of events', link: '/calendar', id: 1 }, - { en: true, title: 'Services', link: '/services/organization', id: 2 }, - { en: true, title: 'About company', link: '/company/aboutus', id: 3 }, - { en: true, title: 'News', link: '/news', id: 4 }, - { en: true, title: 'Contacts', link: '/contacts', id: 5 }, - - // { en: true, title: 'Participants', link: '/members', id: 2 }, - // { en: true, title: 'Visitors', link: '/visitors', id: 3 }, -]; - -interface FooterType { - title: string; - link: string; -} - -export const footerMenu = [ - { title: 'Календарь мероприятий', link: '/calendar', one: true }, - { title: 'Участникам', link: '/members' }, - { title: 'Посетителям', link: '' }, - { title: 'Услуги', link: '/services/organization' }, -]; - -export const footerMenu2: FooterType[] = [ - { title: 'Территория комплекса', link: '' }, - { title: 'О компании', link: '/company/aboutus' }, - { title: 'Пресс-центр', link: '' }, - { title: 'FAQ', link: '/faq' }, - { title: 'Контакты', link: '/contacts' }, - { title: 'Справочный центр', link: '' }, -]; - -export const footerInfo: string[] = [ - 'Адрес: 744000, г. Ашхабад, просп. Битарап Туркменистан, 183', - 'Тел.: +99362006200', - '+99312454111', - 'E-mail: contact@turkmenexpo.com', - 'Address: Ashgabat, 183 Bitarap Turkmenistan', - 'ave.,744000', - 'E-mail: contact@turkmenexpo.com', -]; - -export const topMenu = [ - { - path: 'about', - links: [{ active: 'Главная', default: '/ О компании / Коротко нас' }], - }, - { - path: 'members', - links: [ - { - active: 'Главная', - active2: ' / Участникам', - default: ' / Информация для участников', - }, - ], - }, - { - path: 'members/bid', - links: [ - { - active: 'Главная', - default: ' / Участникам / Онлайн заявка для участников', - }, - ], - }, - { - path: 'events', - links: [ - { - active: 'Главная ', - default: '/ Календарь мероприятий', - }, - ], - }, - { - path: 'faq', - links: [ - { - active: 'Главная', - default: ' / FAQ', - }, - ], - }, - { - path: 'contacts', - links: [ - { - active: 'Главная', - default: ' / Контакты', - }, - ], - }, - { - path: 'calendar', - links: [ - { - active: 'Главная', - default: ' / Календарь мероприятий', - }, - ], - }, - { - path: 'news', - links: [ - { - active: 'Главная', - default: ' / Новости', - }, - ], - }, -]; - -export const burgerLinks = [ - { - title: 'О компании', - about: true, - links: [ - { name: 'Коротко о нас', link: '/about/company' }, - { name: 'Выставочная деятельность', link: '/about/company' }, - { name: 'История и награды', link: '/about/company' }, - { name: 'Партнеры', link: '/about/company' }, - { name: 'Работа в компании', link: '/about/company' }, - { name: 'Наши издания', link: '/about/company' }, - ], - }, - { - title: 'Новости', - news: true, - links: [ - { name: 'Новости', link: '/news' }, - { name: 'Пресс центр', link: '/news' }, - ], - }, - { - title: 'Участникам', - members: true, - links: [ - { name: 'Участникам', link: '/members' }, - { name: 'Онлайн заявка', link: '/members/bid' }, - ], - }, - { - title: 'Контакты', - contacts: true, - links: [ - { name: 'Новости', link: '/news' }, - { name: 'Пресс центр', link: '/news' }, - ], - }, - { - title: 'FAQ', - faq: true, - links: [ - { name: 'faq', link: '/news' }, - { name: 'Пресс центр', link: '/news' }, - ], - }, - { - news: true, - links: [ - { name: 'Новости', link: '/news' }, - { name: 'Пресс центр', link: '/news' }, - ], + titleKey: "nav.news", + pathname: "/news", + drop: "news", + info: [{ titleKey: "sidebar.news", link: "/news" }], }, ]; diff --git a/lib/get-lang.ts b/lib/get-lang.ts deleted file mode 100644 index 1cc78a3..0000000 --- a/lib/get-lang.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { cookies } from "next/headers"; - -export const getLang = () => { - const cookieStore = cookies(); - const langCookie = cookieStore.get("lang")?.value || "ru"; - - // Возвращаем полный объект языка - return { - title: langCookie === "en" ? "English" : "Русский", - localization: langCookie, - }; -}; diff --git a/lib/types.ts b/lib/types.ts index 78d6caf..72c8174 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -22,6 +22,5 @@ export type NewsCardProps = { export type RoadCardProps = { icon: any; - text: string; - enText: string; + textKey: string; }; diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..3f3eee3 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,144 @@ +{ + "common": { + "home": "Home", + "showMore": "Show more", + "send": "Send", + "formSubmitted": "Form submitted", + "of": "of" + }, + "nav": { + "calendar": "Calendar of events", + "services": "Services", + "about": "About company", + "news": "News", + "contacts": "Contacts", + "faq": "FAQ", + "participants": "Participants", + "visitors": "Visitors" + }, + "sidebar": { + "company": "About company", + "aboutUs": "About us", + "participants": "Participants", + "participantsInfo": "Information for participants", + "participantsApplication": "Online application", + "participantsRules": "Rules for participants", + "news": "News", + "visitors": "Visitors", + "visitorsInfo": "Information for visitors", + "visitorsRules": "Entrance rules", + "services": "Services", + "organizeExhibitions": "Organization of exhibitions", + "b2bMeetings": "B2B Meetings & Business Matchmaking", + "businessMissions": "Business Missions", + "marketingServices": "Advertising activity", + "hybridEvents": "Organization of hybrid events", + "certification": "Certification", + "forwarding": "Freight forwarding and customs clearance", + "outsourcing": "Outsourcing of accounting and auditing", + "personalTraining": "Personnel-training", + "businessTours": "Business - tours", + "additionalServices": "Additional services" + }, + "burger": { + "services": "Services", + "about": "About company", + "aboutUs": "About us", + "advertisingActivity": "Advertising activity", + "freightForwarding": "Freight forwarding and customs clearance", + "accountingOutsourcing": "Outsourcing of accounting and auditing", + "personnelTraining": "Personnel-training", + "calendar": "Calendar of events", + "participantsInfo": "Information for participants", + "participantsApplication": "Online application" + }, + "home": { + "exhibitions": "Exhibitions", + "goToWebsite": "Go to website", + "news": "News", + "allNews": "All news", + "upcomingExhibitions": "Upcoming exhibitions and events", + "partners": "Partners", + "feedback": "Feedback" + }, + "events": { + "organiserLabel": "Organiser:", + "coOrganiserLabel": "Co-organiser:", + "registration": "Registration", + "visitSite": "Visit site", + "more": "More", + "showMore": "Show more", + "date": "Date", + "venue": "Venue", + "organiser": "Organiser", + "coOrganiser": "Co-organiser", + "description": "Description", + "theme": "Theme of event" + }, + "contacts": { + "title": "Contacts", + "contactUs": "Contact us", + "name": "Name", + "email": "E-mail", + "phone": "Phone", + "companyName": "Company name", + "message": "Message", + "address": "Address:", + "addressValue": "744000, Ashgabat, 183 Bitarap Turkmenistan ave.", + "phoneNumbers": "+99371871814; 99363719588" + }, + "footer": { + "address": "Address: Ashgabat, 183 Bitarap Turkmenistan ave., 744000", + "phone": "Phone: +99362006200", + "phone2": "+99312454111", + "email": "E-mail: contact@turkmenexpo.com", + "territory": "Complex territory", + "pressCenter": "Press center", + "callCentre": "Call centre" + }, + "search": { + "placeholder": "What to find?", + "all": "Everywhere", + "inEvents": "In events", + "inNews": "In news", + "findButton": "Find", + "byRequest": "By request \"", + "found": "\" found", + "results": "results", + "goToPage": "Go to page", + "noResults": "Nothing found for your request" + }, + "services": { + "byCar": "Car", + "byBus": "Public transport", + "freight": "Freight transport", + "callCentre": "Call centre", + "serviceBureau": "Service bureau", + "advertising": "Advertising", + "breadcrumb": "Services", + "routes": "Routes" + }, + "validation": { + "nameRequired": "Name is required", + "emailRequired": "Email is required", + "phoneRequired": "Phone number is required", + "messageRequired": "Message is required" + }, + "visitors": { + "title": "For visitors", + "information": "Information for visitors", + "rules": "Entrance rules" + }, + "about": { + "title": "About us" + }, + "news": { + "title": "News", + "mediaAboutUs": "Media about us", + "allNews": "All news", + "article": "Article" + }, + "calendar": { + "title": "Calendar of events" + } +} diff --git a/messages/ru.json b/messages/ru.json new file mode 100644 index 0000000..dd10564 --- /dev/null +++ b/messages/ru.json @@ -0,0 +1,144 @@ +{ + "common": { + "home": "Главная", + "showMore": "Показать ещё", + "send": "Отправить", + "formSubmitted": "Форма отправлена", + "of": "из" + }, + "nav": { + "calendar": "Календарь мероприятий", + "services": "Услуги", + "about": "О компании", + "news": "Новости", + "contacts": "Контакты", + "faq": "FAQ", + "participants": "Участникам", + "visitors": "Посетителям" + }, + "sidebar": { + "company": "О компании", + "aboutUs": "Коротко о нас", + "participants": "Участникам", + "participantsInfo": "Информация для участников", + "participantsApplication": "Онлайн заявка для участников", + "participantsRules": "Правила для участников", + "news": "Новости", + "visitors": "Посетителям", + "visitorsInfo": "Информация для посетителей", + "visitorsRules": "Порядок регистрации посетителей", + "services": "Услуги", + "organizeExhibitions": "Организация выставок", + "b2bMeetings": "B2B Встречи и Матчмейкинг", + "businessMissions": "Бизнес-Миссии", + "marketingServices": "Маркетинговые услуги", + "hybridEvents": "Организация гибридных мероприятий", + "certification": "Сертификация", + "forwarding": "Организация перевозки и таможенное оформление", + "outsourcing": "Аутсорсинг бухгалтерского учета и аудита", + "personalTraining": "Тренинг для персонала", + "businessTours": "Бизнес - туры", + "additionalServices": "Дополнительные услуги" + }, + "burger": { + "services": "Услуги", + "about": "О компании", + "aboutUs": "О нас", + "advertisingActivity": "Рекламная деятельность", + "freightForwarding": "Экспедирование грузов и таможенное оформление", + "accountingOutsourcing": "Аутсорсинг бух.учета и аудита", + "personnelTraining": "Обучения персонала", + "calendar": "Календарь мероприятий", + "participantsInfo": "Информация для участников", + "participantsApplication": "Онлайн заявка для участников" + }, + "home": { + "exhibitions": "Выставки", + "goToWebsite": "Перейти на сайт", + "news": "Новости", + "allNews": "Все новости", + "upcomingExhibitions": "Ближайшие выставки и мероприятия", + "partners": "Партнёры", + "feedback": "Обратная связь" + }, + "events": { + "organiserLabel": "Организатор:", + "coOrganiserLabel": "Со-организатор:", + "registration": "Зарегистрироваться", + "visitSite": "Посетить сайт", + "more": "Подробнее", + "showMore": "Показать еще", + "date": "Дата", + "venue": "Место", + "organiser": "Организатор", + "coOrganiser": "Со-организатор", + "description": "Описание", + "theme": "Тематика мероприятия" + }, + "contacts": { + "title": "Контакты", + "contactUs": "Связаться с нами", + "name": "Имя", + "email": "E-mail", + "phone": "Номер", + "companyName": "Название компании", + "message": "Сообщение", + "address": "Адрес:", + "addressValue": "744000, г. Ашхабад, просп. Битарап Туркменистан, 183", + "phoneNumbers": "+99371871814; 99363719588" + }, + "footer": { + "address": "Адрес: 744000, г. Ашхабад, просп. Битарап Туркменистан, 183", + "phone": "Тел.: +99362006200", + "phone2": "+99312454111", + "email": "E-mail: contact@turkmenexpo.com", + "territory": "Территория комплекса", + "pressCenter": "Пресс-центр", + "callCentre": "Справочный центр" + }, + "search": { + "placeholder": "Что найти?", + "all": "Везде", + "inEvents": "В событиях", + "inNews": "В новостях", + "findButton": "Найти", + "byRequest": "По запросу «", + "found": "» нашлось", + "results": "результатов", + "goToPage": "Перейти на страницу", + "noResults": "По вашему запросу ничего не найдено" + }, + "services": { + "byCar": "На машине", + "byBus": "На автобусе", + "freight": "Грузовой транспорт", + "callCentre": "Справочный центр", + "serviceBureau": "Услуги", + "advertising": "Реклама", + "breadcrumb": "Сервисы", + "routes": "Как добраться" + }, + "validation": { + "nameRequired": "Имя необходимо", + "emailRequired": "Email необходим", + "phoneRequired": "Номер телефона необходим", + "messageRequired": "Сообщение необходимо" + }, + "visitors": { + "title": "Посетителям", + "information": "Информация для посетителей", + "rules": "Порядок регистрации посетителей" + }, + "about": { + "title": "Коротко о нас" + }, + "news": { + "title": "Новости", + "mediaAboutUs": "СМИ о нас", + "allNews": "Все новости", + "article": "Статья" + }, + "calendar": { + "title": "Календарь мероприятий" + } +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..4ab5113 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,13 @@ +import createMiddleware from "next-intl/middleware"; +import { locales, defaultLocale } from "./i18n/config"; + +export default createMiddleware({ + locales, + defaultLocale, + localePrefix: "always", + localeDetection: true, +}); + +export const config = { + matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"], +}; diff --git a/next.config.mjs b/next.config.mjs index a9c1ef1..42badec 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,7 +1,11 @@ +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); + /** @type {import('next').NextConfig} */ const nextConfig = { images: { unoptimized: true }, distDir: "build", }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index 280fbbb..1b3e412 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "clsx": "^2.1.0", "framer-motion": "^11.0.25", "next": "14.1.0", + "next-intl": "^4.8.2", "pm2": "^5.3.1", "react": "^18", "react-dom": "^18", @@ -59,6 +60,67 @@ "tslib": "^2.4.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz", + "integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/intl-localematcher": "0.8.1", + "decimal.js": "^10.6.0", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", + "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", + "integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz", + "integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/icu-skeleton-parser": "2.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz", + "integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@googlemaps/js-api-loader": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.2.tgz", @@ -831,6 +893,313 @@ "uuid": "bin/uuid" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1155,6 +1524,178 @@ } } }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz", + "integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.11.tgz", + "integrity": "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz", + "integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz", + "integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz", + "integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz", + "integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz", + "integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz", + "integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz", + "integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz", + "integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -1163,6 +1704,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -2005,6 +2555,12 @@ "ms": "2.0.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2027,9 +2583,10 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -2570,6 +3127,21 @@ "node": ">=0.10.0" } }, + "node_modules/icu-minify": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.2.tgz", + "integrity": "sha512-LHBQV+skKkjZSPd590pZ7ZAHftUgda3eFjeuNwA8/15L8T8loCNBktKQyTlkodAU86KovFXeg/9WntlAo5wA5A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/immer": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", @@ -2598,6 +3170,18 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/intl-messageformat": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz", + "integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/icu-messageformat-parser": "3.5.1", + "tslib": "^2.8.1" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3103,6 +3687,91 @@ } } }, + "node_modules/next-intl": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.2.tgz", + "integrity": "sha512-GuuwyvyEI49/oehQbBXEoY8KSIYCzmfMLhmIwhMXTb+yeBmly1PnJcpgph3KczQ+HTJMXwXCmkizgtT8jBMf3A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.8.2", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.8.2", + "po-parser": "^2.1.1", + "use-intl": "^4.8.2" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.2.tgz", + "integrity": "sha512-sHDs36L1VZmFHj3tPHsD+KZJtnsRudHlNvT0ieIe3iFVn5OpGLTxW3d/Zc/2LXSj5GpGuR6wQeikbhFjU9tMQQ==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.11.tgz", + "integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.11", + "@swc/core-darwin-x64": "1.15.11", + "@swc/core-linux-arm-gnueabihf": "1.15.11", + "@swc/core-linux-arm64-gnu": "1.15.11", + "@swc/core-linux-arm64-musl": "1.15.11", + "@swc/core-linux-x64-gnu": "1.15.11", + "@swc/core-linux-x64-musl": "1.15.11", + "@swc/core-win32-arm64-msvc": "1.15.11", + "@swc/core-win32-ia32-msvc": "1.15.11", + "@swc/core-win32-x64-msvc": "1.15.11" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -3130,6 +3799,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -3651,6 +4326,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -4757,9 +5438,10 @@ "dev": true }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/tv4": { "version": "1.3.0", @@ -4793,7 +5475,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4882,6 +5564,27 @@ "react": ">=16.8.0" } }, + "node_modules/use-intl": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.2.tgz", + "integrity": "sha512-3VNXZgDnPFqhIYosQ9W1Hc6K5q+ZelMfawNbexdwL/dY7BTHbceLUBX5Eeex9lgogxTp0pf1SjHuhYNAjr9H3g==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.8.2", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 8b2d0cd..f5efa36 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "clsx": "^2.1.0", "framer-motion": "^11.0.25", "next": "14.1.0", + "next-intl": "^4.8.2", "pm2": "^5.3.1", "react": "^18", "react-dom": "^18", diff --git a/redux/slices/aboutus.ts b/redux/slices/aboutus.ts index bfdc3da..1131280 100644 --- a/redux/slices/aboutus.ts +++ b/redux/slices/aboutus.ts @@ -1,13 +1,12 @@ import { baseAPI } from "@/lib/API"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { activeLangType } from "./headerSlice"; export const fetchAbout = createAsyncThunk( "about/fetchAbout", - async ({ activeLang }: { activeLang: activeLangType }) => { + async ({ locale }: { locale: string }) => { const res = await fetch(`${baseAPI}settings/about_us`, { headers: { - "Accept-Language": activeLang.localization, + "Accept-Language": locale, }, }); @@ -35,12 +34,9 @@ const aboutSlice = createSlice({ }, extraReducers: (builder) => { - builder.addCase(fetchAbout.pending, (state) => { - // console.log('pending'); - }); + builder.addCase(fetchAbout.pending, (state) => {}); builder.addCase(fetchAbout.fulfilled, (state, action) => { - // console.log('success', action.payload); state.aboutData = action.payload; }); diff --git a/redux/slices/headerSlice.ts b/redux/slices/headerSlice.ts index a27570f..5fd1a12 100644 --- a/redux/slices/headerSlice.ts +++ b/redux/slices/headerSlice.ts @@ -1,21 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from '../store'; -export type activeLangType = { - title: 'Ру' | 'En' | 'Tm' | string; - localization: 'ru' | 'en' | 'tm' | string; -}; - interface HeaderState { - activeLang: activeLangType; showInput: boolean; } const initialState: HeaderState = { - activeLang: { - title: 'Русский', - localization: 'ru', - }, showInput: false, }; @@ -23,9 +13,6 @@ const headerSlice = createSlice({ name: 'header', initialState, reducers: { - setActiveLang(state, action: PayloadAction<activeLangType>) { - state.activeLang = action.payload; - }, setShowInput(state, action: PayloadAction<boolean>) { state.showInput = action.payload; }, @@ -34,6 +21,6 @@ const headerSlice = createSlice({ export const selectHeader = (state: RootState) => state.headerSlice; -export const { setActiveLang, setShowInput } = headerSlice.actions; +export const { setShowInput } = headerSlice.actions; export default headerSlice.reducer; diff --git a/services/services.ts b/services/services.ts index 90c3ef4..6583b4e 100644 --- a/services/services.ts +++ b/services/services.ts @@ -1,21 +1,18 @@ import { baseAPI } from "@/lib/API"; import { ServicesType } from "@/lib/types/Services.data"; -import { cookies } from "next/headers"; - -export const getServices = async () => { - const lang = cookies().get("lang")?.value || "en"; +export const getServices = async (lang: string) => { const res = await fetch(`${baseAPI}services`, { headers: { - "Accept-Language": lang, // Передаём язык в заголовке + "Accept-Language": lang, }, next: { revalidate: 1000, - tags: [`services-${lang}`], // Кешируем отдельно для каждого языка + tags: [`services-${lang}`], }, }); - if (!res.ok) throw new Error("Ошибка при загрузке данных"); + if (!res.ok) throw new Error("Failed to load services"); return (await res.json()) as ServicesType; }; diff --git a/utils/useLang.ts b/utils/useLang.ts deleted file mode 100644 index f12081f..0000000 --- a/utils/useLang.ts +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -export const useLang = (en: string, ru: string, localization: string) => { - if (localization === "en") { - return en; - } else { - return ru; - } -};