Hazem Azzam

All posts
Writing

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.

4 min read
nextjslocalizationi18narchitecturecachingfrontend

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:

  1. Static/UI text (buttons, headings, validation messages)
  2. 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:

  1. repository fetches DTOs
  2. mapper converts DTO + locale -> domain model
  3. UI renders normalized model
  4. useTranslate handles only static UI labels

This keeps concerns separated and testable.

Fallback Strategy You Should Define Explicitly

Document fallback rules centrally, e.g.:

  1. requested locale value
  2. default locale value
  3. empty string / placeholder

Inconsistent fallback behavior is one of the most visible i18n quality issues.

Testing Strategy

Mapper unit tests

  • ar requested + missing Arabic value => fallback to English
  • en requested + 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/_en directly

Common Mistakes

  • locale conditionals inside every JSX component
  • cache keys without locale
  • mixing useTranslate with 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:

  • useTranslate for UI strings
  • API returns localized data (_ar/_en or 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.

No ratings yet