diff --git a/docs/i18n/README.md b/docs/i18n/README.md index ffcd631..b248bf7 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -797,14 +797,531 @@ graph TD > - Best practices for key naming ### ⚙️ Configuration & Setup -> Understanding request.ts and next-intl integration -> -> **Topics covered:** -> - `getRequestConfig()` function -> - Locale validation and fallback logic -> - Dynamic message loading -> - `createNextIntlPlugin()` in next.config.ts -> - Server-side vs. client-side translations + +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('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.subtitle')}
{/* Server-rendered */} +{t('description')}
{/* Static - could be server */} + +{count}
+{t('description')}
{/* Server-rendered */} +{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