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
|
||||
|
||||
## 🎯 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)
|
||||
|
||||
Reference in New Issue
Block a user