# How Internationalization (i18n) Is Implemented in the Lifetrace Next.js Frontend

> Discover how Lifetrace implements internationalization i18n in its Next.js frontend with Next-Intl. Learn about server-side locale detection and React component translation.

- Repository: [FreeU-group/lifetrace](https://github.com/freeu-group/lifetrace)
- Tags: how-to-guide
- Published: 2026-03-02

---

**Lifetrace implements internationalization in its Next.js frontend using Next-Intl, which provides a type-safe, full-stack i18n solution that handles locale detection on the server and translation rendering in React components.**

The repository `freeu-group/lifetrace` contains a Next.js application (located in the `free-todo-frontend` directory) that delivers a complete internationalization pipeline. This implementation supports multiple languages through JSON message bundles, detects user preferences via cookies and HTTP headers, and enforces compile-time type safety for all translation keys.

## Next-Intl Architecture Overview

The i18n stack in Lifetrace consists of five integrated layers working together to provide seamless language switching. Each layer has a distinct responsibility, from detecting the user's locale to rendering translated strings in the UI.

| Layer | Responsibility | Key File |
|-------|----------------|----------|
| **Locale detection** | Validates cookies and `Accept-Language` headers against supported locales | [`lib/i18n/request.ts`](https://github.com/freeu-group/lifetrace/blob/main/lib/i18n/request.ts) |
| **Framework integration** | Wraps Next.js configuration to inject i18n context into every request | [`next.config.ts`](https://github.com/freeu-group/lifetrace/blob/main/next.config.ts) |
| **Message bundles** | Stores translated strings as JSON files | `lib/i18n/messages/*.json` |
| **Component translation** | Exposes the `useTranslations` hook for type-safe string lookup | Any `*.tsx` component |
| **Type definitions** | Provides IDE autocomplete and compile-time checking via global declarations | [`global.d.ts`](https://github.com/freeu-group/lifetrace/blob/main/global.d.ts) |

## Locale Detection and Request Configuration

The entry point for i18n in Lifetrace is [`lib/i18n/request.ts`](https://github.com/freeu-group/lifetrace/blob/main/lib/i18n/request.ts), which exports a configuration object using `getRequestConfig` from `next-intl/server`. This file determines which locale to serve for every incoming request.

```typescript
// lib/i18n/request.ts
import { cookies, headers } from "next/headers";
import { getRequestConfig } from "next-intl/server";

const SUPPORTED_LOCALES = ["zh", "en"] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number];
const DEFAULT_LOCALE: Locale = "en";

function parseAcceptLanguage(acceptLanguage: string | null): Locale | null {
  if (!acceptLanguage) return null;
  const languages = acceptLanguage
    .split(",")
    .map(l => {
      const [code, qValue] = l.trim().split(";q=");
      return { code: code.split("-")[0].toLowerCase(), q: qValue ? Number.parseFloat(qValue) : 1 };
    })
    .sort((a, b) => b.q - a.q);
  for (const { code } of languages) {
    if (SUPPORTED_LOCALES.includes(code as Locale)) return code as Locale;
  }
  return null;
}

export default getRequestConfig(async () => {
  const cookieStore = await cookies();
  const localeCookie = cookieStore.get("locale")?.value;

  const locale: Locale =
    SUPPORTED_LOCALES.includes(localeCookie as Locale)
      ? localeCookie!
      : parseAcceptLanguage((await headers()).get("accept-language")) ?? DEFAULT_LOCALE;

  return {
    locale,
    messages: (await import(`./messages/${locale}.json`)).default,
  };
});

```

**Priority order for locale resolution:**
1. **Cookie value** – If the `locale` cookie exists and matches a code in `SUPPORTED_LOCALES`, it takes precedence
2. **Accept-Language header** – Parsed for quality values (`q` parameters), sorted by weight, and matched against supported codes
3. **Default fallback** – Defaults to `"en"` if no match is found

The `parseAcceptLanguage` function normalizes locale codes (e.g., converting `zh-CN` to `zh`) and handles quality values to respect user preferences.

## Next.js Integration with the Next-Intl Plugin

To apply the request configuration globally, Lifetrace wraps its Next.js configuration using `createNextIntlPlugin` in [`next.config.ts`](https://github.com/freeu-group/lifetrace/blob/main/next.config.ts). This plugin ensures the i18n context is available during server-side rendering and hydration.

```typescript
// next.config.ts
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const withNextIntl = createNextIntlPlugin("./lib/i18n/request.ts");

const nextConfig: NextConfig = {
  output: "standalone",
  reactStrictMode: true,
  typedRoutes: true,
  // ... additional configuration
};

export default withNextIntl(nextConfig);

```

The plugin initialization accepts the path to the request configuration file. By exporting the wrapped configuration (`withNextIntl(nextConfig)`), every page in the application automatically receives the correct `locale` and `messages` objects from the request handler. This setup enables **type-safe client-side hooks** like `useTranslations` without additional boilerplate.

## Component-Level Translation with useTranslations

Components access translated strings through the `useTranslations` hook provided by `next-intl`. This hook accepts a namespace corresponding to top-level keys in the JSON message files.

```tsx
// components/common/ui/LanguageToggle.tsx
import { useTranslations } from "next-intl";

export default function LanguageToggle() {
  const tLang = useTranslations("language");
  const tLayout = useTranslations("layout");

  return (
    <div>
      <label>{tLang("selectLanguage")}</label>
      <button onClick={() => setLocale("zh")}>{tLang("chinese")}</button>
      <button onClick={() => setLocale("en")}>{tLang("english")}</button>
    </div>
  );
}

```

**Namespace organization** allows for modular translation files. For example, the `language` namespace might contain UI labels for the language selector, while the `todoDetail` namespace contains task-specific strings. This prevents key collisions and enables code splitting by feature.

To switch locales on the client, the application updates the `locale` cookie and reloads the page:

```typescript
// utils/localeSwitcher.ts
export async function setLocale(locale: string) {
  document.cookie = `locale=${locale};path=/;max-age=31536000`;
  window.location.reload();
}

```

This approach ensures the next request triggers `getRequestConfig` with the new cookie value, loading the appropriate message bundle.

## Type Safety and Global Type Declarations

Lifetrace enforces compile-time safety for translation keys through TypeScript module augmentation in [`global.d.ts`](https://github.com/freeu-group/lifetrace/blob/main/global.d.ts). By declaring a global `IntlMessages` interface that inherits from the message schema, the IDE provides autocomplete and catches missing keys before runtime.

```typescript
// global.d.ts
import type messages from "./lib/i18n/messages/zh.json";

type Messages = typeof messages;

declare global {
  interface IntlMessages extends Messages {}
}

```

The type system uses the Chinese message file ([`zh.json`](https://github.com/freeu-group/lifetrace/blob/main/zh.json)) as the representative schema. Because all message files share the same key structure, TypeScript validates that any key passed to `useTranslations("namespace")("key")` exists in the schema.

```tsx
// Example of type checking in action
const t = useTranslations("todoDetail");

// ✅ Valid - key exists in schema
t("title");

// ❌ TypeScript error - key not found in schema
// t("nonexistentKey");

```

## Adding New Languages to the Application

Extending the i18n implementation to support additional languages requires three steps:

1. **Update supported locales** – Add the new locale code to the `SUPPORTED_LOCALES` array in [`lib/i18n/request.ts`](https://github.com/freeu-group/lifetrace/blob/main/lib/i18n/request.ts)
2. **Create message bundle** – Add a new JSON file in `lib/i18n/messages/` (e.g., [`fr.json`](https://github.com/freeu-group/lifetrace/blob/main/fr.json)) with the same key hierarchy as existing files
3. **Provide UI option** – Add a button or selector that calls `setLocale("fr")` to write the cookie

No changes are needed in [`next.config.ts`](https://github.com/freeu-group/lifetrace/blob/main/next.config.ts) or component files because the architecture dynamically loads message files based on the resolved locale.

## Summary

- **Next-Intl** provides the core i18n framework for Lifetrace's Next.js frontend, handling both server-side locale detection and client-side translation
- **Locale resolution** follows a strict priority: cookie value first, then `Accept-Language` header parsing with quality value support, falling back to `"en"`
- **Type safety** is enforced globally through [`global.d.ts`](https://github.com/freeu-group/lifetrace/blob/main/global.d.ts), which merges message schemas into the `IntlMessages` interface for compile-time key validation
- **Message bundles** are stored as JSON files in `lib/i18n/messages/` and loaded dynamically based on the resolved locale
- **Language switching** persists the selection in a `locale` cookie and reloads the page to trigger the request configuration

## Frequently Asked Questions

### How does Lifetrace determine which language to display on the first visit?

On the initial request, the application checks for a `locale` cookie. If absent, it parses the `Accept-Language` header, sorts languages by quality value (priority), and selects the first match from `SUPPORTED_LOCALES`. If no match exists, it defaults to English (`"en"`).

### What happens if a translation key is missing in one language file?

TypeScript catches missing keys at compile time because [`global.d.ts`](https://github.com/freeu-group/lifetrace/blob/main/global.d.ts) enforces that all keys must exist in the message schema. At runtime, Next-Intl returns the key name as a fallback if a translation is missing, though the type system prevents this scenario during development.

### Can users switch languages without reloading the page?

The current implementation in `free-todo-frontend` requires a full page reload after setting the `locale` cookie via `window.location.reload()`. This ensures the server re-runs `getRequestConfig` to load the correct message bundle and re-render the page with the new locale context.

### Where are the actual translated strings stored in the repository?

Translation files are located in `free-todo-frontend/lib/i18n/messages/`. The repository includes [`zh.json`](https://github.com/freeu-group/lifetrace/blob/main/zh.json) (Chinese) and [`en.json`](https://github.com/freeu-group/lifetrace/blob/main/en.json) (English) by default. Each file contains nested objects organized by feature namespace, such as `language`, `layout`, and `todoDetail`.