diff --git a/package-lock.json b/package-lock.json index 688ab1e..4dd5c71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "parsaaghayi-front", "version": "0.1.0", "dependencies": { + "cookies-next": "^4.3.0", "next": "14.2.11", "react": "^18", "react-dom": "^18", @@ -17,6 +18,7 @@ }, "devDependencies": { "@next/bundle-analyzer": "^14.2.13", + "@types/negotiator": "^0.6.3", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -913,6 +915,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -927,6 +935,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.16.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", @@ -1760,6 +1775,25 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies-next": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-4.3.0.tgz", + "integrity": "sha512-XxeCwLR30cWwRd94sa9X5lRCDLVujtx73tv+N0doQCFIDl83fuuYdxbu/WQUt9aSV7EJx7bkMvJldjvzuFqr4w==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.7.0" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", diff --git a/package.json b/package.json index 9369b53..e555b31 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "start cmd.exe /k \"C:/laragon/bin/mysql/mysql-8.0.30-winx64/bin/mysqld.exe\" && start cmd.exe /k \"cd C:/Users/p.aghaei/Desktop/Work/parsa/parsaaghayi/backend && php artisan serve\" && start cmd.exe /k \"cd C:/Users/p.aghaei/Desktop/Work/parsa/parsaaghayi/frontend && next dev\"", "build": "next build", "start": "next start", "lint": "next lint", @@ -11,6 +11,7 @@ "analyze": "cross-env ANALYZE=true next build" }, "dependencies": { + "cookies-next": "^4.3.0", "next": "14.2.11", "react": "^18", "react-dom": "^18", @@ -20,6 +21,7 @@ }, "devDependencies": { "@next/bundle-analyzer": "^14.2.13", + "@types/negotiator": "^0.6.3", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/src/app/(routes)/(website)/components/contact/contact.tsx b/src/app/(routes)/(website)/components/contact/contact.tsx index f27016f..df08608 100644 --- a/src/app/(routes)/(website)/components/contact/contact.tsx +++ b/src/app/(routes)/(website)/components/contact/contact.tsx @@ -1,31 +1,43 @@ -import siteTexts from "@/app/_constants/siteTexts"; import React from "react"; -export default function Contact() { +interface ContactProps { + dict: { + homePage: { + contact: { + slogan: string; + description: string; + button: string; + placeholder: string; + }; + }; + }; +} + +export default function Contact({ dict }: ContactProps) { return (
-

- {siteTexts.en.homePage.contact.slogan} +

+ {dict.homePage.contact.slogan}

- {siteTexts.en.homePage.contact.description} + {dict.homePage.contact.description}

-
+
diff --git a/src/app/(routes)/(website)/components/header/header.tsx b/src/app/(routes)/(website)/components/header/header.tsx index 65668f9..babca9e 100644 --- a/src/app/(routes)/(website)/components/header/header.tsx +++ b/src/app/(routes)/(website)/components/header/header.tsx @@ -1,6 +1,8 @@ import React from "react"; import BurgerButton from "../burger-button/burgerButton"; import Link from "next/link"; +import SelectLanguage from "../selectLanguage/selectLanguage"; +import { cookies } from "next/headers"; export default function Header() { return ( @@ -59,6 +61,10 @@ export default function Header() { > Blog +
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4ae0006..69f271e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,9 @@ import localFont from "next/font/local"; import "./globals.css"; +import { getDefaultLocale } from "./lib/functions/utils"; const mikhak = localFont({ - src: "fonts/mikhak-v32/variable/Mikhak[DSTY,KSHD,wght].woff2", + src: "lib/fonts/mikhak-v32/variable/Mikhak[DSTY,KSHD,wght].woff2", variable: "--font-mikhak", weight: "100 900", }); @@ -12,8 +13,9 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + return ( - + Promise } = { + en: () => + import("../dictionaries/en.json").then((module) => module.default), + fa: () => + import("../dictionaries/fa.json").then((module) => module.default), +}; + +export const getDictionary = async ( + locale: keyof typeof dictionaries, + ): Promise => { + const loadDictionary = dictionaries[locale]; + if (loadDictionary) { + return loadDictionary(); + } + throw new Error(`No dictionary found for locale: ${locale}`); + }; + diff --git a/src/app/lib/functions/utils.ts b/src/app/lib/functions/utils.ts new file mode 100644 index 0000000..bfdfb74 --- /dev/null +++ b/src/app/lib/functions/utils.ts @@ -0,0 +1,9 @@ +import { cookies } from "next/headers"; + +export const getDefaultLocale = () => { + const defaultLocaleCookie = cookies().get("defaultLocale"); + const defaultLocale: string = defaultLocaleCookie + ? defaultLocaleCookie.value + : "en"; + return defaultLocale; +}; diff --git a/src/app/types/types.ts b/src/app/types/types.ts index 7540521..03b6505 100644 --- a/src/app/types/types.ts +++ b/src/app/types/types.ts @@ -115,3 +115,5 @@ export type FilteredPostsDataType = { links: LinksType; meta: MetaType; }; + +export type LocaleCookieType = { name: string; value: string }; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..0e00247 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,82 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const response = NextResponse.next(); + const acceptLanguage = request.headers.get("accept-language"); + + // تعیین کیفیت زبان‌ها + const languageQuality: { [key: string]: number } = { + fa: 0, // وزن برای زبان فارسی + en: 0, // وزن برای زبان انگلیسی + }; + + let defaultLocale = "en"; // مقدار پیش‌فرض زبان انگلیسی + + // بررسی هدر Accept-Language + if (acceptLanguage) { + const languages = acceptLanguage.split(",").map((lang) => lang.trim()); + + // بررسی اولویت زبان‌ها + for (const lang of languages) { + const [language, quality] = lang.split(";"); // زبان و کیفیت + const trimmedLanguage = language.trim(); + + // اگر کیفیت مشخص شده باشد + const langQuality = quality ? parseFloat(quality.split("=")[1]) : 1; // کیفیت را به عدد تبدیل کنید یا 1 فرض کنید + + // بررسی وجود زبان در languageQuality و مقایسه با defaultLocale + if (languageQuality.hasOwnProperty(trimmedLanguage)) { + languageQuality[trimmedLanguage] = langQuality; // تنظیم کیفیت + } + } + + // انتخاب زبان با بالاترین کیفیت + for (const [lang, quality] of Object.entries(languageQuality)) { + if (quality > languageQuality[defaultLocale]) { + defaultLocale = lang; // زبان با کیفیت بیشتر انتخاب می‌شود + } + } + } + + // ذخیره کوکی‌ها + const selectedLocale = request.cookies.get("selectedLocale"); + + if (selectedLocale && selectedLocale.value !== "") { + response.cookies.set("defaultLocale", selectedLocale.value, { + path: "/", + httpOnly: true, + }); + } else { + response.cookies.set("defaultLocale", defaultLocale, { + path: "/", + httpOnly: true, + }); + } + + return response; +} + +export const config = { + matcher: [ + { + source: "/((?!api|_next/static|_next/image|favicon.ico).*)", + missing: [ + { type: "header", key: "next-router-prefetch" }, + { type: "header", key: "purpose", value: "prefetch" }, + ], + }, + { + source: "/((?!api|_next/static|_next/image|favicon.ico).*)", + has: [ + { type: "header", key: "next-router-prefetch" }, + { type: "header", key: "purpose", value: "prefetch" }, + ], + }, + { + source: "/((?!api|_next/static|_next/image|favicon.ico).*)", + has: [{ type: "header", key: "x-present" }], + missing: [{ type: "header", key: "x-missing", value: "prefetch" }], + }, + ], +}; diff --git a/tsconfig.json b/tsconfig.json index 0effe2e..5b3758b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,6 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "tailwind.config.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "tailwind.config.ts", "src/middleware.ts"], "exclude": ["node_modules"] }