How to translate page metadata in React Router v7
Translate metadata for search and social
Problem
Page metadata—titles and descriptions—appears outside the page itself, in browser tabs, bookmarks, search results, and social media previews. When this metadata doesn't match the page's language, it creates a jarring inconsistency. A Spanish page with an English title confuses users before they even see the content. Search engines interpret this mismatch as a signal that the page is poorly localized or low quality, potentially lowering its ranking in language-specific results. Users may abandon the page before it loads, assuming it's not in their language.
Solution
Translate page metadata to match the current language by exporting a meta function from your route module. Use react-intl's formatMessage API with Message Descriptors to translate title and description strings, ensuring metadata uses the same translation resources as page content. This keeps everything consistent between what shows in browser tabs, search results, and the page itself.
Steps
1. Create a helper to access intl outside components
The intl object provides formatMessage and can be accessed via the useIntl hook in components or created directly with createIntl in non-React environments. Since the meta function runs outside the React component tree, create a helper that builds an intl instance from your messages.
import { createIntl, createIntlCache } from "react-intl";
const cache = createIntlCache();
export function createIntlForLocale(
locale: string,
messages: Record<string, string>,
) {
return createIntl(
{
locale,
messages,
},
cache,
);
}
This helper creates an intl instance that can format messages in any function, not just React components.
2. Load messages in a parent route loader
Route loaders return data that components access via loaderData props. Load your translation messages in a parent route so they're available to child routes.
import type { Route } from "./+types/root";
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url);
const locale = url.pathname.split("/")[1] || "en";
const messages = await import(`../translations/${locale}.json`);
return {
locale,
messages: messages.default,
};
}
The meta function receives a matches parameter containing loader data from all matched routes, making parent loader data accessible to child route meta functions.
3. Export a meta function that translates metadata
Export a meta function from your route module that returns an array of meta descriptor objects. Access the parent loader data from matches and use your intl helper to translate strings.
import type { Route } from "./+types/product";
import { createIntlForLocale } from "~/utils/intl";
export function meta({ matches }: Route.MetaArgs) {
const rootMatch = matches.find((match) => match.id === "root");
const { locale, messages } = rootMatch?.data || {
locale: "en",
messages: {},
};
const intl = createIntlForLocale(locale, messages);
return [
{
title: intl.formatMessage({
id: "product.meta.title",
defaultMessage: "Product Details",
}),
},
{
name: "description",
content: intl.formatMessage({
id: "product.meta.description",
defaultMessage: "View detailed information about this product",
}),
},
];
}
The formatMessage function accepts a Message Descriptor with an id and defaultMessage, returning the translated string for the current locale.
4. Add translated metadata strings to your message files
Add the metadata translation keys to each locale's message file so formatMessage can find them.
{
"product.meta.title": "Détails du produit",
"product.meta.description": "Voir les informations détaillées sur ce produit"
}
When users navigate to this route, the Meta component in your root layout renders all meta tags created by route meta exports, displaying translated titles and descriptions that match the page language.