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:
- 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
}
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 {} }Override
CustomTypeOptionsinreact-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:

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