# 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 ;
}
```
#### 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 (
{t('title')}
{t('description')}
);
}
```
## 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(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 (
{cmsDoc ? (
) : (
{t('fallbackText')}
)}
);
}
```
### 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 {
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.