[Frontend Note] Smoother multilingual frontend development with i18next/vue-i18n + Typescript

When the translation files get large and have lots of keys, Typescript can help!

Posted by Jamie on Friday, September 30, 2022

Smoother multilingual frontend development with i18next/vue-i18n + Typescript

Preface

I previously took over a frontend project with Chinese, Japanese, and English versions, where the translations had multiple levels of nesting. It was easy to mistype a single character during development, causing i18n to fail to find the key and the wrong text to show on screen. So I went looking to see if there was a way to leverage Typescript so that t("") could give autocomplete suggestions.

I really did find someone sharing a solution for a React project; at the same time, I also found the officially recommended approach for Vue projects. So let me record specifically how this can be applied in React and Vue projects respectively.

Multilingual hints in React

Dependencies: i18next + react-i18next + typescript

First, assume that i18next and react-i18next are already configured in the React project, with the following structure:

  • src
    • App.tsx
    • index.tsx
    • i18n
      • index.ts
      • langs
        • en.ts
        • zh.ts
        • jp.ts
  • package.json
  • tsconfig.json

The contents of ./src/i18n/langs/en.ts are as follows:

export default {
    hello: 'Hello',
    fruit: {
        apple: 'Apple',
        banana: 'Banana'
    }
};

./src/i18n/index.ts:

import en from '@/i18n/langs/en'
import jp from '@/i18n/langs/jp'
import zh from '@/i18n/langs/zh'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'

export enum LangType {
    ZH_HK = 'zh_hk',
    EN_US = 'en_us',
    JA_JP = 'ja_jp',
}
export const resources = {
    [LangType.EN_US]: {
        translation: en,
    },
    [LangType.ZH_HK]: {
        translation: zh,
    },
    [LangType.JA_JP]: {
        translation: jp,
    },
}
i18n.use(initReactI18next).init({
    resources,
    fallbackLng: LangType.EN_US,
    debug: true,
    interpolation: {
        escapeValue: false,
    },
})

export default i18n

./src/index.ts:

import i18n from '@/i18n'
import { I18nextProvider } from 'react-i18next'

///... other settings
root.render(
    <React.StrictMode>
        <I18nextProvider i18n={i18n}>
        		<App />
        </I18nextProvider>
    </React.StrictMode>,
)

That’s the basic setup for using multilingual support in a React project. So how do we get i18n to support Typescript type hints (current versions: i18next: ^21.9.2, react-i18next: ^11.18.6)? Add a *.d.ts at the project root to override the key Types in those two dependencies (let’s name it react-i18n.d.ts for now).

The main idea is:

  1. Import a translation JSON, treat it as a standard Type, and convert it into a key-corresponding type.

./src/react-i18n.d.ts:

// import the original type declarations
import 'react-i18next'

import { LangType, resources } from '@/i18n'
// import all namespaces (for the default language, only)
import en from '@/i18n/langs/en'

type I18nStoreType = typeof en
export type I18nT = {
    (key: keyof I18nStoreType): string
}

  1. Have it extend the return type (TFunction) exposed by i18n.

    declare module 'i18next' {
    // eslint-disable-next-line @typescript-eslint/no-empty-interface
    interface TFuntion extends I18nT {}
    }
    
  2. Override CustomTypeOptions in react-i18next.

    // react-i18next versions higher than 11.11.0
    declare module 'react-i18next' {
    interface CustomTypeOptions {
        resources: typeof resources[LangType.EN_US]
    }
    }
    

That’s basically it. Using t("") in a component will now give Typescript hints: image

Multilingual hints in Vue

First, assume that vue-i18n has already been set up in the Vue project. The main logic is the same as the React project — expose a translation template, convert it into a Type, and override the i18n type. However, looking at the official vue-i18n issue, vue-i18n can additionally type-check all the resources you pass in: if en.ts has a translation but zh.ts doesn’t, an error will be raised at i18n initialization:

  • i18n
    • index.ts
    • langs
      • en.ts
      • zh.ts

In ./i18n/index.ts, in addition to the basic setup, also expose the Type derived from en.ts:

import { createI18n } from 'vue-i18n';
import en from './langs/en.ts';
import zh from './langs/zh.ts';

export type MessageSchema = typeof en;

const i18n = createI18n({
    legacy: false,
    locale: 'en-us',
    globalInjection: true,
    fallbackLocale: 'en-us',
    messages:{
		"en-us":en,
		"zh-tw":zh
    }
});

export default i18n;

Add a TS type file under ./src, named vue-i18n.d.ts for now:

import { DefineLocaleMessage } from 'vue-i18n';
import { MessageSchema } from './i18n';

declare module 'vue-i18n' {
    // eslint-disable-next-line @typescript-eslint/no-empty-interface
    export interface DefineLocaleMessage extends MessageSchema {}
}

Use declare module to override the npm package’s default type definition.

With that, it’s basically done.

ChangeLog

  • 20220930 - init
  • 20260501–translate by claude code