auto-claude: subtask-1-4 - Document request.ts configuration and next-intl setup

- Added comprehensive Configuration & Setup section
- Documented getRequestConfig() function with full implementation
- Explained locale validation logic with examples and security benefits
- Detailed dynamic message loading mechanism and performance comparison
- Documented createNextIntlPlugin() integration in next.config.ts
- Explained plugin composition pattern with MDX
- Added server-side vs client-side translation comparison
- Included best practices for component splitting and performance optimization
- Provided code examples for all patterns and troubleshooting guides

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 06:39:53 +01:00
co-authored by Claude Sonnet 4.5
parent 2abfcbe217
commit c170f75219
+525 -8
View File
@@ -797,14 +797,531 @@ graph TD
> - Best practices for key naming > - Best practices for key naming
### ⚙️ Configuration & Setup ### ⚙️ Configuration & Setup
> Understanding request.ts and next-intl integration
> This section explains how the i18n system integrates with Next.js through next-intl, covering the configuration in `request.ts` and `next.config.ts`.
> **Topics covered:**
> - `getRequestConfig()` function #### `getRequestConfig()` Function
> - Locale validation and fallback logic
> - Dynamic message loading **Location:** `src/i18n/request.ts`
> - `createNextIntlPlugin()` in next.config.ts
> - Server-side vs. client-side translations 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 <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:**
```tsx
'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:**
```tsx
// ❌ 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:**
```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 (
<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:**
```tsx
// ❌ 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:**
```typescript
// 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 ### 🌐 Locale Configuration & Metadata
> Detailed breakdown of locale settings and SEO data > Detailed breakdown of locale settings and SEO data