Added comprehensive documentation for adding translation keys: - Step-by-step guide with namespace identification - Detailed JSON structure explanation with real examples - Namespace organization patterns (meta, common, navigation, pages, etc.) - Code examples using useTranslations() hook - Advanced usage: dynamic keys, string interpolation, rich content - Best practices for key naming and structure consistency - Examples for arrays, objects, and nested translations Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
24 KiB
Internationalization (i18n) Documentation
Complete guide to the multi-language support system in the portfolio application.
Overview
This portfolio supports 3 languages with full internationalization:
| Language | Code | Flag | Region |
|---|---|---|---|
| German (default) | de |
🇩🇪 | Germany |
| English | en |
🇬🇧 | United States |
| Serbian | sr |
🇷🇸 | Serbia |
Tech Stack
- next-intl - Next.js internationalization library
- JSON-based translations - Simple, maintainable translation files
- Type-safe locale handling - TypeScript ensures locale consistency
- SEO optimized - Hreflang tags, Content-Language headers, localized metadata
Key Features
- ✅ Automatic locale detection from URL path
- ✅ Fallback to default locale (German)
- ✅ Server-side translation loading
- ✅ Type-safe locale validation
- ✅ SEO-friendly URL structure (
/de/,/en/,/sr/) - ✅ Localized metadata for social sharing
- ✅ Support for nested translation keys
- ✅ Legacy migration path (locales-old)
Quick Start
Using Translations in Components
import { useTranslations } from 'next-intl';
export default function MyComponent() {
const t = useTranslations('common');
return (
<div>
<h1>{t('nav.home')}</h1>
<p>{t('siteDescription')}</p>
</div>
);
}
Adding a New Translation Key
Step-by-Step Guide
Follow this process to add new translation keys to the application:
1. Identify the Appropriate Namespace
Before adding a new translation key, determine where it should live based on its purpose:
meta- Site metadata, author information, SEO keywordscommon- Shared UI elements (buttons, navigation, errors, cookies)navigation- All navigation-related text (main menu, footer links, mobile menu)footer- Footer content (sections, legal links, copyright)pages- Page-specific content organized by routepages.home- Homepage contentpages.about- About page contentpages.contact- Contact page contentpages.privacy- Privacy policy contentpages.terms- Terms and conditions contentpages.notfound- 404 page content
portfolio- Portfolio page content and filtersblog- Blog page content and UI elementslogin- Login page contentdashboard- Dashboard content and metrics
Best Practice: Use common for elements that appear across multiple pages. Use page-specific namespaces for content unique to a single page.
2. Open All Three Translation Files
You must update all three locale files to maintain consistency:
# Open in your editor:
src/messages/de.json # German (default)
src/messages/en.json # English
src/messages/sr.json # Serbian
3. Add the Translation Key
Add your key to the exact same location in each file. The JSON structure must be identical across all locales.
Example: Adding a new action button
// de.json
{
"common": {
"actions": {
"readMore": "Weiterlesen",
"viewMore": "Mehr anzeigen",
"viewAll": "Alle anzeigen",
"download": "Herunterladen" // ← New key
}
}
}
// en.json
{
"common": {
"actions": {
"readMore": "Read More",
"viewMore": "View More",
"viewAll": "View All",
"download": "Download" // ← New key
}
}
}
// sr.json
{
"common": {
"actions": {
"readMore": "Procitaj vise",
"viewMore": "Prikazi vise",
"viewAll": "Prikazi sve",
"download": "Preuzmi" // ← New key
}
}
}
Example: Adding a new page section
// de.json
{
"pages": {
"services": { // ← New namespace
"hero": {
"title": "Unsere Dienstleistungen",
"subtitle": "Maßgeschneiderte Lösungen für Ihr Unternehmen"
}
}
}
}
// en.json
{
"pages": {
"services": { // ← New namespace
"hero": {
"title": "Our Services",
"subtitle": "Tailored solutions for your business"
}
}
}
}
// sr.json
{
"pages": {
"services": { // ← New namespace
"hero": {
"title": "Naše usluge",
"subtitle": "Prilagođena rešenja za vaše poslovanje"
}
}
}
}
4. Use the Translation in Your Component
Import the useTranslations hook and specify the namespace:
import { useTranslations } from 'next-intl';
export default function MyComponent() {
// Option 1: Access top-level namespace
const t = useTranslations('common.actions');
return (
<button className="btn-primary">
{t('download')}
</button>
);
}
Multiple namespaces in one component:
import { useTranslations } from 'next-intl';
export default function ServicesPage() {
const tHero = useTranslations('pages.services.hero');
const tCommon = useTranslations('common.actions');
return (
<div>
<h1>{tHero('title')}</h1>
<p>{tHero('subtitle')}</p>
<button>{tCommon('readMore')}</button>
</div>
);
}
5. Verify Your Changes
-
Build the application to ensure no TypeScript errors:
npm run build -
Test in all three languages:
- German:
http://localhost:3000/de - English:
http://localhost:3000/en - Serbian:
http://localhost:3000/sr
- German:
-
Check for missing translations - The browser console will warn if a key is missing
JSON Structure & Organization
Hierarchical Namespace Pattern
Translation files use a deeply nested JSON structure to organize translations logically:
{
"namespace": { // Top-level category
"subnamespace": { // Feature or section
"key": "value", // Leaf node (actual translation)
"nested": {
"key": "value" // Deeper nesting allowed
}
}
}
}
Real example from the codebase:
{
"pages": { // Namespace: Page content
"home": { // Subnamespace: Homepage
"hero": { // Section: Hero section
"title": "FULLSTACK DEVELOPER",
"subtitle": "AI AGENTS | VOICE AI",
"navigation": { // Nested object
"experience": "EXPERIENCE",
"skills": "SKILLS"
}
}
}
}
}
Access in code:
const t = useTranslations('pages.home.hero');
console.log(t('title')); // "FULLSTACK DEVELOPER"
console.log(t('navigation.experience')); // "EXPERIENCE"
Special Data Types
Arrays - For lists of items:
{
"meta": {
"site": {
"keywords": [
"Fullstack Developer",
"AI Developer Germany",
"Process Automation"
]
}
}
}
Usage:
const t = useTranslations('meta.site');
const keywords = t.raw('keywords'); // Returns array
Objects - For structured data:
{
"pages": {
"home": {
"experience": {
"positions": [
{
"role": "Process Automation Specialist",
"company": "Everlast Consulting GmbH",
"period": "12/2024 - PRESENT",
"highlights": [
"Development of AI agents",
"Web scraping solutions"
]
}
]
}
}
}
}
Usage:
const t = useTranslations('pages.home.experience');
const positions = t.raw('positions'); // Returns array of objects
positions.map((position) => (
<div key={position.role}>
<h3>{position.role}</h3>
<p>{position.company}</p>
<span>{position.period}</span>
<ul>
{position.highlights.map((highlight) => (
<li key={highlight}>{highlight}</li>
))}
</ul>
</div>
));
Advanced Usage Examples
Dynamic Translation Keys
Access translations dynamically based on variables:
import { useTranslations } from 'next-intl';
export default function CategoryFilter({ categories }: { categories: string[] }) {
const t = useTranslations('portfolio.filters.categories');
return (
<div>
{categories.map((category) => (
<button key={category}>
{t(category as any)} // Dynamic key lookup
</button>
))}
</div>
);
}
// If categories = ["Data Science", "AI Development"]
// Output: "Data Science" → "Data Science" (en) or "Data Science" (de)
// "AI Development" → "AI Development" (en) or "KI-Entwicklung" (de)
String Interpolation
Use variables in translations:
import { useTranslations } from 'next-intl';
export default function Pagination({ start, end, total }: PaginationProps) {
const t = useTranslations('blog.ui.pagination');
// Translation: "Showing {start}-{end} of {total} articles"
return (
<p>
{t('showing', { start, end, total })}
</p>
);
}
In JSON:
{
"blog": {
"ui": {
"pagination": {
"showing": "Showing {start}-{end} of {total} articles"
}
}
}
}
Accessing Rich Content
For translations containing HTML or rich content:
import { useTranslations } from 'next-intl';
export default function RichText() {
const t = useTranslations('pages.about');
return (
<div>
{/* Simple text */}
<h1>{t('hero.title')}</h1>
{/* Raw access for objects/arrays */}
<div>
{t.raw('hero.expertise.processAutomation')}
</div>
</div>
);
}
Common Patterns & Best Practices
✅ DO: Keep Structure Consistent
// Good: Same structure across all locales
// de.json
{ "common": { "actions": { "save": "Speichern" } } }
// en.json
{ "common": { "actions": { "save": "Save" } } }
// sr.json
{ "common": { "actions": { "save": "Sačuvaj" } } }
❌ DON'T: Mix Structures
// Bad: Different structures
// de.json
{ "common": { "actions": { "save": "Speichern" } } }
// en.json
{ "common": { "save": "Save" } } // ❌ Missing "actions" level
✅ DO: Use Semantic Naming
{
"common": {
"actions": {
"submit": "Submit", // ✅ Clear, semantic
"cancel": "Cancel",
"delete": "Delete"
}
}
}
❌ DON'T: Use Generic Names
{
"common": {
"button1": "Submit", // ❌ Non-descriptive
"btn": "Cancel", // ❌ Abbreviated
"text1": "Delete" // ❌ Generic
}
}
✅ DO: Group Related Translations
{
"pages": {
"contact": {
"contactForm": { // ✅ Grouped by feature
"title": "Contact Me",
"name": { "label": "Name", "placeholder": "Your name" },
"email": { "label": "Email", "placeholder": "your@email.com" },
"submit": "Send Message"
}
}
}
}
✅ DO: Reuse Common Translations
// Good: Reuse common translations
import { useTranslations } from 'next-intl';
export default function MyForm() {
const tCommon = useTranslations('common.actions');
return (
<form>
<button type="submit">{tCommon('submit')}</button>
<button type="button">{tCommon('cancel')}</button>
</form>
);
}
Documentation Structure
This documentation is organized into the following sections:
📁 Directory Structure & Organization
The i18n system is organized into three main directories, each serving a specific purpose in the translation infrastructure.
Overview of Directory Structure
src/
├── i18n/ # Configuration Layer
│ ├── config.ts # ✅ Locale definitions & metadata
│ ├── request.ts # ✅ next-intl integration
│ └── locales-old/ # ⚠️ DEPRECATED - Do NOT use
│ ├── de.ts
│ ├── en.ts
│ └── sr.ts
│
└── messages/ # ✅ Active Translation Files
├── de.json # German translations
├── en.json # English translations
└── sr.json # Serbian translations
src/i18n/ - Configuration Directory
This directory contains the core configuration files that define how the i18n system operates.
Purpose: Centralized configuration for locale definitions, metadata, and next-intl integration.
config.ts - Locale Metadata & Settings
Location: src/i18n/config.ts
This is the single source of truth for all locale-related configuration.
What it contains:
-
Locales Array - Defines all supported languages
export const locales = ['de', 'en', 'sr'] as const; -
Default Locale - Fallback language
export const defaultLocale = 'de' as const; -
Locale Type - TypeScript type for type-safe locale handling
export type Locale = (typeof locales)[number]; -
Display Names - Human-readable language names
export const localeNames: Record<Locale, string> = { de: 'Deutsch', en: 'English', sr: 'Srpski', }; -
Locale Flags - Emoji flags for UI
export const localeFlags: Record<Locale, string> = { de: '🇩🇪', en: '🇬🇧', sr: '🇷🇸', }; -
Extended Metadata - SEO and regional data
export const localeMetadata: Record<Locale, { language: string; // ISO 639-1 code (e.g., 'de') region: string; // ISO 3166-1 code (e.g., 'DE') hreflang: string; // For <link rel="alternate"> tags ogLocale: string; // For Open Graph meta tags script: string; // Writing system (e.g., 'Latn') territory: string; // Full country name currency: string; // ISO 4217 code (e.g., 'EUR') }> = { de: { language: 'de', region: 'DE', hreflang: 'de-DE', ogLocale: 'de_DE', script: 'Latn', territory: 'Germany', currency: 'EUR', }, // ... en, sr }; -
SEO Keywords - Localized keywords for each language
export const seoKeywords: Record<Locale, string[]> = { de: ['Fullstack Developer', 'KI Entwickler', 'Köln', ...], en: ['AI Developer', 'Remote Developer Europe', 'London', ...], sr: ['AI Programer Srbija', 'Beograd', 'Novi Sad', ...], };
When to modify this file:
- ✅ Adding a new language
- ✅ Updating SEO keywords
- ✅ Changing metadata for Open Graph or hreflang
- ❌ Adding translation strings (use
src/messages/instead)
request.ts - next-intl Integration
Location: src/i18n/request.ts
This file integrates the translation system with next-intl and handles runtime locale resolution.
What it does:
import { getRequestConfig } from 'next-intl/server';
import { locales, type Locale } from './config';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Validate locale and fallback to default if invalid
if (!locale || !locales.includes(locale as Locale)) {
locale = 'de';
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
Key responsibilities:
- Locale Validation - Ensures only valid locales are used
- Fallback Logic - Defaults to German (
de) if locale is invalid or missing - Dynamic Import - Loads the appropriate translation JSON file at runtime
- Type Safety - Uses the
Localetype fromconfig.tsfor validation
Flow:
- Receives
requestLocalefrom next-intl (extracted from URL) - Validates against the
localesarray - Falls back to
deif validation fails - Dynamically imports the corresponding JSON file from
src/messages/ - Returns locale and messages to next-intl
When to modify this file:
- ✅ Changing fallback locale logic
- ✅ Adding custom locale resolution rules
- ✅ Implementing locale persistence (cookies, etc.)
- ❌ Most use cases don't require changes
src/messages/ - Active Translation Files ✅
Location: src/messages/
This directory contains the active, production-ready translation files in JSON format.
Files:
de.json- German translations (default)en.json- English translationssr.json- Serbian translations
Why JSON?
- ✅ Simple, human-readable format
- ✅ Easy to edit without TypeScript knowledge
- ✅ Smaller bundle size (dynamic imports)
- ✅ Better IDE support for JSON
- ✅ Easier integration with translation tools
Structure:
Each file contains nested JSON objects organized by namespace:
{
"meta": {
"site": { "name": "...", "title": "..." },
"author": { "name": "...", "role": "..." },
"social": { "linkedin": "...", "github": "..." }
},
"common": {
"nav": { "home": "...", "about": "..." },
"actions": { "readMore": "...", "submit": "..." }
},
"pages": {
"home": { "hero": { "title": "..." } },
"about": { "title": "..." }
}
}
Best Practices:
- Keep keys synchronized - All three files should have identical structure
- Use nested namespaces - Organize by feature or page (e.g.,
common,pages.home) - Descriptive keys - Use semantic names like
hero.titleinstead oftext1 - No hardcoded text - All user-facing text should be in these files
When to modify these files:
- ✅ Adding new translation keys
- ✅ Updating existing translations
- ✅ Fixing typos or improving wording
- ✅ Adding new pages or features
src/i18n/locales-old/ - Legacy System ⚠️ DEPRECATED
Location: src/i18n/locales-old/
This directory contains deprecated TypeScript-based translations from a previous implementation.
Files:
de.ts- Old German translationsen.ts- Old English translationssr.ts- Old Serbian translations
⚠️ IMPORTANT: Do NOT use these files!
Why it exists:
- These files were part of an older TypeScript-based translation system
- Kept temporarily during migration for reference
- Not loaded by the application - they are completely inactive
Why the migration happened:
- JSON is simpler and more maintainable than TypeScript objects
- Better performance with dynamic imports
- Easier for non-developers to edit translations
- Better tooling support for JSON translation files
What to do:
- ❌ Do NOT add new keys here
- ❌ Do NOT reference these files in code
- ✅ Use
src/messages/*.jsonfor all translations - ✅ These files can be safely deleted once migration is verified complete
File Relationships
graph TD
A[config.ts] -->|Defines locales| B[request.ts]
B -->|Loads messages| C[messages/*.json]
B -->|Validates locale| A
D[Components] -->|useTranslations| C
E[locales-old/*] -.->|DEPRECATED| F[Not Used]
Summary:
| Directory/File | Status | Purpose | When to Edit |
|---|---|---|---|
src/i18n/config.ts |
✅ Active | Locale definitions & metadata | Adding locales, SEO keywords |
src/i18n/request.ts |
✅ Active | next-intl integration | Rarely (fallback logic) |
src/messages/*.json |
✅ Active | Translation strings | Frequently (new translations) |
src/i18n/locales-old/ |
⚠️ Deprecated | Legacy TypeScript translations | Never (can be deleted) |
🔑 Adding Translation Keys
Step-by-step guide to adding and managing translations
Topics covered:
- JSON structure and namespaces
- Nested translation keys
- Adding keys across all locales
- Code examples using
useTranslations()- Best practices for key naming
⚙️ Configuration & Setup
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
🌐 Locale Configuration & Metadata
Detailed breakdown of locale settings and SEO data
Topics covered:
- Locales array and default locale
- Locale names and flags
- Metadata for hreflang and Open Graph
- SEO keywords per locale
- Currency and territory settings
🔍 SEO & Hreflang Implementation
How multilingual SEO is implemented
Topics covered:
- Hreflang tags in layout.tsx
- alternates.languages configuration
- Content-Language HTTP headers
- Localized meta tags and Open Graph
- Sitemap generation for multiple locales
📦 Legacy Migration (locales-old)
Understanding the deprecated TypeScript-based translation system
Topics covered:
- What locales-old contains
- Why the migration happened
- Do NOT use these files
- Migration from TypeScript to JSON
- Safe removal considerations
🛤️ Routing & URL Structure
How locale-based routing works with Next.js App Router
Topics covered:
[locale]dynamic route segment- URL patterns:
/de/,/en/,/sr/- Locale detection in middleware
generateStaticParams()for static generation- Locale switching and redirects
💡 Usage Examples & Best Practices
Practical examples and common patterns
Topics covered:
useTranslations()hook patterns- Accessing nested keys
- Formatting dates and numbers
- Pluralization (if implemented)
- Common pitfalls and solutions
- Performance considerations
Architecture Overview
Portfolio i18n System
│
├── Configuration Layer
│ ├── src/i18n/config.ts # Locale definitions & metadata
│ └── src/i18n/request.ts # next-intl integration
│
├── Translation Files (Active)
│ ├── src/messages/de.json # German translations
│ ├── src/messages/en.json # English translations
│ └── src/messages/sr.json # Serbian translations
│
├── Legacy System (Deprecated)
│ └── src/i18n/locales-old/ # Old TypeScript translations
│
├── Routing Layer
│ ├── src/app/[locale]/layout.tsx # Locale-based layout
│ ├── src/middleware.ts # Locale detection
│ └── next.config.ts # createNextIntlPlugin
│
└── Application Layer
└── Components using useTranslations()
Translation File Structure
Current structure of active translation files:
{
"meta": {
"site": { ... },
"author": { ... },
"social": { ... }
},
"common": {
"siteTitle": "...",
"nav": { ... },
"actions": { ... },
"errors": { ... },
"cookies": { ... }
},
"home": { ... },
"about": { ... },
"portfolio": { ... },
"blog": { ... },
"contact": { ... }
}
Supported Locales
| Locale | Language | hreflang | OG Locale | Currency | Region |
|---|---|---|---|---|---|
de |
Deutsch | de-DE | de_DE | EUR | Germany |
en |
English | en-US | en_US | USD | United States |
sr |
Srpski | sr-RS | sr_RS | RSD | Serbia |
Important Notes
⚠️ Legacy System: The src/i18n/locales-old/ directory contains deprecated TypeScript-based translations from a previous implementation. Do NOT use these files. All new translations should go in src/messages/*.json.
✅ Active System: Use only the JSON files in src/messages/ for all translation work.
🔒 Type Safety: The locale type is derived from the locales array in config.ts, ensuring compile-time validation of locale codes.
🌍 Default Locale: German (de) is the default locale. Invalid or missing locale parameters will fallback to German.
Getting Help
- Check specific sections above for detailed topics
- Review code examples in the pattern files
- Examine existing translation keys in
src/messages/de.json - Look at component usage in
src/app/[locale]/page.tsx
Framework: next-intl + Next.js 15 App Router Default Locale: German (de) Supported Locales: de, en, sr Translation Format: JSON