Fix: Enabled Manual i18n Routing Without Middleware in Astro

Error message:
Enabled manual internationalization routing without having a middleware.
Internationalization 2025-01-25

What Causes This Error?

This error occurs when you set routing: 'manual' for i18n but don’t have a middleware file. Manual routing requires you to handle locale detection and routing logic yourself via middleware.

The Problem

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr'],
    routing: 'manual',  // ❌ Requires middleware
  },
});

// No src/middleware.ts file exists!

The Fix

Create Middleware File

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware((context, next) => {
  // Your manual i18n routing logic here
  return next();
});

Common Scenarios

Basic Manual i18n Middleware

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

const locales = ['en', 'es', 'fr'];
const defaultLocale = 'en';

export const onRequest = defineMiddleware((context, next) => {
  // Extract locale from URL
  const pathname = context.url.pathname;
  const segments = pathname.split('/').filter(Boolean);
  const firstSegment = segments[0];

  // Check if first segment is a locale
  const locale = locales.includes(firstSegment)
    ? firstSegment
    : defaultLocale;

  // Store locale for pages to use
  context.locals.locale = locale;

  return next();
});

With Accept-Language Detection

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

const locales = ['en', 'es', 'fr'];
const defaultLocale = 'en';

function getPreferredLocale(acceptLanguage: string | null): string {
  if (!acceptLanguage) return defaultLocale;

  const preferred = acceptLanguage
    .split(',')
    .map(lang => lang.split(';')[0].trim().substring(0, 2))
    .find(lang => locales.includes(lang));

  return preferred || defaultLocale;
}

export const onRequest = defineMiddleware((context, next) => {
  const pathname = context.url.pathname;

  // Skip for assets
  if (pathname.startsWith('/_') || pathname.includes('.')) {
    return next();
  }

  // Check URL for locale
  const urlLocale = pathname.split('/')[1];
  if (locales.includes(urlLocale)) {
    context.locals.locale = urlLocale;
    return next();
  }

  // Detect from browser
  const acceptLanguage = context.request.headers.get('accept-language');
  const detectedLocale = getPreferredLocale(acceptLanguage);

  // Redirect to locale-prefixed URL
  if (pathname === '/') {
    return context.redirect(`/${detectedLocale}/`);
  }

  context.locals.locale = defaultLocale;
  return next();
});
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

const locales = ['en', 'es', 'fr'];
const defaultLocale = 'en';

export const onRequest = defineMiddleware((context, next) => {
  // Check cookie first
  const cookieLocale = context.cookies.get('locale')?.value;

  if (cookieLocale && locales.includes(cookieLocale)) {
    context.locals.locale = cookieLocale;
  } else {
    // Fallback to URL or default
    const urlLocale = context.url.pathname.split('/')[1];
    context.locals.locale = locales.includes(urlLocale)
      ? urlLocale
      : defaultLocale;
  }

  return next();
});

Using Astro’s i18n Utilities

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { middleware } from 'astro:i18n';

// Combine custom logic with Astro's i18n middleware
export const onRequest = defineMiddleware(async (context, next) => {
  // Your custom logic first
  console.log('Processing:', context.url.pathname);

  // Then let Astro handle i18n
  return middleware(context, next);
});

Type Definitions

// src/env.d.ts
declare namespace App {
  interface Locals {
    locale: 'en' | 'es' | 'fr';
  }
}

Or Use Automatic Routing

If you don’t need custom logic, use automatic routing instead:

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr'],
    // Remove 'manual' - use automatic routing
    routing: {
      prefixDefaultLocale: false,
    },
  },
});

When to Use Manual Routing

Use manual routing when you need:
- Custom locale detection logic
- Cookie/session-based locale
- Complex redirect rules
- Integration with external i18n libraries
- Non-standard URL patterns

Use automatic routing for:
- Standard /[locale]/path patterns
- Simpler setups
- Built-in URL helpers

Quick Checklist

  • routing: 'manual' requires middleware
  • Create src/middleware.ts
  • Handle locale detection in middleware
  • Store locale in context.locals
  • Consider automatic routing if simpler