diff --git a/package.json b/package.json index 6fd58e3..8fee4df 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,14 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "fp-ts": "^2.16.9", + "i18next": "^23.16.4", + "i18next-resources-to-backend": "^1.2.1", "lucide-react": "^0.454.0", "next": "15.0.2", "postgres": "^3.4.5", "react": "19.0.0-rc-69d4b800-20241021", "react-dom": "19.0.0-rc-69d4b800-20241021", + "react-i18next": "^15.1.0", "reflect-metadata": "^0.2.2", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", diff --git a/src/app/dashboard/components/client/create-random-invoice/create-random-invoice.tsx b/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx similarity index 76% rename from src/app/dashboard/components/client/create-random-invoice/create-random-invoice.tsx rename to src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx index 8a80645..f3f95a4 100644 --- a/src/app/dashboard/components/client/create-random-invoice/create-random-invoice.tsx +++ b/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx @@ -1,7 +1,7 @@ "use client" import Button from "@/app/components/button/button" -import CreateRandomInvoiceButtonVM from "@/app/dashboard/vm/create-random-invoice-button-vm" +import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm" import { useDI } from "@/bootstrap/di/di-context" import { useRef } from "react" diff --git a/src/app/dashboard/components/client/nav-links/nav-link-controller.ts b/src/app/[lang]/dashboard/components/client/nav-links/nav-link-controller.ts similarity index 100% rename from src/app/dashboard/components/client/nav-links/nav-link-controller.ts rename to src/app/[lang]/dashboard/components/client/nav-links/nav-link-controller.ts diff --git a/src/app/dashboard/components/client/nav-links/nav-links.tsx b/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx similarity index 88% rename from src/app/dashboard/components/client/nav-links/nav-links.tsx rename to src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx index 1aa6072..6aa2ce8 100644 --- a/src/app/dashboard/components/client/nav-links/nav-links.tsx +++ b/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx @@ -1,5 +1,5 @@ 'use client' -import navLinkPersonalVM from '@/app/dashboard/components/client/nav-links/nav-link-controller'; +import navLinkPersonalVM from '@/app/[lang]/dashboard/components/client/nav-links/nav-link-controller'; import clsx from 'clsx'; import Link from 'next/link' diff --git a/src/app/dashboard/components/server/card/card-controller.ts b/src/app/[lang]/dashboard/components/server/card/card-controller.ts similarity index 100% rename from src/app/dashboard/components/server/card/card-controller.ts rename to src/app/[lang]/dashboard/components/server/card/card-controller.ts diff --git a/src/app/dashboard/components/server/card/card.tsx b/src/app/[lang]/dashboard/components/server/card/card.tsx similarity index 86% rename from src/app/dashboard/components/server/card/card.tsx rename to src/app/[lang]/dashboard/components/server/card/card.tsx index 1b22154..303eaec 100644 --- a/src/app/dashboard/components/server/card/card.tsx +++ b/src/app/[lang]/dashboard/components/server/card/card.tsx @@ -1,4 +1,4 @@ -import cardController from "@/app/dashboard/components/server/card/card-controller"; +import cardController from "@/app/[lang]/dashboard/components/server/card/card-controller"; diff --git a/src/app/dashboard/components/server/cards/cards-controller.ts b/src/app/[lang]/dashboard/components/server/cards/cards-controller.ts similarity index 100% rename from src/app/dashboard/components/server/cards/cards-controller.ts rename to src/app/[lang]/dashboard/components/server/cards/cards-controller.ts diff --git a/src/app/dashboard/components/server/cards/cards.tsx b/src/app/[lang]/dashboard/components/server/cards/cards.tsx similarity index 75% rename from src/app/dashboard/components/server/cards/cards.tsx rename to src/app/[lang]/dashboard/components/server/cards/cards.tsx index e8f74a9..a0aea58 100644 --- a/src/app/dashboard/components/server/cards/cards.tsx +++ b/src/app/[lang]/dashboard/components/server/cards/cards.tsx @@ -1,5 +1,5 @@ -import { Card } from '@/app/dashboard/components/server/card/card'; -import cardsController from '@/app/dashboard/components/server/cards/cards-controller'; +import { Card } from '@/app/[lang]/dashboard/components/server/card/card'; +import cardsController from '@/app/[lang]/dashboard/components/server/cards/cards-controller'; export default async function CardWrapper() { diff --git a/src/app/dashboard/components/server/latest-invoices/latest-invoices-controller.ts b/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller.ts similarity index 100% rename from src/app/dashboard/components/server/latest-invoices/latest-invoices-controller.ts rename to src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller.ts diff --git a/src/app/dashboard/components/server/latest-invoices/latest-invoices.tsx b/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices.tsx similarity index 89% rename from src/app/dashboard/components/server/latest-invoices/latest-invoices.tsx rename to src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices.tsx index 696af73..3c3e687 100644 --- a/src/app/dashboard/components/server/latest-invoices/latest-invoices.tsx +++ b/src/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices.tsx @@ -1,5 +1,5 @@ -import CreateRandomInvoiceContainer from '@/app/dashboard/components/client/create-random-invoice/create-random-invoice'; -import latestInvoicesController from '@/app/dashboard/components/server/latest-invoices/latest-invoices-controller'; +import CreateRandomInvoiceContainer from '@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice'; +import latestInvoicesController from '@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller'; import { ArrowPathIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; import { isLeft } from 'fp-ts/lib/Either'; diff --git a/src/app/dashboard/components/server/revenue-chart/revenue-chart-controller.ts b/src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller.ts similarity index 100% rename from src/app/dashboard/components/server/revenue-chart/revenue-chart-controller.ts rename to src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller.ts diff --git a/src/app/dashboard/components/server/revenue-chart/revenue-chart.tsx b/src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart.tsx similarity index 93% rename from src/app/dashboard/components/server/revenue-chart/revenue-chart.tsx rename to src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart.tsx index b29a7ae..fb778f2 100644 --- a/src/app/dashboard/components/server/revenue-chart/revenue-chart.tsx +++ b/src/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart.tsx @@ -1,4 +1,4 @@ -import revenueChartController from '@/app/dashboard/components/server/revenue-chart/revenue-chart-controller'; +import revenueChartController from '@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller'; import { CalendarIcon } from '@heroicons/react/24/outline'; export default async function RevenueChart() { diff --git a/src/app/dashboard/components/server/sidenav.tsx b/src/app/[lang]/dashboard/components/server/sidenav.tsx similarity index 87% rename from src/app/dashboard/components/server/sidenav.tsx rename to src/app/[lang]/dashboard/components/server/sidenav.tsx index 0af5a13..bc396c4 100644 --- a/src/app/dashboard/components/server/sidenav.tsx +++ b/src/app/[lang]/dashboard/components/server/sidenav.tsx @@ -1,4 +1,4 @@ -import NavLinks from '@/app/dashboard/components/client/nav-links/nav-links'; +import NavLinks from '@/app/[lang]/dashboard/components/client/nav-links/nav-links'; import Link from 'next/link'; export default function SideNav() { diff --git a/src/app/dashboard/components/server/skeletons/skeletons.tsx b/src/app/[lang]/dashboard/components/server/skeletons/skeletons.tsx similarity index 100% rename from src/app/dashboard/components/server/skeletons/skeletons.tsx rename to src/app/[lang]/dashboard/components/server/skeletons/skeletons.tsx diff --git a/src/app/dashboard/layout.tsx b/src/app/[lang]/dashboard/layout.tsx similarity index 78% rename from src/app/dashboard/layout.tsx rename to src/app/[lang]/dashboard/layout.tsx index 55125e7..4e678cf 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/[lang]/dashboard/layout.tsx @@ -1,6 +1,6 @@ "use client" -import SideNav from "@/app/dashboard/components/server/sidenav"; -import dashboardAppModule from "@/app/dashboard/module/dashboard-app-module"; +import SideNav from "@/app/[lang]/dashboard/components/server/sidenav"; +import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard-app-module"; import { DiContext } from "@/bootstrap/di/di-context"; import { useRef } from "react"; diff --git a/src/app/[lang]/dashboard/loading.tsx b/src/app/[lang]/dashboard/loading.tsx new file mode 100644 index 0000000..9c38c20 --- /dev/null +++ b/src/app/[lang]/dashboard/loading.tsx @@ -0,0 +1,5 @@ +import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons/skeletons"; + +export default function Loading() { + return ; +} \ No newline at end of file diff --git a/src/app/dashboard/module/dashboard-app-module.ts b/src/app/[lang]/dashboard/module/dashboard-app-module.ts similarity index 81% rename from src/app/dashboard/module/dashboard-app-module.ts rename to src/app/[lang]/dashboard/module/dashboard-app-module.ts index 60721eb..10992a6 100644 --- a/src/app/dashboard/module/dashboard-app-module.ts +++ b/src/app/[lang]/dashboard/module/dashboard-app-module.ts @@ -1,4 +1,4 @@ -import CreateRandomInvoiceButtonVM from "@/app/dashboard/vm/create-random-invoice-button-vm"; +import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"; import di from "@/bootstrap/di/init-di" import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase"; diff --git a/src/app/dashboard/page.tsx b/src/app/[lang]/dashboard/page.tsx similarity index 50% rename from src/app/dashboard/page.tsx rename to src/app/[lang]/dashboard/page.tsx index 24fb89a..a77f734 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/[lang]/dashboard/page.tsx @@ -1,15 +1,17 @@ -import { LatestInvoicesSkeleton, RevenueChartSkeleton } from "@/app/dashboard/components/server/skeletons/skeletons"; -import CardWrapper from "@/app/dashboard/components/server/cards/cards"; -import LatestInvoices from "@/app/dashboard/components/server/latest-invoices/latest-invoices"; -import RevenueChart from "@/app/dashboard/components/server/revenue-chart/revenue-chart"; +import { LatestInvoicesSkeleton, RevenueChartSkeleton } from "@/app/[lang]/dashboard/components/server/skeletons/skeletons"; +import CardWrapper from "@/app/[lang]/dashboard/components/server/cards/cards"; +import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices"; +import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart"; import { Suspense } from "react"; +import { getTranslation } from "@/bootstrap/i18n/i18n"; -export default async function Dashboard() { - +export default async function Dashboard(props: {params: Promise<{lang: string}>}) { + const {lang} = await props.params + const { t } = await getTranslation(lang) return (

- Dashboard + {t("global.dashboard")}

diff --git a/src/app/dashboard/vm/create-random-invoice-button-vm.ts b/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts similarity index 100% rename from src/app/dashboard/vm/create-random-invoice-button-vm.ts rename to src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts diff --git a/src/app/page.tsx b/src/app/[lang]/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/[lang]/page.tsx diff --git a/src/app/dashboard/loading.tsx b/src/app/dashboard/loading.tsx deleted file mode 100644 index c50ad40..0000000 --- a/src/app/dashboard/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import DashboardSkeleton from "@/app/dashboard/components/server/skeletons/skeletons"; - -export default function Loading() { - return ; -} \ No newline at end of file diff --git a/src/bootstrap/i18n/dictionaries/en.ts b/src/bootstrap/i18n/dictionaries/en.ts new file mode 100644 index 0000000..3bd88ce --- /dev/null +++ b/src/bootstrap/i18n/dictionaries/en.ts @@ -0,0 +1,15 @@ +import langKey from "@/bootstrap/i18n/dictionaries/lang-key" + +const en: typeof langKey = { + global: { + home: "Home", + dashboard: "Dashboard" + }, + dashboard: { + invoice: { + createButton: "Create random Invoice" + } + } +} + +export default en \ No newline at end of file diff --git a/src/bootstrap/i18n/dictionaries/lang-key.ts b/src/bootstrap/i18n/dictionaries/lang-key.ts new file mode 100644 index 0000000..aed439c --- /dev/null +++ b/src/bootstrap/i18n/dictionaries/lang-key.ts @@ -0,0 +1,13 @@ +const langKey = { + global: { + home: "Дом", + dashboard: "Панель приборов" + }, + dashboard: { + invoice: { + createButton: "Создать случайный счет-фактуру" + } + } +} + +export default langKey; \ No newline at end of file diff --git a/src/bootstrap/i18n/dictionaries/ru.ts b/src/bootstrap/i18n/dictionaries/ru.ts new file mode 100644 index 0000000..5b6c4b4 --- /dev/null +++ b/src/bootstrap/i18n/dictionaries/ru.ts @@ -0,0 +1,15 @@ +import langKey from "@/bootstrap/i18n/dictionaries/lang-key" + +const ru: typeof langKey = { + global: { + home: "Дом", + dashboard: "Панель приборов" + }, + dashboard: { + invoice: { + createButton: "Создать случайный счет-фактуру" + } + } +} + +export default ru \ No newline at end of file diff --git a/src/bootstrap/i18n/i18n.ts b/src/bootstrap/i18n/i18n.ts new file mode 100644 index 0000000..9cc5fb0 --- /dev/null +++ b/src/bootstrap/i18n/i18n.ts @@ -0,0 +1,21 @@ +import { getOptions } from '@/bootstrap/i18n/settings' +import { createInstance } from 'i18next' +import resourcesToBackend from 'i18next-resources-to-backend' +import { initReactI18next } from 'react-i18next/initReactI18next' + +const initI18next = async (lng: string, ns?: string) => { + const i18nInstance = createInstance() + await i18nInstance + .use(initReactI18next) + .use(resourcesToBackend((language: string) => import(`./dictionaries/${language}.ts`))) + .init(getOptions(lng, ns)) + return i18nInstance +} + +export async function getTranslation(lng: string, ns?: string, options: {keyPrefix?: string} = {}) { + const i18nextInstance = await initI18next(lng, ns) + return { + t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix), + i18n: i18nextInstance + } +} \ No newline at end of file diff --git a/src/bootstrap/i18n/settings.ts b/src/bootstrap/i18n/settings.ts new file mode 100644 index 0000000..90f4fa0 --- /dev/null +++ b/src/bootstrap/i18n/settings.ts @@ -0,0 +1,15 @@ +export const fallbackLng = 'en' +export const languages = [fallbackLng, 'ru'] +export const defaultNS = 'translation' + +export function getOptions (lng = fallbackLng, ns = defaultNS) { + return { + // debug: true, + supportedLngs: languages, + fallbackLng, + lng, + fallbackNS: defaultNS, + defaultNS, + ns + } +} \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..ee54225 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,23 @@ +import { fallbackLng, languages } from "@/bootstrap/i18n/settings"; +import { NextRequest, NextResponse } from "next/server"; + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + const pathnameHasLocale = languages.some( + (lang) => pathname.startsWith(`/${lang}/`) || pathname === `/${lang}` + ) + + if (pathnameHasLocale) return + + request.nextUrl.pathname = `/${fallbackLng}${pathname}` + // e.g. incoming request is /products + // The new URL is now /en-US/products + return NextResponse.redirect(request.nextUrl) +} + +export const config = { + matcher: [ + // Skip all internal paths (_next) + '/((?!api|_next/static|_next/image|favicon.ico).*)' + ], +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0d0b9fb..9f3a85a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,6 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/bootstrap/boundaries/db/seed.js", "src/bootstrap/boundaries/db/placeholder-data.js"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/bootstrap/boundaries/db/seed.js", "src/bootstrap/boundaries/db/placeholder-data.js", "src/middleware.ts"], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index 194f43b..4fc4079 100644 --- a/yarn.lock +++ b/yarn.lock @@ -138,7 +138,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/runtime@^7.12.5": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -2342,6 +2342,13 @@ html-encoding-sniffer@^4.0.0: dependencies: whatwg-encoding "^3.1.1" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + http-proxy-agent@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" @@ -2366,6 +2373,20 @@ https-proxy-agent@^7.0.5: agent-base "^7.0.2" debug "4" +i18next-resources-to-backend@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz#fded121e63e3139ce839c9901b9449dbbea7351d" + integrity sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next@^23.16.4: + version "23.16.4" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.16.4.tgz#79c07544f6d6fa803fe8427108d547542c1d6cf4" + integrity sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -3291,6 +3312,14 @@ react-dom@19.0.0-rc-69d4b800-20241021: dependencies: scheduler "0.25.0-rc-69d4b800-20241021" +react-i18next@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.0.tgz#9494e4add2389f04c205dd7628c1aa75747b98a3" + integrity sha512-zj3nJynMnZsy2gPZiOTC7XctCY5eQGqT3tcKMmfJWC9FMvgd+960w/adq61j8iPzpwmsXejqID9qC3Mqu1Xu2Q== + dependencies: + "@babel/runtime" "^7.25.0" + html-parse-stringify "^3.0.1" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -4099,6 +4128,11 @@ vitest@^2.1.4: vite-node "2.1.4" why-is-node-running "^2.3.0" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-xmlserializer@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"