diff --git a/app/bible/page.tsx b/app/[locale]/bible/page.tsx
similarity index 100%
rename from app/bible/page.tsx
rename to app/[locale]/bible/page.tsx
diff --git a/app/chat/page.tsx b/app/[locale]/chat/page.tsx
similarity index 100%
rename from app/chat/page.tsx
rename to app/[locale]/chat/page.tsx
diff --git a/app/dashboard/page.tsx b/app/[locale]/dashboard/page.tsx
similarity index 100%
rename from app/dashboard/page.tsx
rename to app/[locale]/dashboard/page.tsx
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
new file mode 100644
index 0000000..c9c026f
--- /dev/null
+++ b/app/[locale]/layout.tsx
@@ -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 (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/[locale]/page.tsx
similarity index 77%
rename from app/page.tsx
rename to app/[locale]/page.tsx
index 33f7eb2..aa3dd23 100644
--- a/app/page.tsx
+++ b/app/[locale]/page.tsx
@@ -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: ,
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: ,
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: ,
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: ,
path: '/search',
color: theme.palette.info.main,
@@ -58,8 +60,6 @@ export default function Home() {
return (
-
-
{/* Hero Section */}
- Ghid Biblic
+ {t('hero.title')}
- Explorează Scriptura cu ajutorul inteligenței artificiale
+ {t('hero.subtitle')}
- 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')}
}
- onClick={() => router.push('/bible')}
+ onClick={() => router.push(`/${locale}/bible`)}
>
- Începe să citești
+ {t('hero.cta.readBible')}
}
- onClick={() => router.push('/chat')}
+ onClick={() => router.push(`/${locale}/chat`)}
>
- Întreabă AI
+ {t('hero.cta.askAI')}
@@ -126,7 +124,7 @@ export default function Home() {
{/* Features Section */}
- Descoperă funcționalitățile
+ {t('features.title')}
- Totul de ce ai nevoie pentru o experiență completă de studiu biblic
+ {t('features.subtitle')}
@@ -152,7 +150,7 @@ export default function Home() {
boxShadow: 4,
},
}}
- onClick={() => router.push(feature.path)}
+ onClick={() => router.push(`/${locale}${feature.path}`)}
>
@@ -179,19 +177,19 @@ export default function Home() {
66
- Cărți biblice
+ {t('stats.books')}
31,000+
- Versete
+ {t('stats.verses')}
24/7
- Chat AI disponibil
+ {t('stats.aiAvailable')}
@@ -200,19 +198,19 @@ export default function Home() {
{/* CTA Section */}
- Începe călătoria ta spirituală
+ {t('cta.title')}
- Alătură-te comunității noastre și descoperă înțelepciunea Scripturii
+ {t('cta.description')}
}
sx={{ mr: 2 }}
- onClick={() => router.push('/bible')}
+ onClick={() => router.push(`/${locale}/bible`)}
>
- Începe acum
+ {t('cta.startNow')}
diff --git a/app/prayers/page.tsx b/app/[locale]/prayers/page.tsx
similarity index 100%
rename from app/prayers/page.tsx
rename to app/[locale]/prayers/page.tsx
diff --git a/app/search/page.tsx b/app/[locale]/search/page.tsx
similarity index 100%
rename from app/search/page.tsx
rename to app/[locale]/search/page.tsx
diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts
index 8d2c546..94fd874 100644
--- a/app/api/chat/route.ts
+++ b/app/api/chat/route.ts
@@ -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 {
+async function generateBiblicalResponse(message: string, locale: string, history: any[]): Promise {
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
}
}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index fc84ece..e581219 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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 (
-
-
-
- {children}
-
-
-
-
- )
+ return children
}
\ No newline at end of file
diff --git a/components/chat/floating-chat.tsx b/components/chat/floating-chat.tsx
index 74741fe..8c6b422 100644
--- a/components/chat/floating-chat.tsx
+++ b/components/chat/floating-chat.tsx
@@ -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([
{
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() {
{/* Chat Overlay */}
-
+
- Chat AI Biblic
+ {t('title')}
- Asistent pentru întrebări biblice
+ {t('subtitle')}
@@ -244,7 +249,7 @@ export default function FloatingChat() {
{/* Suggested Questions */}
- Întrebări sugerate:
+ {t('suggestions.title')}
{suggestedQuestions.slice(0, 3).map((question, index) => (
@@ -367,7 +372,7 @@ export default function FloatingChat() {
- Scriu răspunsul...
+ {t('loading')}
@@ -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() {
- Enter pentru a trimite • Shift+Enter pentru linie nouă
+ {t('enterToSend')}
>
diff --git a/components/layout/language-switcher.tsx b/components/layout/language-switcher.tsx
new file mode 100644
index 0000000..6dc2087
--- /dev/null
+++ b/components/layout/language-switcher.tsx
@@ -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)
+ const router = useRouter()
+ const pathname = usePathname()
+ const locale = useLocale()
+ const t = useTranslations('navigation')
+
+ const handleOpen = (event: React.MouseEvent) => {
+ 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 (
+ <>
+
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/components/layout/navigation.tsx b/components/layout/navigation.tsx
index cdac0a5..ca3cff2 100644
--- a/components/layout/navigation.tsx
+++ b/components/layout/navigation.tsx
@@ -32,15 +32,8 @@ import {
Logout,
} from '@mui/icons-material'
import { useRouter } from 'next/navigation'
-
-const pages = [
- { name: 'Acasă', path: '/', icon: },
- { name: 'Biblia', path: '/bible', icon: },
- { name: 'Rugăciuni', path: '/prayers', icon: },
- { name: 'Căutare', path: '/search', icon: },
-]
-
-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)
@@ -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: },
+ { name: t('bible'), path: '/bible', icon: },
+ { name: t('prayers'), path: '/prayers', icon: },
+ { name: t('search'), path: '/search', icon: },
+ ]
+
+ const settings = [
+ { name: t('profile'), icon: },
+ { name: t('settings'), icon: },
+ { name: t('logout'), icon: },
+ ]
const handleOpenNavMenu = (event: React.MouseEvent) => {
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() {
))}
+ {/* Language Switcher */}
+
+
{/* User Menu */}
-
+
@@ -202,24 +214,14 @@ export function Navigation() {
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
-
-
-
+ {settings.map((setting) => (
+
+ ))}
diff --git a/i18n.ts b/i18n.ts
new file mode 100644
index 0000000..010764e
--- /dev/null
+++ b/i18n.ts
@@ -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
+ };
+});
\ No newline at end of file
diff --git a/lib/vector-search.ts b/lib/vector-search.ts
index 09cb773..6181056 100644
--- a/lib/vector-search.ts
+++ b/lib/vector-search.ts
@@ -40,6 +40,7 @@ export async function getEmbedding(text: string): Promise {
export async function searchBibleSemantic(
query: string,
+ language: string = 'ro',
limit: number = 10
): Promise {
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 {
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
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 0000000..f8825ba
--- /dev/null
+++ b/messages/en.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/messages/ro.json b/messages/ro.json
new file mode 100644
index 0000000..b24a6e8
--- /dev/null
+++ b/messages/ro.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/middleware-test.ts b/middleware-test.ts
new file mode 100644
index 0000000..6ddff1a
--- /dev/null
+++ b/middleware-test.ts
@@ -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|.*\\..*).*)',
+ ],
+}
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index 3701d5b..c9b3e40 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -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*'
],
diff --git a/next.config.js b/next.config.js
index cc59526..1666002 100644
--- a/next.config.js
+++ b/next.config.js
@@ -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
\ No newline at end of file
+module.exports = withNextIntl(nextConfig)
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 108ec96..10b6f6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 824f377..80eecc2 100644
--- a/package.json
+++ b/package.json
@@ -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",