Localize your app

Developer preview

This feature is in developer preview. Use with caution as it may change unexpectedly. For more information, contact us.

Developer Preview

This page documents the developer preview of the SDK, version 20.81.1-alpha.25.sha-f076f9b857.

Task: show each page in the correct locale, in the live site, the preview and the static build.

The Uniform Route API returns the composition in the locale of the route. The locale comes from the path, or from the locale option of the middleware. For the setup of locales in Uniform, refer to Localization.

The locale goes into the page state, and from there to the Route API. The SDK sets pageState.locale in this sequence:

  1. The locale option of the middleware, when you set it.
  2. Else, the locale dynamic input of the matched project map node. For example, the node /:locale/about matches /fr/about, and the locale is fr. The resolver sets it after the route matches.

Components can read the locale in context.pageState.locale, and the dynamic inputs in context.dynamicInputs.

This is the most frequent setup. The locale is the first segment of the URL, and the project map has a :locale node.

A visitor can open a URL without a locale, for example /about. Use rewriteRequestPath to add the default locale:

middleware.ts

import { vercelUniformEdgeMiddleware } from "@uniformdev/next-app-router/vercel"; const locales = ["en", "fr", "de"]; const defaultLocale = "en"; export default vercelUniformEdgeMiddleware({ rewriteRequestPath: async ({ url }) => { const [firstSegment] = url.pathname.split("/").filter(Boolean); const hasLocale = firstSegment !== undefined && locales.includes(firstSegment); return { path: hasLocale ? url.pathname : `/${defaultLocale}${url.pathname}` }; }, });

The URL in the browser does not change. Only the route path in the code gets the locale.

When the locale is not in the URL, set the locale option for each request:

middleware.ts

import { handleUniformRoute } from "@uniformdev/next-app-router/middleware"; import type { NextRequest } from "next/server"; export default function middleware(request: NextRequest) { const locale = request.cookies.get("NEXT_LOCALE")?.value ?? "en"; return handleUniformRoute({ request, locale }); }

The locale goes into the code. Thus each locale has its own cached page.

You can use a [locale] segment in the app folder, for example to set <html lang> in a layout. Return the new destination from rewriteDestinationPath:

middleware.ts

import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; const locales = ["en", "fr", "de"]; export default uniformMiddleware({ rewriteDestinationPath: async ({ code, pageState, source }) => { // The route path starts with the locale, for example /fr/about. const [firstSegment] = pageState.routePath.split("/").filter(Boolean); const locale = firstSegment !== undefined && locales.includes(firstSegment) ? firstSegment : "en"; return source === "route" ? `/${locale}/uniform/${code}` : `/${locale}/playground/${code}`; }, });

Then put the pages in app/[locale]/uniform/[code]/page.tsx and app/[locale]/playground/[code]/page.tsx.

pageState.locale has a value in the middleware only when you set the locale option. The locale dynamic input of the route is known only after the page resolves the route. Thus the example reads the locale from the route path. A playground request has the composition ID as its route path, so it uses the default locale.

Canvas sends the selected preview locale to the preview handler in the locale query string. The handler gives it to your functions, but it does not add it to the redirect URL of a composition. If your URLs contain the locale, add it in resolveFullPath:

app/api/preview/route.ts

import { createPreviewGETRouteHandler, createPreviewOPTIONSRouteHandler, createPreviewPOSTRouteHandler, } from "@uniformdev/next-app-router/handler"; export const GET = createPreviewGETRouteHandler({ // The project map path can contain the :locale segment already. resolveFullPath: ({ path, slug, locale }) => { const target = path ?? slug; if (!target || !locale || target.startsWith(`/${locale}`)) return target; return `/${locale}${target}`; }, }); export const POST = createPreviewPOSTRouteHandler(); export const OPTIONS = createPreviewOPTIONSRouteHandler();

For a pattern preview, the handler adds locale to the query string of the playground URL. The playground does not use this value. It uses the locale option of the middleware only.

warning

Do not add the locale to the playground path with processPlaygroundPath. The middleware finds a pattern preview only when the path is the same as playgroundPath.

Put each locale and path in the paths of createUniformStaticParams. This example uses a project map with a /:locale node:

app/uniform/[code]/page.tsx

import { createUniformStaticParams, getProjectMapClient } from "@uniformdev/next-app-router"; const locales = ["en", "fr", "de"]; export const generateStaticParams = async () => { const { nodes } = await getProjectMapClient({ cache: { type: "default" } }).getNodes({}); // The node "/:locale/about" gives "/en/about", "/fr/about" and "/de/about". // Nodes with other dynamic segments, for example "/:locale/products/:slug", are skipped. const paths = (nodes ?? []) .map((node) => node.path) .filter((path) => path.startsWith("/:locale") && !path.slice("/:locale".length).includes(":")) .flatMap((path) => locales.map((locale) => path.replace("/:locale", `/${locale}`))); return createUniformStaticParams({ paths }); };

Use the same path format as the middleware. If the middleware sets the locale option, set the same locale in createUniformStaticParams. Then the prebuilt codes are the same as the codes from the middleware:

export const generateStaticParams = () => createUniformStaticParams({ paths: ["/about", "/contact"], locale: "fr" });

A path that has no composition gives no code. createUniformStaticParams does not send the locale to the Route API. The Route API finds the locale from the path.