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>
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 keywordscommon- 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 routepages.home- Homepage contentpages.about- About page contentpages.contact- Contact page contentpages.privacy- Privacy policy contentpages.terms- Terms and conditions contentpages.notfound- 404 page content
portfolio- Portfolio page content and filtersblog- Blog page content and UI elementslogin- Login page contentdashboard- 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
-
Build the application to ensure no TypeScript errors:
npm run build -
Test in all three languages:
- German:
http://localhost:3000/de - English:
http://localhost:3000/en - Serbian:
http://localhost:3000/sr
- German:
-
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
}
}
✅ DO: Group Related Translations
{
"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:
-
Locales Array - Defines all supported languages
export const locales = ['de', 'en', 'sr'] as const; -
Default Locale - Fallback language
export const defaultLocale = 'de' as const; -
Locale Type - TypeScript type for type-safe locale handling
export type Locale = (typeof locales)[number]; -
Display Names - Human-readable language names
export const localeNames: Record<Locale, string> = { de: 'Deutsch', en: 'English', sr: 'Srpski', }; -
Locale Flags - Emoji flags for UI
export const localeFlags: Record<Locale, string> = { de: '🇩🇪', en: '🇬🇧', sr: '🇷🇸', }; -
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 }; -
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:
- Locale Validation - Ensures only valid locales are used
- Fallback Logic - Defaults to German (
de) if locale is invalid or missing - Dynamic Import - Loads the appropriate translation JSON file at runtime
- Type Safety - Uses the
Localetype fromconfig.tsfor validation
Flow:
- Receives
requestLocalefrom next-intl (extracted from URL) - Validates against the
localesarray - Falls back to
deif validation fails - Dynamically imports the corresponding JSON file from
src/messages/ - 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 translationssr.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:
- Keep keys synchronized - All three files should have identical structure
- Use nested namespaces - Organize by feature or page (e.g.,
common,pages.home) - Descriptive keys - Use semantic names like
hero.titleinstead oftext1 - 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 translationsen.ts- Old English translationssr.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/*.jsonfor 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:
- Receives
requestLocale- next-intl extracts the locale from the URL path (e.g.,/de/,/en/,/sr/) - Awaits the locale - The locale is a promise that resolves to the URL segment
- Validates the locale - Checks if it exists and is in the allowed
localesarray - Falls back if invalid - Defaults to
'de'(German) for invalid/missing locales - Returns configuration - Provides the validated locale and dynamically loaded messages
Key Features:
- ✅ Type-safe validation - Uses the
Localetype fromconfig.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:
- Existence check -
!localeensures a value was provided - Whitelist check -
!locales.includes(...)verifies it's an allowed locale - 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:
- Dynamic import - Uses
import()instead of staticimportstatement - Path interpolation - Constructs the file path using the validated locale
- Async loading - Waits for the file to be loaded
- 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, orsrare 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:
- Registers request.ts - Tells next-intl where to find the
getRequestConfigfunction - Enables server-side translations - Sets up the infrastructure for SSR
- Configures webpack - Adds necessary loaders for JSON imports
- Optimizes builds - Ensures proper code splitting for translation files
- 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:
Server Components (Recommended)
Usage:
import { useTranslations } from 'next-intl';
export default function ServerComponent() {
const t = useTranslations('common');
return <h1>{t('nav.home')}</h1>;
}
How it works:
- Component renders on the server
useTranslations()reads messages from thegetRequestConfigresult- Translation is resolved on the server
- HTML with translated text is sent to the client
- 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:
- Component marked with
'use client' - Messages serialized and passed from server to client
useTranslations()reads from client-side context- 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:
- ✅ Default to server components - Use client components only when necessary
- ✅ Split by interactivity - Keep static content in server components
- ✅ Pass translations as props - Avoid duplicating translation calls
- ✅ 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