type-safe i18n library
The aim of typed-i18n
is to provide zero-configuration type-safe i18n feature.
Typical i18n libraries uses the syntax of t('group.key')
, which is apparently not type-safe.
typed-i18n
uses regular JS & TS syntax so you can get these benefits:
Also typed-i18n
supports two translation flows:
engineer-driven translation
Engineers directly write translations. We can completely make them type-safe.
translator-driven translation
translators fill yaml/json/spreadsheets/(any) then compile it. It is a common pattern, but type-safety will be lost to some extent.
npm install --save typed-i18n
Or if you use Yarn:
yarn add typed-i18n
You can write basic translations like this:
import TypedI18n from 'typed-i18n'
const en = {
hello: 'Hello',
goodbye: 'Goodbye',
}
const ja = {
hello: 'こんにちは',
goodbye: 'さようなら',
}
type Lang = 'en' | 'ja'
type Translations = typeof en & typeof ja
const t = new TypedI18n<Lang, Translations>()
.addLocale('en', en)
.addLocale('ja', ja)
t.setLocale('en')
console.log(t.trans.hello) // => Hello
t.setLocale('ja')
console.log(t.trans.hello) // => こんにちは
If there are any mistakes in your translations, TS will check them.
In the above example, the following code will throw TypeScript errors.
// missing the locale
t.setLocale('cn')
// missing the key
t.trans.notExistKey
// trying to add a locale with missing keys
t.addLocale('de', {
hello: 'Guten Tag',
})
Interpolation is one of the most used functionalities in I18N. It enables you to integrate dynamic values into your translations.
There are two options to implement it:
1. use interp
function
import TypedI18n, { interp } from 'typed-i18n'
const en = {
greeting: interp(name => `Hello, ${name}`),
}
const t = new TypedI18n<'en', typeof en>().addLocale('en', en)
console.log(t.trans.greeting('John')) // => Hello, John
2. define $index
and call withArgs
method
import TypedI18n from 'typed-i18n'
const en = {
greeting: `Hello, $1`,
}
const t = new TypedI18n<'en', typeof en>().addLocale('en', en)
console.log(t.withArgs('John').trans.greeting) // => Hello, John
You can nest your translations. There are two options to implement it:
1. use getter functions
import TypedI18n from 'typed-i18n'
const en = {
hello: 'Hello',
goodbye: 'Goodbye',
helloButGoodbye: () => `${en.hello}, but ${en.goodbye}.`,
}
const t = new TypedI18n<'en', typeof en>().addLocale('en', en)
console.log(t.trans.helloButGoodbye) // => Hello, but Goodbye.
2. use $this
expression
import TypedI18n from 'typed-i18n'
const en = {
hello: 'Hello',
goodbye: 'Goodbye',
helloButGoodbye: '$this.hello, but $this.goodbye.',
}
const t = new TypedI18n<'en', typeof en>().addLocale('en', en)
console.log(t.trans.helloButGoodbye) // => Hello, but Goodbye.
You can use typed-i18n
with React:
npm install --save typed-i18n-react
// i18n.ts
import TypedI18n, from 'typed-i18n'
import createContextHooks from 'typed-i18n-react'
const en = {
hello: 'Hello',
changeLocale: 'Change Locale',
}
const ja = {
hello: 'こんにちは',
changeLocale: '言語を変える',
}
type Lang = 'en' | 'ja'
type Translations = typeof en & typeof ja
const t = new TypedI18n<Lang, Translations>()
.addLocale('en', en)
.addLocale('ja', ja)
export const { useTrans, useChangeLocale, Provider } = createContextHooks(t)
// App.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { useTrans, useChangeLocale, Provider } from './i18n'
function App() {
const t = useTrans()
const changeLocale = useChangeLocale()
const changeLang = React.useCallback(() => {
changeLocale(t.locale === 'en' ? 'ja' : 'en')
}, [changeLocale, t.locale])
return (
<div className="App">
<div className="App-title">{t.trans.hello}</div>
<div className="App-btn" onClick={changeLang}>
{t.trans.changeLocale}
</div>
</div>
)
}
ReactDOM.render(
<Provider>
<App />
</Provider>,
document.getElementById('root'),
)