Implement complete multi-language support with Romanian/English

- Added next-intl for internationalization with Romanian as default locale
- Restructured app directory with [locale] routing (/ro, /en)
- Created comprehensive translation files for both languages
- Fixed Next.js 15 async params compatibility in layout components
- Updated all components to use proper i18n hooks and translations
- Configured middleware for locale routing and fallbacks
- Fixed FloatingChat component translation array handling
- Restored complete home page with internationalized content
- Fixed Material-UI Slide component prop error (mountOnExit → unmountOnExit)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andupetcu
2025-09-20 15:43:51 +03:00
parent dd5e1102eb
commit a0969e88df
21 changed files with 695 additions and 123 deletions

55
app/[locale]/layout.tsx Normal file
View File

@@ -0,0 +1,55 @@
import '../globals.css'
import type { Metadata } from 'next'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { MuiThemeProvider } from '@/components/providers/theme-provider'
import { Navigation } from '@/components/layout/navigation'
import FloatingChat from '@/components/chat/floating-chat'
export const metadata: Metadata = {
title: 'Ghid Biblic - Biblical Guide',
description: 'A comprehensive Bible study application with AI chat capabilities',
}
export async function generateStaticParams() {
return [
{ locale: 'ro' },
{ locale: 'en' }
]
}
interface LocaleLayoutProps {
children: React.ReactNode
params: Promise<{ locale: string }>
}
const locales = ['ro', 'en']
export default async function LocaleLayout({
children,
params
}: LocaleLayoutProps) {
const { locale } = await params
// Validate locale
if (!locales.includes(locale)) {
notFound()
}
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages} locale={locale}>
<MuiThemeProvider>
<Navigation />
{children}
<FloatingChat />
</MuiThemeProvider>
</NextIntlClientProvider>
</body>
</html>
)
}

View File

@@ -18,38 +18,40 @@ import {
AutoStories, AutoStories,
Favorite, Favorite,
} from '@mui/icons-material' } from '@mui/icons-material'
import { Navigation } from '@/components/layout/navigation'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslations, useLocale } from 'next-intl'
export default function Home() { export default function Home() {
const theme = useTheme() const theme = useTheme()
const router = useRouter() const router = useRouter()
const t = useTranslations('home')
const locale = useLocale()
const features = [ const features = [
{ {
title: 'Citește Biblia', title: t('features.bible.title'),
description: 'Explorează Scriptura cu o interfață modernă și ușor de folosit', description: t('features.bible.description'),
icon: <MenuBook sx={{ fontSize: 40, color: 'primary.main' }} />, icon: <MenuBook sx={{ fontSize: 40, color: 'primary.main' }} />,
path: '/bible', path: '/bible',
color: theme.palette.primary.main, color: theme.palette.primary.main,
}, },
{ {
title: 'Chat cu AI', title: t('features.chat.title'),
description: 'Pune întrebări despre Scriptură și primește răspunsuri clare', description: t('features.chat.description'),
icon: <Chat sx={{ fontSize: 40, color: 'secondary.main' }} />, icon: <Chat sx={{ fontSize: 40, color: 'secondary.main' }} />,
path: '/chat', path: '/chat',
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
{ {
title: 'Rugăciuni', title: t('features.prayers.title'),
description: 'Partajează rugăciuni și roagă-te împreună cu comunitatea', description: t('features.prayers.description'),
icon: <Prayer sx={{ fontSize: 40, color: 'success.main' }} />, icon: <Prayer sx={{ fontSize: 40, color: 'success.main' }} />,
path: '/prayers', path: '/prayers',
color: theme.palette.success.main, color: theme.palette.success.main,
}, },
{ {
title: 'Căutare', title: t('features.search.title'),
description: 'Caută versete și pasaje din întreaga Scriptură', description: t('features.search.description'),
icon: <Search sx={{ fontSize: 40, color: 'info.main' }} />, icon: <Search sx={{ fontSize: 40, color: 'info.main' }} />,
path: '/search', path: '/search',
color: theme.palette.info.main, color: theme.palette.info.main,
@@ -58,8 +60,6 @@ export default function Home() {
return ( return (
<Box> <Box>
<Navigation />
{/* Hero Section */} {/* Hero Section */}
<Box <Box
sx={{ sx={{
@@ -73,15 +73,13 @@ export default function Home() {
<Grid container spacing={4} alignItems="center"> <Grid container spacing={4} alignItems="center">
<Grid item xs={12} md={8}> <Grid item xs={12} md={8}>
<Typography variant="h2" component="h1" gutterBottom> <Typography variant="h2" component="h1" gutterBottom>
Ghid Biblic {t('hero.title')}
</Typography> </Typography>
<Typography variant="h5" component="h2" sx={{ mb: 3, opacity: 0.9 }}> <Typography variant="h5" component="h2" sx={{ mb: 3, opacity: 0.9 }}>
Explorează Scriptura cu ajutorul inteligenței artificiale {t('hero.subtitle')}
</Typography> </Typography>
<Typography variant="body1" sx={{ mb: 4, opacity: 0.8, maxWidth: 600 }}> <Typography variant="body1" sx={{ mb: 4, opacity: 0.8, maxWidth: 600 }}>
O platformă modernă pentru studiul Bibliei, cu chat AI inteligent, {t('hero.description')}
căutare avansată și o comunitate de rugăciune care te sprijină în
călătoria ta spirituală.
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Button <Button
@@ -92,9 +90,9 @@ export default function Home() {
'&:hover': { bgcolor: 'secondary.dark' }, '&:hover': { bgcolor: 'secondary.dark' },
}} }}
startIcon={<AutoStories />} startIcon={<AutoStories />}
onClick={() => router.push('/bible')} onClick={() => router.push(`/${locale}/bible`)}
> >
Începe citești {t('hero.cta.readBible')}
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
@@ -108,9 +106,9 @@ export default function Home() {
}, },
}} }}
startIcon={<Chat />} startIcon={<Chat />}
onClick={() => router.push('/chat')} onClick={() => router.push(`/${locale}/chat`)}
> >
Întreabă AI {t('hero.cta.askAI')}
</Button> </Button>
</Box> </Box>
</Grid> </Grid>
@@ -126,7 +124,7 @@ export default function Home() {
{/* Features Section */} {/* Features Section */}
<Container maxWidth="lg" sx={{ mb: 8 }}> <Container maxWidth="lg" sx={{ mb: 8 }}>
<Typography variant="h3" component="h2" textAlign="center" sx={{ mb: 2 }}> <Typography variant="h3" component="h2" textAlign="center" sx={{ mb: 2 }}>
Descoperă funcționalitățile {t('features.title')}
</Typography> </Typography>
<Typography <Typography
variant="body1" variant="body1"
@@ -134,7 +132,7 @@ export default function Home() {
color="text.secondary" color="text.secondary"
sx={{ mb: 6, maxWidth: 600, mx: 'auto' }} sx={{ mb: 6, maxWidth: 600, mx: 'auto' }}
> >
Totul de ce ai nevoie pentru o experiență completă de studiu biblic {t('features.subtitle')}
</Typography> </Typography>
<Grid container spacing={4}> <Grid container spacing={4}>
@@ -152,7 +150,7 @@ export default function Home() {
boxShadow: 4, boxShadow: 4,
}, },
}} }}
onClick={() => router.push(feature.path)} onClick={() => router.push(`/${locale}${feature.path}`)}
> >
<CardContent sx={{ flexGrow: 1, textAlign: 'center', p: 3 }}> <CardContent sx={{ flexGrow: 1, textAlign: 'center', p: 3 }}>
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
@@ -179,19 +177,19 @@ export default function Home() {
<Typography variant="h3" color="primary.main" gutterBottom> <Typography variant="h3" color="primary.main" gutterBottom>
66 66
</Typography> </Typography>
<Typography variant="h6">Cărți biblice</Typography> <Typography variant="h6">{t('stats.books')}</Typography>
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<Typography variant="h3" color="secondary.main" gutterBottom> <Typography variant="h3" color="secondary.main" gutterBottom>
31,000+ 31,000+
</Typography> </Typography>
<Typography variant="h6">Versete</Typography> <Typography variant="h6">{t('stats.verses')}</Typography>
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<Typography variant="h3" color="success.main" gutterBottom> <Typography variant="h3" color="success.main" gutterBottom>
24/7 24/7
</Typography> </Typography>
<Typography variant="h6">Chat AI disponibil</Typography> <Typography variant="h6">{t('stats.aiAvailable')}</Typography>
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>
@@ -200,19 +198,19 @@ export default function Home() {
{/* CTA Section */} {/* CTA Section */}
<Container maxWidth="sm" sx={{ textAlign: 'center', mb: 8 }}> <Container maxWidth="sm" sx={{ textAlign: 'center', mb: 8 }}>
<Typography variant="h4" component="h2" gutterBottom> <Typography variant="h4" component="h2" gutterBottom>
Începe călătoria ta spirituală {t('cta.title')}
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}> <Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
Alătură-te comunității noastre și descoperă înțelepciunea Scripturii {t('cta.description')}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
size="large" size="large"
startIcon={<Favorite />} startIcon={<Favorite />}
sx={{ mr: 2 }} sx={{ mr: 2 }}
onClick={() => router.push('/bible')} onClick={() => router.push(`/${locale}/bible`)}
> >
Începe acum {t('cta.startNow')}
</Button> </Button>
</Container> </Container>
</Box> </Box>

View File

@@ -4,6 +4,7 @@ import { searchBibleHybrid, BibleVerse } from '@/lib/vector-search'
const chatRequestSchema = z.object({ const chatRequestSchema = z.object({
message: z.string().min(1), message: z.string().min(1),
locale: z.string().optional().default('ro'),
history: z.array(z.object({ history: z.array(z.object({
id: z.string(), id: z.string(),
role: z.enum(['user', 'assistant']), role: z.enum(['user', 'assistant']),
@@ -15,11 +16,10 @@ const chatRequestSchema = z.object({
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json() const body = await request.json()
const { message, history } = chatRequestSchema.parse(body) const { message, locale, history } = chatRequestSchema.parse(body)
// For now, return a mock response // Generate response using Azure OpenAI with vector search
// TODO: Integrate with Azure OpenAI when ready const response = await generateBiblicalResponse(message, locale, history)
const response = await generateBiblicalResponse(message, history)
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@@ -49,10 +49,10 @@ export async function POST(request: NextRequest) {
} }
} }
async function generateBiblicalResponse(message: string, history: any[]): Promise<string> { async function generateBiblicalResponse(message: string, locale: string, history: any[]): Promise<string> {
try { try {
// Search for relevant Bible verses using vector search // Search for relevant Bible verses using vector search with language filtering
const relevantVerses = await searchBibleHybrid(message, 5) const relevantVerses = await searchBibleHybrid(message, locale, 5)
// Create context from relevant verses // Create context from relevant verses
const versesContext = relevantVerses const versesContext = relevantVerses
@@ -65,8 +65,9 @@ async function generateBiblicalResponse(message: string, history: any[]): Promis
.map(msg => `${msg.role}: ${msg.content}`) .map(msg => `${msg.role}: ${msg.content}`)
.join('\n') .join('\n')
// Construct prompt for Azure OpenAI // Create language-specific system prompts
const systemPrompt = `Ești un asistent AI pentru întrebări biblice în limba română. Răspunde pe baza Scripturii, fiind respectuos și înțelept. const systemPrompts = {
ro: `Ești un asistent AI pentru întrebări biblice în limba română. Răspunde pe baza Scripturii, fiind respectuos și înțelept.
Instrucțiuni: Instrucțiuni:
- Folosește versurile biblice relevante pentru a răspunde la întrebare - Folosește versurile biblice relevante pentru a răspunde la întrebare
@@ -81,7 +82,27 @@ ${versesContext}
Conversația anterioară: Conversația anterioară:
${conversationHistory} ${conversationHistory}
Întrebarea curentă: ${message}` Întrebarea curentă: ${message}`,
en: `You are an AI assistant for biblical questions in English. Answer based on Scripture, being respectful and wise.
Instructions:
- Use the relevant Bible verses to answer the question
- Always cite biblical references (e.g., John 3:16)
- Respond in English
- Be empathetic and encouraging
- If unsure, encourage personal study and prayer
Relevant verses for this question:
${versesContext}
Previous conversation:
${conversationHistory}
Current question: ${message}`
}
const systemPrompt = systemPrompts[locale as keyof typeof systemPrompts] || systemPrompts.en
// Call Azure OpenAI // Call Azure OpenAI
const response = await fetch( const response = await fetch(
@@ -120,11 +141,21 @@ ${conversationHistory}
} catch (error) { } catch (error) {
console.error('Error calling Azure OpenAI:', error) console.error('Error calling Azure OpenAI:', error)
// Fallback to simple response if AI fails // Language-specific fallback responses
return `Îmi pare rău, dar întâmpin o problemă tehnică în acest moment. Te încurajez să cercetezi acest subiect în Scripturi și să te rogi pentru înțelegere. const fallbackResponses = {
ro: `Îmi pare rău, dar întâmpin o problemă tehnică în acest moment. Te încurajez să cercetezi acest subiect în Scripturi și să te rogi pentru înțelegere.
"Cercetați Scripturile, pentru că socotiți că în ele aveți viața veșnică, și tocmai ele mărturisesc despre Mine" (Ioan 5:39). "Cercetați Scripturile, pentru că socotiți că în ele aveți viața veșnică, și tocmai ele mărturisesc despre Mine" (Ioan 5:39).
"Dacă vreunul dintre voi duce lipsă de înțelepciune, să ceară de la Dumnezeu, care dă tuturor cu dărnicie și fără mustrare, și i se va da" (Iacov 1:5).` "Dacă vreunul dintre voi duce lipsă de înțelepciune, să ceară de la Dumnezeu, care dă tuturor cu dărnicie și fără mustrare, și i se va da" (Iacov 1:5).`,
en: `Sorry, I'm experiencing a technical issue at the moment. I encourage you to research this topic in Scripture and pray for understanding.
"You study the Scriptures diligently because you think that in them you have eternal life. These are the very Scriptures that testify about me" (John 5:39).
"If any of you lacks wisdom, you should ask God, who gives generously to all without finding fault, and it will be given to you" (James 1:5).`
}
return fallbackResponses[locale as keyof typeof fallbackResponses] || fallbackResponses.en
} }
} }

View File

@@ -1,26 +1,7 @@
import './globals.css'
import type { Metadata } from 'next'
import { MuiThemeProvider } from '@/components/providers/theme-provider'
import FloatingChat from '@/components/chat/floating-chat'
export const metadata: Metadata = {
title: 'Ghid Biblic - Biblical Guide',
description: 'A comprehensive Bible study application with AI chat capabilities',
}
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return children
<html lang="ro">
<body>
<MuiThemeProvider>
{children}
<FloatingChat />
</MuiThemeProvider>
</body>
</html>
)
} }

View File

@@ -32,6 +32,7 @@ import {
Launch, Launch,
} from '@mui/icons-material' } from '@mui/icons-material'
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import { useTranslations, useLocale } from 'next-intl'
interface ChatMessage { interface ChatMessage {
id: string id: string
@@ -42,13 +43,17 @@ interface ChatMessage {
export default function FloatingChat() { export default function FloatingChat() {
const theme = useTheme() const theme = useTheme()
const t = useTranslations('chat')
const locale = useLocale()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isMinimized, setIsMinimized] = useState(false) const [isMinimized, setIsMinimized] = useState(false)
const [messages, setMessages] = useState<ChatMessage[]>([ const [messages, setMessages] = useState<ChatMessage[]>([
{ {
id: '1', id: '1',
role: 'assistant', role: 'assistant',
content: 'Bună ziua! Sunt asistentul tău AI pentru întrebări biblice. Cum te pot ajuta astăzi să înțelegi mai bine Scriptura?', content: locale === 'ro'
? 'Bună ziua! Sunt asistentul tău AI pentru întrebări biblice. Cum te pot ajuta astăzi să înțelegi mai bine Scriptura?'
: 'Hello! I am your AI assistant for biblical questions. How can I help you understand Scripture better today?',
timestamp: new Date(), timestamp: new Date(),
} }
]) ])
@@ -87,6 +92,7 @@ export default function FloatingChat() {
body: JSON.stringify({ body: JSON.stringify({
message: inputMessage, message: inputMessage,
history: messages.slice(-5), history: messages.slice(-5),
locale: locale,
}), }),
}) })
@@ -99,7 +105,9 @@ export default function FloatingChat() {
const assistantMessage: ChatMessage = { const assistantMessage: ChatMessage = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
role: 'assistant', role: 'assistant',
content: data.response || 'Îmi pare rău, nu am putut procesa întrebarea ta. Te rog încearcă din nou.', content: data.response || (locale === 'ro'
? 'Îmi pare rău, nu am putut procesa întrebarea ta. Te rog încearcă din nou.'
: 'Sorry, I could not process your question. Please try again.'),
timestamp: new Date(), timestamp: new Date(),
} }
@@ -109,7 +117,9 @@ export default function FloatingChat() {
const errorMessage: ChatMessage = { const errorMessage: ChatMessage = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
role: 'assistant', role: 'assistant',
content: 'Îmi pare rău, a apărut o eroare. Te rog verifică conexiunea și încearcă din nou.', content: locale === 'ro'
? 'Îmi pare rău, a apărut o eroare. Te rog verifică conexiunea și încearcă din nou.'
: 'Sorry, an error occurred. Please check your connection and try again.',
timestamp: new Date(), timestamp: new Date(),
} }
setMessages(prev => [...prev, errorMessage]) setMessages(prev => [...prev, errorMessage])
@@ -129,13 +139,8 @@ export default function FloatingChat() {
navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
} }
const suggestedQuestions = [ // Use t.raw() to get the actual array from translations
'Ce spune Biblia despre iubire?', const suggestedQuestions = t.raw('suggestions.questions') as string[]
'Explică-mi parabola semănătorului',
'Care sunt fructele Duhului?',
'Ce înseamnă să fii născut din nou?',
'Cum pot să mă rog mai bine?',
]
const toggleChat = () => { const toggleChat = () => {
setIsOpen(!isOpen) setIsOpen(!isOpen)
@@ -173,7 +178,7 @@ export default function FloatingChat() {
</Zoom> </Zoom>
{/* Chat Overlay */} {/* Chat Overlay */}
<Slide direction="up" in={isOpen} mountOnExit> <Slide direction="up" in={isOpen} unmountOnExit>
<Paper <Paper
elevation={8} elevation={8}
sx={{ sx={{
@@ -207,10 +212,10 @@ export default function FloatingChat() {
</Avatar> </Avatar>
<Box> <Box>
<Typography variant="subtitle1" fontWeight="bold"> <Typography variant="subtitle1" fontWeight="bold">
Chat AI Biblic {t('title')}
</Typography> </Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}> <Typography variant="caption" sx={{ opacity: 0.9 }}>
Asistent pentru întrebări biblice {t('subtitle')}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -244,7 +249,7 @@ export default function FloatingChat() {
{/* Suggested Questions */} {/* Suggested Questions */}
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Întrebări sugerate: {t('suggestions.title')}
</Typography> </Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{suggestedQuestions.slice(0, 3).map((question, index) => ( {suggestedQuestions.slice(0, 3).map((question, index) => (
@@ -367,7 +372,7 @@ export default function FloatingChat() {
</Avatar> </Avatar>
<Paper elevation={1} sx={{ p: 1.5, borderRadius: 2 }}> <Paper elevation={1} sx={{ p: 1.5, borderRadius: 2 }}>
<Typography variant="body2"> <Typography variant="body2">
Scriu răspunsul... {t('loading')}
</Typography> </Typography>
</Paper> </Paper>
</Box> </Box>
@@ -387,7 +392,7 @@ export default function FloatingChat() {
size="small" size="small"
multiline multiline
maxRows={3} maxRows={3}
placeholder="Scrie întrebarea ta despre Biblie..." placeholder={t('placeholder')}
value={inputMessage} value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)} onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
@@ -414,7 +419,7 @@ export default function FloatingChat() {
</Button> </Button>
</Box> </Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}> <Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
Enter pentru a trimite Shift+Enter pentru linie nouă {t('enterToSend')}
</Typography> </Typography>
</Box> </Box>
</> </>

View File

@@ -0,0 +1,88 @@
'use client'
import { useState } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { useLocale, useTranslations } from 'next-intl'
import {
IconButton,
Menu,
MenuItem,
Box,
Typography,
ListItemIcon,
} from '@mui/material'
import { Language, Check } from '@mui/icons-material'
const languages = [
{ code: 'ro', name: 'Română', flag: '🇷🇴' },
{ code: 'en', name: 'English', flag: '🇺🇸' },
]
export function LanguageSwitcher() {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const t = useTranslations('navigation')
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const handleLanguageChange = (newLocale: string) => {
// Remove current locale from pathname and add new one
const pathWithoutLocale = pathname.replace(`/${locale}`, '') || '/'
const newPath = `/${newLocale}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`
router.push(newPath)
handleClose()
}
const currentLanguage = languages.find(lang => lang.code === locale)
return (
<>
<IconButton
onClick={handleOpen}
sx={{ color: 'white' }}
aria-label={t('language')}
>
<Language />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
{languages.map((language) => (
<MenuItem
key={language.code}
onClick={() => handleLanguageChange(language.code)}
selected={language.code === locale}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 120 }}>
<Typography component="span" sx={{ fontSize: '1.2rem' }}>
{language.flag}
</Typography>
<Typography sx={{ flexGrow: 1 }}>
{language.name}
</Typography>
{language.code === locale && (
<ListItemIcon sx={{ minWidth: 'auto' }}>
<Check fontSize="small" />
</ListItemIcon>
)}
</Box>
</MenuItem>
))}
</Menu>
</>
)
}

View File

@@ -32,15 +32,8 @@ import {
Logout, Logout,
} from '@mui/icons-material' } from '@mui/icons-material'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslations, useLocale } from 'next-intl'
const pages = [ import { LanguageSwitcher } from './language-switcher'
{ name: 'Acasă', path: '/', icon: <Home /> },
{ name: 'Biblia', path: '/bible', icon: <MenuBook /> },
{ name: 'Rugăciuni', path: '/prayers', icon: <Prayer /> },
{ name: 'Căutare', path: '/search', icon: <Search /> },
]
const settings = ['Profil', 'Setări', 'Deconectare']
export function Navigation() { export function Navigation() {
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null) const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null)
@@ -49,6 +42,21 @@ export function Navigation() {
const router = useRouter() const router = useRouter()
const theme = useTheme() const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md')) const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const t = useTranslations('navigation')
const locale = useLocale()
const pages = [
{ name: t('home'), path: '/', icon: <Home /> },
{ name: t('bible'), path: '/bible', icon: <MenuBook /> },
{ name: t('prayers'), path: '/prayers', icon: <Prayer /> },
{ name: t('search'), path: '/search', icon: <Search /> },
]
const settings = [
{ name: t('profile'), icon: <AccountCircle /> },
{ name: t('settings'), icon: <Settings /> },
{ name: t('logout'), icon: <Logout /> },
]
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => { const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget) setAnchorElNav(event.currentTarget)
@@ -67,7 +75,8 @@ export function Navigation() {
} }
const handleNavigate = (path: string) => { const handleNavigate = (path: string) => {
router.push(path) const localizedPath = `/${locale}${path === '/' ? '' : path}`
router.push(localizedPath)
handleCloseNavMenu() handleCloseNavMenu()
setDrawerOpen(false) setDrawerOpen(false)
} }
@@ -104,7 +113,7 @@ export function Navigation() {
variant="h6" variant="h6"
noWrap noWrap
component="a" component="a"
href="/" href={`/${locale}`}
sx={{ sx={{
mr: 2, mr: 2,
display: { xs: 'none', md: 'flex' }, display: { xs: 'none', md: 'flex' },
@@ -138,7 +147,7 @@ export function Navigation() {
variant="h5" variant="h5"
noWrap noWrap
component="a" component="a"
href="/" href={`/${locale}`}
sx={{ sx={{
mr: 2, mr: 2,
display: { xs: 'flex', md: 'none' }, display: { xs: 'flex', md: 'none' },
@@ -177,9 +186,12 @@ export function Navigation() {
))} ))}
</Box> </Box>
{/* Language Switcher */}
<LanguageSwitcher />
{/* User Menu */} {/* User Menu */}
<Box sx={{ flexGrow: 0 }}> <Box sx={{ flexGrow: 0 }}>
<Tooltip title="Deschide setări"> <Tooltip title={t('settings')}>
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar sx={{ bgcolor: 'secondary.main' }}> <Avatar sx={{ bgcolor: 'secondary.main' }}>
<AccountCircle /> <AccountCircle />
@@ -202,24 +214,14 @@ export function Navigation() {
open={Boolean(anchorElUser)} open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu} onClose={handleCloseUserMenu}
> >
<MenuItem onClick={handleCloseUserMenu}> {settings.map((setting) => (
<ListItemIcon> <MenuItem key={setting.name} onClick={handleCloseUserMenu}>
<AccountCircle fontSize="small" /> <ListItemIcon>
</ListItemIcon> {setting.icon}
<Typography textAlign="center">Profil</Typography> </ListItemIcon>
</MenuItem> <Typography textAlign="center">{setting.name}</Typography>
<MenuItem onClick={handleCloseUserMenu}> </MenuItem>
<ListItemIcon> ))}
<Settings fontSize="small" />
</ListItemIcon>
<Typography textAlign="center">Setări</Typography>
</MenuItem>
<MenuItem onClick={handleCloseUserMenu}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
<Typography textAlign="center">Deconectare</Typography>
</MenuItem>
</Menu> </Menu>
</Box> </Box>
</Toolbar> </Toolbar>

14
i18n.ts Normal file
View File

@@ -0,0 +1,14 @@
import {getRequestConfig} from 'next-intl/server';
// Can be imported from a shared config
export const locales = ['ro', 'en'];
export default getRequestConfig(async ({locale}) => {
// Ensure locale has a value, default to 'ro' if undefined
const validLocale = locale || 'ro';
return {
locale: validLocale,
messages: (await import(`./messages/${validLocale}.json`)).default
};
});

View File

@@ -40,6 +40,7 @@ export async function getEmbedding(text: string): Promise<number[]> {
export async function searchBibleSemantic( export async function searchBibleSemantic(
query: string, query: string,
language: string = 'ro',
limit: number = 10 limit: number = 10
): Promise<BibleVerse[]> { ): Promise<BibleVerse[]> {
try { try {
@@ -52,11 +53,11 @@ export async function searchBibleSemantic(
SELECT ref, book, chapter, verse, text_raw, SELECT ref, book, chapter, verse, text_raw,
1 - (embedding <=> $1) AS similarity 1 - (embedding <=> $1) AS similarity
FROM bible_passages FROM bible_passages
WHERE embedding IS NOT NULL WHERE embedding IS NOT NULL AND lang = $3
ORDER BY embedding <=> $1 ORDER BY embedding <=> $1
LIMIT $2 LIMIT $2
`, `,
[JSON.stringify(queryEmbedding), limit] [JSON.stringify(queryEmbedding), limit, language]
) )
return result.rows return result.rows
@@ -71,11 +72,15 @@ export async function searchBibleSemantic(
export async function searchBibleHybrid( export async function searchBibleHybrid(
query: string, query: string,
language: string = 'ro',
limit: number = 10 limit: number = 10
): Promise<BibleVerse[]> { ): Promise<BibleVerse[]> {
try { try {
const queryEmbedding = await getEmbedding(query) const queryEmbedding = await getEmbedding(query)
// Use appropriate text search configuration based on language
const textConfig = language === 'ro' ? 'romanian' : 'english'
const client = await pool.connect() const client = await pool.connect()
try { try {
const result = await client.query( const result = await client.query(
@@ -83,25 +88,25 @@ export async function searchBibleHybrid(
WITH vector_search AS ( WITH vector_search AS (
SELECT id, 1 - (embedding <=> $1) AS vector_sim SELECT id, 1 - (embedding <=> $1) AS vector_sim
FROM bible_passages FROM bible_passages
WHERE embedding IS NOT NULL WHERE embedding IS NOT NULL AND lang = $4
ORDER BY embedding <=> $1 ORDER BY embedding <=> $1
LIMIT 100 LIMIT 100
), ),
text_search AS ( text_search AS (
SELECT id, ts_rank(tsv, plainto_tsquery('romanian', $3)) AS text_rank SELECT id, ts_rank(tsv, plainto_tsquery($5, $3)) AS text_rank
FROM bible_passages FROM bible_passages
WHERE tsv @@ plainto_tsquery('romanian', $3) WHERE tsv @@ plainto_tsquery($5, $3) AND lang = $4
) )
SELECT bp.ref, bp.book, bp.chapter, bp.verse, bp.text_raw, SELECT bp.ref, bp.book, bp.chapter, bp.verse, bp.text_raw,
COALESCE(vs.vector_sim, 0) * 0.7 + COALESCE(ts.text_rank, 0) * 0.3 AS combined_score COALESCE(vs.vector_sim, 0) * 0.7 + COALESCE(ts.text_rank, 0) * 0.3 AS combined_score
FROM bible_passages bp FROM bible_passages bp
LEFT JOIN vector_search vs ON vs.id = bp.id LEFT JOIN vector_search vs ON vs.id = bp.id
LEFT JOIN text_search ts ON ts.id = bp.id LEFT JOIN text_search ts ON ts.id = bp.id
WHERE vs.id IS NOT NULL OR ts.id IS NOT NULL WHERE (vs.id IS NOT NULL OR ts.id IS NOT NULL) AND bp.lang = $4
ORDER BY combined_score DESC ORDER BY combined_score DESC
LIMIT $2 LIMIT $2
`, `,
[JSON.stringify(queryEmbedding), limit, query] [JSON.stringify(queryEmbedding), limit, query, language, textConfig]
) )
return result.rows return result.rows

107
messages/en.json Normal file
View File

@@ -0,0 +1,107 @@
{
"navigation": {
"home": "Home",
"bible": "Bible",
"prayers": "Prayers",
"search": "Search",
"profile": "Profile",
"settings": "Settings",
"logout": "Logout",
"language": "Language"
},
"chat": {
"title": "Biblical AI Chat",
"subtitle": "Assistant for biblical questions",
"placeholder": "Ask your biblical question...",
"loading": "Writing response...",
"send": "Send",
"minimize": "Minimize",
"close": "Close",
"openFullPage": "Open full page",
"enterToSend": "Enter to send • Shift+Enter for new line",
"suggestions": {
"title": "Suggested questions:",
"questions": [
"What does the Bible say about love?",
"Explain the parable of the sower",
"What are the fruits of the Spirit?",
"What does it mean to be born again?",
"How can I pray better?"
]
}
},
"home": {
"hero": {
"title": "Biblical Guide",
"subtitle": "Explore Scripture with artificial intelligence",
"description": "A modern platform for Bible study, with intelligent AI chat, advanced search, and a prayer community that supports you on your spiritual journey.",
"cta": {
"readBible": "Start reading",
"askAI": "Ask AI"
}
},
"features": {
"title": "Discover the features",
"subtitle": "Everything you need for a complete Bible study experience",
"bible": {
"title": "Read the Bible",
"description": "Explore Scripture with a modern and easy-to-use interface"
},
"chat": {
"title": "AI Chat",
"description": "Ask questions about Scripture and receive clear answers"
},
"prayers": {
"title": "Prayers",
"description": "Share prayers and pray together with the community"
},
"search": {
"title": "Search",
"description": "Search for verses and passages throughout Scripture"
}
},
"stats": {
"books": "Biblical books",
"verses": "Verses",
"aiAvailable": "AI Chat available"
},
"cta": {
"title": "Begin your spiritual journey",
"description": "Join our community and discover the wisdom of Scripture",
"startNow": "Start now"
}
},
"pages": {
"bible": {
"title": "Bible",
"selectBook": "Select book",
"selectChapter": "Select chapter",
"verse": "Verse",
"chapter": "Chapter"
},
"prayers": {
"title": "Prayers",
"addRequest": "Add prayer request",
"anonymous": "Anonymous",
"prayFor": "Pray for this"
},
"search": {
"title": "Search",
"placeholder": "Search the Bible...",
"results": "Results",
"noResults": "No results found"
}
},
"common": {
"loading": "Loading...",
"error": "An error occurred",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"close": "Close",
"back": "Back",
"next": "Next",
"previous": "Previous"
}
}

107
messages/ro.json Normal file
View File

@@ -0,0 +1,107 @@
{
"navigation": {
"home": "Acasă",
"bible": "Biblia",
"prayers": "Rugăciuni",
"search": "Căutare",
"profile": "Profil",
"settings": "Setări",
"logout": "Deconectare",
"language": "Limba"
},
"chat": {
"title": "Chat AI Biblic",
"subtitle": "Asistent pentru întrebări biblice",
"placeholder": "Scrie întrebarea ta despre Biblie...",
"loading": "Scriu răspunsul...",
"send": "Trimite",
"minimize": "Minimizează",
"close": "Închide",
"openFullPage": "Deschide în pagină completă",
"enterToSend": "Enter pentru a trimite • Shift+Enter pentru linie nouă",
"suggestions": {
"title": "Întrebări sugerate:",
"questions": [
"Ce spune Biblia despre iubire?",
"Explică-mi parabola semănătorului",
"Care sunt fructele Duhului?",
"Ce înseamnă să fii născut din nou?",
"Cum pot să mă rog mai bine?"
]
}
},
"home": {
"hero": {
"title": "Ghid Biblic",
"subtitle": "Explorează Scriptura cu ajutorul inteligenței artificiale",
"description": "O platformă modernă pentru studiul Bibliei, cu chat AI inteligent, căutare avansată și o comunitate de rugăciune care te sprijină în călătoria ta spirituală.",
"cta": {
"readBible": "Începe să citești",
"askAI": "Întreabă AI"
}
},
"features": {
"title": "Descoperă funcționalitățile",
"subtitle": "Totul de ce ai nevoie pentru o experiență completă de studiu biblic",
"bible": {
"title": "Citește Biblia",
"description": "Explorează Scriptura cu o interfață modernă și ușor de folosit"
},
"chat": {
"title": "Chat cu AI",
"description": "Pune întrebări despre Scriptură și primește răspunsuri clare"
},
"prayers": {
"title": "Rugăciuni",
"description": "Partajează rugăciuni și roagă-te împreună cu comunitatea"
},
"search": {
"title": "Căutare",
"description": "Caută versete și pasaje din întreaga Scriptură"
}
},
"stats": {
"books": "Cărți biblice",
"verses": "Versete",
"aiAvailable": "Chat AI disponibil"
},
"cta": {
"title": "Începe călătoria ta spirituală",
"description": "Alătură-te comunității noastre și descoperă înțelepciunea Scripturii",
"startNow": "Începe acum"
}
},
"pages": {
"bible": {
"title": "Biblia",
"selectBook": "Selectează cartea",
"selectChapter": "Selectează capitolul",
"verse": "Versetul",
"chapter": "Capitolul"
},
"prayers": {
"title": "Rugăciuni",
"addRequest": "Adaugă cerere de rugăciune",
"anonymous": "Anonim",
"prayFor": "Mă rog pentru aceasta"
},
"search": {
"title": "Căutare",
"placeholder": "Caută în Biblie...",
"results": "Rezultate",
"noResults": "Nu s-au găsit rezultate"
}
},
"common": {
"loading": "Se încarcă...",
"error": "A apărut o eroare",
"save": "Salvează",
"cancel": "Anulează",
"delete": "Șterge",
"edit": "Editează",
"close": "Închide",
"back": "Înapoi",
"next": "Următorul",
"previous": "Anterior"
}
}

25
middleware-test.ts Normal file
View File

@@ -0,0 +1,25 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
console.log('Middleware called for:', request.nextUrl.pathname)
if (request.nextUrl.pathname === '/') {
console.log('Redirecting / to /ro')
return NextResponse.redirect(new URL('/ro', request.url))
}
if (request.nextUrl.pathname.startsWith('/ro') || request.nextUrl.pathname.startsWith('/en')) {
console.log('Allowing locale route:', request.nextUrl.pathname)
return NextResponse.next()
}
console.log('Default behavior for:', request.nextUrl.pathname)
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!api|_next|_vercel|.*\\..*).*)',
],
}

View File

@@ -2,6 +2,13 @@ import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server' import type { NextRequest } from 'next/server'
import { verifyToken } from '@/lib/auth' import { verifyToken } from '@/lib/auth'
import { prisma } from '@/lib/db' import { prisma } from '@/lib/db'
import createIntlMiddleware from 'next-intl/middleware'
// Internationalization configuration
const intlMiddleware = createIntlMiddleware({
locales: ['ro', 'en'],
defaultLocale: 'ro'
})
// Rate limiting configuration // Rate limiting configuration
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
@@ -58,6 +65,11 @@ async function checkRateLimit(request: NextRequest, endpoint: string, limit: num
} }
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
// Handle internationalization for non-API routes
if (!request.nextUrl.pathname.startsWith('/api')) {
return intlMiddleware(request)
}
// Determine endpoint type for rate limiting // Determine endpoint type for rate limiting
let endpoint = 'general' let endpoint = 'general'
let limit = RATE_LIMITS.general let limit = RATE_LIMITS.general
@@ -155,6 +167,12 @@ export async function middleware(request: NextRequest) {
export const config = { export const config = {
matcher: [ matcher: [
// Match all pathnames except for
// - api routes
// - _next (Next.js internals)
// - static files (images, etc.)
'/((?!api|_next|_vercel|.*\\..*).*)',
// However, match all pathnames within `/api`, except for the Middleware to run there
'/api/:path*', '/api/:path*',
'/dashboard/:path*' '/dashboard/:path*'
], ],

View File

@@ -1,6 +1,9 @@
const withNextIntl = require('next-intl/plugin')('./i18n.ts');
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
typedRoutes: false,
} }
module.exports = nextConfig module.exports = withNextIntl(nextConfig)

138
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@formatjs/intl-localematcher": "^0.6.1",
"@mui/icons-material": "^7.3.2", "@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2", "@mui/material": "^7.3.2",
"@mui/material-nextjs": "^7.3.2", "@mui/material-nextjs": "^7.3.2",
@@ -33,7 +34,9 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"negotiator": "^1.0.0",
"next": "^15.5.3", "next": "^15.5.3",
"next-intl": "^4.3.9",
"openai": "^5.22.0", "openai": "^5.22.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"pg": "^8.16.3", "pg": "^8.16.3",
@@ -1417,6 +1420,57 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.1",
"decimal.js": "^10.4.3",
"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.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/icu-skeleton-parser": "1.8.14",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@img/colour": { "node_modules/@img/colour": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
@@ -3599,6 +3653,12 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT" "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/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.34.41", "version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
@@ -4554,6 +4614,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/accepts/node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/agent-base": { "node_modules/agent-base": {
"version": "7.1.4", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -5428,7 +5497,6 @@
"version": "10.6.0", "version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/decode-named-character-reference": { "node_modules/decode-named-character-reference": {
@@ -6451,6 +6519,18 @@
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/intl-messageformat": {
"version": "10.7.16",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"tslib": "^2.8.0"
}
},
"node_modules/is-alphabetical": { "node_modules/is-alphabetical": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -9180,9 +9260,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -9247,6 +9327,42 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.9.tgz",
"integrity": "sha512-4oSROHlgy8a5Qr2vH69wxo9F6K0uc6nZM2GNzqSe6ET79DEzOmBeSijCRzD5txcI4i+XTGytu4cxFsDXLKEDpQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"negotiator": "^1.0.0",
"use-intl": "^4.3.9"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/next-intl/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/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -11478,6 +11594,20 @@
} }
} }
}, },
"node_modules/use-intl": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.9.tgz",
"integrity": "sha512-bZu+h13HIgOvsoGleQtUe4E6gM49CRm+AH36KnJVB/qb1+Beo7jr7HNrR8YWH8oaOkQfGNm6vh0HTepxng8UTg==",
"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/use-sidecar": { "node_modules/use-sidecar": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",

View File

@@ -24,6 +24,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@formatjs/intl-localematcher": "^0.6.1",
"@mui/icons-material": "^7.3.2", "@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2", "@mui/material": "^7.3.2",
"@mui/material-nextjs": "^7.3.2", "@mui/material-nextjs": "^7.3.2",
@@ -46,7 +47,9 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"negotiator": "^1.0.0",
"next": "^15.5.3", "next": "^15.5.3",
"next-intl": "^4.3.9",
"openai": "^5.22.0", "openai": "^5.22.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"pg": "^8.16.3", "pg": "^8.16.3",