Simple, pragmatic and performant i18n solution for JavaScript applications
Tags: i18nreactnextjsjavas
Published: 2022-09-05 16:09:06

---
title: Simple, pragmatic and performant i18n solution for JavaScript applications
published: true
date: 2022-07-16 00:00:00 UTC
tags: i18nreactnextjsjavas
canonical_url: https://bstefanski.com/simple-pragmatic-and-performant-i18n-for-javascript
---

There're great libraries for i18n that support absolutely everything like interpolation with React components, server-side rendering, and code splitting (or maybe I should call it - JSON splitting). This is one example of such library - [react-i18next](https://react.i18next.com/).

The biggest drawback and issue, something that keeps me from using it is its bundle size. Assuming you're using gzip for compression like everyone else, it will take 20kB+ from your bundle. For some people/teams it's an acceptable amount, but for me, I don't believe it to be a good trade-off.

 ![i18next package size summary](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)<noscript><img alt="i18next package size summary" sizes="100vw" srcset="/_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fi18next-bundle-size.png&amp;w=320&amp;q=75 320w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fi18next-bundle-size.png&amp;w=640&amp;q=75 640w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fi18next-bundle-size.png&amp;w=1080&amp;q=75 1080w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fi18next-bundle-size.png&amp;w=1200&amp;q=75 1200w" src="https://bstefanski.com/_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fi18next-bundle-size.png&amp;w=1200&amp;q=75" decoding="async" data-nimg="fill" style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;object-fit:contain" loading="lazy"></noscript>
<small></small>

 ![react-i18next package size summary](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)<noscript><img alt="react-i18next package size summary" sizes="100vw" srcset="/_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Freact-i18next-bundle-size.png&amp;w=320&amp;q=75 320w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Freact-i18next-bundle-size.png&amp;w=640&amp;q=75 640w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Freact-i18next-bundle-size.png&amp;w=1080&amp;q=75 1080w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Freact-i18next-bundle-size.png&amp;w=1200&amp;q=75 1200w" src="https://bstefanski.com/_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Freact-i18next-bundle-size.png&amp;w=1200&amp;q=75" decoding="async" data-nimg="fill" style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;object-fit:contain" loading="lazy"></noscript>
<small></small>

And that's why I decided to write my implementation that has great DX (supports dotted paths with autocompletion) and is easy to scale/maintain. I used the [`js-cookie`](https://github.com/js-cookie/js-cookie) library to get and parse the cookie with legible & declarative API.

The example is done specifically for Next.js, but you can seamlessly port it to any other library/framework.

```

// i18n.ts 

import Cookies from "js-cookie";

import get from "lodash/get";

import { en } from "./en";

type Locales = "en";

const defaultTranslations: Record<Locales, Partial<typeof en>> = {

 en,

};

export const t = (key: Join<PathsToStringProps<typeof en>, ".">, translations = defaultTranslations) => {

const locale = Cookies.get("NEXT\_LOCALE") as Locales;

return get(translations[locale] || translations["en"], key);

};

type PathsToStringProps<T> = T extends string

? []

: {

[K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];

}[Extract<keyof T, string>];

type Join<T extends string[], D extends string> = T extends []

? never

: T extends [infer F]

? F

: T extends [infer F, ...infer R]

? F extends string

? `${F}${D}${Join<Extract<R, string[]>, D>}`

: never

: string;

```

This is how the translations file looks like

```

// en.ts 

export const en = {

 ctaSection: {

 title: "Some value for demo purposes",

// ...The rest of the items, removed for brevity 

}

}

```

And this is how you use it:

 ![An example of how you could use the "t" function](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)<noscript><img alt='An example of how you could use the "t" function' sizes="100vw" srcset="/_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fusage.png&amp;w=320&amp;q=75 320w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fusage.png&amp;w=640&amp;q=75 640w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fusage.png&amp;w=1080&amp;q=75 1080w, /_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fusage.png&amp;w=1200&amp;q=75 1200w" src="https://bstefanski.com/_next/image?url=%2Fposts%2Fsimple-pragmatic-and-performant-i18n-for-javascript%2Fusage.png&amp;w=1200&amp;q=75" decoding="async" data-nimg="fill" style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;object-fit:contain" loading="lazy"></noscript>
<small></small>

You don't have to worry about the performance unless the file exceeds a few hundred lines. After that, you can use dynamic imports and split the translations file into smaller chunks.