We use https://react.i18next.com/ to handle multi-language translations in our app.

This article assumes you already have react-i18next set up for your project. If you're new to this library, follow the Getting Started Guide

Up until now finding incorrect translation keys, or missing translations was a dread. We had to catch any issues ourselves without any IDE support and this sucked. After adding typesafety to our translations I found plenty of previously hidden issues in our codebase, and could fix them in one go. Isn't typescript just the best? 😉

Adding typesafety for react-i18next

Here's a simplified example of how you can add typesafety for your react-i18next project:

// i18n.ts
// export `defaultNS` and `resources`
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
 
import appDe from "src/app/_locales/de.json";
import appEn from "src/app/_locales/en.json";
import commonDe from "src/common/_locales/de.json";
import commonEn from "src/common/_locales/en.json";
import authDe from "src/auth/_locales/de.json";
import authEn from "src/auth/_locales/en.json";
 
export const defaultNS = "app";
export const resources = {
  de: {
    app: appDe,
    common: commonDe,
    auth: authDe,
  },
  en: {
    app: appEn,
    common: commonEn,
    auth: authEn,
  },
} as const;
 
i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: ["en"],
    defaultNS,
    resources,
    debug: process.env.NODE_ENV !== "production",
    interpolation: {
      escapeValue: false,
    },
  });
 
export default i18n;

Create a type definitions file for your translations module. In vite and cra you can put a new file in the @types folder: src/@types/react-i18next.d.ts

// src/@types/react-i18next.d.ts
import "react-i18next";
import { resources, defaultNS } from "src/i18n";
 
declare module "react-i18next" {
  interface CustomTypeOptions {
    defaultNs: typeof defaultNS;
    resources: (typeof resources)["en"];
  }
}

Thats it. Now you should have fully typesafe translations and autocomplete.

How to correctly use the useTranslation hook

Typescript is now very strict about how and which translations you can use, depending on your usage of the useTranslation hook.

using a single namespace

if using useTranslate('singleNS') dont use NS prefix in trans keys

const { t } = useTranslate("singleNS");
t("singleNS:some.key"); // ❌
t("some.key"); // ✅

using multiple namespaces

if using useTranslate(['nsA', 'nsB']) this will force you to use NS prefixes for everything

const { t } = useTranslate(["nsA", "nsB"]);
// ❌
t("some.key.in.nsA");
t("some.key.in.nsB");
// ✅
t("nsA:some.key.in.nsA");
t("nsB:some.key.in.nsB");

using multiple namespaces with the first as default

if using useTranslate(['nsA', 'nsB'] as const) -> this will allow you to skip the namespace prefix for nsA, namespaces are required for nsB and further.

const { t } = useTranslate(["nsA", "nsB"] as const);
// ✅
t("some.key.in.nsA");
t("nsB:some.key.in.nsB");

using computed translation key names

if you construct translation keys via functions or lookups make sure you return the strings as const so the type checker evaluates them as their value, and not as string

const translationMap = {
  case1: "nsA:somekey",
  case2: "nsA:otherKey",
};
t(translationMap["case1"]); // ❌ the type of translationMap['case1'] is string => invalid
 
const translationMap = {
  case1: "nsA:somekey",
  case2: "nsA:otherKey",
} as const;
t(translationMap["case1"]); // ✅ now the type is 'nsA:somekey' -> valid

The same principal applies when using functions to generate the translation keys. Make sure they dont return the key with the return type string, but the full string as const / union.