zweisprachigkeit implementiert
This commit is contained in:
parent
c587bec2c7
commit
d99c0e2429
|
|
@ -1,7 +1,12 @@
|
|||
import { LanguageProvider } from '../contexts/LanguageContext';
|
||||
import AppRouter from './AppRouter';
|
||||
|
||||
function PortfolioApp() {
|
||||
return <AppRouter />;
|
||||
return (
|
||||
<LanguageProvider>
|
||||
<AppRouter />
|
||||
</LanguageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default PortfolioApp;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import '../scss/language-toggle.scss';
|
||||
|
||||
export default function LanguageToggle() {
|
||||
const { language, setLanguage } = useLanguage();
|
||||
|
||||
const handleLanguageChange = (newLang: 'en' | 'de') => {
|
||||
if (newLang !== language) {
|
||||
setLanguage(newLang);
|
||||
// Announce language change to screen readers
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('role', 'status');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.setAttribute('class', 'sr-only');
|
||||
announcement.textContent = `Language changed to ${newLang === 'en' ? 'English' : 'Deutsch'}`;
|
||||
document.body.appendChild(announcement);
|
||||
setTimeout(() => announcement.remove(), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="language-toggle" role="group" aria-label="Language selection">
|
||||
<button
|
||||
className={`language-toggle__button ${language === 'en' ? 'language-toggle__button--active' : ''}`}
|
||||
onClick={() => handleLanguageChange('en')}
|
||||
aria-label="Switch to English"
|
||||
aria-pressed={language === 'en'}
|
||||
type="button"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
className={`language-toggle__button ${language === 'de' ? 'language-toggle__button--active' : ''}`}
|
||||
onClick={() => handleLanguageChange('de')}
|
||||
aria-label="Zu Deutsch wechseln"
|
||||
aria-pressed={language === 'de'}
|
||||
type="button"
|
||||
>
|
||||
DE
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { getTexts } from '../config/texts';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
|
||||
type Props = Readonly<{
|
||||
theme: 'light' | 'dark';
|
||||
|
|
@ -10,7 +10,7 @@ export default function ThemeToggle({
|
|||
theme,
|
||||
setTheme
|
||||
}: Props) {
|
||||
const texts = getTexts();
|
||||
const { texts } = useLanguage();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'light' ? 'dark' : 'light');
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Github, Linkedin, Mail } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { personalConfig, createEmailLink } from '../../config/personal';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function Footer() {
|
||||
const texts = getTexts();
|
||||
const { texts } = useLanguage();
|
||||
// Email obfuscation function using config
|
||||
const handleEmailClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import ThemeToggle from '../ThemeToggle';
|
||||
import LanguageToggle from '../LanguageToggle';
|
||||
import MobileMenu from './MobileMenu';
|
||||
import { personalConfig } from '../../config/personal';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { scrollToSection } from '../../utils/scrollUtils';
|
||||
|
||||
type Props = {
|
||||
|
|
@ -18,7 +19,7 @@ export default function Navbar({
|
|||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const texts = getTexts();
|
||||
const { texts } = useLanguage();
|
||||
|
||||
const menuItems = texts.navigation.menuItems;
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ export default function Navbar({
|
|||
{item}
|
||||
</button>
|
||||
))}
|
||||
<LanguageToggle />
|
||||
<ThemeToggle theme={theme} setTheme={setTheme} />
|
||||
{/* Burger Button für Mobile */}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Code, Palette, ShieldCheck, Eye } from 'lucide-react';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import type { Service } from '../../data/Service';
|
||||
import saschaImage from '../../assets/sascha.png';
|
||||
|
||||
|
|
@ -13,8 +13,9 @@ interface AboutSectionProps {
|
|||
}
|
||||
|
||||
export default function AboutSection(props: AboutSectionProps = {}) {
|
||||
const texts = getTexts().about;
|
||||
const accessibilityTexts = getTexts().accessibility;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.about;
|
||||
const accessibilityTexts = allTexts.accessibility;
|
||||
|
||||
// Use centralized texts as defaults, allow props to override
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Award } from 'lucide-react';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import type { Certification } from '../../data/Certification';
|
||||
|
||||
interface CertificationsSectionProps {
|
||||
|
|
@ -9,8 +9,9 @@ interface CertificationsSectionProps {
|
|||
}
|
||||
|
||||
export default function CertificationsSection(props: CertificationsSectionProps = {}) {
|
||||
const texts = getTexts().certifications;
|
||||
const accessibilityTexts = getTexts().accessibility;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.certifications;
|
||||
const accessibilityTexts = allTexts.accessibility;
|
||||
|
||||
// Use centralized texts as defaults, allow props to override
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Mail, ExternalLink, Send } from 'lucide-react';
|
||||
import { personalConfig, getObfuscatedEmail } from '../../config/personal';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
interface ContactSectionProps {
|
||||
title?: string;
|
||||
|
|
@ -19,7 +19,8 @@ interface ContactSectionProps {
|
|||
}
|
||||
|
||||
export default function ContactSection(props: ContactSectionProps = {}) {
|
||||
const texts = getTexts().contact;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.contact;
|
||||
|
||||
const {
|
||||
title = texts.title,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Download } from 'lucide-react';
|
|||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { StatItem } from '../../data/StatItem';
|
||||
import { scrollToProjects } from '../../utils/scrollUtils';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import resumePDF from '../../assets/CV_Sascha_Bach.pdf';
|
||||
|
||||
interface HeroSectionProps {
|
||||
|
|
@ -15,8 +15,9 @@ interface HeroSectionProps {
|
|||
}
|
||||
|
||||
export default function HeroSection(props: HeroSectionProps = {}) {
|
||||
const texts = getTexts().hero;
|
||||
const accessibilityTexts = getTexts().accessibility;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.hero;
|
||||
const accessibilityTexts = allTexts.accessibility;
|
||||
|
||||
// Use centralized texts as defaults, allow props to override
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GitBranch, ExternalLink } from 'lucide-react';
|
||||
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { projectsData } from '../../config/projects-config';
|
||||
import type { Project } from '../../data/Project';
|
||||
|
||||
|
|
@ -12,8 +12,9 @@ interface ProjectsSectionProps {
|
|||
}
|
||||
|
||||
export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||
const texts = getTexts().projects;
|
||||
const accessibilityTexts = getTexts().accessibility;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.projects;
|
||||
const accessibilityTexts = allTexts.accessibility;
|
||||
const [activeCardIndex, setActiveCardIndex] = useState<number | null>(null);
|
||||
const [focusedActionIndex, setFocusedActionIndex] = useState<number>(0);
|
||||
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Eye, Lightbulb, Palette, Laptop, Rocket, ShieldCheck } from 'lucide-react';
|
||||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import type { Service } from '../../data/Service';
|
||||
|
||||
interface ServicesSectionProps {
|
||||
|
|
@ -9,8 +9,9 @@ interface ServicesSectionProps {
|
|||
}
|
||||
|
||||
export default function ServicesSection(props: ServicesSectionProps = {}) {
|
||||
const texts = getTexts().services;
|
||||
const accessibilityTexts = getTexts().accessibility;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.services;
|
||||
const accessibilityTexts = allTexts.accessibility;
|
||||
|
||||
// Use centralized texts as defaults, allow props to override
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getTexts } from '../../config/texts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import type { Skill } from '../../data/Skill';
|
||||
|
||||
interface SkillCategory {
|
||||
|
|
@ -27,8 +27,9 @@ const getColorByPosition = (index: number): string => {
|
|||
};
|
||||
|
||||
export default function SkillsSection(props: SkillsSectionProps = {}) {
|
||||
const texts = getTexts().skills;
|
||||
const accessibilityTexts = getTexts().accessibility;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.skills;
|
||||
const accessibilityTexts = allTexts.accessibility;
|
||||
|
||||
// Use centralized texts as defaults, allow props to override
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -209,23 +209,23 @@ export interface TextConfig {
|
|||
privacyPolicy: {
|
||||
title: string;
|
||||
lastUpdated: string;
|
||||
introTitle: string;
|
||||
introText: string;
|
||||
dataCollectionTitle: string;
|
||||
dataCollectionText: string;
|
||||
dataCollectionList: string[];
|
||||
purposeTitle: string;
|
||||
purposeText: string;
|
||||
providerTitle: string;
|
||||
providerText: string;
|
||||
retentionTitle: string;
|
||||
retentionText: string;
|
||||
webhostingTitle: string;
|
||||
webhostingText: string;
|
||||
webhostingProvider: string;
|
||||
webhostingProviderName: string;
|
||||
webhostingProviderAddress: string;
|
||||
webhostingProviderWebsite: string;
|
||||
rightsTitle: string;
|
||||
rightsText: string;
|
||||
rightsList: string[];
|
||||
contactTitle: string;
|
||||
contactText: string;
|
||||
contactEmail: string;
|
||||
legalBasisTitle: string;
|
||||
legalBasisText: string;
|
||||
};
|
||||
|
||||
// Accessibility and Navigation
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import portfolioImage from '../assets/portfolio.PNG';
|
|||
import ergoVRImage from '../assets/ErgoVR.PNG';
|
||||
import icaraceImage from '../assets/icarace.PNG';
|
||||
import dancaAlegriaImage from '../assets/Danca-Alegria.png';
|
||||
import { personalConfig } from './personal';
|
||||
import { projects } from './locales/en/projects';
|
||||
import type { Project } from '../data/Project';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
||||
import { en, type TextConfig } from '../config/locales/en';
|
||||
import { de } from '../config/locales/de';
|
||||
|
||||
type Language = 'en' | 'de';
|
||||
|
||||
interface LanguageContextType {
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
texts: TextConfig;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
const LANGUAGE_KEY = 'preferred-language';
|
||||
|
||||
function getBrowserLanguage(): Language {
|
||||
const browserLang = navigator.language.toLowerCase();
|
||||
if (browserLang.startsWith('de')) {
|
||||
return 'de';
|
||||
}
|
||||
return 'en'; // Default to English for all other languages
|
||||
}
|
||||
|
||||
function getSavedLanguage(): Language | null {
|
||||
const saved = localStorage.getItem(LANGUAGE_KEY);
|
||||
if (saved === 'en' || saved === 'de') {
|
||||
return saved;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
const [language, setLanguageState] = useState<Language>(() => {
|
||||
// First check localStorage, then browser language, then default to English
|
||||
return getSavedLanguage() || getBrowserLanguage();
|
||||
});
|
||||
|
||||
const texts = language === 'de' ? de : en;
|
||||
|
||||
const setLanguage = (lang: Language) => {
|
||||
setLanguageState(lang);
|
||||
localStorage.setItem(LANGUAGE_KEY, lang);
|
||||
// Update HTML lang attribute for accessibility and SEO
|
||||
document.documentElement.lang = lang;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Set initial lang attribute
|
||||
document.documentElement.lang = language;
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage, texts }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
const context = useContext(LanguageContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ import { personalConfig } from '../config/personal';
|
|||
import { getTexts } from '../config/texts';
|
||||
|
||||
export default function ImprintPage() {
|
||||
const texts = getTexts().imprint;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.imprint;
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent indexing of this page
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useEffect } from 'react';
|
||||
import { getTexts } from '../config/texts';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
const texts = getTexts().privacyPolicy;
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.privacyPolicy;
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent indexing of this page
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
@use './variables' as *;
|
||||
@use './globals';
|
||||
|
||||
.language-toggle {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-text-muted);
|
||||
border-radius: $border-radius-sm;
|
||||
padding: 0.25rem;
|
||||
|
||||
&__button {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@extend %hover-lift;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 2px 4px var(--box-shadow-active);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@
|
|||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__button {
|
||||
@extend %button-reset;
|
||||
@extend %hover-lift;
|
||||
|
|
|
|||
Loading…
Reference in New Issue