Next.js usage
The Next adapter is split into:
- one local
18ways.config.tsfile - build-time Next integration in
next.config.js - server helpers from
@18ways/next/server - client helpers in
@18ways/next/client
Shared config
Create 18ways.config.ts in your app root.
// 18ways.config.ts
import type { WaysConfig } from '@18ways/next/config';
export default {
apiKey: process.env.NEXT_PUBLIC_18WAYS_PUBLIC_API_KEY,
baseLocale: 'en-GB',
router: 'app', // 'app', 'path', or 'none' depending on if you use app router, path router, or if you don't want the router to sync with locale at all
} satisfies WaysConfig;Then, in your next.config.js:
// next.config.js
const { withWays } = require('@18ways/next/config');
module.exports = withWays({
// your normal next config here
});App Router path routing
For App Router, locale routing is defined by the route tree itself:
- public localized routes live under
app/[lang]/... - unlocalized routes stay outside
[lang]
Path Router path routing
If you want path-based locale routing, set router: 'path' and provide acceptedLocales.
withWays() then configures native Next i18n path routing for you, with Next’s automatic locale
detection disabled so 18ways stays in charge of locale resolution.
// 18ways.config.ts
import type { WaysConfig } from '@18ways/next/config';
export default {
apiKey: process.env.NEXT_PUBLIC_18WAYS_PUBLIC_API_KEY,
baseLocale: 'en-GB',
router: 'path', // 'app', 'path', or 'none' depending on if you use app router, path router, or if you don't want the router to sync with locale at all
acceptedLocales: ['en-GB', 'fr-FR'], // keep this in sync with the languages enabled in your 18ways dashboard
} satisfies WaysConfig;Use router: 'none' when some existing routing layer already owns locale-aware URLs and you only
want 18ways to manage locale state.
Root layout
Import from @18ways/next/server.
import { WaysRoot, htmlAttrs } from '@18ways/next/server';
export default async function RootLayout({ children, params }) {
const attrs = await htmlAttrs({ params });
return (
<html {...attrs}>
<body>
<WaysRoot params={params}>{children}</WaysRoot>
</body>
</html>
);
}Proxy
In App Router, use proxy.ts to handle the root locale redirect and any domain canonicalization:
export { default, config } from '@18ways/next/proxy';If your app already has its own proxy.ts or middleware.ts, compose getWaysProxyResponse()
and return it before the rest of your app-specific logic:
import type { NextFetchEvent, NextRequest } from 'next/server';
import { getWaysProxyResponse } from '@18ways/next/proxy';
export default async function middleware(request: NextRequest, event: NextFetchEvent) {
const waysResponse = await getWaysProxyResponse(request);
if (waysResponse) {
return waysResponse;
}
// your existing middleware logic
}Client-side locale changes
Use useLocale() from @18ways/next/client.
'use client';
import { LanguageSwitcher } from '@18ways/react';
import { useLocale } from '@18ways/next/client';
export function LocaleControls() {
const { locale, setLocale } = useLocale();
return <LanguageSwitcher currentLocale={locale} onLocaleChange={setLocale} />;
}When the current route is localized, setLocale() preserves the route and updates the locale
segment. When the current route is unlocalized, it keeps the route unlocalized and updates locale
state.
Locale-aware links and navigation
Import from @18ways/next/client:
import { Link, useRouter, useUnlocalizedPathname } from '@18ways/next/client';Linklocalizes internal hrefs automaticallyuseRouter()applies the same rules topush()andreplace()useUnlocalizedPathname()lets you compare route state without the locale prefix
Use the locale override only when you need to force a choice:
locale="fr-FR"forces a localized targetlocale={false}forces an unlocalized target
Translated metadata
generateWaysMetadata() can translate string metadata fields for you.
export async function generateMetadata({ params }) {
return generateWaysMetadata(
(t) => ({
title: t('Pricing'),
description: t('Simple pricing for runtime translation'),
}),
{ params, pathname: '/pricing' }
);
}