0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · درس

تحميل الترجمات من الخادم وكتالوجات الرسائل

حمّل كتالوجات الرسائل ذات مساحات الأسماء في مكونات الخادم دون إرسال جميع اللغات إلى العميل.

تحميل الترجمات من الخادم وكتالوجات الرسائل درس مجاني في Next.js 15 Fullstack (App Router + Server Actions) على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Next.js 15 Fullstack (App Router + Server Actions)، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Next.js 15 Fullstack (App Router + Server Actions) 4 دروس في المجموع.

بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.

Why Server-Side Translation Loading Matters

In Next.js 15 with the App Router, server components run exclusively on the server. This gives us a powerful opportunity: we can load translation files at request time without ever sending unused locale bundles to the client.

The naive approach — importing all translations at build time and bundling them into the client — causes several problems:

  • Large JavaScript bundles that slow initial page load
  • All locale strings shipped even when only one locale is active
  • No ability to lazy-load translations per route or namespace

The server-side approach keeps translation data on the server and delivers only the rendered HTML to the client, keeping bundles lean.

Understanding Message Catalogs and Namespaces

A message catalog is a structured file (JSON, YAML, or similar) containing key-value pairs for a specific locale. Namespacing breaks catalogs into logical domains so you only load what a given page or component actually needs.

A typical project structure might look like:

  • messages/en/common.json — shared UI strings (buttons, labels)
  • messages/en/home.json — homepage-specific strings
  • messages/en/dashboard.json — dashboard strings
  • messages/tr/common.json — Turkish equivalents

When a server component for the dashboard renders, it loads only dashboard.json and common.json for the active locale — not the entire catalog tree.

Setting Up the Messages Directory

Start by creating namespaced JSON files under a messages directory at the project root. Each locale gets its own subfolder, and each namespace gets its own file.

The JSON structure uses nested keys to group related strings. This makes it easy to scale without key collisions across namespaces.

// messages/en/common.json
{
  "nav": {
    "home": "Home",
    "about": "About",
    "dashboard": "Dashboard"
  },
  "actions": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete"
  }
}

// messages/en/dashboard.json
{
  "title": "Your Dashboard",
  "welcome": "Welcome back, {name}!",
  "stats": {
    "totalUsers": "Total Users",
    "revenue": "Revenue",
    "activeNow": "Active Now"
  },
  "empty": "No data available yet."
}

// messages/tr/dashboard.json
{
  "title": "Panonuz",
  "welcome": "Tekrar hoş geldiniz, {name}!",
  "stats": {
    "totalUsers": "Toplam Kullanıcı",
    "revenue": "Gelir",
    "activeNow": "Şu An Aktif"
  },
  "empty": "Henüz veri yok."
}

Building a Type-Safe Translation Loader

Create a utility that reads JSON files from disk at request time. Because this runs in a server component, fs access is available and the result never reaches the client bundle.

Key design decisions in this loader:

  • Accept a locale and namespace parameter to load only what is needed
  • Use TypeScript generics so callers get typed return values
  • Throw a clear error if a namespace file is missing, catching misconfigurations early
// lib/i18n/loader.ts
import { readFile } from 'fs/promises';
import path from 'path';

export type Messages = Record<string, unknown>;

export async function loadMessages<T extends Messages>(
  locale: string,
  namespace: string
): Promise<T> {
  const filePath = path.join(
    process.cwd(),
    'messages',
    locale,
    `${namespace}.json`
  );

  try {
    const raw = await readFile(filePath, 'utf-8');
    return JSON.parse(raw) as T;
  } catch (error) {
    throw new Error(
      `[i18n] Failed to load namespace "${namespace}" for locale "${locale}". ` +
      `Expected file at: ${filePath}`
    );
  }
}

Reading the Active Locale from the Request

In Next.js 15, the active locale is typically stored in the URL path segment (e.g., /en/dashboard, /tr/dashboard). The App Router exposes this through route parameters on layout and page components.

A common pattern is to define an [locale] dynamic segment at the root of the app directory, making locale available as a param throughout the entire subtree.

// app/[locale]/layout.tsx
import { ReactNode } from 'react';

interface LocaleLayoutProps {
  children: ReactNode;
  params: Promise<{ locale: string }>;
}

export default async function LocaleLayout({
  children,
  params,
}: LocaleLayoutProps) {
  // In Next.js 15, params is a Promise — always await it
  const { locale } = await params;

  // Validate locale to prevent path traversal attacks
  const supportedLocales = ['en', 'tr', 'de', 'fr'];
  if (!supportedLocales.includes(locale)) {
    // Middleware should have redirected already, but guard here too
    throw new Error(`Unsupported locale: ${locale}`);
  }

  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

Loading Translations Directly in a Server Component

With the loader utility and locale param in place, a server component can load its namespace translations with a single await call. The data is used during render and never serialized to the client.

Notice how this pattern keeps the component clean: no context providers, no hooks, no client-side re-fetching of translations.

// app/[locale]/dashboard/page.tsx
import { loadMessages } from '@/lib/i18n/loader';

interface DashboardMessages {
  title: string;
  welcome: string;
  stats: {
    totalUsers: string;
    revenue: string;
    activeNow: string;
  };
  empty: string;
}

interface PageProps {
  params: Promise<{ locale: string }>;
}

export default async function DashboardPage({ params }: PageProps) {
  const { locale } = await params;

  // Only dashboard namespace is loaded — not the entire catalog
  const t = await loadMessages<DashboardMessages>(locale, 'dashboard');

  return (
    <main>
      <h1>{t.title}</h1>
      <p>{t.welcome.replace('{name}', 'Mehmet')}</p>
      <ul>
        <li>{t.stats.totalUsers}</li>
        <li>{t.stats.revenue}</li>
        <li>{t.stats.activeNow}</li>
      </ul>
    </main>
  );
}

Caching Translations with React's Cache API

A single request may render multiple server components that need the same namespace. Without caching, each component triggers a separate readFile call for the same file.

React 18+ ships a cache() function that memoizes async functions per request. Wrapping the loader with cache() means the file is read only once per locale+namespace combination per request, even if ten components call it.

// lib/i18n/loader.ts  (updated with per-request caching)
import { readFile } from 'fs/promises';
import { cache } from 'react';
import path from 'path';

export type Messages = Record<string, unknown>;

// cache() deduplicates calls within the same React render pass
const readMessageFile = cache(async (locale: string, namespace: string) => {
  const filePath = path.join(
    process.cwd(),
    'messages',
    locale,
    `${namespace}.json`
  );
  const raw = await readFile(filePath, 'utf-8');
  return JSON.parse(raw);
});

export async function loadMessages<T extends Messages>(
  locale: string,
  namespace: string
): Promise<T> {
  try {
    return await readMessageFile(locale, namespace) as T;
  } catch {
    throw new Error(
      `[i18n] Missing namespace "${namespace}" for locale "${locale}"`
    );
  }
}

Loading Multiple Namespaces in a Layout

A layout component often needs strings from several namespaces — for example, navigation labels from common and page-section headings from a feature namespace. Load them in parallel with Promise.all to avoid waterfall delays.

This pattern is idiomatic in server components: fire all async work concurrently, then use the results synchronously during JSX rendering.

// app/[locale]/dashboard/layout.tsx
import { ReactNode } from 'react';
import { loadMessages } from '@/lib/i18n/loader';

interface CommonMessages {
  nav: { home: string; about: string; dashboard: string };
  actions: { save: string; cancel: string; delete: string };
}

interface DashboardLayoutMessages {
  title: string;
}

export default async function DashboardLayout({
  children,
  params,
}: {
  children: ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;

  // Parallel loading — no sequential waterfall
  const [common, dashboard] = await Promise.all([
    loadMessages<CommonMessages>(locale, 'common'),
    loadMessages<DashboardLayoutMessages>(locale, 'dashboard'),
  ]);

  return (
    <div>
      <nav>
        <a href="/">{common.nav.home}</a>
        <a href="/dashboard">{common.nav.dashboard}</a>
      </nav>
      <h1>{dashboard.title}</h1>
      <main>{children}</main>
    </div>
  );
}

Building a Scoped Translation Helper

Repeatedly writing t.stats.totalUsers gets verbose in large components. A small scoped helper wraps the messages object and resolves dot-notation key paths, making templates more readable.

Because this helper is a pure utility function that runs on the server, it adds zero cost to the client bundle.

// lib/i18n/t.ts
type NestedMessages = { [key: string]: string | NestedMessages };

/**
 * Resolve a dot-notation path from a messages object.
 * Example: get(messages, 'stats.totalUsers') => 'Total Users'
 */
export function get(obj: NestedMessages, path: string): string {
  const parts = path.split('.');
  let current: string | NestedMessages = obj;

  for (const part of parts) {
    if (typeof current !== 'object' || current === null) {
      return path; // Return key as fallback rather than crashing
    }
    current = current[part];
  }

  return typeof current === 'string' ? current : path;
}

/**
 * Bind a messages object to produce a scoped t() function.
 */
export function createTranslator(messages: NestedMessages) {
  return function t(key: string, vars?: Record<string, string>): string {
    let value = get(messages, key);
    if (vars) {
      for (const [k, v] of Object.entries(vars)) {
        value = value.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
      }
    }
    return value;
  };
}

Passing Translated Strings to Client Components

Client components (marked with 'use client') cannot call loadMessages directly — they run in the browser. The correct pattern is to load translations in a server component parent and pass only the needed strings as props.

This keeps the translation loading on the server while still allowing interactivity. The client component receives plain strings — it has no dependency on any i18n library at all.

// components/DeleteButton.tsx  (client component)
'use client';

import { useState } from 'react';

interface DeleteButtonProps {
  // Translated strings passed from server parent — no i18n lib needed here
  labels: {
    confirm: string;
    cancel: string;
    deleting: string;
  };
  onDelete: () => Promise<void>;
}

export function DeleteButton({ labels, onDelete }: DeleteButtonProps) {
  const [pending, setPending] = useState(false);

  async function handleClick() {
    setPending(true);
    await onDelete();
    setPending(false);
  }

  return (
    <button onClick={handleClick} disabled={pending}>
      {pending ? labels.deleting : labels.confirm}
    </button>
  );
}

// app/[locale]/items/page.tsx  (server component — loads & passes strings)
import { loadMessages } from '@/lib/i18n/loader';
import { DeleteButton } from '@/components/DeleteButton';

export default async function ItemsPage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const common = await loadMessages<{
    actions: { delete: string; cancel: string; deleting: string };
  }>(locale, 'common');

  return (
    <DeleteButton
      labels={{
        confirm: common.actions.delete,
        cancel: common.actions.cancel,
        deleting: common.actions.deleting,
      }}
      onDelete={async () => { 'use server'; /* action here */ }}
    />
  );
}

Generating Static Params for Localized Routes

For statically generated pages, Next.js needs to know all valid locale+slug combinations upfront. generateStaticParams runs at build time and returns every combination that should be pre-rendered.

Combining this with server-side translation loading means each locale variant is pre-rendered with its own catalog — no runtime translation fetching needed for static pages.

// app/[locale]/blog/[slug]/page.tsx
import { loadMessages } from '@/lib/i18n/loader';

const SUPPORTED_LOCALES = ['en', 'tr', 'de'] as const;
type SupportedLocale = typeof SUPPORTED_LOCALES[number];

interface BlogPost {
  slug: string;
  title: Record<SupportedLocale, string>;
  content: Record<SupportedLocale, string>;
}

// Simulated data source
const posts: BlogPost[] = [
  { slug: 'getting-started', title: { en: 'Getting Started', tr: 'Başlangıç', de: 'Erste Schritte' }, content: { en: '...', tr: '...', de: '...' } },
  { slug: 'advanced-tips', title: { en: 'Advanced Tips', tr: 'Gelişmiş İpuçları', de: 'Fortgeschrittene Tipps' }, content: { en: '...', tr: '...', de: '...' } },
];

// Build time: enumerate all locale × slug combinations
export async function generateStaticParams() {
  return SUPPORTED_LOCALES.flatMap((locale) =>
    posts.map((post) => ({ locale, slug: post.slug }))
  );
}

export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ locale: string; slug: string }>;
}) {
  const { locale, slug } = await params;
  const t = await loadMessages<{ blog: { readMore: string } }>(locale, 'common');
  const post = posts.find((p) => p.slug === slug)!;
  const loc = locale as SupportedLocale;

  return (
    <article>
      <h1>{post.title[loc]}</h1>
      <p>{post.content[loc]}</p>
    </article>
  );
}

Which approach correctly avoids shipping all locale translations to the client?

A Next.js 15 App Router project supports 5 locales. The team wants to ensure that when a user visits the English dashboard, only English strings are used and no translation data is included in the JavaScript bundle sent to the browser. Which implementation achieves this?

Recap: Server-Side Translation Loading and Message Catalogs

In this lesson you learned how to load namespaced message catalogs entirely on the server in Next.js 15 App Router applications:

  • Message catalogs are organized as messages/{locale}/{namespace}.json files, keeping each language and domain separate
  • A loader utility using fs/promises reads only the required namespace at request time — no unused locales are ever loaded
  • React's cache() function deduplicates file reads within a single render pass, preventing redundant I/O when multiple components need the same namespace
  • The active locale comes from the [locale] route segment via await params (a Promise in Next.js 15)
  • Load multiple namespaces in parallel with Promise.all to avoid sequential waterfalls
  • Client components receive only plain string props — they have no dependency on any i18n library and no translation data in their bundle
  • generateStaticParams enumerates all locale+route combinations at build time, enabling fully static pre-rendered pages per locale

This architecture delivers fast, minimal bundles while keeping all translation logic where it belongs — on the server.

الأسئلة الشائعة

هل درس «تحميل الترجمات من الخادم وكتالوجات الرسائل» مجاني؟

نعم — نص درس «تحميل الترجمات من الخادم وكتالوجات الرسائل» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Next.js 15 Fullstack (App Router + Server Actions)، انتقل إلى CoddyKit PRO. تتضمن دورة Next.js 15 Fullstack (App Router + Server Actions) 4 دروس في المجموع.

ماذا ستتعلم في «تحميل الترجمات من الخادم وكتالوجات الرسائل»؟

حمّل كتالوجات الرسائل ذات مساحات الأسماء في مكونات الخادم دون إرسال جميع اللغات إلى العميل. تتمرن على Next.js 15 Fullstack (App Router + Server Actions) مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Next.js 15 Fullstack (App Router + Server Actions)؟

لا تُشترط خبرة سابقة. Next.js 15 Fullstack (App Router + Server Actions) على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «تحميل الترجمات من الخادم وكتالوجات الرسائل»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Next.js 15 Fullstack (App Router + Server Actions) هذا؟

نعم. كل درس في Next.js 15 Fullstack (App Router + Server Actions) يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. اكتشاف اللغة والتوجيه عبر المسارات الفرعية باستخدام Middleware
  2. تحميل الترجمات من الخادم وكتالوجات الرسائل
  3. البيانات الوصفية المترجمة وخرائط المواقع وhreflang
  4. تنسيق التواريخ والأرقام وصيغ الجمع حسب اللغة
← العودة إلى Next.js 15 Fullstack (App Router + Server Actions)