Add persistent language switcher across all learn/parent pages
useNativeLanguage: Now has setNativeLang() that persists to localStorage. Language selection carries across all pages automatically. LanguageSwitcher: Compact dropdown component added to learn/layout.tsx and parent/layout.tsx — visible on every sub-page (top-right). Parent portal: Language dropdown syncs both UI language and native language. Parents can switch language mid-session (e.g. when both parents speak different languages). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
import { Sidebar } from '@/components/Sidebar'
|
import { Sidebar } from '@/components/Sidebar'
|
||||||
import { useTheme } from '@/lib/ThemeContext'
|
import { useTheme } from '@/lib/ThemeContext'
|
||||||
|
import { useNativeLanguage } from '@/lib/useNativeLanguage'
|
||||||
|
import { LanguageSwitcher } from '@/components/learn/LanguageSwitcher'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared layout for ALL /learn/* pages.
|
* Shared layout for ALL /learn/* pages.
|
||||||
* Provides: Sidebar + gradient background + flex container.
|
* Provides: Sidebar + gradient background + language switcher.
|
||||||
* Individual pages only need to render their content.
|
|
||||||
*/
|
*/
|
||||||
export default function LearnLayout({ children }: { children: React.ReactNode }) {
|
export default function LearnLayout({ children }: { children: React.ReactNode }) {
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
const { nativeLang, setNativeLang, isThirdLanguage } = useNativeLanguage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen flex relative overflow-hidden ${
|
<div className={`min-h-screen flex relative overflow-hidden ${
|
||||||
@@ -21,6 +23,14 @@ export default function LearnLayout({ children }: { children: React.ReactNode })
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto scrollbar-hide" style={{ scrollbarWidth: 'none' }}>
|
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto scrollbar-hide" style={{ scrollbarWidth: 'none' }}>
|
||||||
|
{/* Sticky language switcher at top-right */}
|
||||||
|
<div className="sticky top-0 z-20 flex justify-end px-4 py-2">
|
||||||
|
<LanguageSwitcher
|
||||||
|
currentLang={nativeLang}
|
||||||
|
onLangChange={setNativeLang}
|
||||||
|
isDark={isDark}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
import { Sidebar } from '@/components/Sidebar'
|
import { Sidebar } from '@/components/Sidebar'
|
||||||
import { useTheme } from '@/lib/ThemeContext'
|
import { useTheme } from '@/lib/ThemeContext'
|
||||||
|
import { useNativeLanguage } from '@/lib/useNativeLanguage'
|
||||||
|
import { LanguageSwitcher } from '@/components/learn/LanguageSwitcher'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared layout for ALL /parent/* pages.
|
* Shared layout for ALL /parent/* pages.
|
||||||
* Same design as learn layout — Sidebar + gradient.
|
* Same design as learn layout — Sidebar + gradient + language switcher.
|
||||||
*/
|
*/
|
||||||
export default function ParentLayout({ children }: { children: React.ReactNode }) {
|
export default function ParentLayout({ children }: { children: React.ReactNode }) {
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
const { nativeLang, setNativeLang } = useNativeLanguage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen flex relative overflow-hidden ${
|
<div className={`min-h-screen flex relative overflow-hidden ${
|
||||||
@@ -20,6 +23,14 @@ export default function ParentLayout({ children }: { children: React.ReactNode }
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto scrollbar-hide" style={{ scrollbarWidth: 'none' }}>
|
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto scrollbar-hide" style={{ scrollbarWidth: 'none' }}>
|
||||||
|
{/* Sticky language switcher at top-right */}
|
||||||
|
<div className="sticky top-0 z-20 flex justify-end px-4 py-2">
|
||||||
|
<LanguageSwitcher
|
||||||
|
currentLang={nativeLang}
|
||||||
|
onLangChange={setNativeLang}
|
||||||
|
isDark={isDark}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
|||||||
import { useTheme } from '@/lib/ThemeContext'
|
import { useTheme } from '@/lib/ThemeContext'
|
||||||
import { useLanguage } from '@/lib/LanguageContext'
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
import type { Language } from '@/lib/i18n'
|
import type { Language } from '@/lib/i18n'
|
||||||
|
import { useNativeLanguage } from '@/lib/useNativeLanguage'
|
||||||
|
|
||||||
interface LearningUnit {
|
interface LearningUnit {
|
||||||
id: string
|
id: string
|
||||||
@@ -28,10 +29,19 @@ const parentT: Record<string, Record<string, string>> = {
|
|||||||
export default function ParentPage() {
|
export default function ParentPage() {
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { language, setLanguage } = useLanguage()
|
const { language, setLanguage } = useLanguage()
|
||||||
|
const { nativeLang, setNativeLang } = useNativeLanguage()
|
||||||
const [units, setUnits] = useState<LearningUnit[]>([])
|
const [units, setUnits] = useState<LearningUnit[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
const t = (key: string) => parentT[key]?.[language] || parentT[key]?.['de'] || key
|
// Use nativeLang for translations (synced with localStorage)
|
||||||
|
const activeLang = nativeLang || language
|
||||||
|
const t = (key: string) => parentT[key]?.[activeLang] || parentT[key]?.['de'] || key
|
||||||
|
|
||||||
|
/** Switch both UI language and native language together */
|
||||||
|
const switchLang = (lang: string) => {
|
||||||
|
setLanguage(lang as Language)
|
||||||
|
setNativeLang(lang)
|
||||||
|
}
|
||||||
|
|
||||||
const glassCard = isDark
|
const glassCard = isDark
|
||||||
? 'bg-white/10 backdrop-blur-xl border border-white/10'
|
? 'bg-white/10 backdrop-blur-xl border border-white/10'
|
||||||
@@ -59,19 +69,9 @@ export default function ParentPage() {
|
|||||||
{t('greeting')}
|
{t('greeting')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<span className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||||||
value={language}
|
{activeLang.toUpperCase()}
|
||||||
onChange={(e) => setLanguage(e.target.value as Language)}
|
</span>
|
||||||
className={`text-sm px-2 py-1.5 rounded-lg border-0 ${isDark ? 'bg-white/10 text-white/60' : 'bg-slate-100 text-slate-500'}`}
|
|
||||||
>
|
|
||||||
<option value="de">DE</option>
|
|
||||||
<option value="tr">TR</option>
|
|
||||||
<option value="ar">AR</option>
|
|
||||||
<option value="uk">UK</option>
|
|
||||||
<option value="ru">RU</option>
|
|
||||||
<option value="pl">PL</option>
|
|
||||||
<option value="en">EN</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
studio-v2/components/learn/LanguageSwitcher.tsx
Normal file
58
studio-v2/components/learn/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface LanguageSwitcherProps {
|
||||||
|
currentLang: string
|
||||||
|
onLangChange: (lang: string) => void
|
||||||
|
isDark: boolean
|
||||||
|
compact?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const LANGS = [
|
||||||
|
{ code: 'de', label: 'DE' },
|
||||||
|
{ code: 'en', label: 'EN' },
|
||||||
|
{ code: 'tr', label: 'TR' },
|
||||||
|
{ code: 'ar', label: 'AR' },
|
||||||
|
{ code: 'uk', label: 'UK' },
|
||||||
|
{ code: 'ru', label: 'RU' },
|
||||||
|
{ code: 'pl', label: 'PL' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact language switcher for exercise pages.
|
||||||
|
* Shows as dropdown or pill buttons depending on compact prop.
|
||||||
|
*/
|
||||||
|
export function LanguageSwitcher({ currentLang, onLangChange, isDark, compact = true }: LanguageSwitcherProps) {
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={currentLang}
|
||||||
|
onChange={e => onLangChange(e.target.value)}
|
||||||
|
className={`text-xs px-2 py-1 rounded-lg border-0 cursor-pointer ${
|
||||||
|
isDark ? 'bg-white/10 text-white/70' : 'bg-slate-100 text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{LANGS.map(l => <option key={l.code} value={l.code}>{l.label}</option>)}
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{LANGS.map(l => (
|
||||||
|
<button
|
||||||
|
key={l.code}
|
||||||
|
onClick={() => onLangChange(l.code)}
|
||||||
|
className={`px-2 py-1 rounded-lg text-xs font-medium transition-all ${
|
||||||
|
currentLang === l.code
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: isDark ? 'bg-white/5 text-white/40 hover:bg-white/10' : 'bg-slate-100 text-slate-400 hover:bg-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { exerciseT, type ExerciseKey } from './exerciseTranslations'
|
import { exerciseT, type ExerciseKey } from './exerciseTranslations'
|
||||||
|
|
||||||
const STORAGE_KEY = 'bp_native_language'
|
const STORAGE_KEY = 'bp_native_language'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to read the user's native language.
|
* Hook for native language state + translations.
|
||||||
* Returns translation helper for exercise UI texts.
|
* Persists to localStorage. Can be changed at any time (e.g. parent switches language).
|
||||||
*/
|
*/
|
||||||
export function useNativeLanguage() {
|
export function useNativeLanguage() {
|
||||||
const [nativeLang, setNativeLang] = useState('de')
|
const [nativeLang, setNativeLangState] = useState('de')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
if (stored) setNativeLang(stored)
|
if (stored) setNativeLangState(stored)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/** Change native language (persists to localStorage) */
|
||||||
|
const setNativeLang = useCallback((lang: string) => {
|
||||||
|
setNativeLangState(lang)
|
||||||
|
localStorage.setItem(STORAGE_KEY, lang)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const isThirdLanguage = nativeLang !== 'de' && nativeLang !== 'en'
|
const isThirdLanguage = nativeLang !== 'de' && nativeLang !== 'en'
|
||||||
@@ -34,5 +40,5 @@ export function useNativeLanguage() {
|
|||||||
return typeof entry === 'string' ? entry : entry.text || ''
|
return typeof entry === 'string' ? entry : entry.text || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return { nativeLang, isThirdLanguage, t, wordInNative }
|
return { nativeLang, setNativeLang, isThirdLanguage, t, wordInNative }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user