Files
syndarix/frontend/docs/I18N.md
Felipe Cardoso ebd307cab4 feat: complete Syndarix rebranding from PragmaStack
- 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>
2025-12-29 13:30:45 +01:00

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

  1. Create translation file:

    cp messages/en.json messages/fr.json
    # Translate all values in fr.json
    
  2. Update routing configuration:

    // src/lib/i18n/routing.ts
    export const routing = defineRouting({
      locales: ['en', 'it', 'fr'], // Add 'fr'
      defaultLocale: 'en',
    });
    
  3. Update type definitions:

    // src/lib/i18n/metadata.ts
    export type Locale = 'en' | 'it' | 'fr'; // Add 'fr'
    
  4. 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/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:

- 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.