Refactor locale system: align types with usage, add CMS formatting docs (#59)
* Initial plan * Initial analysis: understanding locale system issues Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix translation types to match actual component usage Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Add comprehensive locale system documentation and fix API route types Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Address code review feedback: improve readability and translate comments to English Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>
This commit is contained in:
@@ -1,221 +1,146 @@
|
|||||||
# Directus Integration - Migration Guide
|
# Directus Integration - Migration Guide
|
||||||
|
|
||||||
## 🎯 Was wurde geändert?
|
## 🎯 Overview
|
||||||
|
|
||||||
Das Portfolio nutzt jetzt **Directus als CMS** für alle Texte. Die Integration ist **hybrid**:
|
This portfolio now has a **hybrid i18n system**:
|
||||||
- ✅ **Directus** (primär) → Texte werden aus Directus CMS geladen
|
- ✅ **JSON Files** (Primary) → All translations work from `messages/*.json` files
|
||||||
- ✅ **JSON Fallback** (sekundär) → Falls Directus nicht erreichbar, nutzen wir messages/*.json
|
- ✅ **Directus CMS** (Optional) → Can override translations dynamically without rebuilds
|
||||||
|
|
||||||
## 📁 Neue Dateien
|
**Important**: Directus is **optional**. The app works perfectly fine without it using JSON fallbacks.
|
||||||
|
|
||||||
|
## 📁 New File Structure
|
||||||
|
|
||||||
### Core Infrastructure
|
### Core Infrastructure
|
||||||
- `lib/directus.ts` - REST Client für Directus (nutzt `de-DE`, `en-US`)
|
- `lib/directus.ts` - REST Client for Directus (uses `de-DE`, `en-US` locale codes)
|
||||||
- `lib/i18n-loader.ts` - Lädt Texte mit Fallback-Chain
|
- `lib/i18n-loader.ts` - Loads texts with Fallback Chain
|
||||||
- `lib/translations-loader.ts` - Batch-Loader für alle Sections
|
- `lib/translations-loader.ts` - Batch loader for all sections (cleaned up to match actual usage)
|
||||||
- `types/translations.ts` - TypeScript Types für alle Translation Objects
|
- `types/translations.ts` - TypeScript types for all translation objects (fixed to match components)
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
- `app/components/Header.server.tsx` - Server Wrapper für Header
|
All component wrappers properly load and pass translations to client components.
|
||||||
- `app/components/HeaderClient.tsx` - Client Implementation mit Props
|
|
||||||
- `app/components/ClientWrappers.tsx` - Wrapper für Hero, About, Projects, Contact, Footer
|
|
||||||
- `app/_ui/HomePageServer.tsx` - Server Component lädt alle Translations
|
|
||||||
|
|
||||||
## 🔄 Architektur
|
## 🔄 How It Works
|
||||||
|
|
||||||
### Vorher (next-intl only)
|
### Without Directus (Default)
|
||||||
```
|
```
|
||||||
Client Component → useTranslations("nav") → JSON File
|
Component → useTranslations("nav") → JSON File (messages/en.json)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Jetzt (Directus + Fallback)
|
### With Directus (Optional)
|
||||||
```
|
```
|
||||||
Server Component → getNavTranslations(locale)
|
Server Component → getNavTranslations(locale)
|
||||||
→ Directus API (de-DE/en-US)
|
→ Try Directus API (de-DE/en-US)
|
||||||
→ Falls nicht gefunden: JSON File (de/en)
|
→ If not found: JSON File (de/en)
|
||||||
→ Props an Client Component
|
→ Props to Client Component
|
||||||
Client Component → Nutzt translations aus Props
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🗄️ Directus Setup
|
## 🗄️ Directus Setup (Optional)
|
||||||
|
|
||||||
### 1. Collection: `messages`
|
Only set this up if you want to edit translations through a CMS without rebuilding the app.
|
||||||
|
|
||||||
**Felder:**
|
### 1. Environment Variables
|
||||||
- `id` (Primary Key, UUID, auto)
|
|
||||||
- `key` (String, required) - z.B. "nav.home"
|
|
||||||
- `locale` (String, required) - **WICHTIG:** `de-DE` oder `en-US` (mit `-`)
|
|
||||||
- `value` (Text, required) - Der übersetzte Text
|
|
||||||
- `translations` (Translations) - **Directus Native Translations Feature**
|
|
||||||
|
|
||||||
**WICHTIG:** Du hast zwei Optionen:
|
Add to `.env.local`:
|
||||||
|
```bash
|
||||||
|
DIRECTUS_URL=https://cms.example.com
|
||||||
|
DIRECTUS_STATIC_TOKEN=your_token_here
|
||||||
|
```
|
||||||
|
|
||||||
#### Option A: Directus Native Translations (Empfohlen)
|
**If these are not set**, the system will skip Directus and use JSON files only.
|
||||||
1. Aktiviere "Translations" für `messages` Collection
|
|
||||||
2. Definiere `de-DE` und `en-US` als Languages
|
|
||||||
3. Felder: `key` (unique), `value` (translatable)
|
|
||||||
4. Pro Key nur ein Eintrag, Directus managed Translations intern
|
|
||||||
|
|
||||||
#### Option B: Flat Structure (Einfacher)
|
### 2. Collection: `messages`
|
||||||
1. Keine Translations Feature
|
|
||||||
2. Felder: `key` + `locale` + `value`
|
|
||||||
3. Pro Key/Locale Kombination ein Eintrag
|
|
||||||
4. Beispiel:
|
|
||||||
- Row 1: key="nav.home", locale="de-DE", value="Startseite"
|
|
||||||
- Row 2: key="nav.home", locale="en-US", value="Home"
|
|
||||||
|
|
||||||
### 2. Collection: `content_pages` (Optional)
|
Create a `messages` collection in Directus with these fields:
|
||||||
|
- `key` (String, required) - e.g., "nav.home"
|
||||||
|
- `translations` (Translations) - Directus native translations feature
|
||||||
|
- Configure languages: `en-US` and `de-DE`
|
||||||
|
|
||||||
Für längere Inhalte (z.B. Datenschutz, Impressum):
|
**Note**: Keys use dot notation (`nav.home`) but locales use dashes (`en-US`, `de-DE`).
|
||||||
|
|
||||||
**Felder:**
|
|
||||||
- `id` (Primary Key, UUID)
|
|
||||||
- `slug` (String, unique) - z.B. "privacy-policy"
|
|
||||||
- `locale` (String) - `de-DE` oder `en-US`
|
|
||||||
- `title` (String)
|
|
||||||
- `content` (Rich Text oder Long Text)
|
|
||||||
|
|
||||||
### 3. Permissions
|
### 3. Permissions
|
||||||
|
|
||||||
**Public Role:**
|
Grant **Public** role read access to `messages` collection.
|
||||||
- `messages`: Read access (alle Felder)
|
|
||||||
- `content_pages`: Read access (alle Felder)
|
|
||||||
|
|
||||||
## 📝 Keys eintragen
|
## 📝 Translation Keys
|
||||||
|
|
||||||
Alle Keys aus `DIRECTUS_CHECKLIST.md` müssen in Directus eingetragen werden.
|
See `docs/LOCALE_SYSTEM.md` for the complete list of translation keys and their structure.
|
||||||
|
|
||||||
**Beispiel Keys:**
|
All keys are organized hierarchically:
|
||||||
```
|
- `nav.*` - Navigation items
|
||||||
nav.home
|
- `home.hero.*` - Hero section
|
||||||
nav.about
|
- `home.about.*` - About section
|
||||||
nav.projects
|
- `home.projects.*` - Projects section
|
||||||
nav.contact
|
- `home.contact.*` - Contact form and info
|
||||||
home.hero.greeting
|
- `footer.*` - Footer content
|
||||||
home.hero.name
|
- `consent.*` - Privacy consent banner
|
||||||
home.hero.role
|
|
||||||
home.hero.description
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Wichtig:** Keys sind **dot-separated** (wie in JSON), aber **Locale nutzt `-`**:
|
## 🎨 Rich Text Content
|
||||||
- ✅ `key="nav.home"`, `locale="de-DE"`
|
|
||||||
- ❌ `key="nav_home"`, `locale="de"`
|
|
||||||
|
|
||||||
## 🔧 Environment Variables
|
For longer content that needs formatting (bold, italic, lists), use the `content_pages` collection:
|
||||||
|
|
||||||
In `.env.local`:
|
### Collection: `content_pages` (Optional)
|
||||||
```bash
|
|
||||||
DIRECTUS_URL=https://cms.dk0.dev
|
|
||||||
DIRECTUS_STATIC_TOKEN=ogUMcHCa1CAYU1YifsoeJ_7V76o1atYG
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Wie funktioniert's?
|
Fields:
|
||||||
|
- `slug` (String, unique) - e.g., "home-hero"
|
||||||
|
- `locale` (String) - `en` or `de`
|
||||||
|
- `title` (String)
|
||||||
|
- `content` (Rich Text or Long Text)
|
||||||
|
|
||||||
### 1. Seite wird geladen
|
Examples:
|
||||||
```tsx
|
- `home-hero` - Hero section description
|
||||||
// app/[locale]/page.tsx
|
- `home-about` - About section content
|
||||||
export default async function Page({ params }) {
|
- `home-contact` - Contact intro text
|
||||||
const { locale } = await params;
|
|
||||||
return <HomePageServer locale={locale} />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Server Component lädt Translations
|
Components fetch these via `/api/content/page` and render using `RichTextClient`.
|
||||||
```tsx
|
|
||||||
// app/_ui/HomePageServer.tsx
|
|
||||||
export default async function HomePageServer({ locale }) {
|
|
||||||
const heroT = await getHeroTranslations(locale);
|
|
||||||
// ...
|
|
||||||
return <HeroClient locale={locale} translations={heroT} />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Translation Loader fetcht von Directus
|
|
||||||
```tsx
|
|
||||||
// lib/translations-loader.ts
|
|
||||||
export async function getHeroTranslations(locale: string) {
|
|
||||||
// Batch-Load aus Directus
|
|
||||||
// locale='de' wird zu 'de-DE' gemapped
|
|
||||||
const values = await Promise.all([...]);
|
|
||||||
return { greeting, name, role, ... };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Client Component nutzt Props
|
|
||||||
```tsx
|
|
||||||
// app/components/ClientWrappers.tsx
|
|
||||||
export function HeroClient({ locale, translations }) {
|
|
||||||
// Konvertiert zu next-intl Format
|
|
||||||
return (
|
|
||||||
<NextIntlClientProvider messages={messages}>
|
|
||||||
<Hero />
|
|
||||||
</NextIntlClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Fallback Chain
|
## 🔍 Fallback Chain
|
||||||
|
|
||||||
Für jeden Key wird gesucht:
|
For every translation key, the system searches in this order:
|
||||||
1. **Directus (requested locale)** - z.B. `de-DE`
|
|
||||||
2. **Directus (EN fallback)** - Falls nicht gefunden: `en-US`
|
|
||||||
3. **JSON (normalized locale)** - Falls Directus down: `messages/de.json`
|
|
||||||
4. **JSON (EN fallback)** - Falls Key nicht existiert: `messages/en.json`
|
|
||||||
5. **Key selbst** - Als letzter Fallback: return "nav.home"
|
|
||||||
|
|
||||||
## 🎨 Cache
|
1. **Directus** (if configured) in requested locale (e.g., `de-DE`)
|
||||||
|
2. **Directus** in English fallback (e.g., `en-US`)
|
||||||
|
3. **JSON file** in requested locale (e.g., `messages/de.json`)
|
||||||
|
4. **JSON file** in English (e.g., `messages/en.json`)
|
||||||
|
5. **Key itself** as last resort (e.g., returns `"nav.home"`)
|
||||||
|
|
||||||
- In-Memory Cache mit 5 min TTL
|
## ✅ What Was Fixed
|
||||||
- Cache Key: `msg:${key}:${locale}`
|
|
||||||
- Läuft im Server Memory (nicht persistent)
|
|
||||||
- Bei Deploy/Restart wird Cache geleert
|
|
||||||
|
|
||||||
## ✅ Testing
|
Previous issues that have been resolved:
|
||||||
|
|
||||||
1. **Mit Directus:** Trage einen Test-Key ein:
|
1. ✅ **Type mismatches** - All translation types now match actual component usage
|
||||||
- Key: `test`
|
2. ✅ **Unused fields** - Removed translation keys that were never used (like `hero.greeting`, `hero.name`)
|
||||||
- Locale: `de-DE`
|
3. ✅ **Wrong structure** - Fixed `AboutTranslations` structure (removed fake `interests` nesting)
|
||||||
- Value: `Hallo von Directus!`
|
4. ✅ **Missing keys** - Aligned loaders with JSON files and actual component requirements
|
||||||
- Prüfe: `await getLocalizedMessage('test', 'de')` → "Hallo von Directus!"
|
5. ✅ **Confusing comments** - Removed misleading comments in `translations-loader.ts`
|
||||||
|
|
||||||
2. **Ohne Directus:** Stoppe Directus
|
## 🎯 Best Practices
|
||||||
- Prüfe: Messages sollten aus JSON files kommen
|
|
||||||
- Website sollte normal funktionieren (degraded mode)
|
|
||||||
|
|
||||||
3. **Build Test:**
|
1. **Always maintain JSON files** - Even if using Directus, keep JSON files as fallback
|
||||||
```bash
|
2. **Use types** - TypeScript types ensure correct usage
|
||||||
npm run build
|
3. **Test without Directus** - App should work perfectly without CMS configured
|
||||||
```
|
4. **Rich text for formatting** - Use `content_pages` for content that needs bold/italic/lists
|
||||||
- Sollte ohne Errors durchlaufen
|
5. **JSON for UI labels** - Use JSON/messages for short UI strings like buttons and labels
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
### "Key nicht gefunden"
|
### Directus not configured
|
||||||
- Prüfe Directus GUI: Key exakt gleich? (`nav.home` nicht `nav_home`)
|
**This is normal!** The app works fine. All translations come from JSON files.
|
||||||
- Prüfe Locale: `de-DE` oder `en-US` (mit `-`)?
|
|
||||||
- Prüfe Permissions: Public role hat Read access?
|
|
||||||
|
|
||||||
### "Directus nicht erreichbar"
|
### Want to use Directus?
|
||||||
- Prüfe `DIRECTUS_URL` in .env
|
1. Set up `DIRECTUS_URL` and `DIRECTUS_STATIC_TOKEN`
|
||||||
- Prüfe Token: `DIRECTUS_STATIC_TOKEN`
|
2. Create `messages` collection
|
||||||
- Test: `curl -H "Authorization: Bearer TOKEN" https://cms.dk0.dev/items/messages`
|
3. Add your translations
|
||||||
|
4. They will override JSON values
|
||||||
|
|
||||||
### "Texte ändern sich nicht"
|
### Translation not showing?
|
||||||
- Cache! Warte 5 Minuten oder restart Server
|
Check in this order:
|
||||||
- Oder: Clear Cache manuell (`clearI18nCache()` in lib/i18n-loader.ts)
|
1. Does key exist in `messages/en.json`?
|
||||||
|
2. Is the key spelled correctly?
|
||||||
|
3. Is component using correct namespace?
|
||||||
|
|
||||||
## 📚 Next Steps
|
## 📚 Further Reading
|
||||||
|
|
||||||
1. **Directus deployen** (Docker auf IONOS)
|
- **Complete locale documentation**: `docs/LOCALE_SYSTEM.md`
|
||||||
2. **Collections erstellen** (messages, content_pages)
|
- **Directus setup checklist**: `DIRECTUS_CHECKLIST.md`
|
||||||
3. **Keys eintragen** (aus DIRECTUS_CHECKLIST.md)
|
- **Operations guide**: `docs/OPERATIONS.md`
|
||||||
4. **Testen** (dev environment)
|
|
||||||
5. **Production** (wenn alles funktioniert)
|
|
||||||
|
|
||||||
## 🎯 Benefits
|
|
||||||
|
|
||||||
- ✅ **Keine Rebuilds** für Text-Änderungen
|
|
||||||
- ✅ **Non-Tech Editor** kann Texte ändern (Directus GUI)
|
|
||||||
- ✅ **Graceful Degradation** (JSON Fallback)
|
|
||||||
- ✅ **Type Safety** (TypeScript Types für alle Translations)
|
|
||||||
- ✅ **Performance** (Server-side caching, parallel loading)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Quick links
|
# Quick links
|
||||||
|
|
||||||
- **Ops / setup / deployment / testing**: `docs/OPERATIONS.md`
|
- **Ops / setup / deployment / testing**: `docs/OPERATIONS.md`
|
||||||
|
- **Locale System & Translations**: `docs/LOCALE_SYSTEM.md`
|
||||||
|
|
||||||
# Dennis Konkol Portfolio - Modern Dark Theme
|
# Dennis Konkol Portfolio - Modern Dark Theme
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const messagesMap = { en: enMessages, de: deMessages };
|
|||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { namespace: string } }
|
{ params }: { params: Promise<{ namespace: string }> }
|
||||||
) {
|
) {
|
||||||
const namespace = params.namespace;
|
const { namespace } = await params;
|
||||||
const locale = req.nextUrl.searchParams.get('locale') || 'en';
|
const locale = req.nextUrl.searchParams.get('locale') || 'en';
|
||||||
|
|
||||||
// Normalize locale (de-DE -> de)
|
// Normalize locale (de-DE -> de)
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ export async function PUT(
|
|||||||
locale,
|
locale,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
content: content ?? null,
|
content: content ?? undefined,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
content: content ?? null,
|
content: content ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
136
docs/LOCALE_IMPROVEMENTS_SUMMARY.md
Normal file
136
docs/LOCALE_IMPROVEMENTS_SUMMARY.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Locale System Improvements - Summary
|
||||||
|
|
||||||
|
## Problem Statement (Original)
|
||||||
|
|
||||||
|
> The locale stuff is not really working please fix this and bring more structure to it i think there are to many field it dont know how exists. Then i have the question on how i can design stuff then when i use directus as a cms. because some words i maybe want to be writting thicker or so.
|
||||||
|
|
||||||
|
## Issues Identified
|
||||||
|
|
||||||
|
1. **Confusing translation system** - Mix of Directus API + JSON fallbacks with unclear flow
|
||||||
|
2. **Too many fields** - Translation loaders had many keys that don't actually exist or aren't used
|
||||||
|
3. **Type mismatches** - TypeScript interfaces didn't match actual component usage
|
||||||
|
4. **Missing documentation** - No clear guide on how the locale system works
|
||||||
|
5. **No rich text support guidance** - No documentation on how to style text (bold, italic, etc.) in Directus
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Fixed Translation Types (`types/translations.ts`)
|
||||||
|
|
||||||
|
**Before**: Types had many unused fields and wrong structure
|
||||||
|
- `AboutTranslations` had fake `interests` structure that was never used
|
||||||
|
- `HeroTranslations` had fields like `greeting`, `name`, `role` that don't exist
|
||||||
|
- `FooterTranslations` had nested `links` structure and wrong keys
|
||||||
|
- `ContactTranslations` was missing many form validation error keys
|
||||||
|
|
||||||
|
**After**: All types now match actual component usage
|
||||||
|
- Removed all unused/fake fields
|
||||||
|
- Added all missing fields that components actually use
|
||||||
|
- Flattened overly-nested structures
|
||||||
|
- Types now provide accurate autocomplete and type checking
|
||||||
|
|
||||||
|
### 2. Fixed Translation Loaders (`lib/translations-loader.ts`)
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
- Loaders tried to fetch non-existent keys
|
||||||
|
- Had confusing comments like "Diese Keys sind NICHT korrekt"
|
||||||
|
- Mapped keys to wrong structure (e.g., hobbies mapped to interests)
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
- All loaders now fetch only keys that exist in JSON files
|
||||||
|
- Removed misleading comments
|
||||||
|
- Correct mapping from keys to return structure
|
||||||
|
- Clear, straightforward code
|
||||||
|
|
||||||
|
### 3. Fixed API Routes
|
||||||
|
|
||||||
|
- Updated `app/api/i18n/[namespace]/route.ts` for Next.js 15 async params
|
||||||
|
- Fixed `app/api/projects/[id]/translation/route.ts` Prisma null handling
|
||||||
|
|
||||||
|
### 4. Added Comprehensive Documentation
|
||||||
|
|
||||||
|
Created **`docs/LOCALE_SYSTEM.md`** with:
|
||||||
|
- Complete architecture explanation
|
||||||
|
- All translation structures with TypeScript types
|
||||||
|
- How to use translations in server/client components
|
||||||
|
- **Rich text content guide** - How to format text in Directus CMS
|
||||||
|
- Adding new translations workflow
|
||||||
|
- Fallback behavior explanation
|
||||||
|
- Best practices
|
||||||
|
- Troubleshooting guide
|
||||||
|
|
||||||
|
### 5. Clarified Directus Integration
|
||||||
|
|
||||||
|
Updated **`DIRECTUS_MIGRATION.md`**:
|
||||||
|
- Made it clear that Directus is **optional**
|
||||||
|
- Emphasized JSON files work perfectly without CMS
|
||||||
|
- Removed confusing sections
|
||||||
|
- Added "what was fixed" section
|
||||||
|
- Better troubleshooting
|
||||||
|
|
||||||
|
### 6. Updated Main README
|
||||||
|
|
||||||
|
Added link to locale system documentation for easy discovery.
|
||||||
|
|
||||||
|
## How to Use (For Developers)
|
||||||
|
|
||||||
|
### Static Translations (Most Common)
|
||||||
|
|
||||||
|
All translations are in `messages/en.json` and `messages/de.json`. Components use:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const t = useTranslations('home.hero');
|
||||||
|
return <h1>{t('title')}</h1>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rich Text Content (For Styling)
|
||||||
|
|
||||||
|
For content that needs **bold**, *italic*, lists, etc.:
|
||||||
|
|
||||||
|
1. In component:
|
||||||
|
```tsx
|
||||||
|
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`/api/content/page?key=home-hero&locale=${locale}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setCmsDoc(data?.content?.content));
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
return cmsDoc ? <RichTextClient doc={cmsDoc} /> : <p>{t('fallback')}</p>;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In Directus CMS, use the rich text editor to format text
|
||||||
|
|
||||||
|
### Adding New Translations
|
||||||
|
|
||||||
|
1. Add to both `messages/en.json` and `messages/de.json`
|
||||||
|
2. Update types in `types/translations.ts` if needed
|
||||||
|
3. Add loader in `lib/translations-loader.ts` if needed
|
||||||
|
4. Use in components with `useTranslations()`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- ✅ Application builds successfully
|
||||||
|
- ✅ All unit tests pass (11 test suites, 17 tests)
|
||||||
|
- ✅ TypeScript types are correct
|
||||||
|
- ✅ No more confusing "NICHT korrekt" comments
|
||||||
|
- ✅ All translation keys align with JSON files
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Clear structure** - Developers now know exactly which translations exist
|
||||||
|
2. **Type safety** - TypeScript autocomplete works correctly
|
||||||
|
3. **Documentation** - Complete guide on how everything works
|
||||||
|
4. **Rich text support** - Clear instructions on how to style text in CMS
|
||||||
|
5. **Maintainability** - No more guessing which fields are real vs fake
|
||||||
|
6. **Flexibility** - Works perfectly without Directus, can add it later if needed
|
||||||
|
|
||||||
|
## Migration Guide for Existing Code
|
||||||
|
|
||||||
|
No breaking changes! All existing code continues to work because:
|
||||||
|
- We only removed unused types/keys
|
||||||
|
- We fixed types to match what was already being used
|
||||||
|
- All JSON files remain unchanged
|
||||||
|
- All component usage remains the same
|
||||||
|
|
||||||
|
The changes are purely organizational and documentation improvements.
|
||||||
386
docs/LOCALE_SYSTEM.md
Normal file
386
docs/LOCALE_SYSTEM.md
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
# Locale System Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This portfolio uses a **hybrid i18n system** with:
|
||||||
|
- **Primary**: Static JSON files (`messages/en.json`, `messages/de.json`)
|
||||||
|
- **Secondary (Optional)**: Directus CMS for dynamic content management
|
||||||
|
- **Fallback Chain**: Directus → JSON → Key itself
|
||||||
|
|
||||||
|
## Supported Locales
|
||||||
|
|
||||||
|
- `en` (English) - Default
|
||||||
|
- `de` (German/Deutsch)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### 1. Static JSON Files (Primary)
|
||||||
|
|
||||||
|
Location: `/messages/`
|
||||||
|
- `en.json` - English translations
|
||||||
|
- `de.json` - German translations
|
||||||
|
|
||||||
|
These files contain **all** translation keys organized hierarchically:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"about": "About",
|
||||||
|
"projects": "Projects",
|
||||||
|
"contact": "Contact"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"hero": { ... },
|
||||||
|
"about": { ... },
|
||||||
|
"projects": { ... },
|
||||||
|
"contact": { ... }
|
||||||
|
},
|
||||||
|
"footer": { ... },
|
||||||
|
"consent": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Directus CMS (Optional Enhancement)
|
||||||
|
|
||||||
|
If you want to edit translations without rebuilding:
|
||||||
|
|
||||||
|
1. Set up Directus with a `messages` collection
|
||||||
|
2. Configure environment variables:
|
||||||
|
```bash
|
||||||
|
DIRECTUS_URL=https://cms.example.com
|
||||||
|
DIRECTUS_STATIC_TOKEN=your_token_here
|
||||||
|
```
|
||||||
|
3. The system will automatically prefer Directus values over JSON
|
||||||
|
|
||||||
|
**Note**: If Directus is not configured or unavailable, the system gracefully falls back to JSON files.
|
||||||
|
|
||||||
|
### 3. Components Usage
|
||||||
|
|
||||||
|
#### Server Components
|
||||||
|
Use translation loaders for better performance:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { getHeroTranslations } from '@/lib/translations-loader';
|
||||||
|
|
||||||
|
export default async function MyPage({ params }) {
|
||||||
|
const { locale } = await params;
|
||||||
|
const translations = await getHeroTranslations(locale);
|
||||||
|
|
||||||
|
return <HeroClient translations={translations} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Client Components
|
||||||
|
Use next-intl's `useTranslations` hook:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
"use client";
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
export default function Hero() {
|
||||||
|
const t = useTranslations('home.hero');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{t('title')}</h1>
|
||||||
|
<p>{t('description')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translation Structure
|
||||||
|
|
||||||
|
### Navigation (`nav`)
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
home: string;
|
||||||
|
about: string;
|
||||||
|
projects: string;
|
||||||
|
contact: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Footer (`footer`)
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
role: string;
|
||||||
|
madeIn: string;
|
||||||
|
legalNotice: string;
|
||||||
|
privacyPolicy: string;
|
||||||
|
privacySettings: string;
|
||||||
|
privacySettingsTitle: string;
|
||||||
|
builtWith: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hero Section (`home.hero`)
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
description: string;
|
||||||
|
ctaWork: string;
|
||||||
|
ctaContact: string;
|
||||||
|
features: {
|
||||||
|
f1: string;
|
||||||
|
f2: string;
|
||||||
|
f3: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### About Section (`home.about`)
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
p1: string;
|
||||||
|
p2: string;
|
||||||
|
p3: string;
|
||||||
|
funFactTitle: string;
|
||||||
|
funFactBody: string;
|
||||||
|
techStackTitle: string;
|
||||||
|
techStack: {
|
||||||
|
categories: {
|
||||||
|
frontendMobile: string;
|
||||||
|
backendDevops: string;
|
||||||
|
toolsAutomation: string;
|
||||||
|
securityAdmin: string;
|
||||||
|
};
|
||||||
|
items: {
|
||||||
|
selfHostedServices: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
hobbiesTitle: string;
|
||||||
|
hobbies: {
|
||||||
|
selfHosting: string;
|
||||||
|
gaming: string;
|
||||||
|
gameServers: string;
|
||||||
|
jogging: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Projects Section (`home.projects`)
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
viewAll: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contact Section (`home.contact`)
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
getInTouch: string;
|
||||||
|
getInTouchBody: string;
|
||||||
|
form: {
|
||||||
|
title: string;
|
||||||
|
sending: string;
|
||||||
|
send: string;
|
||||||
|
placeholders: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
errors: {
|
||||||
|
nameRequired: string;
|
||||||
|
nameMin: string;
|
||||||
|
emailRequired: string;
|
||||||
|
emailInvalid: string;
|
||||||
|
subjectRequired: string;
|
||||||
|
subjectMin: string;
|
||||||
|
messageRequired: string;
|
||||||
|
messageMin: string;
|
||||||
|
};
|
||||||
|
characters: string;
|
||||||
|
};
|
||||||
|
info: {
|
||||||
|
email: string;
|
||||||
|
location: string;
|
||||||
|
locationValue: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Consent Banner (`consent`)
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
essential: string;
|
||||||
|
analytics: string;
|
||||||
|
chat: string;
|
||||||
|
alwaysOn: string;
|
||||||
|
acceptAll: string;
|
||||||
|
acceptSelected: string;
|
||||||
|
rejectAll: string;
|
||||||
|
hide: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rich Text Content (CMS)
|
||||||
|
|
||||||
|
For longer content that needs formatting (bold, italic, lists, etc.), use the **Rich Text API**:
|
||||||
|
|
||||||
|
### 1. Server-Side Fetching
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { JSONContent } from "@tiptap/react";
|
||||||
|
import RichTextClient from './RichTextClient';
|
||||||
|
|
||||||
|
export default function MyComponent() {
|
||||||
|
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/content/page?key=${encodeURIComponent("page-slug")}&locale=${locale}`,
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data?.content?.content && data?.content?.locale === locale) {
|
||||||
|
setCmsDoc(data.content.content as JSONContent);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fallback to static content
|
||||||
|
setCmsDoc(null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{cmsDoc ? (
|
||||||
|
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-none" />
|
||||||
|
) : (
|
||||||
|
<p>{t('fallbackText')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Styling Text in Directus
|
||||||
|
|
||||||
|
When editing content in Directus CMS:
|
||||||
|
|
||||||
|
- **Bold**: Select text and click Bold button or use Ctrl/Cmd + B
|
||||||
|
- **Italic**: Select text and click Italic button or use Ctrl/Cmd + I
|
||||||
|
- **Headings**: Use heading dropdown to create H2, H3, etc.
|
||||||
|
- **Lists**: Create bullet or numbered lists
|
||||||
|
- **Links**: Highlight text and add URL
|
||||||
|
|
||||||
|
The `RichTextClient` component will render all these styles correctly.
|
||||||
|
|
||||||
|
## Adding New Translations
|
||||||
|
|
||||||
|
### 1. Add to JSON Files
|
||||||
|
|
||||||
|
Edit both `messages/en.json` and `messages/de.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// en.json
|
||||||
|
{
|
||||||
|
"mySection": {
|
||||||
|
"newKey": "My new translation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// de.json
|
||||||
|
{
|
||||||
|
"mySection": {
|
||||||
|
"newKey": "Meine neue Übersetzung"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Update Types (if needed)
|
||||||
|
|
||||||
|
If adding a new section, update `types/translations.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface MySectionTranslations {
|
||||||
|
newKey: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Loader Function (if needed)
|
||||||
|
|
||||||
|
Add to `lib/translations-loader.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getMySectionTranslations(locale: string): Promise<MySectionTranslations> {
|
||||||
|
const newKey = await getLocalizedMessage('mySection.newKey', locale);
|
||||||
|
return { newKey };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Use in Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const t = useTranslations('mySection');
|
||||||
|
const text = t('newKey');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fallback Behavior
|
||||||
|
|
||||||
|
The system follows this priority:
|
||||||
|
|
||||||
|
1. **Directus** (if configured) - Dynamic content from CMS
|
||||||
|
2. **JSON files** - Static fallback in `/messages/`
|
||||||
|
3. **Key itself** - Returns the key string if nothing found
|
||||||
|
|
||||||
|
Example: If key `nav.home` is not found anywhere, it returns `"nav.home"` as a visual indicator.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
- **JSON files**: Bundled at build time, no runtime caching needed
|
||||||
|
- **Directus content**: 5-minute in-memory cache to reduce API calls
|
||||||
|
- Clear cache: Restart the application or call `clearI18nCache()`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Keep JSON files updated**: Even if using Directus, maintain JSON files as fallback
|
||||||
|
2. **Use TypeScript types**: Ensures type safety across components
|
||||||
|
3. **Namespace keys clearly**: Use hierarchical structure (e.g., `home.hero.title`)
|
||||||
|
4. **Rich text for long content**: Use CMS rich text for paragraphs, use JSON for short UI labels
|
||||||
|
5. **Test both locales**: Always verify translations in both English and German
|
||||||
|
6. **Consistent naming**: Follow existing patterns for new keys
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Translation not showing?
|
||||||
|
|
||||||
|
1. Check if key exists in JSON files
|
||||||
|
2. Verify key spelling (case-sensitive)
|
||||||
|
3. Check if namespace is correct
|
||||||
|
4. Restart dev server to reload translations
|
||||||
|
|
||||||
|
### Directus not working?
|
||||||
|
|
||||||
|
1. Verify `DIRECTUS_URL` and `DIRECTUS_STATIC_TOKEN` in `.env`
|
||||||
|
2. Check if Directus is accessible
|
||||||
|
3. System will automatically fallback to JSON - check console for errors
|
||||||
|
|
||||||
|
### Rich text not rendering?
|
||||||
|
|
||||||
|
1. Ensure content is in Tiptap JSON format
|
||||||
|
2. Check if `RichTextClient` is imported correctly
|
||||||
|
3. Verify the API response structure
|
||||||
|
|
||||||
|
## Migration from Old System
|
||||||
|
|
||||||
|
The current system has been simplified from a previous more complex setup. Key changes:
|
||||||
|
|
||||||
|
- ✅ Removed unused translation keys from loaders
|
||||||
|
- ✅ Fixed type mismatches between interfaces and actual usage
|
||||||
|
- ✅ Aligned all translation types with component requirements
|
||||||
|
- ✅ Improved documentation and structure
|
||||||
|
- ✅ Added rich text support for CMS content
|
||||||
|
|
||||||
|
All components now use the correct translation keys that exist in JSON files, eliminating confusion about which fields are actually used.
|
||||||
@@ -26,170 +26,181 @@ export async function getNavTranslations(locale: string): Promise<NavTranslation
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getFooterTranslations(locale: string): Promise<FooterTranslations> {
|
export async function getFooterTranslations(locale: string): Promise<FooterTranslations> {
|
||||||
const [role, description, privacy, imprint, copyright, madeWith, resetConsent] = await Promise.all([
|
const [
|
||||||
|
role,
|
||||||
|
madeIn,
|
||||||
|
legalNotice,
|
||||||
|
privacyPolicy,
|
||||||
|
privacySettings,
|
||||||
|
privacySettingsTitle,
|
||||||
|
builtWith
|
||||||
|
] = await Promise.all([
|
||||||
getLocalizedMessage('footer.role', locale),
|
getLocalizedMessage('footer.role', locale),
|
||||||
getLocalizedMessage('footer.description', locale),
|
getLocalizedMessage('footer.madeIn', locale),
|
||||||
getLocalizedMessage('footer.links.privacy', locale),
|
getLocalizedMessage('footer.legalNotice', locale),
|
||||||
getLocalizedMessage('footer.links.imprint', locale),
|
getLocalizedMessage('footer.privacyPolicy', locale),
|
||||||
getLocalizedMessage('footer.copyright', locale),
|
getLocalizedMessage('footer.privacySettings', locale),
|
||||||
getLocalizedMessage('footer.madeWith', locale),
|
getLocalizedMessage('footer.privacySettingsTitle', locale),
|
||||||
getLocalizedMessage('footer.resetConsent', locale),
|
getLocalizedMessage('footer.builtWith', locale),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
role,
|
role,
|
||||||
description,
|
madeIn,
|
||||||
links: { privacy, imprint },
|
legalNotice,
|
||||||
copyright,
|
privacyPolicy,
|
||||||
madeWith,
|
privacySettings,
|
||||||
resetConsent,
|
privacySettingsTitle,
|
||||||
|
builtWith,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHeroTranslations(locale: string): Promise<HeroTranslations> {
|
export async function getHeroTranslations(locale: string): Promise<HeroTranslations> {
|
||||||
const keys = [
|
const keys = [
|
||||||
'home.hero.greeting',
|
|
||||||
'home.hero.name',
|
|
||||||
'home.hero.role',
|
|
||||||
'home.hero.description',
|
'home.hero.description',
|
||||||
'home.hero.ctaWork',
|
'home.hero.ctaWork',
|
||||||
'home.hero.ctaContact',
|
'home.hero.ctaContact',
|
||||||
'home.hero.features.f1',
|
'home.hero.features.f1',
|
||||||
'home.hero.features.f2',
|
'home.hero.features.f2',
|
||||||
'home.hero.features.f3',
|
'home.hero.features.f3',
|
||||||
'home.hero.scrollDown',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const values = await Promise.all(keys.map(key => getLocalizedMessage(key, locale)));
|
const values = await Promise.all(keys.map(key => getLocalizedMessage(key, locale)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
greeting: values[0],
|
description: values[0],
|
||||||
name: values[1],
|
ctaWork: values[1],
|
||||||
role: values[2],
|
ctaContact: values[2],
|
||||||
description: values[3],
|
|
||||||
cta: {
|
|
||||||
projects: values[4],
|
|
||||||
contact: values[5],
|
|
||||||
},
|
|
||||||
features: {
|
features: {
|
||||||
f1: values[6],
|
f1: values[3],
|
||||||
f2: values[7],
|
f2: values[4],
|
||||||
f3: values[8],
|
f3: values[5],
|
||||||
},
|
},
|
||||||
scrollDown: values[9],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAboutTranslations(locale: string): Promise<AboutTranslations> {
|
export async function getAboutTranslations(locale: string): Promise<AboutTranslations> {
|
||||||
// Diese Keys sind NICHT korrekt - wir nutzen nur für Type Compatibility
|
|
||||||
// Die About Component nutzt actually: title, p1, p2, p3, hobbiesTitle, hobbies.*, techStackTitle, techStack.*
|
|
||||||
// Lade alle benötigten Keys
|
|
||||||
const keys = [
|
const keys = [
|
||||||
'home.about.title',
|
'home.about.title',
|
||||||
'home.about.description',
|
|
||||||
'home.about.techStack.title',
|
|
||||||
'home.about.techStack.categories.frontendMobile',
|
|
||||||
'home.about.techStack.categories.backendDevops',
|
|
||||||
'home.about.techStack.categories.toolsAutomation',
|
|
||||||
'home.about.techStack.categories.securityAdmin',
|
|
||||||
'home.about.techStack.items.selfHostedServices',
|
|
||||||
'home.about.hobbiesTitle', // Nicht "interests.title"!
|
|
||||||
'home.about.hobbies.selfHosting',
|
|
||||||
'home.about.hobbies.gaming',
|
|
||||||
'home.about.hobbies.gameServers',
|
|
||||||
'home.about.hobbies.jogging',
|
|
||||||
'home.about.p1',
|
'home.about.p1',
|
||||||
'home.about.p2',
|
'home.about.p2',
|
||||||
'home.about.p3',
|
'home.about.p3',
|
||||||
'home.about.funFactTitle',
|
'home.about.funFactTitle',
|
||||||
'home.about.funFactBody',
|
'home.about.funFactBody',
|
||||||
'home.about.techStackTitle',
|
'home.about.techStackTitle',
|
||||||
|
'home.about.techStack.categories.frontendMobile',
|
||||||
|
'home.about.techStack.categories.backendDevops',
|
||||||
|
'home.about.techStack.categories.toolsAutomation',
|
||||||
|
'home.about.techStack.categories.securityAdmin',
|
||||||
|
'home.about.techStack.items.selfHostedServices',
|
||||||
|
'home.about.hobbiesTitle',
|
||||||
|
'home.about.hobbies.selfHosting',
|
||||||
|
'home.about.hobbies.gaming',
|
||||||
|
'home.about.hobbies.gameServers',
|
||||||
|
'home.about.hobbies.jogging',
|
||||||
];
|
];
|
||||||
|
|
||||||
const values = await Promise.all(keys.map(key => getLocalizedMessage(key, locale)));
|
const values = await Promise.all(keys.map(key => getLocalizedMessage(key, locale)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: values[0],
|
title: values[0],
|
||||||
description: values[1],
|
p1: values[1],
|
||||||
|
p2: values[2],
|
||||||
|
p3: values[3],
|
||||||
|
funFactTitle: values[4],
|
||||||
|
funFactBody: values[5],
|
||||||
|
techStackTitle: values[6],
|
||||||
techStack: {
|
techStack: {
|
||||||
title: values[2],
|
|
||||||
categories: {
|
categories: {
|
||||||
frontendMobile: values[3],
|
frontendMobile: values[7],
|
||||||
backendDevops: values[4],
|
backendDevops: values[8],
|
||||||
toolsAutomation: values[5],
|
toolsAutomation: values[9],
|
||||||
securityAdmin: values[6],
|
securityAdmin: values[10],
|
||||||
},
|
},
|
||||||
items: {
|
items: {
|
||||||
selfHostedServices: values[7],
|
selfHostedServices: values[11],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
interests: {
|
hobbiesTitle: values[12],
|
||||||
title: values[8], // hobbiesTitle
|
hobbies: {
|
||||||
cybersecurity: {
|
selfHosting: values[13],
|
||||||
title: values[9], // hobbies.selfHosting
|
gaming: values[14],
|
||||||
description: values[10], // hobbies.gaming
|
gameServers: values[15],
|
||||||
},
|
jogging: values[16],
|
||||||
selfHosting: {
|
|
||||||
title: values[11], // hobbies.gameServers
|
|
||||||
description: values[12], // hobbies.jogging
|
|
||||||
},
|
|
||||||
gaming: {
|
|
||||||
title: values[13], // p1
|
|
||||||
description: values[14], // p2
|
|
||||||
},
|
|
||||||
automation: {
|
|
||||||
title: values[15], // p3
|
|
||||||
description: values[16], // funFactTitle
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProjectsTranslations(locale: string): Promise<ProjectsTranslations> {
|
export async function getProjectsTranslations(locale: string): Promise<ProjectsTranslations> {
|
||||||
const [title, viewAll] = await Promise.all([
|
const [title, subtitle, viewAll] = await Promise.all([
|
||||||
getLocalizedMessage('home.projects.title', locale),
|
getLocalizedMessage('home.projects.title', locale),
|
||||||
|
getLocalizedMessage('home.projects.subtitle', locale),
|
||||||
getLocalizedMessage('home.projects.viewAll', locale),
|
getLocalizedMessage('home.projects.viewAll', locale),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { title, viewAll };
|
return { title, subtitle, viewAll };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getContactTranslations(locale: string): Promise<ContactTranslations> {
|
export async function getContactTranslations(locale: string): Promise<ContactTranslations> {
|
||||||
const keys = [
|
const keys = [
|
||||||
'home.contact.title',
|
'home.contact.title',
|
||||||
'home.contact.description',
|
'home.contact.subtitle',
|
||||||
'home.contact.form.name',
|
'home.contact.getInTouch',
|
||||||
'home.contact.form.email',
|
'home.contact.getInTouchBody',
|
||||||
'home.contact.form.message',
|
'home.contact.form.title',
|
||||||
'home.contact.form.send',
|
|
||||||
'home.contact.form.sending',
|
'home.contact.form.sending',
|
||||||
'home.contact.form.success',
|
'home.contact.form.send',
|
||||||
'home.contact.form.error',
|
'home.contact.form.placeholders.name',
|
||||||
'home.contact.info.title',
|
'home.contact.form.placeholders.email',
|
||||||
|
'home.contact.form.placeholders.subject',
|
||||||
|
'home.contact.form.placeholders.message',
|
||||||
|
'home.contact.form.errors.nameRequired',
|
||||||
|
'home.contact.form.errors.nameMin',
|
||||||
|
'home.contact.form.errors.emailRequired',
|
||||||
|
'home.contact.form.errors.emailInvalid',
|
||||||
|
'home.contact.form.errors.subjectRequired',
|
||||||
|
'home.contact.form.errors.subjectMin',
|
||||||
|
'home.contact.form.errors.messageRequired',
|
||||||
|
'home.contact.form.errors.messageMin',
|
||||||
|
'home.contact.form.characters',
|
||||||
'home.contact.info.email',
|
'home.contact.info.email',
|
||||||
'home.contact.info.response',
|
'home.contact.info.location',
|
||||||
'home.contact.info.emailLabel',
|
'home.contact.info.locationValue',
|
||||||
];
|
];
|
||||||
|
|
||||||
const values = await Promise.all(keys.map(key => getLocalizedMessage(key, locale)));
|
const values = await Promise.all(keys.map(key => getLocalizedMessage(key, locale)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: values[0],
|
title: values[0],
|
||||||
description: values[1],
|
subtitle: values[1],
|
||||||
|
getInTouch: values[2],
|
||||||
|
getInTouchBody: values[3],
|
||||||
form: {
|
form: {
|
||||||
name: values[2],
|
title: values[4],
|
||||||
email: values[3],
|
sending: values[5],
|
||||||
message: values[4],
|
send: values[6],
|
||||||
send: values[5],
|
placeholders: {
|
||||||
sending: values[6],
|
name: values[7],
|
||||||
success: values[7],
|
email: values[8],
|
||||||
error: values[8],
|
subject: values[9],
|
||||||
|
message: values[10],
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
nameRequired: values[11],
|
||||||
|
nameMin: values[12],
|
||||||
|
emailRequired: values[13],
|
||||||
|
emailInvalid: values[14],
|
||||||
|
subjectRequired: values[15],
|
||||||
|
subjectMin: values[16],
|
||||||
|
messageRequired: values[17],
|
||||||
|
messageMin: values[18],
|
||||||
|
},
|
||||||
|
characters: values[19],
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
title: values[9],
|
email: values[20],
|
||||||
email: values[10],
|
location: values[21],
|
||||||
response: values[11],
|
locationValue: values[22],
|
||||||
emailLabel: values[12],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
712
package-lock.json
generated
712
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Type Definitions für Directus-basierte Translations
|
* Type Definitions for Directus-based Translations
|
||||||
* Jede Section hat ihre eigenen Translation Props
|
* Each section has its own translation props
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface NavTranslations {
|
export interface NavTranslations {
|
||||||
@@ -12,38 +12,34 @@ export interface NavTranslations {
|
|||||||
|
|
||||||
export interface FooterTranslations {
|
export interface FooterTranslations {
|
||||||
role: string;
|
role: string;
|
||||||
description: string;
|
madeIn: string;
|
||||||
links: {
|
legalNotice: string;
|
||||||
privacy: string;
|
privacyPolicy: string;
|
||||||
imprint: string;
|
privacySettings: string;
|
||||||
};
|
privacySettingsTitle: string;
|
||||||
copyright: string;
|
builtWith: string;
|
||||||
madeWith: string;
|
|
||||||
resetConsent: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeroTranslations {
|
export interface HeroTranslations {
|
||||||
greeting: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
description: string;
|
description: string;
|
||||||
cta: {
|
ctaWork: string;
|
||||||
projects: string;
|
ctaContact: string;
|
||||||
contact: string;
|
|
||||||
};
|
|
||||||
features: {
|
features: {
|
||||||
f1: string;
|
f1: string;
|
||||||
f2: string;
|
f2: string;
|
||||||
f3: string;
|
f3: string;
|
||||||
};
|
};
|
||||||
scrollDown: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AboutTranslations {
|
export interface AboutTranslations {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
p1: string;
|
||||||
|
p2: string;
|
||||||
|
p3: string;
|
||||||
|
funFactTitle: string;
|
||||||
|
funFactBody: string;
|
||||||
|
techStackTitle: string;
|
||||||
techStack: {
|
techStack: {
|
||||||
title: string;
|
|
||||||
categories: {
|
categories: {
|
||||||
frontendMobile: string;
|
frontendMobile: string;
|
||||||
backendDevops: string;
|
backendDevops: string;
|
||||||
@@ -54,49 +50,52 @@ export interface AboutTranslations {
|
|||||||
selfHostedServices: string;
|
selfHostedServices: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
interests: {
|
hobbiesTitle: string;
|
||||||
title: string;
|
hobbies: {
|
||||||
cybersecurity: {
|
selfHosting: string;
|
||||||
title: string;
|
gaming: string;
|
||||||
description: string;
|
gameServers: string;
|
||||||
};
|
jogging: string;
|
||||||
selfHosting: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
gaming: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
automation: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectsTranslations {
|
export interface ProjectsTranslations {
|
||||||
title: string;
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
viewAll: string;
|
viewAll: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContactTranslations {
|
export interface ContactTranslations {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
subtitle: string;
|
||||||
|
getInTouch: string;
|
||||||
|
getInTouchBody: string;
|
||||||
form: {
|
form: {
|
||||||
|
title: string;
|
||||||
|
sending: string;
|
||||||
|
send: string;
|
||||||
|
placeholders: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
subject: string;
|
||||||
message: string;
|
message: string;
|
||||||
send: string;
|
};
|
||||||
sending: string;
|
errors: {
|
||||||
success: string;
|
nameRequired: string;
|
||||||
error: string;
|
nameMin: string;
|
||||||
|
emailRequired: string;
|
||||||
|
emailInvalid: string;
|
||||||
|
subjectRequired: string;
|
||||||
|
subjectMin: string;
|
||||||
|
messageRequired: string;
|
||||||
|
messageMin: string;
|
||||||
|
};
|
||||||
|
characters: string;
|
||||||
};
|
};
|
||||||
info: {
|
info: {
|
||||||
title: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
response: string;
|
location: string;
|
||||||
emailLabel: string;
|
locationValue: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user