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>
1460 lines
39 KiB
Markdown
1460 lines
39 KiB
Markdown
# 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 (
|
|
<div>
|
|
<h1>{t('nav.home')}</h1>
|
|
<p>{t('siteDescription')}</p>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Adding a New Translation Key
|
|
|
|
#### Step-by-Step Guide
|
|
|
|
Follow this process to add new translation keys to the application:
|
|
|
|
##### 1. Identify the Appropriate Namespace
|
|
|
|
Before adding a new translation key, determine where it should live based on its purpose:
|
|
|
|
- **`meta`** - Site metadata, author information, SEO keywords
|
|
- **`common`** - Shared UI elements (buttons, navigation, errors, cookies)
|
|
- **`navigation`** - All navigation-related text (main menu, footer links, mobile menu)
|
|
- **`footer`** - Footer content (sections, legal links, copyright)
|
|
- **`pages`** - Page-specific content organized by route
|
|
- `pages.home` - Homepage content
|
|
- `pages.about` - About page content
|
|
- `pages.contact` - Contact page content
|
|
- `pages.privacy` - Privacy policy content
|
|
- `pages.terms` - Terms and conditions content
|
|
- `pages.notfound` - 404 page content
|
|
- **`portfolio`** - Portfolio page content and filters
|
|
- **`blog`** - Blog page content and UI elements
|
|
- **`login`** - Login page content
|
|
- **`dashboard`** - Dashboard content and metrics
|
|
|
|
**Best Practice:** Use `common` for elements that appear across multiple pages. Use page-specific namespaces for content unique to a single page.
|
|
|
|
##### 2. Open All Three Translation Files
|
|
|
|
You must update all three locale files to maintain consistency:
|
|
|
|
```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 (
|
|
<button className="btn-primary">
|
|
{t('download')}
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
**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 (
|
|
<div>
|
|
<h1>{tHero('title')}</h1>
|
|
<p>{tHero('subtitle')}</p>
|
|
<button>{tCommon('readMore')}</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
##### 5. Verify Your Changes
|
|
|
|
1. **Build the application** to ensure no TypeScript errors:
|
|
```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) => (
|
|
<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:
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```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 (
|
|
<p>
|
|
{t('showing', { start, end, total })}
|
|
</p>
|
|
);
|
|
}
|
|
```
|
|
|
|
**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 (
|
|
<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
|
|
|
|
```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 (
|
|
<form>
|
|
<button type="submit">{tCommon('submit')}</button>
|
|
<button type="button">{tCommon('cancel')}</button>
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Documentation Structure
|
|
|
|
This documentation is organized into the following sections:
|
|
|
|
### 📁 Directory Structure & Organization
|
|
|
|
The i18n system is organized into three main directories, each serving a specific purpose in the translation infrastructure.
|
|
|
|
#### Overview of Directory Structure
|
|
|
|
```
|
|
src/
|
|
├── i18n/ # Configuration Layer
|
|
│ ├── config.ts # ✅ Locale definitions & metadata
|
|
│ ├── request.ts # ✅ next-intl integration
|
|
│ └── locales-old/ # ⚠️ DEPRECATED - Do NOT use
|
|
│ ├── de.ts
|
|
│ ├── en.ts
|
|
│ └── sr.ts
|
|
│
|
|
└── messages/ # ✅ Active Translation Files
|
|
├── de.json # German translations
|
|
├── en.json # English translations
|
|
└── sr.json # Serbian translations
|
|
```
|
|
|
|
---
|
|
|
|
#### `src/i18n/` - Configuration Directory
|
|
|
|
This directory contains the core configuration files that define how the i18n system operates.
|
|
|
|
**Purpose:** Centralized configuration for locale definitions, metadata, and next-intl integration.
|
|
|
|
##### `config.ts` - Locale Metadata & Settings
|
|
|
|
**Location:** `src/i18n/config.ts`
|
|
|
|
This is the **single source of truth** for all locale-related configuration.
|
|
|
|
**What it contains:**
|
|
|
|
1. **Locales Array** - Defines all supported languages
|
|
```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<Locale, string> = {
|
|
de: 'Deutsch',
|
|
en: 'English',
|
|
sr: 'Srpski',
|
|
};
|
|
```
|
|
|
|
5. **Locale Flags** - Emoji flags for UI
|
|
```typescript
|
|
export const localeFlags: Record<Locale, string> = {
|
|
de: '🇩🇪',
|
|
en: '🇬🇧',
|
|
sr: '🇷🇸',
|
|
};
|
|
```
|
|
|
|
6. **Extended Metadata** - SEO and regional data
|
|
```typescript
|
|
export const localeMetadata: Record<Locale, {
|
|
language: string; // ISO 639-1 code (e.g., 'de')
|
|
region: string; // ISO 3166-1 code (e.g., 'DE')
|
|
hreflang: string; // For <link rel="alternate"> tags
|
|
ogLocale: string; // For Open Graph meta tags
|
|
script: string; // Writing system (e.g., 'Latn')
|
|
territory: string; // Full country name
|
|
currency: string; // ISO 4217 code (e.g., 'EUR')
|
|
}> = {
|
|
de: {
|
|
language: 'de',
|
|
region: 'DE',
|
|
hreflang: 'de-DE',
|
|
ogLocale: 'de_DE',
|
|
script: 'Latn',
|
|
territory: 'Germany',
|
|
currency: 'EUR',
|
|
},
|
|
// ... en, sr
|
|
};
|
|
```
|
|
|
|
7. **SEO Keywords** - Localized keywords for each language
|
|
```typescript
|
|
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:**
|
|
|
|
```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 <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
|
|
> 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
|