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

View File

@@ -4,6 +4,7 @@ import { searchBibleHybrid, BibleVerse } from '@/lib/vector-search'
const chatRequestSchema = z.object({
message: z.string().min(1),
locale: z.string().optional().default('ro'),
history: z.array(z.object({
id: z.string(),
role: z.enum(['user', 'assistant']),
@@ -15,11 +16,10 @@ const chatRequestSchema = z.object({
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { message, history } = chatRequestSchema.parse(body)
const { message, locale, history } = chatRequestSchema.parse(body)
// For now, return a mock response
// TODO: Integrate with Azure OpenAI when ready
const response = await generateBiblicalResponse(message, history)
// Generate response using Azure OpenAI with vector search
const response = await generateBiblicalResponse(message, locale, history)
return NextResponse.json({
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 {
// Search for relevant Bible verses using vector search
const relevantVerses = await searchBibleHybrid(message, 5)
// Search for relevant Bible verses using vector search with language filtering
const relevantVerses = await searchBibleHybrid(message, locale, 5)
// Create context from relevant verses
const versesContext = relevantVerses
@@ -65,8 +65,9 @@ async function generateBiblicalResponse(message: string, history: any[]): Promis
.map(msg => `${msg.role}: ${msg.content}`)
.join('\n')
// Construct prompt for Azure OpenAI
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.
// Create language-specific system prompts
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:
- Folosește versurile biblice relevante pentru a răspunde la întrebare
@@ -81,7 +82,27 @@ ${versesContext}
Conversația anterioară:
${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
const response = await fetch(
@@ -120,11 +141,21 @@ ${conversationHistory}
} catch (error) {
console.error('Error calling Azure OpenAI:', error)
// Fallback to simple response if AI fails
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.
// Language-specific fallback responses
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).
"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({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ro">
<body>
<MuiThemeProvider>
{children}
<FloatingChat />
</MuiThemeProvider>
</body>
</html>
)
return children
}

View File

@@ -32,6 +32,7 @@ import {
Launch,
} from '@mui/icons-material'
import { useState, useRef, useEffect } from 'react'
import { useTranslations, useLocale } from 'next-intl'
interface ChatMessage {
id: string
@@ -42,13 +43,17 @@ interface ChatMessage {
export default function FloatingChat() {
const theme = useTheme()
const t = useTranslations('chat')
const locale = useLocale()
const [isOpen, setIsOpen] = useState(false)
const [isMinimized, setIsMinimized] = useState(false)
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
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(),
}
])
@@ -87,6 +92,7 @@ export default function FloatingChat() {
body: JSON.stringify({
message: inputMessage,
history: messages.slice(-5),
locale: locale,
}),
})
@@ -99,7 +105,9 @@ export default function FloatingChat() {
const assistantMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
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(),
}
@@ -109,7 +117,9 @@ export default function FloatingChat() {
const errorMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
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(),
}
setMessages(prev => [...prev, errorMessage])
@@ -129,13 +139,8 @@ export default function FloatingChat() {
navigator.clipboard.writeText(text)
}
const suggestedQuestions = [
'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?',
]
// Use t.raw() to get the actual array from translations
const suggestedQuestions = t.raw('suggestions.questions') as string[]
const toggleChat = () => {
setIsOpen(!isOpen)
@@ -173,7 +178,7 @@ export default function FloatingChat() {
</Zoom>
{/* Chat Overlay */}
<Slide direction="up" in={isOpen} mountOnExit>
<Slide direction="up" in={isOpen} unmountOnExit>
<Paper
elevation={8}
sx={{
@@ -207,10 +212,10 @@ export default function FloatingChat() {
</Avatar>
<Box>
<Typography variant="subtitle1" fontWeight="bold">
Chat AI Biblic
{t('title')}
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
Asistent pentru întrebări biblice
{t('subtitle')}
</Typography>
</Box>
</Box>
@@ -244,7 +249,7 @@ export default function FloatingChat() {
{/* Suggested Questions */}
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Întrebări sugerate:
{t('suggestions.title')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{suggestedQuestions.slice(0, 3).map((question, index) => (
@@ -367,7 +372,7 @@ export default function FloatingChat() {
</Avatar>
<Paper elevation={1} sx={{ p: 1.5, borderRadius: 2 }}>
<Typography variant="body2">
Scriu răspunsul...
{t('loading')}
</Typography>
</Paper>
</Box>
@@ -387,7 +392,7 @@ export default function FloatingChat() {
size="small"
multiline
maxRows={3}
placeholder="Scrie întrebarea ta despre Biblie..."
placeholder={t('placeholder')}
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress}
@@ -414,7 +419,7 @@ export default function FloatingChat() {
</Button>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
Enter pentru a trimite Shift+Enter pentru linie nouă
{t('enterToSend')}
</Typography>
</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,
} from '@mui/icons-material'
import { useRouter } from 'next/navigation'
const pages = [
{ 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']
import { useTranslations, useLocale } from 'next-intl'
import { LanguageSwitcher } from './language-switcher'
export function Navigation() {
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null)
@@ -49,6 +42,21 @@ export function Navigation() {
const router = useRouter()
const theme = useTheme()
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>) => {
setAnchorElNav(event.currentTarget)
@@ -67,7 +75,8 @@ export function Navigation() {
}
const handleNavigate = (path: string) => {
router.push(path)
const localizedPath = `/${locale}${path === '/' ? '' : path}`
router.push(localizedPath)
handleCloseNavMenu()
setDrawerOpen(false)
}
@@ -104,7 +113,7 @@ export function Navigation() {
variant="h6"
noWrap
component="a"
href="/"
href={`/${locale}`}
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
@@ -138,7 +147,7 @@ export function Navigation() {
variant="h5"
noWrap
component="a"
href="/"
href={`/${locale}`}
sx={{
mr: 2,
display: { xs: 'flex', md: 'none' },
@@ -177,9 +186,12 @@ export function Navigation() {
))}
</Box>
{/* Language Switcher */}
<LanguageSwitcher />
{/* User Menu */}
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Deschide setări">
<Tooltip title={t('settings')}>
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar sx={{ bgcolor: 'secondary.main' }}>
<AccountCircle />
@@ -202,24 +214,14 @@ export function Navigation() {
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
<MenuItem onClick={handleCloseUserMenu}>
<ListItemIcon>
<AccountCircle fontSize="small" />
</ListItemIcon>
<Typography textAlign="center">Profil</Typography>
</MenuItem>
<MenuItem onClick={handleCloseUserMenu}>
<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>
{settings.map((setting) => (
<MenuItem key={setting.name} onClick={handleCloseUserMenu}>
<ListItemIcon>
{setting.icon}
</ListItemIcon>
<Typography textAlign="center">{setting.name}</Typography>
</MenuItem>
))}
</Menu>
</Box>
</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(
query: string,
language: string = 'ro',
limit: number = 10
): Promise<BibleVerse[]> {
try {
@@ -52,11 +53,11 @@ export async function searchBibleSemantic(
SELECT ref, book, chapter, verse, text_raw,
1 - (embedding <=> $1) AS similarity
FROM bible_passages
WHERE embedding IS NOT NULL
WHERE embedding IS NOT NULL AND lang = $3
ORDER BY embedding <=> $1
LIMIT $2
`,
[JSON.stringify(queryEmbedding), limit]
[JSON.stringify(queryEmbedding), limit, language]
)
return result.rows
@@ -71,11 +72,15 @@ export async function searchBibleSemantic(
export async function searchBibleHybrid(
query: string,
language: string = 'ro',
limit: number = 10
): Promise<BibleVerse[]> {
try {
const queryEmbedding = await getEmbedding(query)
// Use appropriate text search configuration based on language
const textConfig = language === 'ro' ? 'romanian' : 'english'
const client = await pool.connect()
try {
const result = await client.query(
@@ -83,25 +88,25 @@ export async function searchBibleHybrid(
WITH vector_search AS (
SELECT id, 1 - (embedding <=> $1) AS vector_sim
FROM bible_passages
WHERE embedding IS NOT NULL
WHERE embedding IS NOT NULL AND lang = $4
ORDER BY embedding <=> $1
LIMIT 100
),
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
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,
COALESCE(vs.vector_sim, 0) * 0.7 + COALESCE(ts.text_rank, 0) * 0.3 AS combined_score
FROM bible_passages bp
LEFT JOIN vector_search vs ON vs.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
LIMIT $2
`,
[JSON.stringify(queryEmbedding), limit, query]
[JSON.stringify(queryEmbedding), limit, query, language, textConfig]
)
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 { verifyToken } from '@/lib/auth'
import { prisma } from '@/lib/db'
import createIntlMiddleware from 'next-intl/middleware'
// Internationalization configuration
const intlMiddleware = createIntlMiddleware({
locales: ['ro', 'en'],
defaultLocale: 'ro'
})
// Rate limiting configuration
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) {
// Handle internationalization for non-API routes
if (!request.nextUrl.pathname.startsWith('/api')) {
return intlMiddleware(request)
}
// Determine endpoint type for rate limiting
let endpoint = 'general'
let limit = RATE_LIMITS.general
@@ -155,6 +167,12 @@ export async function middleware(request: NextRequest) {
export const config = {
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*',
'/dashboard/:path*'
],

View File

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

138
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@formatjs/intl-localematcher": "^0.6.1",
"@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2",
"@mui/material-nextjs": "^7.3.2",
@@ -33,7 +34,9 @@
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.544.0",
"negotiator": "^1.0.0",
"next": "^15.5.3",
"next-intl": "^4.3.9",
"openai": "^5.22.0",
"pdf-parse": "^1.1.1",
"pg": "^8.16.3",
@@ -1417,6 +1420,57 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
@@ -3599,6 +3653,12 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"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": {
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
@@ -4554,6 +4614,15 @@
"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": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -5428,7 +5497,6 @@
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT"
},
"node_modules/decode-named-character-reference": {
@@ -6451,6 +6519,18 @@
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -9180,9 +9260,9 @@
"license": "MIT"
},
"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==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -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": {
"version": "8.4.31",
"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": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",

View File

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