/** * Detect user's preferred language. * Priority: * 0. user-language cookie (explicit user choice, synced with localStorage) * 1. Most common language among user's notes (DB GROUP BY) * 2. Browser language hint (passed from server component via Accept-Language) * 3. Default: 'en' * * SERVER ONLY — uses next/headers and Prisma. Never import from client components. */ import 'server-only' import { auth } from '@/auth' import { prisma } from '@/lib/prisma' import { unstable_cache } from 'next/cache' import { cookies } from 'next/headers' import { SupportedLanguage } from './load-translations' const SUPPORTED_LANGUAGES = new Set(['en', 'fr', 'es', 'de', 'fa', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi', 'nl', 'pl']) /** * Parse an Accept-Language header string and find the best matching supported language */ export function parseAcceptLanguage(acceptLanguage: string | null): SupportedLanguage | null { if (!acceptLanguage) return null // Parse Accept-Language: "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7" const languages = acceptLanguage .split(',') .map(lang => { const [code, q] = lang.trim().split(';q=') return { code: code.trim().toLowerCase(), quality: q ? parseFloat(q) : 1.0 } }) .sort((a, b) => b.quality - a.quality) for (const { code } of languages) { if (SUPPORTED_LANGUAGES.has(code)) { return code as SupportedLanguage } const base = code.split('-')[0] if (SUPPORTED_LANGUAGES.has(base)) { return base as SupportedLanguage } } return null } const getCachedUserLanguage = unstable_cache( async (userId: string): Promise => { try { const result = await prisma.note.groupBy({ by: ['language'], where: { userId, language: { not: null } }, _sum: { languageConfidence: true }, _count: true, orderBy: { _sum: { languageConfidence: 'desc' } }, take: 1, }) if (result.length > 0 && result[0].language) { const topLanguage = result[0].language as SupportedLanguage if (SUPPORTED_LANGUAGES.has(topLanguage)) { return topLanguage } } return null } catch (error) { console.error('Error detecting user language from notes:', error) return null } }, ['user-language'], { tags: ['user-language'] } ) /** * Detect user language. * @param browserLanguageHint - Optional browser language parsed from Accept-Language header. * Should be passed from server components that have access to headers(). */ export async function detectUserLanguage(browserLanguageHint?: SupportedLanguage | null): Promise { // 0. Cookie has highest priority — set by the client when user picks a language. // This guarantees server and client agree on the language (no hydration mismatch). try { const cookieStore = await cookies() const cookieLang = cookieStore.get('user-language')?.value if (cookieLang && SUPPORTED_LANGUAGES.has(cookieLang)) { return cookieLang as SupportedLanguage } } catch {} const session = await auth() if (!session?.user?.id) { return browserLanguageHint || 'en' } // 1. Try to detect from user's notes const noteLanguage = await getCachedUserLanguage(session.user.id) if (noteLanguage) { return noteLanguage } // 2. Fall back to browser language hint if (browserLanguageHint) { return browserLanguageHint } // 3. Default return 'en' }