---
createdAt: 2025-09-10
updatedAt: 2025-09-10
title: "Per-Component vs. Centralized i18n: A New Approach with Intlayer"
description: A deep dive into internationalization strategies in React, comparing centralized, per-key, and per-component approaches, and introducing Intlayer.
keywords:
  - i18n
  - React
  - Internationalization
  - Intlayer
  - Optimization
  - Bundle Size
slugs:
  - blog
  - per-component-vs-centralized-i18n
---

# Per-Component vs. Centralized i18n

I'm the creator of Intlayer, an i18n solution that takes a **per-component approach**.

The per-component approach isn't exactly new.
On the Vue side, `vue-i18n` has a ['Single File Components'](https://vue-i18n.intlify.dev/guide/advanced/sfc.html) approach.It also exists in Nuxt with per-component translations [here](https://i18n.nuxtjs.org/docs/guide/per-component-translations).

Even in a Flutter app, we can often find this pattern:

```bash
lib/
├── features/
│   ├── login/
│   │   ├── login_screen.dart
│   │   └── login_screen.i18n.dart  <- Translations live here

```

```dart
import 'package:i18n_extension/i18n_extension.dart';

extension Localization on String {
  static var _t = Translations("en_us") +
      {
        "en_us": "Hello",
        "pt_br": "Olá",
        "es": "Hola",
      };

  String get i18n => localize(this, _t);
}

```

However, in the React world, we mainly see different approaches, that I will group in three categories:

1. **Centralized approach** (i18next, next-intl, react-intl, lingui)
2. **Per-component approach** (Intlayer)
3. **Per-key approach** (inlang)

In this blog, I won't focus on compiler-based solutions, which I already covered here: [Compiler vs Declarative i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/en/compiler_vs_declarative_i18n.md).
Note that compiler-based i18n (e.g., Lingui) simply automates the extraction and loading of content. Under the hood, they often share the same limitations as the centralized approach.

So, what are the differences?

- **A Centralized approach** (with no namespaces) considers a single source to retrieve content. By default, you load the content from all pages when your app loads.
- **Per-component and Per-key approaches** fine-grain the content retrieval.

> Note that the more you fine-grain how you retrieve your content, the more you risk inserting additional state and logic into your components.
> For instance, a page can easily include 500 text nodes (`<p>`, `<span>`). A per-key approach will effectively insert 500 new component instances to handle the translation, each potentially using their own `useState`, `useEffect`, or `useContext`.

Here is a visualization of the trade-off:

```bash
Count of useState / Overhead (100 blocks <p> / 10 components)
100 | x Per-key
    |
    |
    ...
    |
    |
    |
    |
 10 |  x Per-component
    |
  1 |                                           x Centralized
    |-------------------------------------------------------->
    0                                          900     1000
           % of unused content (Based on 10 unique pages)

```

In parallel, you should know that when you dynamically load your content (`await import()`), you end up with extra requests to your server. One extra `useState` or hook relates to one extra request to your server.

> To fix this point, Intlayer suggests grouping multiple content definitions under a same key, Intlayer will then merge that content.

### So why is the Centralized approach so popular?

- First, i18next was the first solution to become widely used, following an approach inspired by PHP and Java, which rely on a clear separation of concerns (MVP). i18next arrive even before the trend of frontend frameworks and component based architecture.
- Then, once a library is widely adopted, it becomes difficult to shift the ecosystem to other patterns.
- Using a centralized approach also makes things easier in Translation Management Systems such as Crowdin, Phrase, or Localized.
- The logic behind a per-component approach is more complex than a centralized one and takes extra time to develop, especially when you have to solve problems like identifying where the content is located.

### Okay, but why not just stick to a Centralized approach?

Let me tell you why it can be problematic for your app:

- **Unused Data:** When a page loads, you often load the content from all other pages. (In a 10-page app, that's 90% unused content loaded). You lazy load a modal? The i18n library doesn't care, it loads the strings first anyway.
- **Performance:** For each re-render, every single one of your components is hydrated with a massive JSON payload, which impacts your app's reactivity as it grows.
- **Maintenance:** Maintaining large JSON files is painful. You have to jump across files to insert a translation, ensuring no translations are missing and no **orphan keys** are left behind.
- **Design-system:** It creates incompatibility with design systems (e.g., a `LoginForm` component) and constrains component duplication across different apps.

**"But we invented Namespaces!"**

Sure, and that's an massive move forward. Let's look at the comparison of the main bundle size of a Vite + React + React Router v7 + Intlayer setup. We simulated a 20-page application.

The first example does not include lazy-loaded translations per locale and no namespace splitting. The second includes content purging + dynamic loading for translations.

| Optimized bundle                                                                                      | Bundle not optimized                                                                                                     |
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| ![optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png?raw=true) | ![no optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png?raw=true) |

So thanks for namespaces, we moved from this structure:

```bash
locale/
├── en.json
├── fr.json
├── es.json

```

To this one:

```bash
locale/
├── en/
│   ├── common.json
│   ├── navbar.json
│   ├── footer.json
│   ├── home.json
│   ├── about.json
├── fr/
│   ├── ...
├── es/
│   ├── ...

```

Now you have to finely manage what part of your app content should be loaded, and where. Conclusion, 90% of projects just skip this part due to the complexity (see [next-i18next guide](https://github.com/aymericzip/intlayer/blob/main/docs/blog/en/i18n_using_next-i18next.md) for instance to see the challenges that represents (just) following good practices).
Consequently, 95% of projects end up with the massive JSON loading problem explained earlier.

However, not all per-component approaches solve this. For instance, `vue-i18n`'s approach does not inherently lazy load the translations, so you face the same bundle issues.
Moreover, without proper separation of concerns, it becomes much more difficult to extract and provide your translations to translators for review.

### How Intlayer's per-component approach solves this

Intlayer proceeds in several steps:

1. **Declaration:** Declare your content anywhere in your codebase using `.content.{ts|jsx|cjs|json|json5|...}` files. This ensures separation of concerns while keeping content colocated. A content file can be per-locale or multilingual.
2. **Processing:** Intlayer runs a build step to process JS logic, handle missing translation fallbacks, generate TypeScript types, manage duplicated content, and fetch content from your CMS, and more.
3. **Purging:** When your app builds, Intlayer purges unused content (a bit like how Tailwind manages your classes) by replacing the content as follows:

**Declaration:**

```tsx
// src/MyComponent.tsx
export const MyComponent = () => {
  const content = useIntlayer("my-key");
  return <h1>{content.title}</h1>;
};
```

```tsx
// src/myComponent.content.ts
export const {
  key: "my-key",
  content: t({
    en: { title: "My title" },
    fr: { title: "Mon titre" }
  })
}

```

**Processing:** Intlayer builds the dictionary based on the `.content` file and generates:

```json5
// .intlayer/dynamic_dictionary/en/my-key.json
{
  "key": "my-key",
  "content": { "title": "My title" },
}
```

**Replacement:** intlayer extracts your component during the application build.

**- Static Import Mode:**

```tsx
// Representation of the component in JSX-like syntax
export const MyComponent = () => {
  const content = useDictionary({
    key: "my-key",
    content: {
      nodeType: "translation",
      translation: {
        en: { title: "My title" },
        fr: { title: "Mon titre" },
      },
    },
  });

  return <h1>{content.title}</h1>;
};
```

**- Dynamic Import Mode:**

```tsx
// Representation of the component in JSX-like syntax
export const MyComponent = () => {
  const content = useDictionaryAsync({
    en: () =>
      import(".intlayer/dynamic_dictionary/en/my-key.json", {
        with: { type: "json" },
      }).then((mod) => mod.default),
    // Same for other languages
  });

  return <h1>{content.title}</h1>;
};
```

> `useDictionaryAsync` uses a Suspense-like mechanism to load the localized JSON only when needed.

**Key benefits of this per-component approach:**

- Keeping your content declaration close to your components allows better maintainability (e.g moving a components to another app or design system. Deleting the component folder remove the related content too, as you probably already do for your `.test`, `.stories`)

- A per-component approach prevents AI agents from needing to jump across all your different files. It treats all translations in one place, limiting the complexity of the task, and the amount of tokens used.

### Limitations

Of course, this approach comes with trade-offs:

- It is harder to connect to other l10n systems and extra tooling.
- You get locked in (which is basically already the case with any i18n solution due to their specific syntax).

That's the reason why Intlayer tries to provide a complete toolset for i18n (100% free and OSS), including AI translation using your own AI Provider and API keys. Intlayer also provides tooling to synchronize your JSON, functioning like ICU / vue-i18n / i18next message formatters to map the content to their specific formats.

---

Is it overkill? Or will it become the next Tailwind for content?
I'm happy to take your honest feedback about it. Your objections really help me build a better product.
