The universal
formatter for Next.js
Unified number, date, and currency formatting across every App Router boundary. Consistent between Server and Client, always.
Multi-boundary
Works identically in RSC, Server Actions, and Client Hooks — zero extra config.
Smart Locales
Resolves from request headers or cookies. Fully async by default.
Safe ICU Hydration
Prevents narrow-space and currency symbol mismatches between renders.
Installation
Install the package and wrap your root layout with the provider. Define config once, use it everywhere.
import { getFormatter as _getFormatter } from "next-formatter/server"; import type { NextFormatterConfig } from "next-formatter"; const config: NextFormatterConfig = { locale: "en-US", currency: "USD", rules: { compactThreshold: 10000, minimumFractionDigits: 0, maximumFractionDigits: 2 } }; export const formatterConfig = config; export const getFormatter = () => _getFormatter(config);
"use client"; import { FormatterProvider } from "next-formatter/client"; import { formatterConfig } from "@/lib/formatter"; import { ReactNode } from "react"; export function AppFormatterProvider({ children }: { children: ReactNode }) { return ( <FormatterProvider config={formatterConfig}> {children} </FormatterProvider> ); }
import { AppFormatterProvider } from "@/app/providers/formatter-provider"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <AppFormatterProvider> {children} </AppFormatterProvider> </body> </html> ); }
import { getFormatter } from "@/lib/formatter"; export default async function Page() { const { currency } = await getFormatter(); return <p>{currency(49900)}</p>; // → $49.90K }
"use client"; import { useFormatter } from "next-formatter/client"; export function Price({ value }) { const { currency } = useFormatter(); return <span>{currency(value)}</span>; }
Example Use Cases
Real-world patterns for everywhere from Server Components to background API logic.
import { getFormatter } from "@/lib/formatter"; const fmt = await getFormatter(); fmt.currency(49.99);
import { getFormatter } from "next-formatter/server"; const fmt = await getFormatter({ locale: "de-DE" }); fmt.currency(49.99);
"use client"; import { useFormatter } from "next-formatter/client"; export function PriceChart({ data }) { const { currency } = useFormatter(); return ( <Tooltip formatter={(v) => currency(v, { currencyDisplay: "symbol" })} /> ); }
import { getFormatter } from "@/lib/formatter"; export async function GET() { const { currency } = await getFormatter(); return Response.json({ formattedPrice: currency(49.99) // → Auto-detects locale from headers! }); }
export const formatterConfig: NextFormatterConfig = { getLocale: async () => { // Read from cookies or database return cookies().get("pref-locale")?.value; }, getCurrency: async () => { const user = await getCurrentUser(); return user?.preferredCurrency; } };
API Reference
Four core formatters — each locale-aware, hydration-safe, and boundary-agnostic.
Configuration
The FormatterProvider accepts a config
object that
defines global behavior across all server and client boundaries.
| Option | Type | Default | Description |
|---|---|---|---|
locale |
string |
"en-US" |
Fallback locale if detection fails. |
currency |
string |
"USD" |
Default ISO-4217 code for currency formatting. |
fallback |
string |
"—" |
String rendered for null or undefined values. |
rules.compactThreshold |
number |
10000 |
Value at which compact notation (K, M) is triggered. |
rules.minimumFractionDigits |
number |
0 |
Global min fraction digits (0, 1, 2). |
rules.maximumFractionDigits |
number |
2 |
Global max fraction digits. |
rules.currencyDisplay |
string |
"narrowSymbol" |
symbol, narrowSymbol, code, or name. |
Hydration & ICU
Native Intl often fails in Next.js because Node.js and Browser ICU engines produce different
characters.
Unicode Normalization
Converts narrow-no-break-space (U+202F) and non-breaking-space (U+00A0) to standard spaces.
Fraction Alignment
Automatically sets min = max fraction digits when they differ, preventing "54.1" (client)
vs "54.10" (server)
mismatches.
Strict Stability
Ensures that the server-rendered HTML is byte-for-byte identical to the client's first hydration pass.
Dynamic Resolvers
Detect locale and currency from anywhere: request headers, cookies, or your user database.
export const formatterConfig: NextFormatterConfig = { getLocale: async () => { const session = await auth(); return session?.user?.locale; }, getCurrency: async () => { return cookies().get("currency")?.value; } };
Patterns
Consistent formatting across all Server and Client boundaries.
"use server"; import { getFormatter } from "@/lib/formatter"; export async function checkoutAction(val: number) { const fmt = await getFormatter(); // Perfect for formatted response messages return { message: `Processed ${fmt.currency(val)}` }; }
Architecture
Built for Next.js 14+ module isolation. The provider resolves configuration once per request on the server, then distributes it to all boundaries.
| Target | Method | Boundary |
|---|---|---|
| Server Components | getFormatter() |
Server |
| Server Actions | getFormatter() |
Server |
| Route Handlers | getFormatter() |
Server |
| Client Components | useFormatter() |
Client |
Alternatives
When to use next-formatter vs other solutions.
Choose this for full site translation. It's a heavy-duty framework including messages, routing, and formatting.
Choose this for formatting-only. Lightweight, zero-config, and specifically built to fix hydration errors.
FAQ
now timestamp from the server prevents this.
Intl to avoid shipping heavy ICU
data to your users.