diff --git a/docs/I18N_IMPLEMENTATION_PLAN.md b/docs/I18N_IMPLEMENTATION_PLAN.md
new file mode 100644
index 0000000..60c3283
--- /dev/null
+++ b/docs/I18N_IMPLEMENTATION_PLAN.md
@@ -0,0 +1,528 @@
+# Internationalization (i18n) Implementation Plan
+## State-of-the-Art Next.js 15 + FastAPI i18n System (2025)
+
+**Last Updated**: 2025-11-17
+**Status**: โ
Approved - Ready for Implementation
+**Languages**: English (en) - Default, Italian (it) - Showcase
+
+---
+
+## ๐ EXECUTIVE SUMMARY
+
+This document provides a comprehensive plan for implementing production-grade internationalization (i18n) in the FastNext Template. The implementation showcases best practices with 2 languages (English and Italian), making it easy for users to extend with additional languages.
+
+### Technology Stack (2025 Best Practices)
+
+- **Frontend**: next-intl 4.0 (ESM, TypeScript-first, App Router native)
+- **Backend**: FastAPI with BCP 47 locale storage in PostgreSQL
+- **Testing**: Playwright (parameterized locale tests) + Jest (i18n test utils)
+- **SEO**: Automatic hreflang tags, sitemap generation, metadata per locale
+- **Validation**: Automated translation key validation in CI/CD
+
+### Why Only 2 Languages?
+
+This is a **template showcase**, not a production deployment:
+
+โ
Clean example of i18n implementation
+โ
Easy to understand patterns
+โ
Users can add languages by copying the Italian example
+โ
Faster testing and implementation
+โ
Smaller bundle size for demonstration
+
+### Quality Standards
+
+- โ
**Test Coverage**: Backend โฅ97%, comprehensive E2E tests
+- โ
**Zero Breaking Changes**: All existing 743 backend + 56 frontend tests must pass
+- โ
**Type Safety**: Full autocomplete for translation keys
+- โ
**Performance**: Core Web Vitals maintained (LCP < 2.5s, INP < 200ms, CLS < 0.1)
+- โ
**SEO**: Lighthouse SEO score 100 for both locales
+- โ
**GDPR Compliant**: User locale preferences handled appropriately
+
+---
+
+## ๐ฏ IMPLEMENTATION PHASES
+
+### Phase 0: Documentation & Planning (2 hours)
+- Create this implementation plan document
+- Document architecture decisions
+
+### Phase 1: Backend Foundation (4 hours)
+- Add `locale` column to User model
+- Create database migration
+- Update Pydantic schemas
+- Create locale detection dependency
+- Add backend tests
+
+### Phase 2: Frontend Setup (4 hours)
+- Install and configure next-intl
+- Create translation files (EN, IT)
+- Configure TypeScript autocomplete
+- Restructure App Router for [locale] pattern
+- Fix tests
+
+### Phase 3: Component Translation (4 hours)
+- Create LocaleSwitcher component
+- Translate auth components
+- Translate navigation components
+- Translate settings components
+- Review and test
+
+### Phase 4: SEO & Metadata (3 hours)
+- Implement locale-aware metadata
+- Generate multilingual sitemap
+- Configure robots.txt
+- SEO validation
+
+### Phase 5: Performance Optimization (3 hours)
+- Measure Core Web Vitals baseline
+- Optimize translation loading
+- Prevent CLS with font loading
+- Performance validation
+
+### Phase 6: Comprehensive Testing (4 hours)
+- Backend integration tests
+- Frontend E2E locale tests
+- Frontend unit tests
+- Translation validation automation
+
+### Phase 7: Documentation & Polish (2 hours)
+- Update technical documentation
+- Create migration guide
+- Final SEO and performance validation
+
+### Phase 8: Deployment Prep (2 hours)
+- Update README
+- Create release notes
+- Deployment checklist
+
+**Total Time**: ~28 hours (~3.5 days) + 20% buffer = **4 days**
+
+---
+
+## ๐๏ธ ARCHITECTURE DECISIONS
+
+### 1. Locale Format: BCP 47
+
+**Decision**: Use BCP 47 language tags (e.g., "en", "it", "en-US", "it-IT")
+
+**Rationale**:
+- Industry standard (used by HTTP Accept-Language, HTML lang attribute, ICU libraries)
+- Based on ISO 639-1 (language) + ISO 3166-1 (region)
+- Flexible: start simple with 2-letter codes, add region qualifiers when needed
+- Future-proof for dialects and scripts (e.g., "zh-Hans" for Simplified Chinese)
+
+**Implementation**:
+```python
+# Backend validation
+SUPPORTED_LOCALES = {"en", "it", "en-US", "en-GB", "it-IT"}
+```
+
+---
+
+### 2. URL Structure: Subdirectory
+
+**Decision**: `/[locale]/[path]` format (e.g., `/en/about`, `/it/about`)
+
+**Alternatives Considered**:
+- โ Subdomain (`en.example.com`) - Too complex for template
+- โ Country-code TLD (`example.it`) - Too expensive, not needed for template
+- โ URL parameters (`?lang=en`) - Poor SEO, harder to crawl
+
+**Rationale**:
+- โ
**Best SEO**: Google explicitly recommends subdirectories for most sites
+- โ
**Simple Infrastructure**: Single domain, single deployment
+- โ
**Low Cost**: No multiple domain purchases
+- โ
**Easy to Maintain**: Centralized analytics and tooling
+- โ
**Clear URLs**: Users can easily switch locales by changing URL
+
+**SEO Benefits**:
+- Domain authority consolidates to one domain
+- Backlinks benefit all language versions
+- Easier to build authority than multiple domains
+- Works seamlessly with hreflang tags
+
+---
+
+### 3. Database Schema: Dedicated `locale` Column
+
+**Decision**: Add `locale VARCHAR(10)` column to `users` table, NOT JSONB
+
+**Alternatives Considered**:
+- โ Store in `preferences` JSONB field - 2-10x slower queries, no optimizer statistics
+
+**Rationale**:
+- โ
**Performance**: B-tree index vs GIN index (smaller, faster)
+- โ
**Query Optimization**: PostgreSQL can maintain statistics on column values
+- โ
**Disk Space**: JSONB stores keys repeatedly (inefficient for common fields)
+- โ
**Update Performance**: Updating JSONB requires rewriting entire field + indexes
+
+**Schema**:
+```sql
+ALTER TABLE users ADD COLUMN locale VARCHAR(10) DEFAULT NULL;
+CREATE INDEX ix_users_locale ON users(locale);
+```
+
+**Why Nullable?**
+- Distinguish "never set" (NULL) from "explicitly set to English"
+- Allows lazy loading on first request (more accurate than backfilling)
+
+**Why No Database DEFAULT?**
+- Application-level default provides more flexibility
+- Can implement locale detection logic (Accept-Language header)
+- Easier to change default without migration
+
+---
+
+### 4. Locale Detection Priority
+
+**Decision**: Three-tier fallback system
+
+1. **User's Saved Preference** (highest priority)
+ - If authenticated and `user.locale` is set, use it
+ - Persists across sessions and devices
+
+2. **Accept-Language Header** (second priority)
+ - For unauthenticated users
+ - Parse `Accept-Language: it-IT,it;q=0.9,en;q=0.8` โ "it-IT"
+ - Validate against supported locales
+
+3. **Default to English** (fallback)
+ - If no user preference and no header match
+
+**Implementation** (Backend):
+```python
+async def get_locale(
+ request: Request,
+ current_user: User | None = Depends(get_optional_current_user)
+) -> str:
+ if current_user and current_user.locale:
+ return current_user.locale
+
+ accept_language = request.headers.get("accept-language", "")
+ if accept_language:
+ locale = accept_language.split(',')[0].split(';')[0].strip()
+ lang_code = locale.split('-')[0].lower()
+ if lang_code in {"en", "it"}:
+ return locale.lower()
+
+ return "en"
+```
+
+---
+
+### 5. Translation Storage: Server-Side Only (next-intl)
+
+**Decision**: Use next-intl's server-component pattern, NOT client-side translation loading
+
+**Rationale**:
+- โ
**Zero Client Bundle Overhead**: Translations never sent to browser
+- โ
**Instant Page Loads**: No translation parsing on client
+- โ
**Better SEO**: Fully rendered HTML for search engines
+- โ
**Reduced Bandwidth**: Especially important for mobile users
+
+**Implementation**:
+```typescript
+// Server Component (preferred 95% of the time)
+import { getTranslations } from 'next-intl/server';
+
+export default async function Page() {
+ const t = await getTranslations('HomePage');
+ return
{t('title')}
;
+}
+
+// Client Component (only when needed for interactivity)
+'use client';
+import { useTranslations } from 'next-intl';
+
+export function LoginForm() {
+ const t = useTranslations('auth.Login');
+ return ;
+}
+```
+
+**Performance Impact**:
+- next-intl core: ~9.2kb gzipped
+- Translation files: 0kb on client (server-side only)
+- Tree-shaking: Automatic with ESM build
+
+---
+
+### 6. Translation File Structure: Nested Namespaces
+
+**Decision**: Use nested JSON structure with namespaces, not flat keys
+
+**Bad (Flat)**:
+```json
+{
+ "auth_login_title": "Sign in",
+ "auth_login_email_label": "Email",
+ "auth_register_title": "Sign up"
+}
+```
+
+**Good (Nested)**:
+```json
+{
+ "auth": {
+ "Login": {
+ "title": "Sign in",
+ "emailLabel": "Email"
+ },
+ "Register": {
+ "title": "Sign up"
+ }
+ }
+}
+```
+
+**Rationale**:
+- โ
**Better Organization**: Logical grouping by feature
+- โ
**Easier Maintenance**: Find related translations quickly
+- โ
**Type Safety**: Full autocomplete with TypeScript integration
+- โ
**Scalability**: Easy to split into separate files later
+
+**Usage**:
+```typescript
+const t = useTranslations('auth.Login');
+t('title'); // "Sign in"
+t('emailLabel'); // "Email"
+```
+
+---
+
+### 7. SEO Strategy: Hreflang + Sitemap + Metadata
+
+**Decision**: Dual implementation for comprehensive SEO
+
+**1. HTML `` Tags** (via `generateMetadata`):
+```typescript
+export async function generateMetadata({ params: { locale } }) {
+ return {
+ alternates: {
+ canonical: `/${locale}`,
+ languages: {
+ 'x-default': '/en',
+ 'en': '/en',
+ 'it': '/it',
+ },
+ },
+ };
+}
+```
+
+**2. XML Sitemap** (with hreflang):
+```typescript
+export default function sitemap() {
+ return routes.flatMap(route =>
+ locales.map(locale => ({
+ url: `${baseUrl}/${locale}${route.path}`,
+ alternates: {
+ languages: { en: `/en${route.path}`, it: `/it${route.path}` }
+ }
+ }))
+ );
+}
+```
+
+**Why Both?**
+- HTML tags: Google's preferred method for page-level precision
+- Sitemap: Helps discovery, provides backup if HTML tags malfunction
+- **Never use HTTP headers** - avoid confusion with mixed methods
+
+**x-default Locale**:
+- Points to English (`/en`) as fallback for unsupported locales
+- Used when user's language doesn't match any hreflang tags
+
+---
+
+### 8. Testing Strategy: Smoke Tests + Parameterization
+
+**Decision**: Don't test all translations, test the i18n mechanism works
+
+**Backend Tests**:
+- 10 new integration tests covering locale CRUD, validation, detection
+- Test both EN and IT locale values
+- Test Accept-Language header parsing
+
+**Frontend E2E Tests**:
+- 12 new parameterized tests for locale switching
+- Test critical flows in both EN and IT (login, register)
+- **NOT** duplicating all 56 existing tests per locale
+- Use parameterization: `for (const locale of ['en', 'it']) { test(...) }`
+
+**Frontend Unit Tests**:
+- 8 new component tests with i18n wrappers
+- Test LocaleSwitcher functionality
+- Test translated components render correctly
+
+**Translation Validation**:
+- Automated CI check for missing keys
+- Validate Italian has all keys that English has
+- Detect unused keys
+
+**Rationale**:
+- โ
**Efficient**: Test mechanism, not content
+- โ
**Maintainable**: Adding Italian tests doesn't double test time
+- โ
**Comprehensive**: Critical paths tested in both locales
+- โ
**Fast CI**: ~13-18 minutes total (vs 60+ if we duplicate everything)
+
+---
+
+### 9. Performance Budget
+
+**Core Web Vitals Targets** (both EN and IT):
+- **LCP** (Largest Contentful Paint): < 2.5s
+- **INP** (Interaction to Next Paint): < 200ms
+- **CLS** (Cumulative Layout Shift): < 0.1
+
+**Bundle Size Impact**:
+- next-intl: ~9.2kb gzipped (acceptable)
+- Translation files: 0kb on client (server-side)
+- Total increase: < 15kb
+
+**Lighthouse Scores**:
+- Performance: โฅ 90
+- Accessibility: โฅ 95
+- SEO: 100
+
+**Font Loading Strategy**:
+- Use `display: swap` to prevent CLS
+- Preload Inter font with Latin + Latin-ext subsets (for Italian accents: ร , รจ, รฌ, รฒ, รน)
+- Fallback to system fonts
+
+---
+
+### 10. GDPR Compliance
+
+**Classification**: User locale preference IS personal data (GDPR Article 4)
+
+**Lawful Basis**: Legitimate interest (service improvement) โ
+
+**User Rights**:
+- โ
**Access**: User can view locale in profile (GET /users/me)
+- โ
**Rectification**: User can update locale (PATCH /users/me)
+- โ
**Erasure**: Locale deleted when user deleted (CASCADE)
+- โ
**Portability**: Included in user data export
+
+**Privacy Policy Requirements**:
+- "We store your language preference to personalize your experience"
+- "You can change this in Settings > Profile at any time"
+
+**Data Minimization**: โ
PASS
+- Locale is necessary for service personalization
+- No excessive data collection (not storing geolocation)
+
+---
+
+## ๐ GETTING STARTED
+
+### For Developers Implementing This Plan
+
+1. **Read This Document**: Understand the architecture decisions and rationale
+2. **Follow Phases Sequentially**: Each phase builds on the previous
+3. **Run Tests After Each Phase**: Ensure no regressions
+4. **Use Parallel Agents**: Where indicated in the plan for efficiency
+5. **Document Decisions**: Update this file if you deviate from the plan
+
+### For Users of the Template
+
+1. **See `/docs/I18N.md`**: User-facing guide on using i18n
+2. **See `/docs/I18N_MIGRATION_GUIDE.md`**: Deploying to existing projects
+3. **Adding Languages**: Copy the Italian example, follow `/docs/I18N.md`
+
+---
+
+## ๐ REFERENCES
+
+### Official Documentation
+- [next-intl](https://next-intl.dev) - Next.js 15 i18n library
+- [BCP 47 Language Tags](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) - Locale format standard
+- [Google Multilingual SEO](https://developers.google.com/search/docs/specialty/international) - SEO guidelines
+
+### Research Sources
+- next-intl 4.0 release notes (2025)
+- Next.js 15 App Router i18n patterns
+- PostgreSQL performance: JSONB vs columns
+- Lighthouse CI best practices
+- Playwright i18n testing patterns
+
+---
+
+## โ
COMPLETION CHECKLIST
+
+Use this checklist to verify implementation is complete:
+
+### Backend
+- [ ] `locale` column added to `users` table
+- [ ] Database migration created and tested
+- [ ] Pydantic schemas updated (UserUpdate, UserResponse)
+- [ ] Locale detection dependency created
+- [ ] 10+ backend tests added
+- [ ] All existing 743 tests still pass
+- [ ] Coverage โฅ97% maintained
+
+### Frontend
+- [ ] next-intl installed and configured
+- [ ] Translation files created (en.json, it.json)
+- [ ] TypeScript autocomplete working
+- [ ] App Router restructured to `[locale]` pattern
+- [ ] LocaleSwitcher component created
+- [ ] All components translated (auth, navigation, settings)
+- [ ] All existing 56 E2E tests still pass
+- [ ] 12+ new E2E locale tests added
+- [ ] 8+ new unit tests added
+
+### SEO
+- [ ] Metadata implemented per locale
+- [ ] Sitemap generated with hreflang
+- [ ] robots.txt configured
+- [ ] Lighthouse SEO = 100 (both EN and IT)
+
+### Performance
+- [ ] Core Web Vitals measured (both locales)
+- [ ] LCP < 2.5s
+- [ ] INP < 200ms
+- [ ] CLS < 0.1
+- [ ] Bundle analysis shows minimal impact
+
+### Documentation
+- [ ] This implementation plan complete
+- [ ] `/docs/I18N.md` created (user guide)
+- [ ] `/docs/I18N_MIGRATION_GUIDE.md` created
+- [ ] `CLAUDE.md` updated with i18n patterns
+- [ ] README.md updated with i18n feature
+- [ ] CHANGELOG.md updated
+
+### Testing
+- [ ] Translation validation in CI
+- [ ] All tests passing
+- [ ] No flaky tests
+- [ ] Coverage targets met
+
+---
+
+## ๐ NEXT STEPS
+
+After completing this implementation:
+
+1. **Deploy to Staging**: Test in production-like environment
+2. **Gather Feedback**: From team and early users
+3. **Optimize Further**: Based on real-world usage data
+4. **Add Languages**: If needed, follow the Italian example
+
+---
+
+## ๐ CHANGE LOG
+
+| Date | Author | Change |
+|------|--------|--------|
+| 2025-11-17 | Claude | Initial plan created based on 2025 research |
+| 2025-11-17 | Claude | Updated to 2 languages (EN, IT) per user request |
+
+---
+
+**End of Implementation Plan**
+
+This plan represents state-of-the-art i18n implementation for 2025. It balances best practices, performance, SEO, and developer experience while remaining simple enough for a template showcase.
+
+For questions or clarifications, refer to the detailed task descriptions in each phase.
diff --git a/frontend/jest.config.js b/frontend/jest.config.js
index e78de6c..76f0e8c 100644
--- a/frontend/jest.config.js
+++ b/frontend/jest.config.js
@@ -14,7 +14,7 @@ const customJestConfig = {
},
testMatch: ['/tests/**/*.test.ts', '/tests/**/*.test.tsx'],
transformIgnorePatterns: [
- 'node_modules/(?!(react-syntax-highlighter|refractor|hastscript|hast-.*|unist-.*|property-information|space-separated-tokens|comma-separated-tokens|web-namespaces)/)',
+ 'node_modules/(?!(react-syntax-highlighter|refractor|hastscript|hast-.*|unist-.*|property-information|space-separated-tokens|comma-separated-tokens|web-namespaces|next-intl|use-intl)/)',
],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
diff --git a/frontend/next.config.ts b/frontend/next.config.ts
index ee1ceac..0e92a78 100755
--- a/frontend/next.config.ts
+++ b/frontend/next.config.ts
@@ -2,7 +2,7 @@ import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
// Initialize next-intl plugin with i18n request config path
-const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
+const withNextIntl = createNextIntlPlugin('./src/lib/i18n/request.ts');
const nextConfig: NextConfig = {
output: 'standalone',
diff --git a/frontend/src/i18n/request.ts b/frontend/src/lib/i18n/request.ts
similarity index 96%
rename from frontend/src/i18n/request.ts
rename to frontend/src/lib/i18n/request.ts
index 2bb350d..645aec2 100644
--- a/frontend/src/i18n/request.ts
+++ b/frontend/src/lib/i18n/request.ts
@@ -14,7 +14,7 @@
*/
import { getRequestConfig } from 'next-intl/server';
-import { routing } from './routing';
+import { routing } from '@/lib/i18n/routing';
export default getRequestConfig(async ({ locale }) => {
// Validate that the incoming `locale` parameter is valid
diff --git a/frontend/src/i18n/routing.ts b/frontend/src/lib/i18n/routing.ts
similarity index 100%
rename from frontend/src/i18n/routing.ts
rename to frontend/src/lib/i18n/routing.ts
diff --git a/frontend/src/lib/i18n/utils.ts b/frontend/src/lib/i18n/utils.ts
index 204c306..06983e6 100644
--- a/frontend/src/lib/i18n/utils.ts
+++ b/frontend/src/lib/i18n/utils.ts
@@ -2,11 +2,10 @@
/**
* Utility functions for internationalization.
*
- * This file demonstrates type-safe translation usage.
+ * This file provides pure utility functions for i18n without React dependencies.
+ * For React hooks, see hooks.ts
*/
-import { useTranslations } from 'next-intl';
-
/**
* Get the display name for a locale code.
*
@@ -54,28 +53,6 @@ export function getLocaleFlag(locale: string): string {
return flags[locale] || flags.en;
}
-/**
- * Hook to get common translations.
- * This demonstrates type-safe usage of useTranslations.
- *
- * @returns Object with commonly used translation functions
- */
-export function useCommonTranslations() {
- const t = useTranslations('common');
-
- return {
- loading: () => t('loading'),
- error: () => t('error'),
- success: () => t('success'),
- cancel: () => t('cancel'),
- save: () => t('save'),
- delete: () => t('delete'),
- edit: () => t('edit'),
- close: () => t('close'),
- confirm: () => t('confirm'),
- };
-}
-
/**
* Format a relative time string (e.g., "2 hours ago").
* This is a placeholder for future implementation with next-intl's date/time formatting.
diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts
index 161054f..33054ab 100644
--- a/frontend/src/middleware.ts
+++ b/frontend/src/middleware.ts
@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import createMiddleware from 'next-intl/middleware';
-import { routing } from './i18n/routing';
+import { routing } from './lib/i18n/routing';
// Create next-intl middleware for locale handling
const intlMiddleware = createMiddleware(routing);
diff --git a/frontend/tests/lib/i18n/utils.test.ts b/frontend/tests/lib/i18n/utils.test.ts
new file mode 100644
index 0000000..48cb4a1
--- /dev/null
+++ b/frontend/tests/lib/i18n/utils.test.ts
@@ -0,0 +1,242 @@
+/**
+ * Tests for i18n utility functions
+ */
+
+import {
+ getLocaleName,
+ getLocaleNativeName,
+ getLocaleFlag,
+ formatRelativeTime,
+} from '@/lib/i18n/utils';
+
+describe('i18n Utility Functions', () => {
+ describe('getLocaleName', () => {
+ it('should return correct name for English locale', () => {
+ expect(getLocaleName('en')).toBe('English');
+ });
+
+ it('should return correct name for Italian locale', () => {
+ expect(getLocaleName('it')).toBe('Italiano');
+ });
+
+ it('should return English for unsupported locale', () => {
+ expect(getLocaleName('fr')).toBe('English');
+ expect(getLocaleName('de')).toBe('English');
+ expect(getLocaleName('es')).toBe('English');
+ });
+
+ it('should handle empty string', () => {
+ expect(getLocaleName('')).toBe('English');
+ });
+
+ it('should handle undefined as string', () => {
+ expect(getLocaleName('undefined')).toBe('English');
+ });
+
+ it('should handle locale codes with region (fallback)', () => {
+ expect(getLocaleName('en-US')).toBe('English');
+ expect(getLocaleName('en-GB')).toBe('English');
+ expect(getLocaleName('it-IT')).toBe('English'); // Not exact match, falls back
+ });
+ });
+
+ describe('getLocaleNativeName', () => {
+ it('should return native name for English locale', () => {
+ expect(getLocaleNativeName('en')).toBe('English');
+ });
+
+ it('should return native name for Italian locale', () => {
+ expect(getLocaleNativeName('it')).toBe('Italiano');
+ });
+
+ it('should return English for unsupported locale', () => {
+ expect(getLocaleNativeName('fr')).toBe('English');
+ expect(getLocaleNativeName('de')).toBe('English');
+ });
+
+ it('should match getLocaleName output for supported locales', () => {
+ expect(getLocaleNativeName('en')).toBe(getLocaleName('en'));
+ expect(getLocaleNativeName('it')).toBe(getLocaleName('it'));
+ });
+
+ it('should handle case variations (fallback behavior)', () => {
+ expect(getLocaleNativeName('EN')).toBe('English');
+ expect(getLocaleNativeName('IT')).toBe('English');
+ });
+ });
+
+ describe('getLocaleFlag', () => {
+ it('should return US flag for English locale', () => {
+ expect(getLocaleFlag('en')).toBe('๐บ๐ธ');
+ });
+
+ it('should return Italian flag for Italian locale', () => {
+ expect(getLocaleFlag('it')).toBe('๐ฎ๐น');
+ });
+
+ it('should return US flag for unsupported locale', () => {
+ expect(getLocaleFlag('fr')).toBe('๐บ๐ธ');
+ expect(getLocaleFlag('de')).toBe('๐บ๐ธ');
+ expect(getLocaleFlag('es')).toBe('๐บ๐ธ');
+ });
+
+ it('should return valid emoji flags', () => {
+ const enFlag = getLocaleFlag('en');
+ const itFlag = getLocaleFlag('it');
+
+ // Check that these are unicode emoji characters
+ expect(enFlag).toMatch(/[\u{1F1E6}-\u{1F1FF}]{2}/u);
+ expect(itFlag).toMatch(/[\u{1F1E6}-\u{1F1FF}]{2}/u);
+ });
+
+ it('should handle empty string gracefully', () => {
+ expect(getLocaleFlag('')).toBe('๐บ๐ธ');
+ });
+ });
+
+ describe('formatRelativeTime', () => {
+ const now = new Date();
+
+ describe('English locale', () => {
+ it('should format "just now" for times less than 60 seconds', () => {
+ const date = new Date(now.getTime() - 30 * 1000); // 30 seconds ago
+ expect(formatRelativeTime(date, 'en')).toBe('just now');
+ });
+
+ it('should format minutes correctly', () => {
+ const date1 = new Date(now.getTime() - 1 * 60 * 1000); // 1 minute ago
+ expect(formatRelativeTime(date1, 'en')).toBe('1 minute ago');
+
+ const date2 = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago
+ expect(formatRelativeTime(date2, 'en')).toBe('5 minutes ago');
+
+ const date3 = new Date(now.getTime() - 30 * 60 * 1000); // 30 minutes ago
+ expect(formatRelativeTime(date3, 'en')).toBe('30 minutes ago');
+ });
+
+ it('should format hours correctly', () => {
+ const date1 = new Date(now.getTime() - 1 * 60 * 60 * 1000); // 1 hour ago
+ expect(formatRelativeTime(date1, 'en')).toBe('1 hour ago');
+
+ const date2 = new Date(now.getTime() - 5 * 60 * 60 * 1000); // 5 hours ago
+ expect(formatRelativeTime(date2, 'en')).toBe('5 hours ago');
+
+ const date3 = new Date(now.getTime() - 23 * 60 * 60 * 1000); // 23 hours ago
+ expect(formatRelativeTime(date3, 'en')).toBe('23 hours ago');
+ });
+
+ it('should format days correctly', () => {
+ const date1 = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000); // 1 day ago
+ expect(formatRelativeTime(date1, 'en')).toBe('1 day ago');
+
+ const date2 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
+ expect(formatRelativeTime(date2, 'en')).toBe('7 days ago');
+
+ const date3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
+ expect(formatRelativeTime(date3, 'en')).toBe('30 days ago');
+ });
+
+ it('should default to English when locale not specified', () => {
+ const date = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
+ expect(formatRelativeTime(date)).toBe('2 minutes ago');
+ });
+ });
+
+ describe('Italian locale', () => {
+ it('should format "proprio ora" for times less than 60 seconds', () => {
+ const date = new Date(now.getTime() - 45 * 1000); // 45 seconds ago
+ expect(formatRelativeTime(date, 'it')).toBe('proprio ora');
+ });
+
+ it('should format minutes correctly with Italian grammar', () => {
+ const date1 = new Date(now.getTime() - 1 * 60 * 1000); // 1 minuto ago
+ expect(formatRelativeTime(date1, 'it')).toBe('1 minuto fa');
+
+ const date2 = new Date(now.getTime() - 5 * 60 * 1000); // 5 minuti ago
+ expect(formatRelativeTime(date2, 'it')).toBe('5 minuti fa');
+ });
+
+ it('should format hours correctly with Italian grammar', () => {
+ const date1 = new Date(now.getTime() - 1 * 60 * 60 * 1000); // 1 ora ago
+ expect(formatRelativeTime(date1, 'it')).toBe('1 ora fa');
+
+ const date2 = new Date(now.getTime() - 5 * 60 * 60 * 1000); // 5 ore ago
+ expect(formatRelativeTime(date2, 'it')).toBe('5 ore fa');
+ });
+
+ it('should format days correctly with Italian grammar', () => {
+ const date1 = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000); // 1 giorno ago
+ expect(formatRelativeTime(date1, 'it')).toBe('1 giorno fa');
+
+ const date2 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 giorni ago
+ expect(formatRelativeTime(date2, 'it')).toBe('7 giorni fa');
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('should handle dates exactly at boundaries', () => {
+ // Exactly 60 seconds
+ const date1 = new Date(now.getTime() - 60 * 1000);
+ const result1 = formatRelativeTime(date1, 'en');
+ expect(result1).toBe('1 minute ago');
+
+ // Exactly 1 hour
+ const date2 = new Date(now.getTime() - 60 * 60 * 1000);
+ const result2 = formatRelativeTime(date2, 'en');
+ expect(result2).toBe('1 hour ago');
+
+ // Exactly 24 hours
+ const date3 = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ const result3 = formatRelativeTime(date3, 'en');
+ expect(result3).toBe('1 day ago');
+ });
+
+ it('should handle future dates (negative time)', () => {
+ // Date in the future - implementation treats it as "just now" or "0 units ago"
+ const futureDate = new Date(now.getTime() + 60 * 1000);
+ const result = formatRelativeTime(futureDate, 'en');
+ // Depending on implementation, might show negative or just now
+ expect(result).toBeDefined();
+ });
+
+ it('should handle very old dates', () => {
+ const oldDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); // 1 year ago
+ const result = formatRelativeTime(oldDate, 'en');
+ expect(result).toBe('365 days ago');
+ });
+ });
+
+ describe('Unsupported locale fallback', () => {
+ it('should fallback to English for unsupported locales', () => {
+ const date = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
+ expect(formatRelativeTime(date, 'fr')).toBe('2 minutes ago');
+ expect(formatRelativeTime(date, 'de')).toBe('2 minutes ago');
+ expect(formatRelativeTime(date, 'es')).toBe('2 minutes ago');
+ });
+ });
+ });
+
+ describe('Locale code consistency', () => {
+ it('should handle the same locale codes across all functions', () => {
+ const locales = ['en', 'it'];
+
+ locales.forEach((locale) => {
+ // All functions should return non-empty strings
+ expect(getLocaleName(locale)).toBeTruthy();
+ expect(getLocaleNativeName(locale)).toBeTruthy();
+ expect(getLocaleFlag(locale)).toBeTruthy();
+ });
+ });
+
+ it('should have consistent fallback behavior', () => {
+ const unsupportedLocales = ['fr', 'de', 'es', 'invalid', ''];
+
+ unsupportedLocales.forEach((locale) => {
+ // All should fall back to English
+ expect(getLocaleName(locale)).toBe('English');
+ expect(getLocaleNativeName(locale)).toBe('English');
+ expect(getLocaleFlag(locale)).toBe('๐บ๐ธ');
+ });
+ });
+ });
+});