From 37ba7e04061d6d58af2b8b96f32532efc6c501ff Mon Sep 17 00:00:00 2001 From: Ilgeldi Date: Sat, 1 Feb 2025 14:47:48 +0500 Subject: [PATCH] added internationalization and some routes --- eslint.config.mjs | 2 +- next.config.ts | 11 ++-- package-lock.json | 113 ++++++++++++++++++++++++++++++++ package.json | 13 ++-- src/app/[locale]/layout.tsx | 36 ++++++++++ src/app/{ => [locale]}/page.tsx | 0 src/app/layout.tsx | 34 ---------- src/i18n/request.ts | 17 +++++ src/i18n/routing.ts | 13 ++++ src/messages/en.json | 6 ++ src/messages/ru.json | 6 ++ src/messages/tm.json | 6 ++ src/middleware.ts | 9 +++ 13 files changed, 220 insertions(+), 46 deletions(-) create mode 100644 src/app/[locale]/layout.tsx rename src/app/{ => [locale]}/page.tsx (100%) delete mode 100644 src/app/layout.tsx create mode 100644 src/i18n/request.ts create mode 100644 src/i18n/routing.ts create mode 100644 src/messages/en.json create mode 100644 src/messages/ru.json create mode 100644 src/messages/tm.json create mode 100644 src/middleware.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index c85fb67..9d8207e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,7 +10,7 @@ const compat = new FlatCompat({ }); const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + ...compat.extends("next/core-web-vitals", "next/typescript",), ]; export default eslintConfig; diff --git a/next.config.ts b/next.config.ts index e9ffa30..9ff2c4a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,8 @@ -import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; -const nextConfig: NextConfig = { - /* config options here */ -}; +const withNextIntl = createNextIntlPlugin(); -export default nextConfig; +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default withNextIntl(nextConfig); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 532eea1..ec9ab72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "next": "15.1.6", + "next-intl": "^3.26.3", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -175,6 +176,57 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz", + "integrity": "sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/intl-localematcher": "0.5.10", + "decimal.js": "10", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", + "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.0.tgz", + "integrity": "sha512-Hp81uTjjdTk3FLh/dggU5NK7EIsVWc5/ZDWrIldmf2rBuPejuZ13CZ/wpVE2SToyi4EiroPTQ1XJcJuZFIxTtw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/icu-skeleton-parser": "1.8.12", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz", + "integrity": "sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "tslib": "2" + } + }, + "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/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1898,6 +1950,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3208,6 +3266,18 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "10.7.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.14.tgz", + "integrity": "sha512-mMGnE4E1otdEutV5vLUdCxRJygHB5ozUBxsPB5qhitewssrS/qGruq9bmvIRkkGsNeK5ZWLfYRld18UHGTIifQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/icu-messageformat-parser": "2.11.0", + "tslib": "2" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3972,6 +4042,15 @@ "dev": true, "license": "MIT" }, + "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": { "version": "15.1.6", "resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz", @@ -4026,6 +4105,27 @@ } } }, + "node_modules/next-intl": { + "version": "3.26.3", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.26.3.tgz", + "integrity": "sha512-6Y97ODrDsEE1J8cXKMHwg1laLdtkN66QMIqG8BzH4zennJRUNTtM8UMtBDyhfmF6uiZ+xsbWLXmHUgmUymUsfQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "negotiator": "^1.0.0", + "use-intl": "^3.26.3" + }, + "peerDependencies": { + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5666,6 +5766,19 @@ "punycode": "^2.1.0" } }, + "node_modules/use-intl": { + "version": "3.26.3", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.26.3.tgz", + "integrity": "sha512-yY0a2YseO17cKwHA9M6fcpiEJ2Uo81DEU0NOUxNTp6lJVNOuI6nULANPVVht6IFdrYFtlsMmMoc97+Eq9/Tnng==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 6c4ddb2..0201f4f 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,20 @@ "lint": "next lint" }, "dependencies": { + "next": "15.1.6", + "next-intl": "^3.26.3", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.1.6" + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "postcss": "^8", - "tailwindcss": "^3.4.1", "eslint": "^9", "eslint-config-next": "15.1.6", - "@eslint/eslintrc": "^3" + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" } } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..bd5eef0 --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import "../globals.css"; + +type Props = { + children: React.ReactNode; + params: Promise<{ locale: "en" | "ru" | "tm" }>; +}; + +export async function generateMetadata({ params }: Props): Promise { + const locale = (await params).locale; + + const message = await (await import(`../../messages/${locale}.json`)).default; + + return { + title: message.meta.title, + description: message.meta.description, + }; +} + +export default async function RootLayout({ children, params }: Props) { + const { locale } = await params; + + const messages = await getMessages(); + + return ( + + + + {children} + + + + ); +} diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/[locale]/page.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index f7fa87e..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/src/i18n/request.ts b/src/i18n/request.ts new file mode 100644 index 0000000..01e6e04 --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,17 @@ +import { getRequestConfig } from "next-intl/server"; +import { routing } from "./routing"; + +export default getRequestConfig(async ({ requestLocale }) => { + let locale = await requestLocale; + + // Ensure that a valid locale is used + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!locale || !routing.locales.includes(locale as any)) { + locale = routing.defaultLocale; + } + + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts new file mode 100644 index 0000000..77586f9 --- /dev/null +++ b/src/i18n/routing.ts @@ -0,0 +1,13 @@ +import { defineRouting } from "next-intl/routing"; +import { createNavigation } from "next-intl/navigation"; + +export const routing = defineRouting({ + // A list of all locales that are supported + locales: ["en", "tm", "ru"], + + // Used when no locale matches + defaultLocale: "en", +}); + +export const { Link, redirect, usePathname, useRouter, getPathname } = + createNavigation(routing); diff --git a/src/messages/en.json b/src/messages/en.json new file mode 100644 index 0000000..8623dfc --- /dev/null +++ b/src/messages/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "News Orient", + "description": "News Orient" + } +} diff --git a/src/messages/ru.json b/src/messages/ru.json new file mode 100644 index 0000000..3081de8 --- /dev/null +++ b/src/messages/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Новости Orient", + "description": "Новости Orient" + } +} diff --git a/src/messages/tm.json b/src/messages/tm.json new file mode 100644 index 0000000..18c98ea --- /dev/null +++ b/src/messages/tm.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Habarlar Orient", + "description": "Habarlar Orient" + } +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..2780960 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,9 @@ +import createMiddleware from "next-intl/middleware"; +import { routing } from "./i18n/routing"; + +export default createMiddleware(routing); + +export const config = { + // Match only internationalized pathnames + matcher: ["/", "/(tm|en|ru)/:path*"], +};