OneApp Docs
PackagesFeatures

@repo/internationalization

Multi-language support with type-safe translations. Automatic locale detection, formatted dates/numbers per region, and pluralization. Works in client and server components with zero configuration.

Quick Start

Add multi-language support in 10 minutes:

pnpm add @repo/internationalization

Type-safe translations, automatic locale detection, formatted numbers and dates. Skip to Quick Start →

Why @repo/internationalization?

Hard-coded English text limits your app to English-speaking users. Manual translation management is error-prone ("Login" becomes "Iniciar sesión" in 15 files?). Date formats vary by region (MM/DD/YYYY vs DD/MM/YYYY). Pluralization rules differ between languages ("1 item" vs "2 items" vs "0 items"). Currency symbols change by locale.

@repo/internationalization solves this with centralized translations, automatic locale detection, and region-specific formatting.

Production-ready with next-intl integration, type-safe translation keys, pluralization, rich text support, and locale-aware routing.

Use cases

  • Global SaaS — Support users in 50+ countries with localized UI and content
  • E-commerce — Display prices in local currency, dates in local format
  • Content platforms — Serve articles in user's preferred language automatically
  • Admin dashboards — Translate complex forms and validation messages
  • Marketing sites — Localized landing pages for different regions

How it works

@repo/internationalization provides type-safe translations with automatic locale detection:

import { getTranslations } from "@repo/internationalization/server";

export default async function Page() {
  const t = await getTranslations("common");

  return (
    <div>
      <h1>{t("welcome", { name: "John" })}</h1>
      <p>{t("items", { count: 5 })}</p>

  );
}
// Output (English): "Welcome, John!" and "5 items"
// Output (Spanish): "¡Bienvenido, John!" and "5 artículos"

Uses next-intl for translations, middleware for automatic locale detection, and formatters for dates/numbers/currency.

Key features

Type-safe translations — Autocomplete for translation keys, compile-time validation

Automatic locale detection — Detects user's language from Accept-Language header

Formatting utilities — Dates, numbers, currency, relative time, lists per locale

Pluralization — Automatic plural forms ("1 item" vs "2 items") per language rules

Rich text support — Embed React components in translations

Server + Client — Works in both server and client components

Quick Start

1. Install the package

pnpm add @repo/internationalization

2. Configure supported locales

i18n.config.ts
export const locales = ["en", "es", "fr", "de", "ja"] as const;
export const defaultLocale = "en" as const;

export type Locale = (typeof locales)[number];

3. Add middleware for automatic locale detection

middleware.ts
import { createI18nMiddleware } from "@repo/internationalization";
import { locales, defaultLocale } from "./i18n.config";

export default createI18nMiddleware({
  locales,
  defaultLocale,
  localePrefix: "as-needed" // Locale in URL only when needed
});

export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)"]
};

4. Create translation files and use in components

messages/en.json
{
  "common": {
    "welcome": "Welcome, {name}!",
    "items": "{count, plural, =0 {No items} =1 {One item} other {# items}}"
  }
}
app/page.tsx
import { getTranslations } from "@repo/internationalization/server";

export default async function Page() {
  const t = await getTranslations("common");

  return (
    <div>
      <h1>{t("welcome", { name: "John" })}</h1>
      <p>{t("items", { count: 5 })}</p>

  );
}

That's it! Your app now automatically detects user language and displays translated content.

Client components

Use translations in client components with the hook:

"use client";
import { useTranslations } from "@repo/internationalization/client";

export function LoginButton() {
  const t = useTranslations("auth");
  return <button>{t("login")}</button>;
}

Distribution

This package is available as @oneapp/internationalization for use outside the monorepo.

npm install @oneapp/internationalization

Build configuration: Uses tsdown with createDistConfig('react', ...) for distribution builds.


Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationpackages/internationalization
Dependenciesnext-intl, negotiator
FrameworkNext.js 16+

Export Paths

PathDescription
@repo/internationalizationMain exports
@repo/internationalization/serverServer utilities
@repo/internationalization/clientClient utilities

Locale Detection

import { detectLocale } from "@repo/internationalization";

// highlight-start
// Detect from request headers
const locale = detectLocale(request.headers);

// Detect with preferences
const locale = detectLocale(request.headers, {
  supported: ["en", "es", "fr"],
  default: "en"
});
// highlight-end

Formatting

Numbers

import { useFormatter } from "@repo/internationalization/client";

function PriceDisplay({ amount }) {
  const format = useFormatter();

  return (
    <span>
      {/* highlight-start */}
      {format.number(amount, {
        style: "currency",
        currency: "USD",
      })}
      {/* highlight-end */}
    </span>
  );
}

Dates

import { useFormatter } from "@repo/internationalization/client";

function DateDisplay({ date }) {
  const format = useFormatter();

  return (
    <span>
      {/* highlight-start */}
      {format.dateTime(date, {
        year: "numeric",
        month: "long",
        day: "numeric",
      })}
      {/* highlight-end */}
    </span>
  );
}

Relative Time

import { useFormatter } from "@repo/internationalization/client";

function TimeAgo({ date }) {
  const format = useFormatter();

  // highlight-next-line
  return <span>{format.relativeTime(date)}</span>;
  // "2 hours ago", "yesterday", etc.
}

Lists

import { useFormatter } from "@repo/internationalization/client";

function TagList({ tags }) {
  const format = useFormatter();

  // highlight-next-line
  return <span>{format.list(tags, { type: "conjunction" })}</span>;
  // "React, TypeScript, and Next.js"
}

Language Switcher

"use client";

import { useLocale } from "@repo/internationalization/client";
import { useRouter, usePathname } from "next/navigation";
import { locales } from "#/i18n.config";

export function LanguageSwitcher() {
  // highlight-next-line
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  const switchLocale = (newLocale: string) => {
    router.push(pathname, { locale: newLocale });
  };

  return (
    <select value={locale} onChange={(e) => switchLocale(e.target.value)}>
      {locales.map((loc) => (
        <option key={loc} value={loc}>
          {loc.toUpperCase()}
        </option>
      ))}
    </select>
  );
}

Type-Safe Translations

Generate Types

# Generate types from translation files
pnpm --filter @repo/internationalization generate-types

Usage

Type Safety

Generated types provide autocomplete and type checking for all translation keys.

import type { TranslationKeys } from "@repo/internationalization/types";

// highlight-start
// t() is fully typed
const t = await getTranslations("common");
t("welcome", { name: "John" }); // ✅ Type-safe
t("nonexistent"); // ❌ Type error
// highlight-end

Rich Text

// messages/en.json
{
  "terms": "By signing up, you agree to our <link>Terms of Service</link>."
}
import { useTranslations } from "@repo/internationalization/client";

function Terms() {
  const t = useTranslations();

  return (
    <p>
      {/* highlight-start */}
      {t.rich("terms", {
        link: (chunks) => <a href="/terms">{chunks}</a>,
      })}
      {/* highlight-end */}
    </p>
  );
}

Locale-Specific Content

import { getLocale } from "@repo/internationalization/server";

export default async function Page() {
  // highlight-next-line
  const locale = await getLocale();

  // Fetch locale-specific content
  const content = await getContent({ locale });

  return <div>{ content };
}

Environment Variables

# Default locale (optional, defaults to 'en')
DEFAULT_LOCALE="en"

On this page