diff --git a/next.config.ts b/next.config.ts index b9fbeab..3e44d22 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,9 @@ import type { NextConfig } from "next"; +import createNextIntlPlugin from 'next-intl/plugin'; const nextConfig: NextConfig = { /* output: 'export' */ }; -export default nextConfig; +const withNextIntl = createNextIntlPlugin(); +export default withNextIntl(nextConfig); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5e29af6..0b7e3d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "jose": "^6.1.2", "next": "^16.0.7", "next-auth": "5.0.0-beta.30", + "next-intl": "^4.5.8", "react": "19.2.0", "react-dom": "19.2.0", "tailwindcss": "^4.1.17", @@ -485,6 +486,66 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "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", @@ -1592,6 +1653,178 @@ "dev": true, "license": "MIT" }, + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz", + "integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz", + "integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==", + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz", + "integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz", + "integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==", + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz", + "integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==", + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz", + "integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==", + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz", + "integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==", + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz", + "integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==", + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz", + "integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==", + "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.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz", + "integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==", + "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.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1601,6 +1834,15 @@ "tslib": "^2.8.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/@tailwindcss/node": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", @@ -3166,6 +3408,12 @@ } } }, + "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-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4408,6 +4656,18 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5409,6 +5669,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": "16.0.7", "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", @@ -5488,6 +5757,91 @@ } } }, + "node_modules/next-intl": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.5.8.tgz", + "integrity": "sha512-BdN6494nvt09WtmW5gbWdwRhDDHC/Sg7tBMhN7xfYds3vcRCngSDXat81gmJkblw9jYOv8zXzzFJyu5VYXnJzg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "@swc/core": "^1.15.2", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.5.8", + "po-parser": "^1.0.2", + "use-intl": "^4.5.8" + }, + "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.5.8", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.5.8.tgz", + "integrity": "sha512-hscCKUv+5GQ0CCNbvqZ8gaxnAGToCgDTbL++jgCq8SCk/ljtZDEeQZcMk46Nm6Ynn49Q/JKF4Npo/Sq1mpbusA==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", + "integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==", + "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.3", + "@swc/core-darwin-x64": "1.15.3", + "@swc/core-linux-arm-gnueabihf": "1.15.3", + "@swc/core-linux-arm64-gnu": "1.15.3", + "@swc/core-linux-arm64-musl": "1.15.3", + "@swc/core-linux-x64-gnu": "1.15.3", + "@swc/core-linux-x64-musl": "1.15.3", + "@swc/core-win32-arm64-msvc": "1.15.3", + "@swc/core-win32-ia32-msvc": "1.15.3", + "@swc/core-win32-x64-msvc": "1.15.3" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -5762,6 +6116,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/po-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-1.0.2.tgz", + "integrity": "sha512-yTIQL8PZy7V8c0psPoJUx7fayez+Mo/53MZgX9MPuPHx+Dt+sRPNuRbI+6Oqxnddhkd68x4Nlgon/zizL1Xg+w==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6767,7 +7127,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6903,6 +7263,20 @@ "punycode": "^2.1.0" } }, + "node_modules/use-intl": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz", + "integrity": "sha512-rWPV2Sirw55BQbA/7ndUBtsikh8WXwBrUkZJ1mD35+emj/ogPPqgCZdv1DdrEFK42AjF1g5w8d3x8govhqPH6Q==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "@schummar/icu-type-parser": "1.21.5", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index d52f4d0..59f7b32 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "jose": "^6.1.2", "next": "^16.0.7", "next-auth": "5.0.0-beta.30", + "next-intl": "^4.5.8", "react": "19.2.0", "react-dom": "19.2.0", "tailwindcss": "^4.1.17", diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..fc35eea --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,41 @@ +import Providers from '@/app/providers/getQueryServer' +import type { Metadata } from "next"; +import { NextIntlClientProvider, hasLocale } from 'next-intl'; +import { notFound } from 'next/navigation'; +import { routing } from '@/i18n/routing'; +import "../styles/globals.css"; +import "../styles/global-styles.scss"; + +export function generateStaticParams() { + return routing.locales.map((locale: any) => ({ locale })); +} + +export const metadata: Metadata = { + title: "No copy", + description: "No copy", +}; + +export default async function RootLayout({ + children, + params +}: Readonly<{ + children: React.ReactNode; + params: Promise<{ locale: string }>; +}>) { + const { locale } = await params; + if (!hasLocale(routing.locales, locale)) { + notFound(); + } + + return ( + + + + + {children} + + + + + ); +} diff --git a/src/app/login/page.tsx b/src/app/[locale]/login/page.tsx similarity index 72% rename from src/app/login/page.tsx rename to src/app/[locale]/login/page.tsx index 9252d4b..22194bf 100644 --- a/src/app/login/page.tsx +++ b/src/app/[locale]/login/page.tsx @@ -3,45 +3,58 @@ import styles from '@/app/styles/login.module.scss' import LogoIcon from '@/app/ui/logo-icon'; import Link from 'next/link'; import LoginForm from '@/app/ui/login-form'; +import LanguageSwitcher from '@/app/components/LanguageSwitcher'; +import { useTranslations } from 'next-intl'; export const metadata: Metadata = { title: 'Login', }; export default function Page() { + const t = useTranslations('Login-register-form'); + return (
+
+ +

NO COPY

-

Защита авторских прав

+

+ {t('copyright-protection')} +

- или войти через + + {t('or-log-in-via')} +
-

Нет аккаунта? Зарегистрироваться

+

{t('no-account')}? + {t('register')} +

- Защита контента скрытыми водяными знаками + {t('protecting-watermark')}
- Мониторинг нарушений в реальном времени + {t('monitoring-violations')}
- Надежное хранение и безопасность данных + {t('safe-storage')}
diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx similarity index 81% rename from src/app/page.tsx rename to src/app/[locale]/page.tsx index 98243a1..0ac4b69 100644 --- a/src/app/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,17 +1,21 @@ import styles from "@/app/styles/page.module.scss"; import { Metadata } from 'next'; import Link from 'next/link'; +import { useTranslations } from 'next-intl'; export const metadata: Metadata = { title: 'Home page', }; export default function Home() { + const t = useTranslations('HomePage'); + return (
Home page + {t('title')}
...}> -

Статистика

+

+ {t("statistics")} +

diff --git a/src/app/pages/emptypage/page.tsx b/src/app/[locale]/pages/emptypage/page.tsx similarity index 100% rename from src/app/pages/emptypage/page.tsx rename to src/app/[locale]/pages/emptypage/page.tsx diff --git a/src/app/pages/layout.tsx b/src/app/[locale]/pages/layout.tsx similarity index 93% rename from src/app/pages/layout.tsx rename to src/app/[locale]/pages/layout.tsx index b9830f5..795f79d 100644 --- a/src/app/pages/layout.tsx +++ b/src/app/[locale]/pages/layout.tsx @@ -1,6 +1,6 @@ import NavLinks from '@/app/ui/nav-links' import styles from '@/app/styles/page.module.scss' -import HeaderPanel from '../ui/header/headerPanel'; +import HeaderPanel from '@/app/ui/header/headerPanel'; import { getQueryClient } from '@/app/providers/getQueryClient'; import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; import { getUserData } from '@/app/actions/action'; diff --git a/src/app/pages/marking-audio/page.tsx b/src/app/[locale]/pages/marking-audio/page.tsx similarity index 100% rename from src/app/pages/marking-audio/page.tsx rename to src/app/[locale]/pages/marking-audio/page.tsx diff --git a/src/app/pages/marking-photo/page.tsx b/src/app/[locale]/pages/marking-photo/page.tsx similarity index 100% rename from src/app/pages/marking-photo/page.tsx rename to src/app/[locale]/pages/marking-photo/page.tsx diff --git a/src/app/pages/marking-video/page.tsx b/src/app/[locale]/pages/marking-video/page.tsx similarity index 100% rename from src/app/pages/marking-video/page.tsx rename to src/app/[locale]/pages/marking-video/page.tsx diff --git a/src/app/pages/payment/page.tsx b/src/app/[locale]/pages/payment/page.tsx similarity index 100% rename from src/app/pages/payment/page.tsx rename to src/app/[locale]/pages/payment/page.tsx diff --git a/src/app/pages/reports/page.tsx b/src/app/[locale]/pages/reports/page.tsx similarity index 100% rename from src/app/pages/reports/page.tsx rename to src/app/[locale]/pages/reports/page.tsx diff --git a/src/app/pages/settings/page.tsx b/src/app/[locale]/pages/settings/page.tsx similarity index 100% rename from src/app/pages/settings/page.tsx rename to src/app/[locale]/pages/settings/page.tsx diff --git a/src/app/register/page.tsx b/src/app/[locale]/register/page.tsx similarity index 54% rename from src/app/register/page.tsx rename to src/app/[locale]/register/page.tsx index ad79eec..763202a 100644 --- a/src/app/register/page.tsx +++ b/src/app/[locale]/register/page.tsx @@ -2,26 +2,39 @@ import LogoIcon from '@/app/ui/logo-icon'; import styles from '@/app/styles/login.module.scss' import Link from 'next/link'; import RegisterForm from '@/app/ui/register-form'; +import LanguageSwitcher from '@/app/components/LanguageSwitcher'; +import {useTranslations} from 'next-intl'; export default function Page() { + const t = useTranslations('Login-register-form'); + return (
+
+ +

NO COPY

-

Создать аккаунт

+

+ {t('create-an-account')} +

-

🎉 Бесплатный старт

-

При регистрации вы получаете подписку "ДЕМО" с возможностью маркировки до 3 файлов в месяц и 10 МБ хранилища.

+

🎉 {t('free-start')}

+

+ {t('free-start-description')} +

- или зарегистрироваться через + + {t('or-register-via')} +
-

Уже есть аккаунт? Войти

+

{t('already-have-an-account')}? {t('login')}

diff --git a/src/app/terms-and-privacy-policy/page.tsx b/src/app/[locale]/terms-and-privacy-policy/page.tsx similarity index 100% rename from src/app/terms-and-privacy-policy/page.tsx rename to src/app/[locale]/terms-and-privacy-policy/page.tsx diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index 221ae8b..347ed36 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -83,7 +83,7 @@ export async function authorization( let parsed = await response.json(); await createSession(parsed.token, parsed.email); } else { - throw (`Запрос завершился с ошибкой: ${response.status}`); + throw ('request-ended-with-an-error'); } } catch (error) { @@ -146,7 +146,7 @@ export async function registration( path: string }) => { if (errors[obj.path[0]]) { - errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`; + errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`; } else { errors[obj.path[0]] = obj.message; } @@ -168,9 +168,9 @@ export async function registration( try { const { full_name, email, password, phone } = validatedFields.data; - const response = await fetch(`${API_BASE_URL}/v1/api/auth/register`, { + const response = await fetch(`${API_BASE_URL}/v1/api/auth/register1`, { method: 'POST', - body: JSON.stringify({ fullName: full_name, email, password, companyName: company, phone: phone}), + body: JSON.stringify({ fullName: full_name, email, password, companyName: company, phone: phone }), headers: { "Content-Type": "application/json", "Accept": "application/json" @@ -186,7 +186,7 @@ export async function registration( } catch (error) { if (error) { const errors: Record = {}; - errors['server'] = `Запрос завершился с ошибкой: ${error}` + errors['server'] = 'request-ended-with-an-error' return { previousState: { diff --git a/src/app/actions/definitions.ts b/src/app/actions/definitions.ts index f18a5a5..ea31844 100644 --- a/src/app/actions/definitions.ts +++ b/src/app/actions/definitions.ts @@ -20,26 +20,26 @@ export const SignupFormSchema = z .object({ full_name: z .string() - .min(3, { error: 'Имя должно содержать не менее 3 символов.' }) + .min(3, { error: 'register-error-name' }) .trim(), - email: z.email({ error: 'Пожалуйста, введите правильный адрес электронной почты.' }).trim(), + email: z.email({ error: 'register-error-valid-email' }).trim(), phone: z .string() - .min(12, { error: 'Пожалуйста, введите телефонный номер.' }), + .min(12, { error: 'register-error-phone' }), password: z .string() - .min(8, { error: 'Длина пароля должна быть не менее 8 символов.' }) - .regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'Пароль должен содержать как минимум 1 букву.' }) - .regex(/[0-9]/, { error: 'Пароль должен содержать как минимум 1 цифру.' }) + .min(8, { error: 'register-error-password-symbols' }) + .regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter'}) + .regex(/[0-9]/, { error: 'register-error-password-one-digit' }) .trim(), confirm_password: z .string(), agree: z .string() - .min(2, { error: 'Пожалуйста, ознакомьтесь с условиями использования и политикой конфиденциальности.' }) + .min(2, { error: 'register-error-agree' }) }) .refine((data) => data.password === data.confirm_password, { - message: 'Повторите пароль', + message: 'repeat-password', path: ['confirm_password'], }); @@ -47,11 +47,11 @@ export const loginFormSchema = z .object({ email: z .string() - .min(1, { error: 'Адрес электронной почты не должен быть пустым.' }) + .min(1, { error: 'login-error-email' }) .trim(), password: z .string() - .min(1, { error: 'Пароль не должен быть пустым.' }) + .min(1, { error: 'login-error-password' }) .trim(), }) diff --git a/src/app/components/LanguageSwitcher.tsx b/src/app/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..3dab9e0 --- /dev/null +++ b/src/app/components/LanguageSwitcher.tsx @@ -0,0 +1,85 @@ +'use client' + +import { useLocale } from 'next-intl' +import { usePathname, useRouter } from '@/i18n/navigation' +import { routing } from '@/i18n/routing' +import { useState, useRef, useEffect } from 'react' +import { useClickOutside } from '@/app/hooks/useClickOutside'; + +export default function LanguageSwitcher() { + const locale = useLocale() + const router = useRouter() + const pathname = usePathname() + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + + const switchLanguage = (newLocale: string) => { + router.replace(pathname, { locale: newLocale }) + setIsOpen(false) + } + + useClickOutside(dropdownRef, () => { + setIsOpen(false); + }); + + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsOpen(false) + } + + document.addEventListener('keydown', handleEscape) + return () => document.removeEventListener('keydown', handleEscape) + }, []); + + return ( +
+ + + {isOpen && ( +
+
    + {routing.locales.map((lang) => ( +
  • + +
  • + ))} +
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index 0cb2844..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Providers from './providers/getQueryServer' -import type { Metadata } from "next"; -import "./styles/globals.css"; -import "./styles/global-styles.scss" - -export const metadata: Metadata = { - title: "No copy", - description: "No copy", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - - {children} - - - - ); -} diff --git a/src/app/styles/login.module.scss b/src/app/styles/login.module.scss index cac5e5e..e3f652f 100644 --- a/src/app/styles/login.module.scss +++ b/src/app/styles/login.module.scss @@ -15,6 +15,12 @@ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1); width: 100%; max-width: 500px; + + &-language { + position: fixed; + right: 20px; + top: 20px; + } } .logo { diff --git a/src/app/ui/header/headerPanel.tsx b/src/app/ui/header/headerPanel.tsx index d878c51..1e75321 100644 --- a/src/app/ui/header/headerPanel.tsx +++ b/src/app/ui/header/headerPanel.tsx @@ -1,11 +1,13 @@ import NotificationsButton from './notificationsButton'; import UserMenuButton from './userMenuButton'; import Link from 'next/link'; +import LanguageSwitcher from '@/app/components/LanguageSwitcher'; export default function HeaderPanel() { return (
+
- + {state?.error?.email && ( -

{state?.error?.email}

+

+ {t(state?.error?.email)} +

)}
- + {state?.error?.password && ( -

{state?.error?.password}

+

+ {t(state?.error?.password)} +

)}
{state?.error.server && ( -

{state?.error.server}

+

+ {t(state?.error.server)} +

)}
- +
- Забыли пароль? + + {t('forgot-password')}? +
- + ) } \ No newline at end of file diff --git a/src/app/ui/register-form.tsx b/src/app/ui/register-form.tsx index 94c892d..4a5b2cb 100644 --- a/src/app/ui/register-form.tsx +++ b/src/app/ui/register-form.tsx @@ -5,12 +5,14 @@ import { useActionState } from 'react'; import { registration } from '@/app/actions/auth'; import PhoneInput from '@/app/ui/inputs/phone-input'; import Link from 'next/link'; +import { useTranslations } from 'next-intl'; export default function RegisterForm() { const [state, formAction, isPending] = useActionState( registration, undefined, ); + const t = useTranslations('Login-register-form'); return (
{ @@ -20,23 +22,27 @@ export default function RegisterForm() { {state?.error?.full_name && ( -

{state?.error?.full_name}

+

+ {t(state?.error?.full_name)} +

)}
- + {state?.error?.email && ( -

{state?.error?.email}

+

+ {t(state?.error?.email)} +

)}
- + {state?.error?.phone && ( -

{state?.error?.phone}

+

+ {t(state?.error?.phone)} +

)}
@@ -61,43 +73,55 @@ export default function RegisterForm() {
- + {state?.error?.password && ( -

{state?.error?.password}

+

+ { + state?.error?.password.split('&').map((e) => { + return t(e) + }) + } +

)}
- + {state?.error?.confirm_password && ( -

{state?.error?.confirm_password}

+

+ {t(state?.error?.confirm_password)} +

)}
@@ -108,22 +132,28 @@ export default function RegisterForm() { name="agree" defaultChecked={state?.previousState?.agree ? true : false} /> -
{state?.error?.agree && ( -

{state?.error?.agree}

+

+ {t(state?.error?.agree)} +

)} {state?.error?.server && ( -

{state?.error?.server}

+

+ {t(state?.error?.server)} +

)} - + ) } \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json new file mode 100644 index 0000000..d4a32cc --- /dev/null +++ b/src/i18n/messages/en.json @@ -0,0 +1,51 @@ +{ + "HomePage": { + "title": "Welcome to our application", + "description": "This is a demo application with i18n support" + }, + "Global": { + "statistics": "Statistik" + }, + "Login-register-form": { + "already-have-an-account": "Already have an account", + "company": "Company", + "company-placeholder": "Your company", + "confirm-password": "Confirm password", + "copyright-protection": "Copyright protection", + "create-an-account": "Create an account", + "email-adress": "Email adress", + "enter-email": "Enter your email", + "enter-password": "Enter your password", + "forgot-password": "Forgot your password", + "free-start": "Free start", + "free-start-description": "Upon registration, you receive a \"DEMO\" subscription with the ability to tag up to 3 files per month and 10 MB of storage.", + "full-name": "Full name", + "i-agree-to": "I agree to the", + "login": "Login", + "login-error-email": "Email address must not be empty.", + "login-error-password": "The password must not be empty.", + "monitoring-violations": "Real-time monitoring of violations", + "name-placeholder": "Ivanov Ivan Ivanovich", + "no-account": "No account", + "or-log-in-via": "or log in via", + "or-register-via": "or register via", + "password": "Password", + "password-placeholder": "Minimum 8 characters", + "phone": "Phone", + "protecting-watermark": "Protecting content with hidden watermarks", + "register": "Register", + "register-error-agree": "Please review the terms of use and privacy policy.", + "register-error-name": "Name must be at least 3 characters long.", + "register-error-password-one-digit": "Password must contain at least 1 digit.", + "register-error-password-one-letter": "Password must contain at least 1 letter.", + "register-error-password-symbols": "Password must be at least 8 characters long.", + "register-error-phone": "Please enter a phone number.", + "register-error-valid-email": "Please enter a valid email address.", + "remember-me": "Remember me", + "repeat-password": "Repeat password", + "request-ended-with-an-error": "The request ended with an error", + "safe-storage": "Reliable storage and data security", + "sign-in": "Sign in", + "terms-of-use": "terms of use and privacy policy" + } +} \ No newline at end of file diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json new file mode 100644 index 0000000..1b87b17 --- /dev/null +++ b/src/i18n/messages/ru.json @@ -0,0 +1,51 @@ +{ + "HomePage": { + "title": "Добро пожаловать в наше приложение", + "description": "Это демонстрационное приложение с поддержкой i18n" + }, + "Global": { + "statistics": "Статистика" + }, + "Login-register-form": { + "already-have-an-account": "Уже есть аккаунт", + "company": "Компания", + "company-placeholder": "ООО «Ваша компания»", + "confirm-password": "Подтвердите пароль", + "copyright-protection": "Защита авторских прав", + "create-an-account": "Создать аккаунт", + "email-adress": "Email адрес", + "enter-email": "Введите ваш email", + "enter-password": "Введите пароль", + "forgot-password": "Забыли пароль", + "free-start": "Бесплатный старт", + "free-start-description": "При регистрации вы получаете подписку \"ДЕМО\" с возможностью маркировки до 3 файлов в месяц и 10 МБ хранилища.", + "full-name": "Полное имя", + "i-agree-to": "Я соглашаюсь с", + "login": "Войти", + "login-error-email": "Адрес электронной почты не должен быть пустым.", + "login-error-password": "Пароль не должен быть пустым.", + "monitoring-violations": "Мониторинг нарушений в реальном времени", + "name-placeholder": "Иванов Иван Иванович", + "no-account": "Нет аккаунта", + "or-log-in-via": "или войти через", + "or-register-via": "или зарегистрироваться через", + "password": "Пароль", + "password-placeholder": "Минимум 8 символов", + "phone": "Телефон", + "protecting-watermark": "Защита контента скрытыми водяными знаками", + "register": "Зарегистрироваться", + "register-error-agree": "Пожалуйста, ознакомьтесь с условиями использования и политикой конфиденциальности.", + "register-error-name": "Имя должно содержать не менее 3 символов.", + "register-error-password-one-digit": "Пароль должен содержать как минимум 1 цифру.", + "register-error-password-one-letter": "Пароль должен содержать как минимум 1 букву.", + "register-error-password-symbols": "Длина пароля должна быть не менее 8 символов.", + "register-error-phone": "Пожалуйста, введите телефонный номер.", + "register-error-valid-email": "Пожалуйста, введите правильный адрес электронной почты.", + "remember-me": "Запомнить меня", + "repeat-password": "Повторите пароль", + "request-ended-with-an-error": "Запрос завершился с ошибкой", + "safe-storage": "Надежное хранение и безопасность данных", + "sign-in": "Войти в систему", + "terms-of-use": "условиями использования и политикой конфиденциальности" + } +} \ No newline at end of file diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts new file mode 100644 index 0000000..3c37594 --- /dev/null +++ b/src/i18n/navigation.ts @@ -0,0 +1,5 @@ +import { createNavigation } from 'next-intl/navigation'; +import { routing } from './routing'; + +export const { Link, redirect, usePathname, useRouter, getPathname } = + createNavigation(routing); \ No newline at end of file diff --git a/src/i18n/request.ts b/src/i18n/request.ts new file mode 100644 index 0000000..525965b --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,15 @@ +import { getRequestConfig } from 'next-intl/server'; +import { hasLocale } from 'next-intl'; +import { routing } from './routing'; + +export default getRequestConfig(async ({ requestLocale }) => { + const requested = await requestLocale; + const locale = hasLocale(routing.locales, requested) + ? requested + : routing.defaultLocale; + + return { + locale, + messages: (await import(`./messages/${locale}.json`)).default + }; +}); \ No newline at end of file diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts new file mode 100644 index 0000000..5863b33 --- /dev/null +++ b/src/i18n/routing.ts @@ -0,0 +1,6 @@ +import { defineRouting } from 'next-intl/routing'; + +export const routing = defineRouting({ + locales: ['ru', 'en'], + defaultLocale: 'ru' +}); \ No newline at end of file diff --git a/src/proxy.ts b/src/proxy.ts index 62733a5..6155f7d 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,27 +1,61 @@ import { NextResponse, NextRequest } from 'next/server'; import { cookies } from 'next/headers' import { decrypt } from '@/app/actions/session'; +import createIntlMiddleware from 'next-intl/middleware' +import { routing } from '@/i18n/routing' -const protectedRoutes = ['/pages'] -const publicRoutes = ['/login', '/register', '/'] +const protectedRoutes = ['/pages']; +const publicRoutes = ['/login', '/register', '/']; +const intlMiddleware = createIntlMiddleware(routing); export default async function proxy(request: NextRequest) { const path = request.nextUrl.pathname; - const isProtectedRoute = protectedRoutes.includes(path.slice(0, 6)); - const isPublicRoute = publicRoutes.includes(path); - const cookie = (await cookies()).get('session')?.value; - const session = await decrypt(cookie); - - if (isProtectedRoute && !session?.token) { - return NextResponse.redirect(new URL('/login', request.nextUrl)); + if ( + path.startsWith('/_next') || + path.includes('/api/') || + path.includes('.') + ) { + return NextResponse.next() } - if (path === '/') { - return NextResponse.redirect(new URL('/pages/dashboard', request.nextUrl)); + const intlResponse = intlMiddleware(request); + + if (intlResponse) { + const localizedPath = intlResponse.headers.get('x-middleware-request') || + intlResponse.headers.get('x-middleware-next') || + path + + const isProtectedRoute = protectedRoutes.some(route => + localizedPath.includes(route) || + path.includes(route) + ) + + const isPublicRoute = publicRoutes.some(route => + localizedPath === `/${request.nextUrl.locale}${route}` || + localizedPath === route + ) + + + const cookie = (await cookies()).get('session')?.value; + const session = await decrypt(cookie); + + if (isProtectedRoute && !session?.token) { + const loginUrl = new URL('/login', request.nextUrl) + loginUrl.searchParams.set('locale', request.nextUrl.locale || 'ru') + return NextResponse.redirect(loginUrl) + } + + if (path === '/en' || path === '/ru' || localizedPath === `/${request.nextUrl.locale}`) { + const dashboardUrl = new URL('/pages/dashboard', request.nextUrl) + dashboardUrl.searchParams.set('locale', request.nextUrl.locale || 'ru') + return NextResponse.redirect(dashboardUrl) + } + + return intlResponse } - return NextResponse.next(); + return NextResponse.next(); } export const config = {