Practical Localization in Next.js: useTranslate, Localized APIs, Mappers, and Caching
A practical architecture guide for multilingual Next.js apps using useTranslate, backend fields like description_ar/description_en, mapper-layer normalization, and cache-aware fetching so UI components stay clean.
Practical Localization in Next.js: useTranslate, Localized APIs, Mappers, and Caching
Localization in production apps is more than replacing labels. The real challenge is keeping translation concerns out of your UI components while still handling localized API content efficiently.
This guide covers a practical approach for Next.js apps:
- UI labels via
useTranslate - content localization from API fields like
description_ar/description_en - mapper-layer normalization to keep UI clean
- caching strategies that respect locale boundaries
Two Different Localization Problems
Most apps have both:
- Static/UI text (buttons, headings, validation messages)
- Dynamic/content text from backend (descriptions, titles, bios, CMS-like content)
Use separate mechanisms for each. Mixing them usually creates messy components.
useTranslate for UI Strings
A hook like useTranslate is ideal for static UI copy.
Example intent:
const t = useTranslate();
return <button>{t("common.save")}</button>;
Best practices
- use stable keys (
profile.edit.title) instead of literal strings - organize by domain (
common,auth,projects) - avoid embedding business logic in translation calls
useTranslate should handle presentation strings, not API field selection rules.
Localizing API Responses: _ar and _en Fields
A common backend shape:
{
"id": 12,
"title_ar": "هندسة البرمجيات",
"title_en": "Software Engineering",
"description_ar": "...",
"description_en": "..."
}
This is simple and explicit. The challenge is where to pick the right field.
Don’t do this in UI components
Avoid repeating this across JSX:
<p>{locale === "ar" ? item.description_ar : item.description_en}</p>
If done everywhere, you get:
- duplicated logic
- fallback inconsistencies
- harder testing
- harder future API changes
Use a Mapper Layer (Clean UI)
Move locale-aware selection into a mapper inside your data/infrastructure layer.
Example mapper intent:
function pickLocalized(
locale: "ar" | "en",
ar?: string | null,
en?: string | null,
): string {
if (locale === "ar") return ar || en || "";
return en || ar || "";
}
Then map API DTO to UI/domain model:
function mapProject(dto: ProjectDto, locale: Locale): Project {
return {
id: dto.id,
title: pickLocalized(locale, dto.title_ar, dto.title_en),
description: pickLocalized(locale, dto.description_ar, dto.description_en),
};
}
Now UI only uses project.title and project.description with no locale conditionals.
Why Mapper-First Localization Scales
Benefits:
- single source of truth for fallback rules
- consistent behavior app-wide
- UI stays focused on rendering
- easy to migrate backend field schema later
If backend changes from _ar/_en to nested objects, only mapper changes.
Locale Source in Next.js
Choose one canonical locale source (avoid multiple truth sources):
- route segment (
/ar/...,/en/...) - cookie
- Accept-Language negotiation on first visit
Expose locale via context/hook once, then pass to data layer where needed.
API Design Options for Localized Content
There are several valid API strategies.
Option A: separate fields (title_ar, title_en)
Pros
- explicit and easy to query
- simple serializer implementation
Cons
- schema grows with each locale
- mapper required to normalize
Option B: nested localized object
{
"title": { "ar": "...", "en": "..." },
"description": { "ar": "...", "en": "..." }
}
Pros
- extensible for many locales
- cleaner domain concept
Cons
- needs JSON support/validation conventions
Option C: locale-param response
GET /projects?locale=ar returns already localized fields.
Pros
- lightweight payload
- frontend simpler
Cons
- less flexible when client switches locale without refetch
- cache keying becomes locale-sensitive
No single best option for all teams. Mapper-based normalization works with all three.
Caching Without Locale Bugs
Localization + caching fails when locale is not part of cache identity.
Frontend query cache
If using TanStack Query, include locale in query key:
useQuery({
queryKey: ["projects", locale],
queryFn: () => fetchProjects(locale),
});
This prevents Arabic data being reused in English views and vice versa.
Server cache / revalidation
For Next.js data caching:
- include locale in fetch URL or cache tags
- separate revalidation scope per locale
Example patterns:
/api/projects?locale=ar- tags:
projects:ar,projects:en
Mapper-level memoization
If mapping is expensive, memoize mapped output by (id, locale, updatedAt) or equivalent stable keys.
Do not memoize with locale-agnostic keys.
Suggested Architecture (Feature-Based)
features/projects/
application/
use-cases/
domain/
entities/
infrastructure/
api/
ProjectApiClient.ts
mappers/
ProjectMapper.ts
repositories/
ProjectRepository.ts
Flow:
- repository fetches DTOs
- mapper converts DTO + locale -> domain model
- UI renders normalized model
useTranslatehandles only static UI labels
This keeps concerns separated and testable.
Fallback Strategy You Should Define Explicitly
Document fallback rules centrally, e.g.:
- requested locale value
- default locale value
- empty string / placeholder
Inconsistent fallback behavior is one of the most visible i18n quality issues.
Testing Strategy
Mapper unit tests
arrequested + missing Arabic value => fallback to Englishenrequested + missing English value => fallback to Arabic- both missing => empty/placeholder
Integration tests
- cache key includes locale
- route locale switches trigger correct data rendering
UI tests
- components render normalized fields, not
_ar/_endirectly
Common Mistakes
- locale conditionals inside every JSX component
- cache keys without locale
- mixing
useTranslatewith API field picking logic - exposing raw DTO shape directly to presentation layer
Final Takeaway
A maintainable localization system in Next.js usually looks like this:
useTranslatefor UI strings- API returns localized data (
_ar/_enor equivalent) - mapper selects correct locale + fallback
- cache keys are locale-aware
- UI consumes normalized models only
That separation keeps components clean, prevents locale cache bugs, and makes localization easier to scale as your content grows.
Which Translation Library Provides useTranslation?
Good catch: in many Next.js projects, the hook name is useTranslation (not useTranslate).
Common libraries:
- react-i18next / next-i18next: exposes
useTranslation() - next-intl: commonly uses
useTranslations()
If your project uses react-i18next, typical usage is:
import { useTranslation } from "react-i18next";
export function SaveButton() {
const { t, i18n } = useTranslation();
return <button>{t("common.save")}</button>;
}
If your codebase has a custom wrapper hook like useTranslate, that usually sits on top of one of these libraries to standardize usage across the app.
The architecture guidance stays the same:
- use translation hooks (
useTranslation/useTranslations/ wrapper hooks) for UI labels - use mappers for localized API content (
description_ar,description_en) - keep locale-aware caching keyed by locale
Rate this post
All fields are optional. Just stars is fine.