Skip to main content

Internationalized routing

Ness serves each locale from its own URL prefix. Routes are written once; the router mounts the tree under a :locale segment.

Configuration​

ness.config.mjs
import { defineNessConfig } from '@nessframework/router';

export const i18n = {
locales: ['en', 'de', 'pt-BR'],
defaultLocale: 'en',
strategy: 'prefix-except-default',
};

export default defineNessConfig({
router: { i18n },
});
app/routes.ts
import { nessRoutes } from '@nessframework/router';
import { i18n } from '../ness.config.mjs';

export default nessRoutes({ i18n });

The configuration is passed in both places on purpose. ness.config.mjs validates it and records it in the build manifest; app/routes.ts is where routes are actually generated, and injecting a route tree behind your back would silently override a hand-written one.

Each locale becomes its own static path segment β€” /de/..., /fr/... β€” not a shared :locale parameter. A parameter would outrank both your not-found route and any top-level dynamic route, so /anything would be read as a locale and your 404 would never render. There is consequently no params.locale; read the locale with useLocale or getLocale below.

Strategies​

StrategyDefault localeOther locales
prefix-except-default (default)/pricing/de/pricing
prefix/en/pricing/de/pricing

prefix-except-default keeps existing URLs working when a site adds its first translation. Choose prefix when no locale should be privileged.

An unknown prefix β€” /xx/pricing β€” returns 404 rather than rendering the page under a locale that does not exist.

Reading the locale​

import { useLocale } from '@nessframework/router/i18n';
import { i18n } from '../../ness.config.mjs';

export default function Page() {
const locale = useLocale(i18n);
return <p>{locale}</p>;
}

In a loader or action, read it from the request:

import { getLocale } from '@nessframework/router/i18n';

export async function loader({ request }) {
return getProducts(getLocale(request, i18n));
}

Both return the default locale on unprefixed routes, so nothing has to special-case it.

Linking between locales​

import { localizePath } from '@nessframework/router';

localizePath('/pricing', 'de', i18n); // '/de/pricing'
localizePath('/de/pricing', 'en', i18n); // '/pricing'

Detecting a visitor's language​

app/routes/middleware.ts
import { createLocaleMiddleware } from '@nessframework/router/i18n';
import { i18n } from '../../ness.config.mjs';

export default createLocaleMiddleware(i18n);

Unprefixed requests are redirected to the locale from Accept-Language; quality values are honoured and a region falls back to its base language, so de-AT matches a configured de. The choice is remembered in a cookie and preferred over the header afterwards, so switching language manually is not undone on the next navigation.

The redirect sets Vary: accept-language, cookie so a shared cache cannot serve one visitor's language to another.