- Updated all references, metadata, and templates to reflect the new branding, including layout files, components, and documentation. - Replaced hardcoded color tokens like `green-600` with semantic tokens (`success`, `warning`, etc.) for improved design consistency. - Enhanced `globals.css` with new color tokens for success, warning, and destructive states using the OKLCH color model. - Added comprehensive branding guidelines and updated the design system documentation to align with the new identity. - Updated tests and mocks to reflect the branding changes and ensured all visual/verbal references match "PragmaStack". - Added new `branding/README.md` and `branding` docs for mission, values, and visual identity definition.
407 lines
9.5 KiB
Markdown
407 lines
9.5 KiB
Markdown
# Internationalization (i18n) Guide
|
|
|
|
This document describes the internationalization implementation in the PragmaStack.
|
|
|
|
## 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.
|