Files
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00
..
2026-02-01 15:07:20 +01:00

Internationalization (i18n) Documentation

Complete guide to the multi-language support system in the portfolio application.

Overview

This portfolio supports 3 languages with full internationalization:

Language Code Flag Region
German (default) de 🇩🇪 Germany
English en 🇬🇧 United States
Serbian sr 🇷🇸 Serbia

Tech Stack

  • next-intl - Next.js internationalization library
  • JSON-based translations - Simple, maintainable translation files
  • Type-safe locale handling - TypeScript ensures locale consistency
  • SEO optimized - Hreflang tags, Content-Language headers, localized metadata

Key Features

  • Automatic locale detection from URL path
  • Fallback to default locale (German)
  • Server-side translation loading
  • Type-safe locale validation
  • SEO-friendly URL structure (/de/, /en/, /sr/)
  • Localized metadata for social sharing
  • Support for nested translation keys
  • Legacy migration path (locales-old)

Quick Start

Using Translations in Components

import { useTranslations } from 'next-intl';

export default function MyComponent() {
  const t = useTranslations('common');

  return (
    <div>
      <h1>{t('nav.home')}</h1>
      <p>{t('siteDescription')}</p>
    </div>
  );
}

Adding a New Translation Key

Step-by-Step Guide

Follow this process to add new translation keys to the application:

1. Identify the Appropriate Namespace

Before adding a new translation key, determine where it should live based on its purpose:

  • meta - Site metadata, author information, SEO keywords
  • common - Shared UI elements (buttons, navigation, errors, cookies)
  • navigation - All navigation-related text (main menu, footer links, mobile menu)
  • footer - Footer content (sections, legal links, copyright)
  • pages - Page-specific content organized by route
    • pages.home - Homepage content
    • pages.about - About page content
    • pages.contact - Contact page content
    • pages.privacy - Privacy policy content
    • pages.terms - Terms and conditions content
    • pages.notfound - 404 page content
  • portfolio - Portfolio page content and filters
  • blog - Blog page content and UI elements
  • login - Login page content
  • dashboard - Dashboard content and metrics

Best Practice: Use common for elements that appear across multiple pages. Use page-specific namespaces for content unique to a single page.

2. Open All Three Translation Files

You must update all three locale files to maintain consistency:

# Open in your editor:
src/messages/de.json    # German (default)
src/messages/en.json    # English
src/messages/sr.json    # Serbian
3. Add the Translation Key

Add your key to the exact same location in each file. The JSON structure must be identical across all locales.

Example: Adding a new action button

// de.json
{
  "common": {
    "actions": {
      "readMore": "Weiterlesen",
      "viewMore": "Mehr anzeigen",
      "viewAll": "Alle anzeigen",
      "download": "Herunterladen"  // ← New key
    }
  }
}

// en.json
{
  "common": {
    "actions": {
      "readMore": "Read More",
      "viewMore": "View More",
      "viewAll": "View All",
      "download": "Download"  // ← New key
    }
  }
}

// sr.json
{
  "common": {
    "actions": {
      "readMore": "Procitaj vise",
      "viewMore": "Prikazi vise",
      "viewAll": "Prikazi sve",
      "download": "Preuzmi"  // ← New key
    }
  }
}

Example: Adding a new page section

// de.json
{
  "pages": {
    "services": {  // ← New namespace
      "hero": {
        "title": "Unsere Dienstleistungen",
        "subtitle": "Maßgeschneiderte Lösungen für Ihr Unternehmen"
      }
    }
  }
}

// en.json
{
  "pages": {
    "services": {  // ← New namespace
      "hero": {
        "title": "Our Services",
        "subtitle": "Tailored solutions for your business"
      }
    }
  }
}

// sr.json
{
  "pages": {
    "services": {  // ← New namespace
      "hero": {
        "title": "Naše usluge",
        "subtitle": "Prilagođena rešenja za vaše poslovanje"
      }
    }
  }
}
4. Use the Translation in Your Component

Import the useTranslations hook and specify the namespace:

import { useTranslations } from 'next-intl';

export default function MyComponent() {
  // Option 1: Access top-level namespace
  const t = useTranslations('common.actions');

  return (
    <button className="btn-primary">
      {t('download')}
    </button>
  );
}

Multiple namespaces in one component:

import { useTranslations } from 'next-intl';

export default function ServicesPage() {
  const tHero = useTranslations('pages.services.hero');
  const tCommon = useTranslations('common.actions');

  return (
    <div>
      <h1>{tHero('title')}</h1>
      <p>{tHero('subtitle')}</p>
      <button>{tCommon('readMore')}</button>
    </div>
  );
}
5. Verify Your Changes
  1. Build the application to ensure no TypeScript errors:

    npm run build
    
  2. Test in all three languages:

    • German: http://localhost:3000/de
    • English: http://localhost:3000/en
    • Serbian: http://localhost:3000/sr
  3. Check for missing translations - The browser console will warn if a key is missing

JSON Structure & Organization

Hierarchical Namespace Pattern

Translation files use a deeply nested JSON structure to organize translations logically:

{
  "namespace": {           // Top-level category
    "subnamespace": {      // Feature or section
      "key": "value",      // Leaf node (actual translation)
      "nested": {
        "key": "value"     // Deeper nesting allowed
      }
    }
  }
}

Real example from the codebase:

{
  "pages": {                    // Namespace: Page content
    "home": {                   // Subnamespace: Homepage
      "hero": {                 // Section: Hero section
        "title": "FULLSTACK DEVELOPER",
        "subtitle": "AI AGENTS | VOICE AI",
        "navigation": {         // Nested object
          "experience": "EXPERIENCE",
          "skills": "SKILLS"
        }
      }
    }
  }
}

Access in code:

const t = useTranslations('pages.home.hero');
console.log(t('title'));                    // "FULLSTACK DEVELOPER"
console.log(t('navigation.experience'));    // "EXPERIENCE"
Special Data Types

Arrays - For lists of items:

{
  "meta": {
    "site": {
      "keywords": [
        "Fullstack Developer",
        "AI Developer Germany",
        "Process Automation"
      ]
    }
  }
}

Usage:

const t = useTranslations('meta.site');
const keywords = t.raw('keywords'); // Returns array

Objects - For structured data:

{
  "pages": {
    "home": {
      "experience": {
        "positions": [
          {
            "role": "Process Automation Specialist",
            "company": "Everlast Consulting GmbH",
            "period": "12/2024 - PRESENT",
            "highlights": [
              "Development of AI agents",
              "Web scraping solutions"
            ]
          }
        ]
      }
    }
  }
}

Usage:

const t = useTranslations('pages.home.experience');
const positions = t.raw('positions'); // Returns array of objects

positions.map((position) => (
  <div key={position.role}>
    <h3>{position.role}</h3>
    <p>{position.company}</p>
    <span>{position.period}</span>
    <ul>
      {position.highlights.map((highlight) => (
        <li key={highlight}>{highlight}</li>
      ))}
    </ul>
  </div>
));

Advanced Usage Examples

Dynamic Translation Keys

Access translations dynamically based on variables:

import { useTranslations } from 'next-intl';

export default function CategoryFilter({ categories }: { categories: string[] }) {
  const t = useTranslations('portfolio.filters.categories');

  return (
    <div>
      {categories.map((category) => (
        <button key={category}>
          {t(category as any)}  // Dynamic key lookup
        </button>
      ))}
    </div>
  );
}

// If categories = ["Data Science", "AI Development"]
// Output: "Data Science" → "Data Science" (en) or "Data Science" (de)
//         "AI Development" → "AI Development" (en) or "KI-Entwicklung" (de)
String Interpolation

Use variables in translations:

import { useTranslations } from 'next-intl';

export default function Pagination({ start, end, total }: PaginationProps) {
  const t = useTranslations('blog.ui.pagination');

  // Translation: "Showing {start}-{end} of {total} articles"
  return (
    <p>
      {t('showing', { start, end, total })}
    </p>
  );
}

In JSON:

{
  "blog": {
    "ui": {
      "pagination": {
        "showing": "Showing {start}-{end} of {total} articles"
      }
    }
  }
}
Accessing Rich Content

For translations containing HTML or rich content:

import { useTranslations } from 'next-intl';

export default function RichText() {
  const t = useTranslations('pages.about');

  return (
    <div>
      {/* Simple text */}
      <h1>{t('hero.title')}</h1>

      {/* Raw access for objects/arrays */}
      <div>
        {t.raw('hero.expertise.processAutomation')}
      </div>
    </div>
  );
}

Common Patterns & Best Practices

DO: Keep Structure Consistent
// Good: Same structure across all locales
// de.json
{ "common": { "actions": { "save": "Speichern" } } }

// en.json
{ "common": { "actions": { "save": "Save" } } }

// sr.json
{ "common": { "actions": { "save": "Sačuvaj" } } }
DON'T: Mix Structures
// Bad: Different structures
// de.json
{ "common": { "actions": { "save": "Speichern" } } }

// en.json
{ "common": { "save": "Save" } }  // ❌ Missing "actions" level
DO: Use Semantic Naming
{
  "common": {
    "actions": {
      "submit": "Submit",      // ✅ Clear, semantic
      "cancel": "Cancel",
      "delete": "Delete"
    }
  }
}
DON'T: Use Generic Names
{
  "common": {
    "button1": "Submit",  // ❌ Non-descriptive
    "btn": "Cancel",      // ❌ Abbreviated
    "text1": "Delete"     // ❌ Generic
  }
}
{
  "pages": {
    "contact": {
      "contactForm": {              // ✅ Grouped by feature
        "title": "Contact Me",
        "name": { "label": "Name", "placeholder": "Your name" },
        "email": { "label": "Email", "placeholder": "your@email.com" },
        "submit": "Send Message"
      }
    }
  }
}
DO: Reuse Common Translations
// Good: Reuse common translations
import { useTranslations } from 'next-intl';

export default function MyForm() {
  const tCommon = useTranslations('common.actions');

  return (
    <form>
      <button type="submit">{tCommon('submit')}</button>
      <button type="button">{tCommon('cancel')}</button>
    </form>
  );
}

Documentation Structure

This documentation is organized into the following sections:

📁 Directory Structure & Organization

The i18n system is organized into three main directories, each serving a specific purpose in the translation infrastructure.

Overview of Directory Structure

src/
├── i18n/                          # Configuration Layer
│   ├── config.ts                  # ✅ Locale definitions & metadata
│   ├── request.ts                 # ✅ next-intl integration
│   └── locales-old/               # ⚠️ DEPRECATED - Do NOT use
│       ├── de.ts
│       ├── en.ts
│       └── sr.ts
│
└── messages/                      # ✅ Active Translation Files
    ├── de.json                    # German translations
    ├── en.json                    # English translations
    └── sr.json                    # Serbian translations

src/i18n/ - Configuration Directory

This directory contains the core configuration files that define how the i18n system operates.

Purpose: Centralized configuration for locale definitions, metadata, and next-intl integration.

config.ts - Locale Metadata & Settings

Location: src/i18n/config.ts

This is the single source of truth for all locale-related configuration.

What it contains:

  1. Locales Array - Defines all supported languages

    export const locales = ['de', 'en', 'sr'] as const;
    
  2. Default Locale - Fallback language

    export const defaultLocale = 'de' as const;
    
  3. Locale Type - TypeScript type for type-safe locale handling

    export type Locale = (typeof locales)[number];
    
  4. Display Names - Human-readable language names

    export const localeNames: Record<Locale, string> = {
      de: 'Deutsch',
      en: 'English',
      sr: 'Srpski',
    };
    
  5. Locale Flags - Emoji flags for UI

    export const localeFlags: Record<Locale, string> = {
      de: '🇩🇪',
      en: '🇬🇧',
      sr: '🇷🇸',
    };
    
  6. Extended Metadata - SEO and regional data

    export const localeMetadata: Record<Locale, {
      language: string;      // ISO 639-1 code (e.g., 'de')
      region: string;        // ISO 3166-1 code (e.g., 'DE')
      hreflang: string;      // For <link rel="alternate"> tags
      ogLocale: string;      // For Open Graph meta tags
      script: string;        // Writing system (e.g., 'Latn')
      territory: string;     // Full country name
      currency: string;      // ISO 4217 code (e.g., 'EUR')
    }> = {
      de: {
        language: 'de',
        region: 'DE',
        hreflang: 'de-DE',
        ogLocale: 'de_DE',
        script: 'Latn',
        territory: 'Germany',
        currency: 'EUR',
      },
      // ... en, sr
    };
    
  7. SEO Keywords - Localized keywords for each language

    export const seoKeywords: Record<Locale, string[]> = {
      de: ['Fullstack Developer', 'KI Entwickler', 'Köln', ...],
      en: ['AI Developer', 'Remote Developer Europe', 'London', ...],
      sr: ['AI Programer Srbija', 'Beograd', 'Novi Sad', ...],
    };
    

When to modify this file:

  • Adding a new language
  • Updating SEO keywords
  • Changing metadata for Open Graph or hreflang
  • Adding translation strings (use src/messages/ instead)

request.ts - next-intl Integration

Location: src/i18n/request.ts

This file integrates the translation system with next-intl and handles runtime locale resolution.

What it does:

import { getRequestConfig } from 'next-intl/server';
import { locales, type Locale } from './config';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  // Validate locale and fallback to default if invalid
  if (!locale || !locales.includes(locale as Locale)) {
    locale = 'de';
  }

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

Key responsibilities:

  1. Locale Validation - Ensures only valid locales are used
  2. Fallback Logic - Defaults to German (de) if locale is invalid or missing
  3. Dynamic Import - Loads the appropriate translation JSON file at runtime
  4. Type Safety - Uses the Locale type from config.ts for validation

Flow:

  1. Receives requestLocale from next-intl (extracted from URL)
  2. Validates against the locales array
  3. Falls back to de if validation fails
  4. Dynamically imports the corresponding JSON file from src/messages/
  5. Returns locale and messages to next-intl

When to modify this file:

  • Changing fallback locale logic
  • Adding custom locale resolution rules
  • Implementing locale persistence (cookies, etc.)
  • Most use cases don't require changes

src/messages/ - Active Translation Files

Location: src/messages/

This directory contains the active, production-ready translation files in JSON format.

Files:

  • de.json - German translations (default)
  • en.json - English translations
  • sr.json - Serbian translations

Why JSON?

  • Simple, human-readable format
  • Easy to edit without TypeScript knowledge
  • Smaller bundle size (dynamic imports)
  • Better IDE support for JSON
  • Easier integration with translation tools

Structure:

Each file contains nested JSON objects organized by namespace:

{
  "meta": {
    "site": { "name": "...", "title": "..." },
    "author": { "name": "...", "role": "..." },
    "social": { "linkedin": "...", "github": "..." }
  },
  "common": {
    "nav": { "home": "...", "about": "..." },
    "actions": { "readMore": "...", "submit": "..." }
  },
  "pages": {
    "home": { "hero": { "title": "..." } },
    "about": { "title": "..." }
  }
}

Best Practices:

  1. Keep keys synchronized - All three files should have identical structure
  2. Use nested namespaces - Organize by feature or page (e.g., common, pages.home)
  3. Descriptive keys - Use semantic names like hero.title instead of text1
  4. No hardcoded text - All user-facing text should be in these files

When to modify these files:

  • Adding new translation keys
  • Updating existing translations
  • Fixing typos or improving wording
  • Adding new pages or features

src/i18n/locales-old/ - Legacy System ⚠️ DEPRECATED

Location: src/i18n/locales-old/

This directory contains deprecated TypeScript-based translations from a previous implementation.

Files:

  • de.ts - Old German translations
  • en.ts - Old English translations
  • sr.ts - Old Serbian translations

⚠️ IMPORTANT: Do NOT use these files!

Why it exists:

  • These files were part of an older TypeScript-based translation system
  • Kept temporarily during migration for reference
  • Not loaded by the application - they are completely inactive

Why the migration happened:

  • JSON is simpler and more maintainable than TypeScript objects
  • Better performance with dynamic imports
  • Easier for non-developers to edit translations
  • Better tooling support for JSON translation files

What to do:

  • Do NOT add new keys here
  • Do NOT reference these files in code
  • Use src/messages/*.json for all translations
  • These files can be safely deleted once migration is verified complete

File Relationships

graph TD
    A[config.ts] -->|Defines locales| B[request.ts]
    B -->|Loads messages| C[messages/*.json]
    B -->|Validates locale| A
    D[Components] -->|useTranslations| C
    E[locales-old/*] -.->|DEPRECATED| F[Not Used]

Summary:

Directory/File Status Purpose When to Edit
src/i18n/config.ts Active Locale definitions & metadata Adding locales, SEO keywords
src/i18n/request.ts Active next-intl integration Rarely (fallback logic)
src/messages/*.json Active Translation strings Frequently (new translations)
src/i18n/locales-old/ ⚠️ Deprecated Legacy TypeScript translations Never (can be deleted)

🔑 Adding Translation Keys

Step-by-step guide to adding and managing translations

Topics covered:

  • JSON structure and namespaces
  • Nested translation keys
  • Adding keys across all locales
  • Code examples using useTranslations()
  • Best practices for key naming

⚙️ Configuration & Setup

This section explains how the i18n system integrates with Next.js through next-intl, covering the configuration in request.ts and next.config.ts.

getRequestConfig() Function

Location: src/i18n/request.ts

The getRequestConfig() function is the core integration point between next-intl and the application. It handles locale resolution and message loading for every request.

Full Implementation:

import { getRequestConfig } from 'next-intl/server';
import { locales, type Locale } from './config';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  // Validate locale and fallback to default if invalid
  if (!locale || !locales.includes(locale as Locale)) {
    locale = 'de';
  }

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

How it works:

  1. Receives requestLocale - next-intl extracts the locale from the URL path (e.g., /de/, /en/, /sr/)
  2. Awaits the locale - The locale is a promise that resolves to the URL segment
  3. Validates the locale - Checks if it exists and is in the allowed locales array
  4. Falls back if invalid - Defaults to 'de' (German) for invalid/missing locales
  5. Returns configuration - Provides the validated locale and dynamically loaded messages

Key Features:

  • Type-safe validation - Uses the Locale type from config.ts
  • Automatic fallback - Never breaks, always provides a valid locale
  • Dynamic imports - Only loads the required translation file (better performance)
  • Server-side execution - Runs on the server for every request

Locale Validation Logic

Purpose: Ensure only valid locales are used, preventing errors and security issues.

Validation Flow:

if (!locale || !locales.includes(locale as Locale)) {
  locale = 'de';
}

What it checks:

  1. Existence check - !locale ensures a value was provided
  2. Whitelist check - !locales.includes(...) verifies it's an allowed locale
  3. Fallback - Defaults to 'de' if either check fails

Examples:

URL Path requestLocale Valid? Result Reason
/de/about 'de' Yes 'de' Valid locale
/en/contact 'en' Yes 'en' Valid locale
/sr/portfolio 'sr' Yes 'sr' Valid locale
/fr/home 'fr' No 'de' Not in locales array
/ null No 'de' No locale provided
/invalid123 'invalid123' No 'de' Not in whitelist

Why this matters:

  • 🔒 Security - Prevents path traversal attacks (e.g., ../../etc/passwd)
  • 🛡️ Reliability - Guarantees the application always has a valid locale
  • 📦 Bundle safety - Ensures only existing translation files are loaded
  • 🎯 User experience - Provides predictable behavior for invalid URLs

Alternative approaches (not used):

// ❌ DON'T: No validation (unsafe)
export default getRequestConfig(async ({ requestLocale }) => {
  const locale = await requestLocale;
  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default, // Could fail!
  };
});

// ❌ DON'T: Throwing errors (breaks user experience)
export default getRequestConfig(async ({ requestLocale }) => {
  const locale = await requestLocale;
  if (!locales.includes(locale as Locale)) {
    throw new Error('Invalid locale'); // User sees error page
  }
  return { locale, messages: ... };
});

// ✅ DO: Validate and fallback (current implementation)

Dynamic Message Loading

How translations are loaded:

messages: (await import(`../messages/${locale}.json`)).default

This line performs several critical functions:

  1. Dynamic import - Uses import() instead of static import statement
  2. Path interpolation - Constructs the file path using the validated locale
  3. Async loading - Waits for the file to be loaded
  4. Default export - Extracts the JSON content from the module

Benefits of dynamic imports:

Feature Static Import Dynamic Import (Current)
Bundle size All locales loaded upfront Only requested locale loaded
Initial load time Slower (3 files) Faster (1 file)
Memory usage Higher Lower
Server-side rendering Supported Supported
Code splitting No Yes

Performance comparison:

// ❌ Static imports (loads ALL locales on every request)
import deMessages from '../messages/de.json';
import enMessages from '../messages/en.json';
import srMessages from '../messages/sr.json';

const messageMap = { de: deMessages, en: enMessages, sr: srMessages };
return { locale, messages: messageMap[locale] };
// Bundle size: ~150KB (all locales)

// ✅ Dynamic import (loads ONLY requested locale)
messages: (await import(`../messages/${locale}.json`)).default
// Bundle size: ~50KB (single locale)

File path resolution:

// If locale = 'de'
await import(`../messages/de.json`)  // Resolves to: src/messages/de.json

// If locale = 'en'
await import(`../messages/en.json`)  // Resolves to: src/messages/en.json

// If locale = 'sr'
await import(`../messages/sr.json`)  // Resolves to: src/messages/sr.json

Error handling:

If a translation file is missing, Next.js will throw a build error:

Error: Cannot find module '../messages/fr.json'

Why this is safe:

  • Locale validation ensures only de, en, or sr are used
  • All three files are required to exist for the build to succeed
  • TypeScript catches typos in locale codes at compile time

createNextIntlPlugin() in next.config.ts

Location: next.config.ts

The createNextIntlPlugin() function wraps the Next.js configuration to integrate next-intl into the build process.

Implementation:

import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

// Create the next-intl plugin, pointing to request.ts
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');

const nextConfig: NextConfig = {
  // ... Next.js configuration options
};

// Wrap the config with the plugin
export default withNextIntl(nextConfig);

What createNextIntlPlugin() does:

  1. Registers request.ts - Tells next-intl where to find the getRequestConfig function
  2. Enables server-side translations - Sets up the infrastructure for SSR
  3. Configures webpack - Adds necessary loaders for JSON imports
  4. Optimizes builds - Ensures proper code splitting for translation files
  5. Sets up middleware hooks - Integrates with Next.js request lifecycle

Plugin composition:

This project uses multiple plugins together:

import createMDX from '@next/mdx';
import createNextIntlPlugin from 'next-intl/plugin';

// Create both plugins
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const withMDX = createMDX({ options: { ... } });

// Compose plugins: next-intl wraps MDX wraps config
export default withNextIntl(withMDX(nextConfig));

Plugin order matters:

nextConfig → withMDX → withNextIntl → Final Config

Why this pattern:

  • Modular configuration - Each plugin adds specific functionality
  • Composable - Plugins can be added/removed independently
  • Maintainable - Clear separation of concerns
  • Type-safe - TypeScript validates the entire config chain

Without the plugin:

If you remove withNextIntl, the application would:

  • Fail to resolve useTranslations() calls
  • Not load translation messages
  • Break server-side rendering of translated content
  • Lose locale detection capabilities

Plugin configuration path:

createNextIntlPlugin('./src/i18n/request.ts')
                      
                      Relative to next.config.ts
                      Points to getRequestConfig export

Troubleshooting:

Issue Cause Solution
"Cannot find module 'next-intl/server'" Plugin not installed Run npm install next-intl
"getRequestConfig is not defined" Wrong path in plugin Check path matches request.ts location
Translations not loading Plugin not applied Ensure withNextIntl(...) wraps config
Build errors with MDX Wrong plugin order Place withNextIntl outermost

Server-Side vs. Client-Side Translations

How translations work in different contexts:

Usage:

import { useTranslations } from 'next-intl';

export default function ServerComponent() {
  const t = useTranslations('common');

  return <h1>{t('nav.home')}</h1>;
}

How it works:

  1. Component renders on the server
  2. useTranslations() reads messages from the getRequestConfig result
  3. Translation is resolved on the server
  4. HTML with translated text is sent to the client
  5. No translation data sent to the browser

Benefits:

  • Faster initial load - No client-side JavaScript for translations
  • Better SEO - Translated content available immediately
  • Smaller bundle - Translations not included in client JS
  • Secure - Translation logic stays on the server
Client Components

Usage:

'use client';

import { useTranslations } from 'next-intl';

export default function ClientComponent() {
  const t = useTranslations('common');

  return <button>{t('actions.submit')}</button>;
}

How it works:

  1. Component marked with 'use client'
  2. Messages serialized and passed from server to client
  3. useTranslations() reads from client-side context
  4. Translations available for dynamic updates

When to use:

  • Interactive components (forms, modals)
  • Components with onClick handlers
  • Real-time updates based on user actions
  • Stateful UI elements

Performance consideration:

// ❌ DON'T: Use client components unnecessarily
'use client';

export default function StaticText() {
  const t = useTranslations('common');
  return <p>{t('siteDescription')}</p>; // This could be a server component
}

// ✅ DO: Use server components for static content
export default function StaticText() {
  const t = useTranslations('common');
  return <p>{t('siteDescription')}</p>; // Server component (default)
}
Hybrid Pattern (Best Practice)

Split components by interactivity:

// app/[locale]/page.tsx (Server Component)
import { useTranslations } from 'next-intl';
import ContactForm from '@/components/ContactForm'; // Client component

export default function ContactPage() {
  const t = useTranslations('pages.contact');

  return (
    <div>
      <h1>{t('hero.title')}</h1>          {/* Server-rendered */}
      <p>{t('hero.subtitle')}</p>         {/* Server-rendered */}
      <ContactForm />                     {/* Client interactive */}
    </div>
  );
}

// components/ContactForm.tsx (Client Component)
'use client';

import { useTranslations } from 'next-intl';

export default function ContactForm() {
  const t = useTranslations('pages.contact.contactForm');

  return (
    <form>
      <label>{t('name.label')}</label>
      <input placeholder={t('name.placeholder')} />
      <button type="submit">{t('submit')}</button>
    </form>
  );
}

Translation data flow:

Request → getRequestConfig() → Messages loaded
                ↓
Server Components → useTranslations() → Direct access
                ↓
Client Components → Serialized via props → useTranslations() → Client access

Bundle size impact:

Scenario Server Component Client Component
JavaScript sent 0 KB (translations) ~50 KB (translations)
HTML size Larger (translated text) Smaller (placeholders)
Time to Interactive Faster Slower
SEO crawlability Perfect ⚠️ May need hydration

Best practices:

  1. Default to server components - Use client components only when necessary
  2. Split by interactivity - Keep static content in server components
  3. Pass translations as props - Avoid duplicating translation calls
  4. Minimize client boundaries - Fewer 'use client' directives = better performance

Example optimization:

// ❌ BEFORE: Everything is a client component
'use client';

export default function Dashboard() {
  const t = useTranslations('dashboard');
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>{t('title')}</h1>              {/* Static - could be server */}
      <p>{t('description')}</p>           {/* Static - could be server */}
      <button onClick={() => setCount(count + 1)}>
        {t('increment')}                  {/* Interactive - needs client */}
      </button>
      <p>{count}</p>
    </div>
  );
}

// ✅ AFTER: Split into server + client components
// app/[locale]/dashboard/page.tsx (Server)
import { useTranslations } from 'next-intl';
import Counter from '@/components/Counter';

export default function Dashboard() {
  const t = useTranslations('dashboard');

  return (
    <div>
      <h1>{t('title')}</h1>              {/* Server-rendered */}
      <p>{t('description')}</p>           {/* Server-rendered */}
      <Counter />                         {/* Client component */}
    </div>
  );
}

// components/Counter.tsx (Client)
'use client';

import { useTranslations } from 'next-intl';
import { useState } from 'react';

export default function Counter() {
  const t = useTranslations('dashboard');
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        {t('increment')}
      </button>
      <p>{count}</p>
    </>
  );
}

Result:

  • 📦 Bundle size reduced by ~40 KB (translations moved to server)
  • Faster initial render (server-rendered HTML)
  • 🎯 Better SEO (title and description immediately available)

Summary: Configuration & Setup

Key files:

File Purpose When to modify
src/i18n/request.ts Locale resolution & message loading Rarely (fallback logic, custom validation)
next.config.ts next-intl plugin integration When adding/removing plugins
src/i18n/config.ts Locale definitions When adding new languages

Core concepts:

  • getRequestConfig() - Validates locale and loads messages
  • Locale validation - Whitelist check with fallback to 'de'
  • Dynamic imports - Only load required translation file
  • createNextIntlPlugin() - Integrates next-intl with Next.js build
  • Server components - Preferred for better performance
  • Client components - Use only for interactive elements

Common patterns:

// Pattern 1: Server component (default)
export default function Page() {
  const t = useTranslations('namespace');
  return <h1>{t('key')}</h1>;
}

// Pattern 2: Client component (interactive)
'use client';
export default function Interactive() {
  const t = useTranslations('namespace');
  return <button>{t('action')}</button>;
}

// Pattern 3: Multiple namespaces
export default function Complex() {
  const tCommon = useTranslations('common');
  const tPage = useTranslations('pages.home');
  return <div>...</div>;
}

🌐 Locale Configuration & Metadata

Detailed breakdown of locale settings and SEO data

Topics covered:

  • Locales array and default locale
  • Locale names and flags
  • Metadata for hreflang and Open Graph
  • SEO keywords per locale
  • Currency and territory settings

🔍 SEO & Hreflang Implementation

How multilingual SEO is implemented

Topics covered:

  • Hreflang tags in layout.tsx
  • alternates.languages configuration
  • Content-Language HTTP headers
  • Localized meta tags and Open Graph
  • Sitemap generation for multiple locales

📦 Legacy Migration (locales-old)

Understanding the deprecated TypeScript-based translation system

Topics covered:

  • What locales-old contains
  • Why the migration happened
  • Do NOT use these files
  • Migration from TypeScript to JSON
  • Safe removal considerations

🛤️ Routing & URL Structure

How locale-based routing works with Next.js App Router

Topics covered:

  • [locale] dynamic route segment
  • URL patterns: /de/, /en/, /sr/
  • Locale detection in middleware
  • generateStaticParams() for static generation
  • Locale switching and redirects

💡 Usage Examples & Best Practices

Practical examples and common patterns

Topics covered:

  • useTranslations() hook patterns
  • Accessing nested keys
  • Formatting dates and numbers
  • Pluralization (if implemented)
  • Common pitfalls and solutions
  • Performance considerations

Architecture Overview

Portfolio i18n System
│
├── Configuration Layer
│   ├── src/i18n/config.ts          # Locale definitions & metadata
│   └── src/i18n/request.ts         # next-intl integration
│
├── Translation Files (Active)
│   ├── src/messages/de.json        # German translations
│   ├── src/messages/en.json        # English translations
│   └── src/messages/sr.json        # Serbian translations
│
├── Legacy System (Deprecated)
│   └── src/i18n/locales-old/       # Old TypeScript translations
│
├── Routing Layer
│   ├── src/app/[locale]/layout.tsx # Locale-based layout
│   ├── src/middleware.ts           # Locale detection
│   └── next.config.ts              # createNextIntlPlugin
│
└── Application Layer
    └── Components using useTranslations()

Translation File Structure

Current structure of active translation files:

{
  "meta": {
    "site": { ... },
    "author": { ... },
    "social": { ... }
  },
  "common": {
    "siteTitle": "...",
    "nav": { ... },
    "actions": { ... },
    "errors": { ... },
    "cookies": { ... }
  },
  "home": { ... },
  "about": { ... },
  "portfolio": { ... },
  "blog": { ... },
  "contact": { ... }
}

Supported Locales

Locale Language hreflang OG Locale Currency Region
de Deutsch de-DE de_DE EUR Germany
en English en-US en_US USD United States
sr Srpski sr-RS sr_RS RSD Serbia

Important Notes

⚠️ Legacy System: The src/i18n/locales-old/ directory contains deprecated TypeScript-based translations from a previous implementation. Do NOT use these files. All new translations should go in src/messages/*.json.

Active System: Use only the JSON files in src/messages/ for all translation work.

🔒 Type Safety: The locale type is derived from the locales array in config.ts, ensuring compile-time validation of locale codes.

🌍 Default Locale: German (de) is the default locale. Invalid or missing locale parameters will fallback to German.

Getting Help

  • Check specific sections above for detailed topics
  • Review code examples in the pattern files
  • Examine existing translation keys in src/messages/de.json
  • Look at component usage in src/app/[locale]/page.tsx

Framework: next-intl + Next.js 15 App Router Default Locale: German (de) Supported Locales: de, en, sr Translation Format: JSON