IntlPull
Tutorial
25 min read

next-intl: Guía completa para la internacionalización de Next.js (2026)

Dominar next-intl para Next.js App Router. Tutorial completo que cubre la configuración, el enrutamiento, los componentes del servidor, el formato, TypeScript y las mejores prácticas de producción.

IntlPull Team
IntlPull Team
03 Feb 2026, 11:44 AM [PST]
On this page
Summary

Dominar next-intl para Next.js App Router. Tutorial completo que cubre la configuración, el enrutamiento, los componentes del servidor, el formato, TypeScript y las mejores prácticas de producción.

Respuesta rápida

next-intl es la mejor librería de internacionalización (i18n) para Next.js App Router. Instalar con npm install next-intl, create a [locale] folder in your app directory, configure middleware for locale detection, and use getTranslations() in Server Components or useTranslations() in Client Components. next-intl is specifically designed for Next.js 13-15+ with native Server Component support, automatic routing, and a ~2KB bundle size.


What is next-intl?

next-intl is a lightweight internationalization library built specifically for Next.js. Unlike general-purpose React i18n libraries, next-intl was designed from the ground up for the App Router and Server Components.

Why next-intl Over Other Libraries?

Featurenext-intlreact-i18nextreact-intl
Built for Next.jsYesNo (React general)No (React general)
Server ComponentsNativeNeeds wrapperNeeds wrapper
Bundle size~2KB~8KB~12KB
App Router supportFirst-classPartialPartial
TypeScriptExcellentGoodGood
Routing integrationBuilt-inManualManual

Key benefits:

  • Zero client-side JS for server-rendered translations - translations stay on the server
  • Automatic locale routing - /en/about, /es/about handled automatically
  • ICU message format - proper pluralization, gender, and formatting
  • Type-safe - full TypeScript support with autocompletion

When you scale beyond JSON files, IntlPull integrates seamlessly with next-intl for translation management, AI translation, and team collaboration.


Installation & Setup

Step 1: Install next-intl

Terminal
1npm install next-intl
2# or
3yarn add next-intl
4# or
5pnpm add next-intl

Step 2: Project Structure

Create this folder structure:

/your-nextjs-app
├── /app
│   └── /[locale]           # Dynamic locale segment
│       ├── layout.tsx      # Locale-aware layout
│       ├── page.tsx        # Home page
│       └── /about
│           └── page.tsx    # About page
├── /messages               # Translation files
│   ├── en.json
│   ├── es.json
│   └── de.json
├── /i18n
│   ├── config.ts           # Locale configuration
│   └── request.ts          # Server-side config
├── middleware.ts           # Locale detection & routing
└── next.config.js

Step 3: Configure Locales

Create i18n/config.ts:

TypeScript
1export const locales = ['en', 'es', 'de', 'fr', 'ja'] as const;
2export type Locale = (typeof locales)[number];
3export const defaultLocale: Locale = 'en';
4
5// Optional: locale metadata for UI
6export const localeNames: Record<Locale, string> = {
7  en: 'English',
8  es: 'Español',
9  de: 'Deutsch',
10  fr: 'Français',
11  ja: '日本語',
12};

Step 4: Create Request Configuration

Create i18n/request.ts:

TypeScript
1import { getRequestConfig } from 'next-intl/server';
2import { notFound } from 'next/navigation';
3import { locales } from './config';
4
5export default getRequestConfig(async ({ locale }) => {
6  // Validate that the incoming locale is supported
7  if (!locales.includes(locale as any)) {
8    notFound();
9  }
10
11  return {
12    messages: (await import(`../messages/${locale}.json`)).default,
13  };
14});

Step 5: Configure next.config.js

JavaScript
1const createNextIntlPlugin = require('next-intl/plugin');
2
3const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
4
5/** @type {import('next').NextConfig} */
6const nextConfig = {
7  // Your other Next.js config
8};
9
10module.exports = withNextIntl(nextConfig);

Step 6: Create Middleware

Create middleware.ts in your project root:

TypeScript
1import createMiddleware from 'next-intl/middleware';
2import { locales, defaultLocale } from './i18n/config';
3
4export default createMiddleware({
5  locales,
6  defaultLocale,
7  localePrefix: 'always', // or 'as-needed' or 'never'
8});
9
10export const config = {
11  // Match all pathnames except for
12  // - API routes
13  // - _next (Next.js internals)
14  // - Static files (images, etc.)
15  matcher: ['/((?!api|_next|.*\\..*).*)'],
16};

Step 7: Create Translation Files

Create messages/en.json:

JSON
1{
2  "common": {
3    "welcome": "Welcome to our app",
4    "loading": "Loading...",
5    "error": "Something went wrong"
6  },
7  "home": {
8    "title": "Home",
9    "description": "This is the home page",
10    "cta": "Get Started"
11  },
12  "navigation": {
13    "home": "Home",
14    "about": "About",
15    "contact": "Contact"
16  }
17}

Create messages/es.json:

JSON
1{
2  "common": {
3    "welcome": "Bienvenido a nuestra aplicación",
4    "loading": "Cargando...",
5    "error": "Algo salió mal"
6  },
7  "home": {
8    "title": "Inicio",
9    "description": "Esta es la página de inicio",
10    "cta": "Comenzar"
11  },
12  "navigation": {
13    "home": "Inicio",
14    "about": "Acerca de",
15    "contact": "Contacto"
16  }
17}

Step 8: Create Root Layout

Create app/[locale]/layout.tsx:

TypeScript
1import { NextIntlClientProvider } from 'next-intl';
2import { getMessages } from 'next-intl/server';
3import { notFound } from 'next/navigation';
4import { locales } from '@/i18n/config';
5
6export function generateStaticParams() {
7  return locales.map((locale) => ({ locale }));
8}
9
10export default async function LocaleLayout({
11  children,
12  params: { locale },
13}: {
14  children: React.ReactNode;
15  params: { locale: string };
16}) {
17  // Validate locale
18  if (!locales.includes(locale as any)) {
19    notFound();
20  }
21
22  // Get messages for the current locale
23  const messages = await getMessages();
24
25  return (
26    <html lang={locale}>
27      <body>
28        <NextIntlClientProvider messages={messages}>
29          {children}
30        </NextIntlClientProvider>
31      </body>
32    </html>
33  );
34}

Using Translations

In Server Components, use getTranslations():

TypeScript
1import { getTranslations } from 'next-intl/server';
2
3export default async function HomePage() {
4  const t = await getTranslations('home');
5
6  return (
7    <main>
8      <h1>{t('title')}</h1>
9      <p>{t('description')}</p>
10      <button>{t('cta')}</button>
11    </main>
12  );
13}

This is the recommended approach because:

  • Translations render on the server
  • Zero JavaScript shipped to client for these strings
  • Better performance and SEO

Client Components

For interactive components, use useTranslations():

TypeScript
1'use client';
2
3import { useTranslations } from 'next-intl';
4
5export function AddToCartButton() {
6  const t = useTranslations('product');
7
8  const handleClick = () => {
9    // Client-side logic
10  };
11
12  return (
13    <button onClick={handleClick}>
14      {t('addToCart')}
15    </button>
16  );
17}

Accessing Multiple Namespaces

TypeScript
1import { getTranslations } from 'next-intl/server';
2
3export default async function Page() {
4  const t = await getTranslations('home');
5  const tCommon = await getTranslations('common');
6
7  return (
8    <div>
9      <h1>{t('title')}</h1>
10      <p>{tCommon('welcome')}</p>
11    </div>
12  );
13}

Variables and Interpolation

Basic Variables

JSON
1{
2  "greeting": "Hello, {name}!",
3  "items": "You have {count} items in your cart"
4}
TypeScript
t('greeting', { name: 'John' })  // "Hello, John!"
t('items', { count: 5 })         // "You have 5 items in your cart"

Pluralization (ICU Format)

next-intl uses ICU message format for proper pluralization:

JSON
{
  "cartItems": "{count, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}"
}
TypeScript
t('cartItems', { count: 0 })   // "Your cart is empty"
t('cartItems', { count: 1 })   // "1 item in cart"
t('cartItems', { count: 5 })   // "5 items in cart"

ICU plural categories by language:

LanguageCategories
Englishone, other
Frenchone, other (sometimes many)
Russianone, few, many, other
Arabiczero, one, two, few, many, other
Japaneseother (no plurals)

next-intl handles all of this automatically based on the locale.

Select (Gender, Status, etc.)

JSON
{
  "userStatus": "{status, select, online {User is online} offline {User is offline} away {User is away} other {Unknown status}}"
}
TypeScript
t('userStatus', { status: 'online' })  // "User is online"

Rich Text (HTML/Components)

JSON
{
  "terms": "By signing up, you agree to our <terms>Terms of Service</terms> and <privacy>Privacy Policy</privacy>."
}
TypeScript
1import { useTranslations } from 'next-intl';
2import Link from 'next/link';
3
4function SignupForm() {
5  const t = useTranslations('auth');
6
7  return (
8    <p>
9      {t.rich('terms', {
10        terms: (chunks) => <Link href="/terms">{chunks}</Link>,
11        privacy: (chunks) => <Link href="/privacy">{chunks}</Link>,
12      })}
13    </p>
14  );
15}

Number, Date, and Currency Formatting

Number Formatting

TypeScript
1import { useFormatter } from 'next-intl';
2
3function PriceDisplay({ price }: { price: number }) {
4  const format = useFormatter();
5
6  return (
7    <span>
8      {format.number(price, { style: 'currency', currency: 'USD' })}
9    </span>
10  );
11}
12
13// en-US: "$1,234.56"
14// de-DE: "1.234,56 $"
15// ja-JP: "$1,234.56"

Date Formatting

TypeScript
1import { useFormatter } from 'next-intl';
2
3function DateDisplay({ date }: { date: Date }) {
4  const format = useFormatter();
5
6  return (
7    <>
8      <p>{format.dateTime(date, { dateStyle: 'full' })}</p>
9      <p>{format.dateTime(date, { timeStyle: 'short' })}</p>
10      <p>{format.relativeTime(date)}</p>
11    </>
12  );
13}
14
15// en-US: "Friday, January 17, 2026"
16// de-DE: "Freitag, 17. Januar 2026"

Relative Time

TypeScript
1const format = useFormatter();
2
3format.relativeTime(new Date('2026-01-10'))  // "7 days ago"
4format.relativeTime(new Date('2026-01-20'))  // "in 3 days"

List Formatting

TypeScript
1const format = useFormatter();
2
3format.list(['Apple', 'Banana', 'Orange'], { type: 'conjunction' })
4// en: "Apple, Banana, and Orange"
5// de: "Apple, Banana und Orange"

Routing and Navigation

Use next-intl/link for automatic locale prefixing:

TypeScript
1import Link from 'next-intl/link';
2
3function Navigation() {
4  return (
5    <nav>
6      <Link href="/">Home</Link>
7      <Link href="/about">About</Link>
8      <Link href="/contact">Contact</Link>
9    </nav>
10  );
11}
12// Automatically renders as /en/about, /es/about based on current locale

Locale-Aware useRouter

TypeScript
1'use client';
2
3import { useRouter } from 'next-intl/client';
4
5function SearchForm() {
6  const router = useRouter();
7
8  const handleSearch = (query: string) => {
9    router.push(`/search?q=${query}`);
10    // Automatically includes locale prefix
11  };
12
13  return (/* form */);
14}

Language Switcher

TypeScript
1'use client';
2
3import { useLocale } from 'next-intl';
4import { useRouter, usePathname } from 'next-intl/client';
5import { locales, localeNames } from '@/i18n/config';
6
7export function LanguageSwitcher() {
8  const locale = useLocale();
9  const router = useRouter();
10  const pathname = usePathname();
11
12  const handleChange = (newLocale: string) => {
13    router.replace(pathname, { locale: newLocale });
14  };
15
16  return (
17    <select value={locale} onChange={(e) => handleChange(e.target.value)}>
18      {locales.map((loc) => (
19        <option key={loc} value={loc}>
20          {localeNames[loc]}
21        </option>
22      ))}
23    </select>
24  );
25}

URL Strategies

Configure in middleware:

TypeScript
1// Option 1: Always show locale prefix
2// /en/about, /es/about, /de/about
3localePrefix: 'always'
4
5// Option 2: Hide default locale
6// /about (English), /es/about (Spanish)
7localePrefix: 'as-needed'
8
9// Option 3: Never show prefix (use cookies/headers)
10localePrefix: 'never'

Recommendation: Use 'always' for best SEO and clearest URLs.


TypeScript Integration

Type-Safe Translations

Create global.d.ts:

TypeScript
1import en from './messages/en.json';
2
3type Messages = typeof en;
4
5declare global {
6  interface IntlMessages extends Messages {}
7}

Now you get autocompletion and type checking:

TypeScript
1const t = useTranslations('home');
2
3t('title')        // ✅ Valid
4t('nonExistent')  // ❌ TypeScript error

Nested Keys

TypeScript
// For deeply nested messages
const t = useTranslations('settings.account.privacy');
t('title')  // settings.account.privacy.title

SEO and Metadata

Locale-Aware Metadata

TypeScript
1import { getTranslations } from 'next-intl/server';
2import { Metadata } from 'next';
3
4export async function generateMetadata({
5  params: { locale },
6}: {
7  params: { locale: string };
8}): Promise<Metadata> {
9  const t = await getTranslations({ locale, namespace: 'meta' });
10
11  return {
12    title: t('title'),
13    description: t('description'),
14  };
15}

Hreflang Tags

TypeScript
1import { locales } from '@/i18n/config';
2
3export async function generateMetadata({
4  params: { locale },
5}: {
6  params: { locale: string };
7}) {
8  const baseUrl = 'https://example.com';
9
10  return {
11    alternates: {
12      canonical: `${baseUrl}/${locale}`,
13      languages: Object.fromEntries(
14        locales.map((loc) => [loc, `${baseUrl}/${loc}`])
15      ),
16    },
17  };
18}

This generates proper hreflang tags for SEO:

HTML
<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="es" href="https://example.com/es" />
<link rel="alternate" hreflang="de" href="https://example.com/de" />

Performance Optimization

Namespace-Based Code Splitting

Only load translations needed for each page:

TypeScript
1// In i18n/request.ts
2export default getRequestConfig(async ({ locale }) => {
3  return {
4    messages: {
5      // Load only common namespace by default
6      common: (await import(`../messages/${locale}/common.json`)).default,
7    },
8  };
9});
10
11// In specific pages, load additional namespaces
12import { getMessages } from 'next-intl/server';
13
14export default async function CheckoutPage() {
15  const messages = await getMessages({ namespace: 'checkout' });
16  // ...
17}

Static Generation

Ensure all locale pages are statically generated:

TypeScript
export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

Avoiding Hydration Mismatches

Pass server time to prevent date/time mismatches:

TypeScript
1<NextIntlClientProvider
2  messages={messages}
3  timeZone="Europe/Berlin"
4  now={new Date()}
5>
6  {children}
7</NextIntlClientProvider>

Managing Translations at Scale

As your app grows, managing JSON files manually becomes problematic:

  • Missing translations are hard to track
  • Merge conflicts when multiple developers edit translation files
  • No context for translators
  • Slow workflow with manual export/import

IntlPull Integration

IntlPull provides seamless next-intl integration:

Terminal
1# Initialize IntlPull in your project
2npx @intlpullhq/cli init
3
4# Upload your existing translations
5npx @intlpullhq/cli upload
6
7# Get AI translations for new languages
8npx @intlpullhq/cli translate --languages es,de,fr,ja
9
10# Pull translations back to your project
11npx @intlpullhq/cli download
12
13# Watch mode for real-time sync during development
14npx @intlpullhq/cli watch

Benefits:

  • AI-powered translation with GPT-4, Claude, DeepL
  • Visual editor with screenshot context
  • Team collaboration with review workflows
  • Automatic sync with your codebase
  • Translation memory for consistency

Start free with IntlPull →


Common Patterns

Loading States with Suspense

TypeScript
1import { Suspense } from 'react';
2
3export default function Page() {
4  return (
5    <Suspense fallback={<Skeleton />}>
6      <TranslatedContent />
7    </Suspense>
8  );
9}

Error Boundaries

TypeScript
1import { useTranslations } from 'next-intl';
2
3function ErrorBoundary({ error }: { error: Error }) {
4  const t = useTranslations('errors');
5
6  return (
7    <div>
8      <h2>{t('title')}</h2>
9      <p>{t('message')}</p>
10    </div>
11  );
12}

Dynamic Routes with Locales

TypeScript
1// app/[locale]/blog/[slug]/page.tsx
2import { getTranslations } from 'next-intl/server';
3
4export default async function BlogPost({
5  params: { locale, slug },
6}: {
7  params: { locale: string; slug: string };
8}) {
9  const t = await getTranslations('blog');
10
11  // Fetch localized content
12  const post = await getPost(slug, locale);
13
14  return (
15    <article>
16      <h1>{post.title}</h1>
17      <p>{t('readTime', { minutes: post.readTime })}</p>
18    </article>
19  );
20}

RTL Language Support

For Arabic, Hebrew, Persian, and other RTL languages:

TypeScript
1import { locales } from '@/i18n/config';
2
3const rtlLocales = ['ar', 'he', 'fa'];
4
5export default async function LocaleLayout({
6  children,
7  params: { locale },
8}: {
9  children: React.ReactNode;
10  params: { locale: string };
11}) {
12  const dir = rtlLocales.includes(locale) ? 'rtl' : 'ltr';
13
14  return (
15    <html lang={locale} dir={dir}>
16      <body>{children}</body>
17    </html>
18  );
19}

Use CSS logical properties:

CSS
1/* Instead of margin-left, use: */
2margin-inline-start: 1rem;
3
4/* Instead of padding-right, use: */
5padding-inline-end: 1rem;
6
7/* Instead of text-align: left, use: */
8text-align: start;

Testing

Unit Testing Components

TypeScript
1import { render, screen } from '@testing-library/react';
2import { NextIntlClientProvider } from 'next-intl';
3import messages from '../messages/en.json';
4import { HomePage } from './page';
5
6function renderWithIntl(ui: React.ReactNode, locale = 'en') {
7  return render(
8    <NextIntlClientProvider locale={locale} messages={messages}>
9      {ui}
10    </NextIntlClientProvider>
11  );
12}
13
14test('renders home page title', () => {
15  renderWithIntl(<HomePage />);
16  expect(screen.getByText('Home')).toBeInTheDocument();
17});

Testing Multiple Locales

TypeScript
1const locales = ['en', 'es', 'de'];
2
3describe.each(locales)('HomePage in %s', (locale) => {
4  test('renders without crashing', () => {
5    const messages = require(`../messages/${locale}.json`);
6    renderWithIntl(<HomePage />, locale, messages);
7    // Assert based on expected translations
8  });
9});

Migrating to next-intl

From react-i18next

TypeScript
1// Before (react-i18next)
2import { useTranslation } from 'react-i18next';
3const { t } = useTranslation();
4t('home.title')
5
6// After (next-intl)
7import { useTranslations } from 'next-intl';
8const t = useTranslations('home');
9t('title')

From next-translate

TypeScript
1// Before (next-translate)
2import useTranslation from 'next-translate/useTranslation';
3const { t } = useTranslation('common');
4
5// After (next-intl)
6import { useTranslations } from 'next-intl';
7const t = useTranslations('common');

Translation file format is similar—most JSON files work without changes.


next-intl vs Alternatives

Aspectnext-intlreact-i18nextnext-translate
Next.js optimizedYesNoYes
Server ComponentsNativeWrapper neededNative
Bundle size~2KB~8KB~1.5KB
ICU formatFullPlugin neededBasic
TypeScriptExcellentGoodBasic
RoutingBuilt-inManualBuilt-in
Active developmentVery activeActiveModerate
DocumentationExcellentGoodGood

Recommendation: Use next-intl for any new Next.js App Router project. It's the most complete, best-maintained option specifically designed for Next.js.


Troubleshooting

"Missing message" Errors

TypeScript
1// In development, show key name; in production, return empty
2<NextIntlClientProvider
3  messages={messages}
4  onError={(error) => {
5    if (process.env.NODE_ENV === 'development') {
6      console.error(error);
7    }
8  }}
9  getMessageFallback={({ key }) => {
10    return process.env.NODE_ENV === 'development' ? `[${key}]` : '';
11  }}
12>

Hydration Mismatch

Usually caused by date/time differences between server and client:

TypeScript
1// Pass server time explicitly
2<NextIntlClientProvider
3  messages={messages}
4  now={new Date()}
5  timeZone="UTC"
6>

Middleware Not Running

Check your matcher pattern:

TypeScript
1export const config = {
2  matcher: [
3    // Match all paths except static files and API
4    '/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)',
5  ],
6};

Frequently Asked Questions

What is next-intl?

next-intl is an internationalization (i18n) library specifically built for Next.js. It provides translation management, locale-based routing, date/number formatting, and full support for Server Components and the App Router. It's the recommended i18n solution for Next.js 13+ projects due to its native integration and small bundle size (~2KB).

Is next-intl better than react-i18next for Next.js?

Yes, next-intl is better than react-i18next for Next.js App Router projects. next-intl was designed specifically for Next.js with native Server Component support, built-in routing, and zero configuration for the App Router. react-i18next is a general React library that requires additional setup for Server Components and doesn't integrate as seamlessly with Next.js routing.

How do I set up next-intl with Next.js App Router?

To set up next-intl: (1) Install with npm install next-intl, (2) Create a [locale] folder in app directory, (3) Create translation JSON files in a messages folder, (4) Configure middleware.ts for locale detection, (5) Create i18n/request.ts for server config, (6) Wrap your app with NextIntlClientProvider. See the complete setup section above for detailed code.

Does next-intl work with Server Components?

Yes, next-intl has native Server Component support. Use getTranslations() from next-intl/server in Server Components—translations render on the server with zero client-side JavaScript. This is a major advantage over react-i18next which requires wrapper components for Server Component support.

How do I add a language switcher with next-intl?

Use useLocale() and useRouter() from next-intl: Get the current locale with useLocale(), get the pathname with usePathname() from next-intl/client, then call router.replace(pathname, { locale: newLocale }) to switch languages. See the Language Switcher section above for complete code.

How do I handle pluralization in next-intl?

next-intl uses ICU message format for pluralization. Define plural messages like: "{count, plural, =0 {No items} one {# item} other {# items}}". Then call t('key', { count: 5 }). next-intl automatically selects the correct plural form based on the locale—handling languages with 2 forms (English) to 6 forms (Arabic) correctly.

Can I use next-intl with TypeScript?

Yes, next-intl has excellent TypeScript support. Create a global.d.ts file that extends IntlMessages with your message types, and you'll get full autocompletion and type checking for translation keys. This catches missing or misspelled keys at compile time rather than runtime.

How do I manage translations at scale with next-intl?

Use a Translation Management System (TMS) like IntlPull when you have 500+ strings or 3+ languages. IntlPull provides: CLI sync (npx @intlpullhq/cli upload/download), AI-powered translation, team collaboration, visual context for translators, and seamless next-intl integration. This eliminates manual JSON file management and merge conflicts.

What's the bundle size of next-intl?

next-intl adds approximately 2KB to your client bundle (gzipped). This is significantly smaller than react-i18next (~8KB) or react-intl (~12KB). Additionally, translations rendered in Server Components add zero bytes to the client bundle since they're rendered server-side.

Does next-intl support RTL languages?

Yes, next-intl fully supports RTL languages like Arabic, Hebrew, and Persian. You need to set the dir="rtl" attribute on your HTML element based on locale and use CSS logical properties (margin-inline-start instead of margin-left). See the RTL Language Support section for implementation details.

How do I test components using next-intl?

Wrap components with NextIntlClientProvider in tests. Create a helper function that wraps your component with the provider, locale, and messages. Use this for both unit tests and integration tests. You can test multiple locales using describe.each() to ensure translations work across all supported languages.


Summary

next-intl is the best choice for Next.js internationalization in 2026. Key takeaways:

  1. Built for Next.js - Native App Router and Server Component support
  2. Lightweight - ~2KB bundle, zero JS for server-rendered translations
  3. Complete - Routing, formatting, pluralization, TypeScript
  4. Well-maintained - Active development, excellent documentation

Getting started:

  1. npm install next-intl
  2. Create [locale] folder structure
  3. Configure middleware and i18n/request.ts
  4. Use getTranslations() in Server Components
  5. Scale with IntlPull for AI translation and team collaboration

Ready to manage next-intl translations professionally? Start free with IntlPull — AI translation, visual editor, and CLI sync included.

Tags
next-intl
nextjs
i18n
internationalization
app-router
server-components
react
2026
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.