Merge branch 'feature-2-i18n'
This commit is contained in:
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -138,5 +138,11 @@
|
|||||||
},
|
},
|
||||||
"[handlebars]": {
|
"[handlebars]": {
|
||||||
"editor.defaultFormatter": "vscode.html-language-features"
|
"editor.defaultFormatter": "vscode.html-language-features"
|
||||||
}
|
},
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"content/i18n",
|
||||||
|
"components/x-date-pickers/locales"
|
||||||
|
],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.sourceLanguage": "bg"
|
||||||
}
|
}
|
||||||
55
components/languageSwitcher.tsx
Normal file
55
components/languageSwitcher.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import TranslateIcon from '@mui/icons-material/Translate';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
// using https://next-intl-docs.vercel.app/docs/getting-started/pages-router
|
||||||
|
const LanguageSwitcher = () => {
|
||||||
|
const t = useTranslations('common');
|
||||||
|
const router = useRouter();
|
||||||
|
const { locale, locales, asPath } = router;
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeLanguage = (lng) => {
|
||||||
|
router.push(asPath, asPath, { locale: lng });
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton onClick={handleClick} color="inherit">
|
||||||
|
<TranslateIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
id="language-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
keepMounted
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
{locales.map((lng) => {
|
||||||
|
if (lng === locale) return null;
|
||||||
|
return (
|
||||||
|
<MenuItem key={lng} onClick={() => changeLanguage(lng)}>
|
||||||
|
{t('changeTo')} {t(lng.toUpperCase())}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageSwitcher;
|
||||||
@@ -68,4 +68,5 @@ export default function Layout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import { useRouter } from 'next/router';
|
|||||||
import sidemenu, { footerMenu } from './sidemenuData.js'; // Move sidemenu data to a separate file
|
import sidemenu, { footerMenu } from './sidemenuData.js'; // Move sidemenu data to a separate file
|
||||||
import axiosInstance from "src/axiosSecure";
|
import axiosInstance from "src/axiosSecure";
|
||||||
import common from "src/helpers/common";
|
import common from "src/helpers/common";
|
||||||
|
import LanguageSwitcher from "./languageSwitcher";
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import ProtectedPage from "pages/examples/protected";
|
||||||
|
import ProtectedRoute from "./protectedRoute";
|
||||||
|
import { UserRole } from "@prisma/client";
|
||||||
//get package version from package.json
|
//get package version from package.json
|
||||||
const packageVersion = require('../package.json').version;
|
const packageVersion = require('../package.json').version;
|
||||||
|
|
||||||
function SidebarMenuItem({ item, session, isSubmenu }) {
|
function SidebarMenuItem({ item, session, isSubmenu }) {
|
||||||
|
// const tMenu = useTranslations('menu');
|
||||||
|
const t = useTranslations('common');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isActive = router.pathname.includes(item.url);
|
const isActive = router.pathname.includes(item.url);
|
||||||
|
|
||||||
@@ -91,6 +98,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
|||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const sidebarWidth = 226; // Simplify by using a constant
|
const sidebarWidth = 226; // Simplify by using a constant
|
||||||
const sidebarRef = useRef(null);
|
const sidebarRef = useRef(null);
|
||||||
|
const t = useTranslations('menu');
|
||||||
//const [locations, setLocations] = useState([]);
|
//const [locations, setLocations] = useState([]);
|
||||||
|
|
||||||
|
|
||||||
@@ -136,6 +144,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
|||||||
<div className="flex flex-col justify-between pb-4">
|
<div className="flex flex-col justify-between pb-4">
|
||||||
<nav>
|
<nav>
|
||||||
{sidemenu.map((item, index) => (
|
{sidemenu.map((item, index) => (
|
||||||
|
item.text = t(item.id) || item.text, // Use the translation if available
|
||||||
<SidebarMenuItem key={index} item={item} session={session} />
|
<SidebarMenuItem key={index} item={item} session={session} />
|
||||||
))}
|
))}
|
||||||
<hr className="my-3 border-gray-200 dark:border-gray-600" />
|
<hr className="my-3 border-gray-200 dark:border-gray-600" />
|
||||||
@@ -145,6 +154,9 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
|||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<hr className="border-gray-200 dark:border-gray-600 text-align-bottom" />
|
<hr className="border-gray-200 dark:border-gray-600 text-align-bottom" />
|
||||||
<FooterSection />
|
<FooterSection />
|
||||||
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</ProtectedRoute >
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,14 +174,16 @@ function UserSection({ session }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SignInButton() {
|
function SignInButton() {
|
||||||
|
const t = useTranslations('common');
|
||||||
return (
|
return (
|
||||||
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
|
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
|
||||||
<button>Впишете се</button>
|
<button>{t('login')}</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserDetails({ session }) {
|
function UserDetails({ session }) {
|
||||||
|
const t = useTranslations('common');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<hr className="m-0" />
|
<hr className="m-0" />
|
||||||
@@ -180,7 +194,7 @@ function UserDetails({ session }) {
|
|||||||
<div className="ml-3 overflow-hidden">
|
<div className="ml-3 overflow-hidden">
|
||||||
<p className="mx-1 mt-1 text-sm font-medium text-gray-800 dark:text-gray-200">{session.user.name}</p>
|
<p className="mx-1 mt-1 text-sm font-medium text-gray-800 dark:text-gray-200">{session.user.name}</p>
|
||||||
<p className="mx-1 mt-1 text-sm font-medium text-gray-600 dark:text-gray-400">{session.user.role}</p>
|
<p className="mx-1 mt-1 text-sm font-medium text-gray-600 dark:text-gray-400">{session.user.role}</p>
|
||||||
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>Изход</a>
|
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>{t('logout')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import { UserRole } from "@prisma/client";
|
import { UserRole } from "@prisma/client";
|
||||||
|
|
||||||
|
|
||||||
const sidemenu = [
|
const sidemenu = [
|
||||||
{
|
{
|
||||||
id: "dashboard",
|
id: "dashboard",
|
||||||
@@ -118,6 +119,12 @@ const sidemenu = [
|
|||||||
url: "/cart/reports/coverMe",
|
url: "/cart/reports/coverMe",
|
||||||
roles: [UserRole.ADMIN, UserRole.POWERUSER],
|
roles: [UserRole.ADMIN, UserRole.POWERUSER],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "translations",
|
||||||
|
text: "Преводи",
|
||||||
|
url: "/cart/translations",
|
||||||
|
roles: [UserRole.ADMIN, UserRole.POWERUSER],
|
||||||
|
},
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
35
content/i18n/bg.json
Normal file
35
content/i18n/bg.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"greeting": "Здравей",
|
||||||
|
"farewell": "Довиждане",
|
||||||
|
"changeTo": "Смени на",
|
||||||
|
"BG": "Български",
|
||||||
|
"EN": "Английски",
|
||||||
|
"RU": "Руски",
|
||||||
|
"login": "Вход",
|
||||||
|
"logout": "Изход"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"dashboard": "Възможности",
|
||||||
|
"shedule": "График",
|
||||||
|
"myshedule": "Моя График",
|
||||||
|
"locations": "Местоположения",
|
||||||
|
"cart-report": "Отчет",
|
||||||
|
"cart-experience": "Случки",
|
||||||
|
"guidelines": "Напътствия",
|
||||||
|
"permits": "Разрешителни",
|
||||||
|
"contactAll": "Участници",
|
||||||
|
"feedback": "Отзиви",
|
||||||
|
"contactUs": "За връзка",
|
||||||
|
"admin": "Админ",
|
||||||
|
"cart-places": "Места",
|
||||||
|
"cart-publishers": "Вестители",
|
||||||
|
"cart-events": "План",
|
||||||
|
"cart-calendar": "Календар",
|
||||||
|
"cart-reports": "Отчети",
|
||||||
|
"statistics": "Статистика",
|
||||||
|
"coverMeLogs": "Замествания",
|
||||||
|
"translations": "Преводи"
|
||||||
|
},
|
||||||
|
"specialno-svidetelstvane-na-obshestveni-mesta-v-sofiya-kontakti": "Специално свидетелстване на обществени места в София - Контакти"
|
||||||
|
}
|
||||||
34
content/i18n/bg.modified.json
Normal file
34
content/i18n/bg.modified.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"greeting": "Здравей",
|
||||||
|
"farewell": "Довиждане",
|
||||||
|
"changeTo": "",
|
||||||
|
"BG": "Български",
|
||||||
|
"EN": "Английски",
|
||||||
|
"RU": "Руски",
|
||||||
|
"login": "Вход",
|
||||||
|
"logout": "Изход"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"dashboard": "Възможности",
|
||||||
|
"shedule": "График",
|
||||||
|
"myshedule": "Моя График",
|
||||||
|
"locations": "Местоположения",
|
||||||
|
"cart-report": "Отчет",
|
||||||
|
"cart-experience": "Случки",
|
||||||
|
"guidelines": "Напътствия",
|
||||||
|
"permits": "Разрешителни",
|
||||||
|
"contactAll": "Участници",
|
||||||
|
"feedback": "Отзиви",
|
||||||
|
"contactUs": "За връзка",
|
||||||
|
"admin": "Админ",
|
||||||
|
"cart-places": "Места",
|
||||||
|
"cart-publishers": "Вестители",
|
||||||
|
"cart-events": "План",
|
||||||
|
"cart-calendar": "Календар",
|
||||||
|
"cart-reports": "Отчети",
|
||||||
|
"statistics": "Статистика",
|
||||||
|
"coverMeLogs": "Замествания",
|
||||||
|
"translations": "Преводи"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
content/i18n/en.json
Normal file
12
content/i18n/en.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"greeting": "Hello",
|
||||||
|
"farewell": "Goodbye",
|
||||||
|
"changeTo": "Change to",
|
||||||
|
"BG": "Bulgarian",
|
||||||
|
"EN": "English",
|
||||||
|
"RU": "Russian",
|
||||||
|
"login": "login",
|
||||||
|
"logout": "logout"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
content/i18n/en.modified.json
Normal file
15
content/i18n/en.modified.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"greeting": "Hello",
|
||||||
|
"farewell": "Goodbye",
|
||||||
|
"changeTo": "Change to",
|
||||||
|
"BG": "Bulgarian",
|
||||||
|
"EN": "English",
|
||||||
|
"RU": "Russian",
|
||||||
|
"login": "login",
|
||||||
|
"logout": "logout"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"dashboard": "Dashboard"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
content/i18n/ru.json
Normal file
10
content/i18n/ru.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"greeting": "Здравей",
|
||||||
|
"farewell": "Довиждане",
|
||||||
|
"changeTo": "Смени на",
|
||||||
|
"BG": "[RU] Български",
|
||||||
|
"EN": "[RU] Английски",
|
||||||
|
"RU": "[RU] Руски"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,4 +50,8 @@ module.exports = withPWA({
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
i18n: { // using https://next-intl-docs.vercel.app/docs/getting-started/pages-router
|
||||||
|
locales: ['bg', 'en', 'ru'],
|
||||||
|
defaultLocale: 'bg',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
147
package-lock.json
generated
147
package-lock.json
generated
@@ -55,6 +55,7 @@
|
|||||||
"next": "^14.1.0",
|
"next": "^14.1.0",
|
||||||
"next-auth": "^4.24.6",
|
"next-auth": "^4.24.6",
|
||||||
"next-connect": "^1.0.0",
|
"next-connect": "^1.0.0",
|
||||||
|
"next-intl": "^3.12.0",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"node-excel-export": "^1.4.4",
|
"node-excel-export": "^1.4.4",
|
||||||
"node-telegram-bot-api": "^0.64.0",
|
"node-telegram-bot-api": "^0.64.0",
|
||||||
@@ -2194,6 +2195,92 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
||||||
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "1.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
|
||||||
|
"integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "0.5.4",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/fast-memoize": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.11.4",
|
||||||
|
"@formatjs/icu-skeleton-parser": "1.3.6",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "1.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
|
||||||
|
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "0.2.25",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.2.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
|
||||||
|
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.11.4",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "1.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
|
||||||
|
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "0.2.25",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.2.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
|
||||||
|
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.2.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
|
||||||
|
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@heroicons/react": {
|
"node_modules/@heroicons/react": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz",
|
||||||
@@ -8947,6 +9034,34 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intl-messageformat": {
|
||||||
|
"version": "9.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
|
||||||
|
"integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.11.4",
|
||||||
|
"@formatjs/fast-memoize": "1.2.1",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.1.0",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "1.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
|
||||||
|
"integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "0.2.25",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.2.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
|
||||||
|
"integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/invariant": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
@@ -10681,6 +10796,26 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-intl": {
|
||||||
|
"version": "3.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.12.0.tgz",
|
||||||
|
"integrity": "sha512-N3DHT6ce6K4VHVA3y2p3U7wfBx4c31qEgQSTFCFJuNnE7XYzy49O576ewEz7/k2YaB/U5bfxaWWaMMkskofwoQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/amannn"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "^0.2.32",
|
||||||
|
"negotiator": "^0.6.3",
|
||||||
|
"use-intl": "^3.12.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-pwa": {
|
"node_modules/next-pwa": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-pwa/-/next-pwa-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-pwa/-/next-pwa-5.6.0.tgz",
|
||||||
@@ -17428,6 +17563,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
|
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/use-intl": {
|
||||||
|
"version": "3.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.12.0.tgz",
|
||||||
|
"integrity": "sha512-tTJBSaxpVF1ZHqJ5+1JOaBsPmvBPscXHR0soMNQFWIURZspOueLaMXx1XHNdBv9KZGwepBju5aWXkJ0PM6ztXg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "^1.11.4",
|
||||||
|
"intl-messageformat": "^9.3.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"next": "^14.1.0",
|
"next": "^14.1.0",
|
||||||
"next-auth": "^4.24.6",
|
"next-auth": "^4.24.6",
|
||||||
"next-connect": "^1.0.0",
|
"next-connect": "^1.0.0",
|
||||||
|
"next-intl": "^3.12.0",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"node-excel-export": "^1.4.4",
|
"node-excel-export": "^1.4.4",
|
||||||
"node-telegram-bot-api": "^0.64.0",
|
"node-telegram-bot-api": "^0.64.0",
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import "tailwindcss/tailwind.css"
|
|||||||
|
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { useEffect } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
|
||||||
// for fontawesome
|
// for fontawesome
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
@@ -22,10 +25,40 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
export default function App({
|
export default function App({ Component, pageProps: { session, ...pageProps }, }: AppProps<{ session: Session }>) {
|
||||||
Component,
|
// console.log(pageProps);
|
||||||
pageProps: { session, ...pageProps },
|
const router = useRouter();
|
||||||
}: AppProps<{ session: Session }>) {
|
const [messages, setMessages] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Current locale:", router.locale);
|
||||||
|
async function loadLocaleData() {
|
||||||
|
// Replace the static import with a fetch request
|
||||||
|
const res = await fetch(`/api/translations/${router.locale}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const localeMessages = await res.json();
|
||||||
|
console.log("Loaded messages for locale:", router.locale, localeMessages);
|
||||||
|
setMessages(localeMessages);
|
||||||
|
} else {
|
||||||
|
const localeMessages = await import(`../content/i18n/${router.locale}.json`); setMessages(localeMessages.default);
|
||||||
|
}
|
||||||
|
console.log("Loaded locale '", router.locale, "' ",);
|
||||||
|
//console.log("Loaded messages for locale:", router.locale, localeMessages.default);
|
||||||
|
|
||||||
|
}
|
||||||
|
loadLocaleData();
|
||||||
|
}, [router.locale]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// async function loadLocaleData() {
|
||||||
|
// const locale = router.locale || 'en';
|
||||||
|
// const messages = await fetch(`/api/translations/${locale}`).then(res => res.json());
|
||||||
|
// setMessages(messages);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// loadLocaleData();
|
||||||
|
// }, [router.locale]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const use = async () => {
|
const use = async () => {
|
||||||
@@ -67,11 +100,32 @@ export default function App({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SessionProvider session={session} >
|
<NextIntlClientProvider
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
locale={router.locale}
|
||||||
<Component {...pageProps} />
|
timeZone="Europe/Sofia"
|
||||||
</LocalizationProvider>
|
messages={messages}
|
||||||
</SessionProvider>
|
>
|
||||||
|
<SessionProvider session={session} >
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</LocalizationProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build time localization. Is it working for _app.tsx?d
|
||||||
|
// export async function getStaticProps(context) {
|
||||||
|
// const messages = (await import(`../content/i18n/${locale}.json`)).default;
|
||||||
|
// console.log("Loaded messages for locale:", locale, messages);
|
||||||
|
// return {
|
||||||
|
// props: {
|
||||||
|
// // You can get the messages from anywhere you like. The recommended
|
||||||
|
// // pattern is to put them in JSON files separated by locale and read
|
||||||
|
// // the desired one based on the `locale` received from Next.js.
|
||||||
|
// // messages: (await import(`../content/i18n/${context.locale}.json`)).default
|
||||||
|
// messages
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class MyDocument extends Document {
|
|||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Your PWA Name" />
|
<meta name="apple-mobile-web-app-title" content="CCOM" />
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="/old-192x192.png"></link>
|
<link rel="apple-touch-icon" href="/old-192x192.png"></link>
|
||||||
</Head>
|
</Head>
|
||||||
|
|||||||
84
pages/api/translations/[...locale].ts
Normal file
84
pages/api/translations/[...locale].ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import common from "../../../src/helpers/common";
|
||||||
|
|
||||||
|
function flattenTranslations(data) {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
function recurse(cur, prop) {
|
||||||
|
if (Object(cur) !== cur) {
|
||||||
|
result[prop] = cur;
|
||||||
|
} else if (Array.isArray(cur)) {
|
||||||
|
for (let i = 0, l = cur.length; i < l; i++)
|
||||||
|
recurse(cur[i], prop ? prop + "." + i : "" + i);
|
||||||
|
if (l == 0)
|
||||||
|
result[prop] = [];
|
||||||
|
} else {
|
||||||
|
let isEmpty = true;
|
||||||
|
for (let p in cur) {
|
||||||
|
isEmpty = false;
|
||||||
|
recurse(cur[p], prop ? prop + "." + p : p);
|
||||||
|
}
|
||||||
|
if (isEmpty)
|
||||||
|
result[prop] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recurse(data, "");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unflattenTranslations(data) {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for (let i in data) {
|
||||||
|
const keys = i.split('.');
|
||||||
|
keys.reduce((r, e, j) => {
|
||||||
|
return r[e] || (r[e] = isNaN(Number(keys[j + 1])) ? (keys.length - 1 === j ? data[i] : {}) : []);
|
||||||
|
}, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { locale } = req.query;
|
||||||
|
const filePath = path.join(process.cwd(), `content/i18n/${locale.join(".")}.json`);
|
||||||
|
const modifiedFilePath = path.join(process.cwd(), `content/i18n/${locale}.modified.json`);
|
||||||
|
|
||||||
|
switch (req.method) {
|
||||||
|
case 'GET':
|
||||||
|
let flat = common.parseBool(req.query.flat);
|
||||||
|
try {
|
||||||
|
const fileContents = fs.readFileSync(filePath, 'utf8');
|
||||||
|
let translations = JSON.parse(fileContents);
|
||||||
|
if (fs.existsSync(modifiedFilePath)) {
|
||||||
|
const modifiedTranslations = JSON.parse(fs.readFileSync(modifiedFilePath, 'utf8'));
|
||||||
|
translations = { ...translations, ...modifiedTranslations };
|
||||||
|
}
|
||||||
|
if (flat) {
|
||||||
|
translations = flattenTranslations(translations);
|
||||||
|
}
|
||||||
|
res.status(200).json(translations);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading translation file:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to read translation file' });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
try {
|
||||||
|
const newTranslations = req.body;
|
||||||
|
const reconstructedTranslations = unflattenTranslations(newTranslations);
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(reconstructedTranslations, null, 2), 'utf8');
|
||||||
|
res.status(200).json({ status: 'Updated' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing translation file:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update translation file' });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.setHeader('Allow', ['GET', 'POST']);
|
||||||
|
res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
pages/cart/translations/index.tsx
Normal file
110
pages/cart/translations/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import axiosInstance from '../../../src/axiosSecure';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import ProtectedRoute from "../../../components/protectedRoute";
|
||||||
|
import { UserRole } from "@prisma/client";
|
||||||
|
import Layout from 'components/layout';
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
|
|
||||||
|
const locales = ['bg', 'en', 'ru'];
|
||||||
|
|
||||||
|
const AdminTranslations = () => {
|
||||||
|
const [translations, setTranslations] = useState({});
|
||||||
|
// set locale to the current locale by default. get it from the useRouter
|
||||||
|
let router = useRouter();
|
||||||
|
|
||||||
|
const [locale, setLocale] = useState(router.locale);
|
||||||
|
const [baseTranslations, setBaseTranslations] = useState(locales[0]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axiosInstance.get(`/api/translations/${locale}?flat=true`).then(res => setTranslations(res.data));
|
||||||
|
axiosInstance.get(`/api/translations/${locales[0]}?flat=true`).then(res => setBaseTranslations(res.data));
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
axiosInstance.post(`/api/translations/${locale}`, translations)
|
||||||
|
.then(res => {
|
||||||
|
if (res.data.status === 'Updated') {
|
||||||
|
toast.success('Translations updated!');
|
||||||
|
} else {
|
||||||
|
toast.error('Something went wrong!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => toast.error('Failed to update translations: ' + err.message));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (key, value) => {
|
||||||
|
setTranslations(prev => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
|
||||||
|
<div className="mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="sticky top-0 z-10 bg-white shadow-md py-4 px-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-xl font-semibold leading-tight text-gray-800">Edit Translations</h1>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
id="locale-select"
|
||||||
|
onChange={e => setLocale(e.target.value)}
|
||||||
|
value={locale}
|
||||||
|
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
||||||
|
>
|
||||||
|
{locales.map(l => (
|
||||||
|
<option key={l} value={l}>{l.toUpperCase()}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||||
|
<table className="w-full text-sm text-left text-gray-500">
|
||||||
|
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" className="py-3 px-6 w-2/12">Key</th> {/* Adjusted width */}
|
||||||
|
<th scope="col" className="py-3 px-6 w-3/12">Base Translation</th> {/* Adjusted width */}
|
||||||
|
<th scope="col" className="py-3 px-6 w-7/12">Translation</th> {/* Adjusted width */}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(baseTranslations).map(([key, baseValue]) => (
|
||||||
|
<tr key={key} className="bg-white border-b hover:bg-gray-50">
|
||||||
|
<td className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap text-ellipsis">{key}</td>
|
||||||
|
<td className="py-4 px-6">{baseValue}</td>
|
||||||
|
<td className="py-4 px-6">
|
||||||
|
<textarea
|
||||||
|
value={translations[key] || ''}
|
||||||
|
placeholder='Въведи превод...'
|
||||||
|
onChange={e => handleChange(key, e.target.value)}
|
||||||
|
className="block w-60hv text-base px-2 py-1 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 rounded placeholder-gray-400"
|
||||||
|
style={{ width: '100%', resize: 'both', transition: 'box-shadow .3s', boxShadow: translations[key] ? '0 0 0px 1px rgba(59, 130, 246, 0.5)' : 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/* <button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="mt-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</ProtectedRoute>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminTranslations;
|
||||||
@@ -6,7 +6,7 @@ const ContactsPage = () => {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="mx-auto my-8 p-6 max-w-4xl bg-white rounded-lg shadow-md">
|
<div className="mx-auto my-8 p-6 max-w-4xl bg-white rounded-lg shadow-md">
|
||||||
<h1 className="text-2xl font-bold text-gray-800 mb-4">Специално свидетелстване на обществени места в София - Контакти</h1>
|
<h1 className="text-2xl font-bold text-gray-800 mb-4">{t('specialno-svidetelstvane-na-obshestveni-mesta-v-sofiya-kontakti')}</h1>
|
||||||
<ul className="list-disc pl-5">
|
<ul className="list-disc pl-5">
|
||||||
<li className="text-gray-700 mb-2">Янко Ванчев - <a href="tel:+359878224467" className="text-blue-500 hover:text-blue-600">+359 878 22 44 67</a></li>
|
<li className="text-gray-700 mb-2">Янко Ванчев - <a href="tel:+359878224467" className="text-blue-500 hover:text-blue-600">+359 878 22 44 67</a></li>
|
||||||
<li className="text-gray-700">Крейг Смит - <a href="tel:+359878994573" className="text-blue-500 hover:text-blue-600">+359 878 994 573</a></li>
|
<li className="text-gray-700">Крейг Смит - <a href="tel:+359878994573" className="text-blue-500 hover:text-blue-600">+359 878 994 573</a></li>
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
props: {
|
props: {
|
||||||
initialItems: items,
|
initialItems: items,
|
||||||
userId: session?.user.id,
|
userId: session?.user.id,
|
||||||
|
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user