* 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>
8.4 KiB
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) - Defaultde(German/Deutsch)
Architecture
1. Static JSON Files (Primary)
Location: /messages/
en.json- English translationsde.json- German translations
These files contain all translation keys organized hierarchically:
{
"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:
- Set up Directus with a
messagescollection - Configure environment variables:
DIRECTUS_URL=https://cms.example.com DIRECTUS_STATIC_TOKEN=your_token_here - 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:
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:
"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)
{
home: string;
about: string;
projects: string;
contact: string;
}
Footer (footer)
{
role: string;
madeIn: string;
legalNotice: string;
privacyPolicy: string;
privacySettings: string;
privacySettingsTitle: string;
builtWith: string;
}
Hero Section (home.hero)
{
description: string;
ctaWork: string;
ctaContact: string;
features: {
f1: string;
f2: string;
f3: string;
};
}
About Section (home.about)
{
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)
{
title: string;
subtitle: string;
viewAll: string;
}
Contact Section (home.contact)
{
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)
{
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
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:
// 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:
export interface MySectionTranslations {
newKey: string;
}
3. Create Loader Function (if needed)
Add to lib/translations-loader.ts:
export async function getMySectionTranslations(locale: string): Promise<MySectionTranslations> {
const newKey = await getLocalizedMessage('mySection.newKey', locale);
return { newKey };
}
4. Use in Components
const t = useTranslations('mySection');
const text = t('newKey');
Fallback Behavior
The system follows this priority:
- Directus (if configured) - Dynamic content from CMS
- JSON files - Static fallback in
/messages/ - 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
- Keep JSON files updated: Even if using Directus, maintain JSON files as fallback
- Use TypeScript types: Ensures type safety across components
- Namespace keys clearly: Use hierarchical structure (e.g.,
home.hero.title) - Rich text for long content: Use CMS rich text for paragraphs, use JSON for short UI labels
- Test both locales: Always verify translations in both English and German
- Consistent naming: Follow existing patterns for new keys
Troubleshooting
Translation not showing?
- Check if key exists in JSON files
- Verify key spelling (case-sensitive)
- Check if namespace is correct
- Restart dev server to reload translations
Directus not working?
- Verify
DIRECTUS_URLandDIRECTUS_STATIC_TOKENin.env - Check if Directus is accessible
- System will automatically fallback to JSON - check console for errors
Rich text not rendering?
- Ensure content is in Tiptap JSON format
- Check if
RichTextClientis imported correctly - 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.