If your Next.js app is translated but the browser tab, the 404 page, the form errors and the link preview are still in English, the cause is almost always the same: those strings are produced outside a component, so a useTranslations call never touched them. globalize.now is AI-powered localization infrastructure, and this is the layer we see left half-done when an AI agent adds i18n to an App Router project: the visible page is converted, the rest of the surface is not.

Below is where each of those strings lives and the next-intl call that handles it. Every example follows the current next-intl documentation, fetched 2026-10-28.

Where do untranslated strings hide in an App Router app?

They hide in six places that are not React components rendering text. A coding agent that adds i18n by working through your components will convert the visible UI and walk straight past these:

  1. Page titles and descriptions in generateMetadata.
  2. Validation errors and result messages in Server Actions.
  3. not-found.tsx, the 404 page.
  4. error.tsx, the runtime error page.
  5. Generated files such as opengraph-image.tsx, manifest.ts and sitemap.ts.
  6. Route Handlers that return text.

The common thread is that none of them is rendered inside your [locale] layout tree in the normal way, so the locale has to be handed to them or read differently. The Next.js 16 routing post covers the routing wiring; this one is about what that wiring does not reach.

How do you translate page titles and descriptions?

Call getTranslations from next-intl/server inside generateMetadata. The function can be exported from a page or a layout, and it is async, so the awaitable API is the right one:

// app/[locale]/page.tsx
import {getTranslations} from 'next-intl/server';

export async function generateMetadata() {
  const t = await getTranslations('Metadata');
  return {title: t('title'), description: t('description')};
}

The trap is the static form. Agents often write export const metadata = {title: 'Dashboard'} because it is shorter, and a constant cannot call a translation function. Convert it to the function form, and add the same keys to the catalog for every locale. This is also the search-facing text, which is why translating the app without translating its metadata does little for rankings.

How do you translate Server Action and validation messages?

Pass the locale to the action and call getTranslations with both a namespace and that locale. A Server Action does not know which language the page is in, so next-intl's documentation has you supply it, for example as an argument:

'use server';
import {getTranslations} from 'next-intl/server';

export async function subscribe(locale: string, formData: FormData) {
  const t = await getTranslations({locale, namespace: 'Subscribe'});
  // ...validate, then return t('success') or t('invalidEmail')
}

One caveat from the same documentation: consider what happens if a visitor can switch language while an action's message is on screen. With a [locale] segment in the URL this is handled automatically, because the route changes. Without one, you may want to clear the message yourself, for instance by giving the component key={locale} so its state resets.

For form validation with Zod, the documented approach is an error map. Zod accepts one per safeParse call, and the map can call getTranslations. Put that in one async helper and pass its result as the error option on each parse. Then your schemas hold rules, not English sentences, and AI-generated forms stop shipping "Invalid email" into a Latvian page.

How do you translate the 404 and error pages?

Put not-found.tsx inside app/[locale]/, then add a catch-all so it actually fires. Next.js renders that file when a segment calls notFound(), and an unknown URL does not call it by itself:

// app/[locale]/[...rest]/page.tsx
import {notFound} from 'next/navigation';

export default function CatchAllPage() {
  notFound();
}

Requests that the next-intl middleware does not match carry no locale at all, so the [locale] 404 never applies to them. The documentation's answer is app/global-not-found.tsx, which Next.js documents as experimental and which renders its own html and body. For an unknown [locale] value, the documentation says to call notFound(), typically in i18n/request.ts.

error.tsx is different. Next.js requires it to be a Client Component, and it renders under your layout, so it needs a NextIntlClientProvider carrying at least the Error namespace. Pass only those messages rather than the whole catalog:

// app/[locale]/layout.tsx (excerpt)
const messages = await getMessages();
// ...
<NextIntlClientProvider locale={locale} messages={pick(messages, 'Error')}>

Then useTranslations('Error') works inside error.tsx. The page does not cover global-error, so check that file by hand if you have one.

What about share images, manifest and sitemap?

They each need the locale supplied, because they live outside the layout tree. For opengraph-image.tsx, read locale from params and call getTranslations({locale, namespace}). With a customized localePrefix, the documentation warns the generated route can become unreachable, because Next.js does not see proxy (middleware) rewrites; the fix is to make the matcher skip paths ending in opengraph-image.

app/manifest.ts sits outside [locale], so next-intl cannot infer a locale from the path. Pick a representative one, usually the default, and pass it explicitly. For sitemap.ts, add alternates.languages entries and build each URL with the async getPathname({locale, href}) from your navigation module, prefixed with the host. next-intl also sends a link response header advertising the language versions by default; a sitemap is the option when you need control over CMS-driven slugs.

How do you handle Route Handlers?

Take the locale from a search param, a segment or the accept-language header, validate it with hasLocale(routing.locales, locale), and return a 400 if it fails. Then call getTranslations({locale, namespace}). Skipping the validation means an arbitrary string reaches your catalog lookup.

How do you find the English your agent left behind?

Run these from the project root. They are rough by design; the goal is a list to read, not a score.

# 1. Static metadata that cannot be translated
grep -rn "export const metadata" app

# 2. Titles and descriptions already in generateMetadata
grep -rn "title:" app --include=page.tsx --include=layout.tsx

# 3. Server Actions with literal return messages
grep -rln "use server" app src 2>/dev/null | xargs grep -n "message:\|error:"

# 4. Zod schemas with inline messages
grep -rn "message:\s*['\"]" app src 2>/dev/null

# 5. 404 and error files, and whether a catch-all exists
ls app/\[locale\]/not-found.tsx app/\[locale\]/error.tsx "app/[locale]/[...rest]/page.tsx" 2>&1

Anything in the first four results that is a plain English literal belongs in the catalog. Why agents keep adding hardcoded strings explains why this recurs: the agent follows the visible pattern and the visible pattern is the component. If you use Copilot, the instruction-file limits for Copilot apply here too.

What does this change about your catalog?

It adds keys, and keys are where drift starts. Every item above pulls new entries into the catalog, such as Metadata, Error, Subscribe and the validation messages, and each needs a translation in every locale you ship. A project that translates its components once and later adds a Server Action or a new page title is the same project described in why translation files drift out of sync.

next-intl serves the translations at runtime and we do not replace it. globalize.now sits one layer up: you connect the repository in the app, the codebase is converted once into a catalog, and push jobs then translate new catalog units as they appear, delivered as changes you review. The Next.js integration describes the setup, and what we do and do not touch in an existing i18n setup covers the case where next-intl is already wired in by hand.

Where to start

Run the five greps, convert any static metadata export, add the catch-all route, and give error.tsx its provider. Then check the catalog for the new namespaces in every locale. If you shipped an AI-built app and want conversion and translation handled rather than maintained, the vibe coders page is the overview, the developer reference covers agent setups, and pricing is on its own page.

globalize.now turns hardcoded app copy into translation-ready locale files and keeps them updated as you ship.

Try globalize.now free