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:
Copilot
2026-01-22 21:25:41 +01:00
committed by GitHub
parent 37a1bc4e18
commit 7604e00e0f
9 changed files with 1452 additions and 342 deletions

View File

@@ -1,221 +1,146 @@
# Directus Integration - Migration Guide
## 🎯 Was wurde geändert?
## 🎯 Overview
Das Portfolio nutzt jetzt **Directus als CMS** für alle Texte. Die Integration ist **hybrid**:
-**Directus** (primär) → Texte werden aus Directus CMS geladen
-**JSON Fallback** (sekundär) → Falls Directus nicht erreichbar, nutzen wir messages/*.json
This portfolio now has a **hybrid i18n system**:
-**JSON Files** (Primary) → All translations work from `messages/*.json` files
-**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
- `lib/directus.ts` - REST Client für Directus (nutzt `de-DE`, `en-US`)
- `lib/i18n-loader.ts` - Lädt Texte mit Fallback-Chain
- `lib/translations-loader.ts` - Batch-Loader für alle Sections
- `types/translations.ts` - TypeScript Types für alle Translation Objects
- `lib/directus.ts` - REST Client for Directus (uses `de-DE`, `en-US` locale codes)
- `lib/i18n-loader.ts` - Loads texts with Fallback Chain
- `lib/translations-loader.ts` - Batch loader for all sections (cleaned up to match actual usage)
- `types/translations.ts` - TypeScript types for all translation objects (fixed to match components)
### Components
- `app/components/Header.server.tsx` - Server Wrapper für Header
- `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
All component wrappers properly load and pass translations to client components.
## 🔄 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)
→ Directus API (de-DE/en-US)
Falls nicht gefunden: JSON File (de/en)
→ Props an Client Component
Client Component → Nutzt translations aus Props
Try Directus API (de-DE/en-US)
If not found: JSON File (de/en)
→ Props to Client Component
```
## 🗄️ 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:**
- `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**
### 1. Environment Variables
**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)
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
**If these are not set**, the system will skip Directus and use JSON files only.
#### Option B: Flat Structure (Einfacher)
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: `messages`
### 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):
**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)
**Note**: Keys use dot notation (`nav.home`) but locales use dashes (`en-US`, `de-DE`).
### 3. Permissions
**Public Role:**
- `messages`: Read access (alle Felder)
- `content_pages`: Read access (alle Felder)
Grant **Public** role read access to `messages` collection.
## 📝 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:**
```
nav.home
nav.about
nav.projects
nav.contact
home.hero.greeting
home.hero.name
home.hero.role
home.hero.description
...
```
All keys are organized hierarchically:
- `nav.*` - Navigation items
- `home.hero.*` - Hero section
- `home.about.*` - About section
- `home.projects.*` - Projects section
- `home.contact.*` - Contact form and info
- `footer.*` - Footer content
- `consent.*` - Privacy consent banner
**Wichtig:** Keys sind **dot-separated** (wie in JSON), aber **Locale nutzt `-`**:
-`key="nav.home"`, `locale="de-DE"`
-`key="nav_home"`, `locale="de"`
## 🎨 Rich Text Content
## 🔧 Environment Variables
For longer content that needs formatting (bold, italic, lists), use the `content_pages` collection:
In `.env.local`:
```bash
DIRECTUS_URL=https://cms.dk0.dev
DIRECTUS_STATIC_TOKEN=ogUMcHCa1CAYU1YifsoeJ_7V76o1atYG
```
### Collection: `content_pages` (Optional)
## 🚀 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
```tsx
// app/[locale]/page.tsx
export default async function Page({ params }) {
const { locale } = await params;
return <HomePageServer locale={locale} />;
}
```
Examples:
- `home-hero` - Hero section description
- `home-about` - About section content
- `home-contact` - Contact intro text
### 2. Server Component lädt Translations
```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>
);
}
```
Components fetch these via `/api/content/page` and render using `RichTextClient`.
## 🔍 Fallback Chain
Für jeden Key wird gesucht:
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"
For every translation key, the system searches in this order:
## 🎨 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
- Cache Key: `msg:${key}:${locale}`
- Läuft im Server Memory (nicht persistent)
- Bei Deploy/Restart wird Cache geleert
## ✅ What Was Fixed
## ✅ Testing
Previous issues that have been resolved:
1. **Mit Directus:** Trage einen Test-Key ein:
- Key: `test`
- Locale: `de-DE`
- Value: `Hallo von Directus!`
- Prüfe: `await getLocalizedMessage('test', 'de')` → "Hallo von Directus!"
1. **Type mismatches** - All translation types now match actual component usage
2.**Unused fields** - Removed translation keys that were never used (like `hero.greeting`, `hero.name`)
3.**Wrong structure** - Fixed `AboutTranslations` structure (removed fake `interests` nesting)
4.**Missing keys** - Aligned loaders with JSON files and actual component requirements
5.**Confusing comments** - Removed misleading comments in `translations-loader.ts`
2. **Ohne Directus:** Stoppe Directus
- Prüfe: Messages sollten aus JSON files kommen
- Website sollte normal funktionieren (degraded mode)
## 🎯 Best Practices
3. **Build Test:**
```bash
npm run build
```
- Sollte ohne Errors durchlaufen
1. **Always maintain JSON files** - Even if using Directus, keep JSON files as fallback
2. **Use types** - TypeScript types ensure correct usage
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
5. **JSON for UI labels** - Use JSON/messages for short UI strings like buttons and labels
## 🐛 Troubleshooting
### "Key nicht gefunden"
- Prüfe Directus GUI: Key exakt gleich? (`nav.home` nicht `nav_home`)
- Prüfe Locale: `de-DE` oder `en-US` (mit `-`)?
- Prüfe Permissions: Public role hat Read access?
### Directus not configured
**This is normal!** The app works fine. All translations come from JSON files.
### "Directus nicht erreichbar"
- Prüfe `DIRECTUS_URL` in .env
- Prüfe Token: `DIRECTUS_STATIC_TOKEN`
- Test: `curl -H "Authorization: Bearer TOKEN" https://cms.dk0.dev/items/messages`
### Want to use Directus?
1. Set up `DIRECTUS_URL` and `DIRECTUS_STATIC_TOKEN`
2. Create `messages` collection
3. Add your translations
4. They will override JSON values
### "Texte ändern sich nicht"
- Cache! Warte 5 Minuten oder restart Server
- Oder: Clear Cache manuell (`clearI18nCache()` in lib/i18n-loader.ts)
### Translation not showing?
Check in this order:
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)
2. **Collections erstellen** (messages, content_pages)
3. **Keys eintragen** (aus DIRECTUS_CHECKLIST.md)
4. **Testen** (dev environment)
5. **Production** (wenn alles funktioniert)
- **Complete locale documentation**: `docs/LOCALE_SYSTEM.md`
- **Directus setup checklist**: `DIRECTUS_CHECKLIST.md`
- **Operations guide**: `docs/OPERATIONS.md`
## 🎯 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)