Refactor i18n integration and update tests for improved localization
- Updated test components (`PasswordResetConfirmForm`, `PasswordChangeForm`) to use i18n keys directly, ensuring accurate validation messages. - Refined translations in `it.json` to standardize format and content. - Replaced text-based labels with localized strings in `PasswordResetRequestForm` and `RegisterForm`. - Introduced `generateLocalizedMetadata` utility and updated layout metadata generation for locale-aware SEO. - Enhanced e2e tests with locale-prefixed routes and updated assertions for consistency. - Added comprehensive i18n documentation (`I18N.md`) for usage, architecture, and testing.
This commit is contained in:
406
frontend/docs/I18N.md
Normal file
406
frontend/docs/I18N.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# Internationalization (i18n) Guide
|
||||
|
||||
This document describes the internationalization implementation in the FastNext Template.
|
||||
|
||||
## Overview
|
||||
|
||||
The application supports multiple languages using [next-intl](https://next-intl-docs.vercel.app/) 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`:
|
||||
|
||||
```typescript
|
||||
'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`:
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"save": "Save"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "Sign in to your account",
|
||||
"emailLabel": "Email",
|
||||
"passwordLabel": "Password"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Access Patterns
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
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`:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
test('navigates to login', async ({ page }) => {
|
||||
await page.goto('/en/login'); // Include locale prefix
|
||||
await expect(page).toHaveURL('/en/login');
|
||||
});
|
||||
```
|
||||
|
||||
## Adding New Languages
|
||||
|
||||
1. **Create translation file:**
|
||||
|
||||
```bash
|
||||
cp messages/en.json messages/fr.json
|
||||
# Translate all values in fr.json
|
||||
```
|
||||
|
||||
2. **Update routing configuration:**
|
||||
|
||||
```typescript
|
||||
// src/lib/i18n/routing.ts
|
||||
export const routing = defineRouting({
|
||||
locales: ['en', 'it', 'fr'], // Add 'fr'
|
||||
defaultLocale: 'en',
|
||||
});
|
||||
```
|
||||
|
||||
3. **Update type definitions:**
|
||||
|
||||
```typescript
|
||||
// src/lib/i18n/metadata.ts
|
||||
export type Locale = 'en' | 'it' | 'fr'; // Add 'fr'
|
||||
```
|
||||
|
||||
4. **Update LocaleSwitcher:**
|
||||
```typescript
|
||||
// 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/navigation` directly
|
||||
- Don't use template strings for validation messages (use static messages)
|
||||
- Don't forget to update both `en.json` and `it.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:
|
||||
|
||||
```diff
|
||||
- import { useRouter } from 'next/navigation'
|
||||
+ import { useRouter } from '@/lib/i18n/routing'
|
||||
```
|
||||
|
||||
### E2E tests failing with "Page not found"
|
||||
|
||||
**Solution**: Add locale prefix to URLs:
|
||||
|
||||
```diff
|
||||
- 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:
|
||||
|
||||
```diff
|
||||
// ❌ Bad (ICU format not configured)
|
||||
- "minLength": "Must be at least {count} characters"
|
||||
|
||||
// ✅ Good
|
||||
+ "minLength": "Must be at least 8 characters"
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [next-intl Documentation](https://next-intl-docs.vercel.app/)
|
||||
- [Next.js Internationalization](https://nextjs.org/docs/app/building-your-application/routing/internationalization)
|
||||
- [Implementation Plan](./I18N_IMPLEMENTATION_PLAN.md)
|
||||
|
||||
## 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](https://next-intl-docs.vercel.app/) or check existing tests for examples.
|
||||
Reference in New Issue
Block a user