diff --git a/docs/i18n/README.md b/docs/i18n/README.md new file mode 100644 index 0000000..b248bf7 --- /dev/null +++ b/docs/i18n/README.md @@ -0,0 +1,1459 @@ +# 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 + +```tsx +import { useTranslations } from 'next-intl'; + +export default function MyComponent() { + const t = useTranslations('common'); + + return ( +
+

{t('nav.home')}

+

{t('siteDescription')}

+
+ ); +} +``` + +### 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: + +```bash +# 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** + +```json +// 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** + +```json +// 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: + +```tsx +import { useTranslations } from 'next-intl'; + +export default function MyComponent() { + // Option 1: Access top-level namespace + const t = useTranslations('common.actions'); + + return ( + + ); +} +``` + +**Multiple namespaces in one component:** + +```tsx +import { useTranslations } from 'next-intl'; + +export default function ServicesPage() { + const tHero = useTranslations('pages.services.hero'); + const tCommon = useTranslations('common.actions'); + + return ( +
+

{tHero('title')}

+

{tHero('subtitle')}

+ +
+ ); +} +``` + +##### 5. Verify Your Changes + +1. **Build the application** to ensure no TypeScript errors: + ```bash + 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: + +```json +{ + "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:** + +```json +{ + "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:** + +```tsx +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: + +```json +{ + "meta": { + "site": { + "keywords": [ + "Fullstack Developer", + "AI Developer Germany", + "Process Automation" + ] + } + } +} +``` + +**Usage:** + +```tsx +const t = useTranslations('meta.site'); +const keywords = t.raw('keywords'); // Returns array +``` + +**Objects** - For structured data: + +```json +{ + "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:** + +```tsx +const t = useTranslations('pages.home.experience'); +const positions = t.raw('positions'); // Returns array of objects + +positions.map((position) => ( +
+

{position.role}

+

{position.company}

+ {position.period} + +
+)); +``` + +#### Advanced Usage Examples + +##### Dynamic Translation Keys + +Access translations dynamically based on variables: + +```tsx +import { useTranslations } from 'next-intl'; + +export default function CategoryFilter({ categories }: { categories: string[] }) { + const t = useTranslations('portfolio.filters.categories'); + + return ( +
+ {categories.map((category) => ( + + ))} +
+ ); +} + +// 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: + +```tsx +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 ( +

+ {t('showing', { start, end, total })} +

+ ); +} +``` + +**In JSON:** + +```json +{ + "blog": { + "ui": { + "pagination": { + "showing": "Showing {start}-{end} of {total} articles" + } + } + } +} +``` + +##### Accessing Rich Content + +For translations containing HTML or rich content: + +```tsx +import { useTranslations } from 'next-intl'; + +export default function RichText() { + const t = useTranslations('pages.about'); + + return ( +
+ {/* Simple text */} +

{t('hero.title')}

+ + {/* Raw access for objects/arrays */} +
+ {t.raw('hero.expertise.processAutomation')} +
+
+ ); +} +``` + +#### Common Patterns & Best Practices + +##### βœ… DO: Keep Structure Consistent + +```json +// 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 + +```json +// Bad: Different structures +// de.json +{ "common": { "actions": { "save": "Speichern" } } } + +// en.json +{ "common": { "save": "Save" } } // ❌ Missing "actions" level +``` + +##### βœ… DO: Use Semantic Naming + +```json +{ + "common": { + "actions": { + "submit": "Submit", // βœ… Clear, semantic + "cancel": "Cancel", + "delete": "Delete" + } + } +} +``` + +##### ❌ DON'T: Use Generic Names + +```json +{ + "common": { + "button1": "Submit", // ❌ Non-descriptive + "btn": "Cancel", // ❌ Abbreviated + "text1": "Delete" // ❌ Generic + } +} +``` + +##### βœ… DO: Group Related Translations + +```json +{ + "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 + +```tsx +// Good: Reuse common translations +import { useTranslations } from 'next-intl'; + +export default function MyForm() { + const tCommon = useTranslations('common.actions'); + + return ( +
+ + +
+ ); +} +``` + +## 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 + ```typescript + export const locales = ['de', 'en', 'sr'] as const; + ``` + +2. **Default Locale** - Fallback language + ```typescript + export const defaultLocale = 'de' as const; + ``` + +3. **Locale Type** - TypeScript type for type-safe locale handling + ```typescript + export type Locale = (typeof locales)[number]; + ``` + +4. **Display Names** - Human-readable language names + ```typescript + export const localeNames: Record = { + de: 'Deutsch', + en: 'English', + sr: 'Srpski', + }; + ``` + +5. **Locale Flags** - Emoji flags for UI + ```typescript + export const localeFlags: Record = { + de: 'πŸ‡©πŸ‡ͺ', + en: 'πŸ‡¬πŸ‡§', + sr: 'πŸ‡·πŸ‡Έ', + }; + ``` + +6. **Extended Metadata** - SEO and regional data + ```typescript + export const localeMetadata: Record 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 + ```typescript + export const seoKeywords: Record = { + 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:** + +```typescript +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: + +```json +{ + "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 + +```mermaid +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:** + +```typescript +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:** + +```typescript +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):** + +```typescript +// ❌ 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:** + +```typescript +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:** + +```typescript +// ❌ 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:** + +```typescript +// 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: + +```bash +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:** + +```typescript +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: + +```typescript +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:** + +```typescript +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:** + +```tsx +import { useTranslations } from 'next-intl'; + +export default function ServerComponent() { + const t = useTranslations('common'); + + return

{t('nav.home')}

; +} +``` + +**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:** + +```tsx +'use client'; + +import { useTranslations } from 'next-intl'; + +export default function ClientComponent() { + const t = useTranslations('common'); + + return ; +} +``` + +**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:** + +```tsx +// ❌ DON'T: Use client components unnecessarily +'use client'; + +export default function StaticText() { + const t = useTranslations('common'); + return

{t('siteDescription')}

; // This could be a server component +} + +// βœ… DO: Use server components for static content +export default function StaticText() { + const t = useTranslations('common'); + return

{t('siteDescription')}

; // Server component (default) +} +``` + +##### Hybrid Pattern (Best Practice) + +**Split components by interactivity:** + +```tsx +// 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 ( +
+

{t('hero.title')}

{/* Server-rendered */} +

{t('hero.subtitle')}

{/* Server-rendered */} + {/* Client interactive */} +
+ ); +} + +// components/ContactForm.tsx (Client Component) +'use client'; + +import { useTranslations } from 'next-intl'; + +export default function ContactForm() { + const t = useTranslations('pages.contact.contactForm'); + + return ( +
+ + + +
+ ); +} +``` + +**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:** + +```tsx +// ❌ BEFORE: Everything is a client component +'use client'; + +export default function Dashboard() { + const t = useTranslations('dashboard'); + const [count, setCount] = useState(0); + + return ( +
+

{t('title')}

{/* Static - could be server */} +

{t('description')}

{/* Static - could be server */} + +

{count}

+
+ ); +} + +// βœ… 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 ( +
+

{t('title')}

{/* Server-rendered */} +

{t('description')}

{/* Server-rendered */} + {/* Client component */} +
+ ); +} + +// 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 ( + <> + +

{count}

+ + ); +} +``` + +**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:** + +```typescript +// Pattern 1: Server component (default) +export default function Page() { + const t = useTranslations('namespace'); + return

{t('key')}

; +} + +// Pattern 2: Client component (interactive) +'use client'; +export default function Interactive() { + const t = useTranslations('namespace'); + return ; +} + +// Pattern 3: Multiple namespaces +export default function Complex() { + const tCommon = useTranslations('common'); + const tPage = useTranslations('pages.home'); + return
...
; +} +``` + +### 🌐 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: + +```json +{ + "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