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:
@@ -1,4 +1,7 @@
|
||||
import { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
// Code-split LoginForm - heavy with react-hook-form + validation
|
||||
const LoginForm = dynamic(
|
||||
@@ -15,6 +18,17 @@ const LoginForm = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -3,9 +3,28 @@
|
||||
* Users set a new password using the token from their email
|
||||
*/
|
||||
|
||||
import { Metadata } from 'next';
|
||||
import { Suspense } from 'react';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import PasswordResetConfirmContent from './PasswordResetConfirmContent';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'auth.passwordResetConfirm' });
|
||||
|
||||
return generatePageMetadata(
|
||||
locale as Locale,
|
||||
t('title'),
|
||||
t('instructions'),
|
||||
'/password-reset/confirm'
|
||||
);
|
||||
}
|
||||
|
||||
export default function PasswordResetConfirmPage() {
|
||||
return (
|
||||
<Suspense
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
* Users enter their email to receive reset instructions
|
||||
*/
|
||||
|
||||
import { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
// Code-split PasswordResetRequestForm
|
||||
const PasswordResetRequestForm = dynamic(
|
||||
@@ -22,6 +25,17 @@ const PasswordResetRequestForm = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'auth.passwordReset' });
|
||||
|
||||
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/password-reset');
|
||||
}
|
||||
|
||||
export default function PasswordResetPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
// Code-split RegisterForm (313 lines)
|
||||
const RegisterForm = dynamic(
|
||||
@@ -15,6 +18,17 @@ const RegisterForm = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'auth.register' });
|
||||
|
||||
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/register');
|
||||
}
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -3,16 +3,28 @@
|
||||
* Displayed when users try to access resources they don't have permission for
|
||||
*/
|
||||
|
||||
/* istanbul ignore next - Next.js type import for metadata */
|
||||
import type { Metadata } from 'next';
|
||||
import { Link } from '@/lib/i18n/routing';
|
||||
import { ShieldAlert } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export const metadata: Metadata = /* istanbul ignore next */ {
|
||||
title: '403 - Forbidden',
|
||||
description: 'You do not have permission to access this resource',
|
||||
};
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'errors' });
|
||||
|
||||
return generatePageMetadata(
|
||||
locale as Locale,
|
||||
t('unauthorized'),
|
||||
t('unauthorizedDescription'),
|
||||
'/forbidden'
|
||||
);
|
||||
}
|
||||
|
||||
export default function ForbiddenPage() {
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/lib/i18n/routing';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import { generateLocalizedMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import '../globals.css';
|
||||
import { Providers } from '../providers';
|
||||
import { AuthProvider } from '@/lib/auth/AuthContext';
|
||||
@@ -23,10 +24,14 @@ const geistMono = Geist_Mono({
|
||||
preload: false, // Only preload primary font
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'FastNext Template',
|
||||
description: 'FastAPI + Next.js Template',
|
||||
};
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
return generateLocalizedMetadata(locale as Locale);
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
|
||||
21
frontend/src/app/robots.ts
Normal file
21
frontend/src/app/robots.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
/**
|
||||
* Generate robots.txt
|
||||
* Configures search engine crawler behavior
|
||||
*/
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
// Disallow authenticated routes
|
||||
disallow: ['/admin/', '/settings/'],
|
||||
},
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
49
frontend/src/app/sitemap.ts
Normal file
49
frontend/src/app/sitemap.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { routing } from '@/lib/i18n/routing';
|
||||
|
||||
/**
|
||||
* Generate multilingual sitemap
|
||||
* Includes all public routes for each supported locale
|
||||
*/
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
|
||||
// Define public routes that should be in sitemap
|
||||
const publicRoutes = [
|
||||
'', // homepage
|
||||
'/login',
|
||||
'/register',
|
||||
'/password-reset',
|
||||
'/demos',
|
||||
'/dev',
|
||||
'/dev/components',
|
||||
'/dev/layouts',
|
||||
'/dev/forms',
|
||||
'/dev/docs',
|
||||
];
|
||||
|
||||
// Generate sitemap entries for each locale
|
||||
const sitemapEntries: MetadataRoute.Sitemap = [];
|
||||
|
||||
routing.locales.forEach((locale) => {
|
||||
publicRoutes.forEach((route) => {
|
||||
const path = route === '' ? `/${locale}` : `/${locale}${route}`;
|
||||
|
||||
sitemapEntries.push({
|
||||
url: `${baseUrl}${path}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: route === '' ? 'daily' : 'weekly',
|
||||
priority: route === '' ? 1.0 : 0.8,
|
||||
// Language alternates for this URL
|
||||
alternates: {
|
||||
languages: {
|
||||
en: `${baseUrl}/en${route}`,
|
||||
it: `${baseUrl}/it${route}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return sitemapEntries;
|
||||
}
|
||||
Reference in New Issue
Block a user