* 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>
387 lines
8.4 KiB
Markdown
387 lines
8.4 KiB
Markdown
# 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.
|