Documentation
Documentation
Introduction

Getting Started

Getting started
Getting StartedInstallationQuick StartProject Structure

Configuration

Configuration
ConfigurationEnvironment ConfigurationEdge ConfigDatabaseAuth SecretStripeFirebaseStorageGoogle Maps And Cloud Service AccountOAuth ProvidersEmail DeliverySentryFeature Flags

Architecture

Architecture
Architecture OverviewTech StackoRPC MiddlewareDesign Principles

Patterns

Patterns
Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

Database
DatabaseSetupSchema DefinitionDatabase OperationsMigrationsCaching
Data Tables

API

oRPCProceduresRoutersoRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

Storage
StorageConfigurationUsageBuckets
Stripe Billing

Extra

Caching

Templates

Templates
Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate oRPC RouterAdd Translations

Development

Development
DevelopmentCommandsAI AgentsBest Practices
Pulling Updates

Locale Routing

URL-based locale detection

The project uses locale-based routing with automatic locale detection and localized navigation utilities.

URL Structure

All routes are prefixed with the locale:

/en/about
/it/about
/en/dashboard
/it/dashboard

The locale segment is automatically handled by the middleware.

Automatic Locale Detection

The middleware detects the user's locale from multiple sources (in order of priority):

  1. URL path segment (highest priority)
  2. Cookie (NEXT_LOCALE)
  3. Accept-Language header
  4. Default locale (fallback)

Navigation Utilities

All navigation utilities from @/i18n/navigation automatically handle locale prefixing.

The locale parameter is optional in all navigation functions. The current locale is automatically used from context.

Link Component

import { Link } from "@/i18n/navigation";
import { PageAbout } from "@/routes";

<Link href={PageAbout()}>About</Link>
// Renders: /en/about (or /{locale}/about)

// With declarative route
<PageAbout.Link>About</PageAbout.Link>

Client-Side Navigation

"use client";

import { useRouter } from "@/i18n/navigation";
import { PageDashboard, PageUserId } from "@/routes";

function MyComponent() {
  const router = useRouter();

  const handleNavigate = () => {
    // Navigate to a route - locale is automatically applied
    router.push(PageDashboard()); // → /en/dashboard

    // With params
    router.push(PageUserId({ id: "123" }));

    // Replace instead of push
    router.replace(PageHome());

    // Explicit locale (rarely needed)
    router.push(PageDashboard({ locale: "it" })); // → /it/dashboard
  };

  return <Button onClick={handleNavigate}>Go to Dashboard</Button>;
}

Server-Side Redirects

import { redirect } from "@/i18n/navigation";
import { PageLogin } from "@/routes";
import { getAuth } from "@/lib/auth/server";

export default async function ProtectedPage() {
  const auth = await getAuth();

  if (!auth) {
    redirect(PageLogin()); // Locale is automatically applied
  }

  return <div>Protected content</div>;
}

Important

The Next.js redirect() function only works in:

  • Server Components
  • Server Actions

It does NOT work in:

  • oRPC procedures (return redirect URL instead)
  • Middleware (use NextResponse.redirect())
  • API Routes (use NextResponse.redirect())

In oRPC Procedures

Return the redirect URL in the response and handle it client-side:

// Server: oRPC procedure
login: publicProcedure
  .input(loginSchema)
  .mutation(async ({ ctx, input }) => {
    const result = await signIn(input);

    return {
      success: true,
      payload: {
        redirectUrl: PageDashboard(),
      },
    };
  }),

// Client: Handle redirect
const mutation = api.auth.login.useMutation({
  onSuccess: (data) => {
    if (data.payload?.redirectUrl) {
      router.push(data.payload.redirectUrl);
    }
  },
});

usePathname Hook

import { usePathname } from "@/i18n/navigation";
import { usePathnameWithoutLocale } from "@/lib/utils/navigation-utils";

const pathname = usePathname();
// URL: /en/dashboard → pathname: /en/dashboard

const pathname = usePathnameWithoutLocale();
// URL: /en/dashboard → pathname: /dashboard

Check Active Route

import { navUtils } from "@/lib/utils/navigation-utils";

const pathname = usePathname();
const isActive = navUtils.pathMatches(pathname, PageDashboard);

Locale Switching

Locale Switcher Component

import { LocaleSwitcher } from "@/components/locale-switcher";

<LocaleSwitcher />

This renders a dropdown with all configured locales.

Programmatic Locale Switch

"use client";

import { useRouter, usePathname } from "@/i18n/navigation";

function SwitchLocale() {
  const router = useRouter();
  const pathname = usePathname();

  const switchToItalian = () => {
    router.push(pathname, { locale: "it" });
  };

  return <Button onClick={switchToItalian}>Switch to Italian</Button>;
}

Configuration

Supported Locales

Configure locales in src/i18n/config.tsx:

export const locales = ["en", "it"] as const;
export const defaultLocale = "en";

Locale Cookie

The locale is stored in a cookie for persistence:

// src/config/app.ts
Config.Navigation.LOCALE_COOKIE_KEY = "NEXT_LOCALE";

Adding a New Locale

Step 1: Update config

Add the locale to src/i18n/config.tsx

Step 2: Create translation files

cp -r src/messages/dictionaries/en src/messages/dictionaries/es

Step 3: Translate content

Update all JSON files in the new locale folder

Step 4: Add Zod errors

cp src/messages/zod-errors/en.json src/messages/zod-errors/es.json

Step 5: Test routing

Navigate to /es/* routes to verify

Middleware Configuration

The locale proxy handles detection and routing in src/proxy.ts:

import { i18n } from "@/i18n/routing";
import createMiddleware from "next-intl/middleware";
import { NextResponse, type NextRequest } from "next/server";

const intlMiddleware = createMiddleware(i18n);

export const config = {
  matcher: ["/((?!api|monitoring|icons|assets|_next|_vercel|.*\\..*).*)"],
};

export async function proxy(request: NextRequest) {
  return intlMiddleware(request) ?? NextResponse.next();
}

SEO & Metadata

Localized Metadata

import { getLocalizedMetadata } from "@/i18n/seo";

export async function generateMetadata({ params }: Props) {
  const { locale } = await params;

  return getLocalizedMetadata({
    locale,
    namespace: "pageAbout",
    titleKey: "meta.title",
    descriptionKey: "meta.description",
  });
}

Alternate Links

The middleware automatically adds alternate locale links for SEO:

<link rel="alternate" hreflang="en" href="/en/about" />
<link rel="alternate" hreflang="it" href="/it/about" />

Best Practices

  1. Use declarative routing - Always use route functions like PageHome() instead of hardcoded paths
  2. Let middleware handle locale - Don't manually add locale prefixes to URLs
  3. Handle redirects correctly - Use the right redirect method based on context (server component vs oRPC)
  4. Test all locales - Verify navigation works correctly in all supported locales

Always use the localized navigation utilities from @/i18n/navigation instead of Next.js built-in navigation.

On this page

URL Structure
Automatic Locale Detection
Navigation Utilities
Link Component
Client-Side Navigation
Server-Side Redirects
In oRPC Procedures
usePathname Hook
Check Active Route
Locale Switching
Locale Switcher Component
Programmatic Locale Switch
Configuration
Supported Locales
Locale Cookie
Adding a New Locale
Step 1: Update config
Step 2: Create translation files
Step 3: Translate content
Step 4: Add Zod errors
Step 5: Test routing
Middleware Configuration
SEO & Metadata
Localized Metadata
Alternate Links
Best Practices