Bart Stefanski
Published on

๐ŸŒŽ Simple, pragmatic and performant i18n solution for JavaScript applications

Authors

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