Files
portfolio/docs/LOCALE_SYSTEM.md
Copilot 7604e00e0f 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>
2026-01-22 21:25:41 +01:00

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) - 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:

{
  "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:
    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:

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;
}
{
  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;
  };
}
{
  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:

  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.