- Update PROJECT_NAME to Syndarix in backend config - Update all frontend components with Syndarix branding - Replace all GitHub URLs with Gitea Syndarix repo URLs - Update metadata, headers, footers with new branding - Update tests to match new URLs - Update E2E tests for new repo references - Preserve "Built on PragmaStack" attribution in docs Closes #13 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.4 KiB
Internationalization (i18n) Guide
This document describes the internationalization implementation in Syndarix.
Overview
The application supports multiple languages using next-intl v4.5.3, a modern i18n library for Next.js 15 App Router.
Supported Locales:
- English (
en) - Default - Italian (
it)
Architecture
URL Structure
All routes are locale-prefixed using the [locale] dynamic segment:
/en/login → English login page
/it/login → Italian login page
/en/settings → English settings
/it/settings → Italian settings
Core Files
frontend/
├── src/
│ ├── lib/
│ │ ├── i18n/
│ │ │ ├── routing.ts # Locale routing configuration
│ │ │ ├── utils.ts # Locale utility functions
│ │ │ └── metadata.ts # SEO metadata helpers
│ ├── app/
│ │ └── [locale]/ # Locale-specific routes
│ │ └── layout.tsx # Locale layout with NextIntlClientProvider
│ └── components/
│ └── navigation/
│ └── LocaleSwitcher.tsx # Language switcher component
├── messages/
│ ├── en.json # English translations
│ └── it.json # Italian translations
└── types/
└── next-intl.d.ts # TypeScript type definitions
Usage
Client Components
Use the useTranslations hook from next-intl:
'use client';
import { useTranslations } from 'next-intl';
export function MyComponent() {
const t = useTranslations('namespace');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
Server Components
Use getTranslations from next-intl/server:
import { getTranslations } from 'next-intl/server';
export default async function MyPage() {
const t = await getTranslations('namespace');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
Navigation
Always use the locale-aware navigation utilities:
import { Link, useRouter, usePathname } from '@/lib/i18n/routing';
// Link component (automatic locale prefix)
<Link href="/settings">Settings</Link> // → /en/settings
// Router hooks
function MyComponent() {
const router = useRouter();
const pathname = usePathname();
const handleClick = () => {
router.push('/dashboard'); // → /en/dashboard
};
return <button onClick={handleClick}>Go to Dashboard</button>;
}
⚠️ Never import from next/navigation directly - always use @/lib/i18n/routing
Forms with Validation
Create dynamic Zod schemas that accept translation functions:
import { useTranslations } from 'next-intl';
import { z } from 'zod';
const createLoginSchema = (t: (key: string) => string) =>
z.object({
email: z.string().min(1, t('validation.required')).email(t('validation.email')),
password: z.string().min(1, t('validation.required')),
});
export function LoginForm() {
const t = useTranslations('auth.login');
const tValidation = useTranslations('validation');
const schema = createLoginSchema((key) => {
if (key.startsWith('validation.')) {
return tValidation(key.replace('validation.', ''));
}
return t(key);
});
// Use schema with react-hook-form...
}
Translation Files
Structure
Translations are organized hierarchically in JSON files:
{
"common": {
"loading": "Loading...",
"save": "Save"
},
"auth": {
"login": {
"title": "Sign in to your account",
"emailLabel": "Email",
"passwordLabel": "Password"
}
}
}
Access Patterns
// Nested namespace access
const t = useTranslations('auth.login');
t('title'); // → "Sign in to your account"
// Multiple namespaces
const tAuth = useTranslations('auth.login');
const tCommon = useTranslations('common');
tAuth('title'); // → "Sign in to your account"
tCommon('loading'); // → "Loading..."
SEO & Metadata
Page Metadata
Use the generatePageMetadata helper for locale-aware metadata:
import { Metadata } from 'next';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.login' });
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/login');
}
This automatically generates:
- Open Graph tags
- Twitter Card tags
- Language alternates (hreflang)
- Canonical URLs
Sitemap
The sitemap (/sitemap.xml) automatically includes all public routes for both locales with language alternates.
Robots.txt
The robots.txt (/robots.txt) allows crawling of public routes and references the sitemap.
Locale Switching
Users can switch languages using the LocaleSwitcher component in the header:
import { LocaleSwitcher } from '@/components/navigation/LocaleSwitcher';
<LocaleSwitcher />
The switcher:
- Displays the current locale
- Lists available locales
- Preserves the current path when switching
- Uses the locale-aware router
Type Safety
TypeScript autocomplete for translation keys is enabled via types/next-intl.d.ts:
import en from '@/messages/en.json';
type Messages = typeof en;
declare global {
interface IntlMessages extends Messages {}
}
This provides:
- Autocomplete for translation keys
- Type errors for missing keys
- IntelliSense in IDEs
Testing
Unit Tests
Mock next-intl in jest.setup.js:
jest.mock('next-intl', () => ({
useTranslations: (namespace) => {
return (key) => key; // Return key as-is for tests
},
useLocale: () => 'en',
}));
jest.mock('next-intl/server', () => ({
getTranslations: jest.fn(async () => (key) => key),
}));
E2E Tests
Update Playwright tests to include locale prefixes:
test('navigates to login', async ({ page }) => {
await page.goto('/en/login'); // Include locale prefix
await expect(page).toHaveURL('/en/login');
});
Adding New Languages
-
Create translation file:
cp messages/en.json messages/fr.json # Translate all values in fr.json -
Update routing configuration:
// src/lib/i18n/routing.ts export const routing = defineRouting({ locales: ['en', 'it', 'fr'], // Add 'fr' defaultLocale: 'en', }); -
Update type definitions:
// src/lib/i18n/metadata.ts export type Locale = 'en' | 'it' | 'fr'; // Add 'fr' -
Update LocaleSwitcher:
// src/components/navigation/LocaleSwitcher.tsx const localeNames: Record<string, string> = { en: 'English', it: 'Italiano', fr: 'Français', // Add French };
Best Practices
DO ✅
- Use locale-aware navigation (
@/lib/i18n/routing) - Create dynamic validation schemas
- Use nested translation keys for organization
- Test with multiple locales
- Keep translation files in sync
- Use TypeScript types for autocomplete
DON'T ❌
- Don't hardcode text in components
- Don't import from
next/navigationdirectly - Don't use template strings for validation messages (use static messages)
- Don't forget to update both
en.jsonandit.json - Don't skip testing translated components
Performance
The implementation is optimized for performance:
- Server-side translation loading: Messages loaded on server, passed to client
- Lazy loading: Only current locale messages are loaded
- Font optimization:
display: 'swap'prevents layout shift - Preloading: Primary font preloaded, secondary font lazy-loaded
- Caching: Next.js automatically caches translation files
Common Issues
"Module not found: Can't resolve 'next/navigation'"
Solution: Use @/lib/i18n/routing instead:
- import { useRouter } from 'next/navigation'
+ import { useRouter } from '@/lib/i18n/routing'
E2E tests failing with "Page not found"
Solution: Add locale prefix to URLs:
- await page.goto('/login')
+ await page.goto('/en/login')
TypeScript error: "Property does not exist on type"
Solution: Ensure the key exists in messages/en.json and restart TypeScript server.
ICU message format error
Solution: Use static messages instead of templates:
// ❌ Bad (ICU format not configured)
- "minLength": "Must be at least {count} characters"
// ✅ Good
+ "minLength": "Must be at least 8 characters"
Resources
Summary
The i18n implementation provides:
- ✅ Multi-language support (EN, IT) with easy extensibility
- ✅ Type-safe translations with autocomplete
- ✅ SEO-optimized with proper metadata and sitemaps
- ✅ Performance-optimized loading
- ✅ Comprehensive test coverage
- ✅ Developer-friendly API
For questions or issues, refer to the next-intl documentation or check existing tests for examples.