@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/internationalizationType-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/internationalization2. Configure supported locales
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
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
{
"common": {
"welcome": "Welcome, {name}!",
"items": "{count, plural, =0 {No items} =1 {One item} other {# items}}"
}
}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/internationalizationBuild configuration: Uses tsdown with
createDistConfig('react', ...) for distribution builds.
Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | packages/internationalization |
| Dependencies | next-intl, negotiator |
| Framework | Next.js 16+ |
Export Paths
| Path | Description |
|---|---|
@repo/internationalization | Main exports |
@repo/internationalization/server | Server utilities |
@repo/internationalization/client | Client 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-endFormatting
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-typesUsage
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-endRich 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"Related Packages
- @repo/seo - Localized SEO metadata