Simple, pragmatic and performant i18n solution for JavaScript applications

16th July 2022

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.

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
react-i18next package size summary

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 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>> = {
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

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.

Bart Stefański

A self-taught full-stack software engineer based in Poland, working in React.js & Nest.js Stack. Passionate about Clean Code, Object-Oriented Architecture and fast web.