A fully type-safe and lightweight internationalization library for all your TypeScript and JavaScript projects.
🐤 lightweight (~1kb)
👌 easy to use syntax
🏃 fast and efficient
:safety_vest: prevents you from making mistakes (also in plain JavaScript projects)
👷 creates boilerplate code for you
💬 supports plural rules
📅 allows formatting of values e.g. locale-dependent date or number formats
↔️ supports switch-case statements e.g. for gender-specific output
⬇️ option for asynchronous loading of locales
📚 supports multiple namespaces
⏱ supports SSR (Server-Side Rendering)
🤝 can be used for frontend, backend and API projects
🔍 locale-detection for browser and server environments
🔄 import and export translations from/to files or services
⛔️ no external dependencies
Click here to see an interactive demo of typesafe-i18n
showing some key aspects of the type-checking capabilities of this internationalization library.
typesafe-i18n
to your projecttypesafe-i18n
add to your bundle sizetypesafe-i18n
implemented⌨️ Run the setup process and automatically detect the config needed
npx typesafe-i18n --setup-auto
or manually configure typesafe-i18n
by answering a few questions
npx typesafe-i18n --setup
It didn't work? See here for possible troubleshooting.
👀 Take a look at the generated files and it's folder-structure
📖 Explore the assets
typesafe-i18n
offers a lot. Just presscmd + F
to search on this page or see the table of contents that will link you to more specific subpages with more details.
⭐️ Star this project on GitHub
Thanks! This helps the project to grow.
Having trouble setting up typesafe-i18n
? Reach out to us via Github Discussions or on Discord.
npm install typesafe-i18n
The changelog of this project can be found here
5.x.x
: see the release post
4.x.x
: see the release post
3.x.x
: see the release post
Curious about what comes next? See this discussion to learn more about the plans for the future of this project.
If you would like to get involved within this project, take a look at this discussion.
The package can be used inside JavaScript and TypeScript applications. You will get a lot of benefits by running the generator since it will create a few wrappers to provide you with full typesafety.
You can use typesafe-i18n
in a variety of project-setups:
All you need is inside the generated file i18n-utils.ts
. You can use the functions in there to create a small wrapper for your application.
Feel free to open a new discussion if you need a guide for a specific framework.
See here if you want to learn how you can use typesafe-i18n
to implement your own specific use-case.
The library should work in all modern browsers. It uses some functionality from the Intl
namespace. You can see the list of supported browsers here. If you want to support older browsers that don't include these functions, you would need to include a polyfill like intl-pluralrules.
Here you can see some examples where typesafe-i18n
can help you:
The typesafe-i18n
package allows us to be 100% typesafe for our translation functions and even the translations for other locales itself. The generator
outputs TypeScript definitions based on your base locale.
You will also benefit from full typesafe JavaScript code via JSDoc-annotations.
typesafe-i18n
comes with an API that allows other services to read and update translations. You can connect ot other services by using the importer
and exporter
functionality:
The footprint of the typesafe-i18n
package is smaller compared to other existing i18n packages. Most of the magic happens in development mode, where the generator creates TypeScript definitions for your translations. This means, you don't have to ship the whole package to your users. The only two parts, that are needed in production are:
These parts are bundled into the core functions. The sizes of the core functionalities are:
Apart from that there can be a small overhead depending on which utilities and wrappers you use.
There also exists a useful wrapper for some frameworks:
typesafe-i18n
angular-service: 1398 bytes gzippedtypesafe-i18n
react-context: 1576 bytes gzippedtypesafe-i18n
solid-context: 1409 bytes gzippedtypesafe-i18n
svelte-store: 1346 bytes gzippedtypesafe-i18n
vue-plugin: 1257 bytes gzippedThe package was optimized for performance:
If you use typesafe-i18n
you will get a smaller bundle compared to other i18n solutions. But that doesn't mean, we should stop there. There are some possible optimizations planned to decrease the bundle size even further.
Become a sponsor ❤️ if you want to support my open source contributions.
Thanks for sponsoring my open source work!
Dou you still have some questions? Reach out to us via Github Discussions or on Discord.
typesafe-i18n
failsRunning the npx
command with a npm
version <7.0.0
will probably fail because it will not include peerDependencies
.
You could try installing it locally via:
npm install typesafe-i18n
and then run the setup-command from within the node_modules
folder via:
./node_modules/typesafe-i18n/cli/typesafe-i18n.mjs --setup-auto
here is the original issue with some additional information: #142
Property 'XYZ' does not exist on type 'TranslationFunctions'
Make sure to run the generator after you make changes to your base translation file. The generator will generate and update the types for you.
typesafe-i18n
inside JavaScript applications?Yes, you can. See the usage section for instructions. Even if you don't use TypeScript you can still improve from some typesafety features via JSDoc-annotations.
The generator will only look for changes in your base locale file. Make sure to always update your base locale file first, in order to get the correct auto-generated types. If you want to change your base locale file, make sure to give it the type of BaseTranslation
. All other locales should have the type of Translation
. E.g. if you set your base locale to italian, you would need to do it like this:
set your base locale to italian (it
) in .typesafe-i18n.json`:
{
"baseLocale": "it"
}
define the type of your base locale as BaseTranslation
// file 'src/i18n/it/index.ts'
import type { BaseTranslation } from '../i18n-types'
const it: BaseTranslation = {
WELCOME: "Benvenuto!"
}
export default it
define the type of your other locales as Translation
// file 'src/i18n/en/index.ts'
import type { Translation } from '../i18n-types'
const en: Translation = {
WELCOME: "Welcome!"
}
export default en
The generator creates some helpful wrappers for you. If you want to write your own wrappers, you can disable the generation of these files by setting the generateOnlyTypes
option to true
.
typesafe-i18n
supported by i18n-ally
?Yes, you can configure i18n-ally
like this. There is currently also an open PR
that will add official support for typesafe-i18n
.
When you want to dynamically access a translation, you can use the usual JavaScript syntax to access a property via a variable (myObject[myVariable]
).
// i18n/en.ts
import type { BaseTranslation } from '../i18n-types'
const en: BaseTranslation = {
category: {
simple: {
title: 'Simple title',
description: 'I am a description for the "simple" category',
},
advanced: {
title: 'Advanced title',
description: 'I am a description for the "advanced" category',
}
}
}
export default en
<script lang="ts">
// Component.svelte
import LL from '$i18n/i18n-svelte'
import type { Translation } from '$i18n/i18n-types'
// ! do not type it as `string`
// by restricting the type, you don't loose the typesafety features
export let category: keyof Translation['category'] = 'simple'
</script>
<h2>{$LL.category[category].title()}
<p>
{$LL.category[category].description()}
<p>
By default typesafe-i18n
at this time does not provide such a functionality. But you could easily write a function like this:
import { LocalizedString } from 'typesafe-i18n'
// create a component that handles the translated message
interface WrapTranslationPropsType {
message: LocalizedString,
renderComponent: (messagePart: LocalizedString) => JSX.Element
}
export function WrapTranslation({ message, renderComponent }: WrapTranslationPropsType) {
// define a split character, in this case '<>'
let [prefix, infix, postfix] = message.split('<>') as LocalizedString[]
// render infix only if the message doesn't have any split characters
if (!infix && !postfix) {
infix = prefix
prefix = '' as LocalizedString
}
return <>
{prefix}
{renderComponent(infix)}
{prefix}
</>
}
// your translations would look something like this
const en = {
'WELCOME': 'Hi {name:string}, click <>here<> to create your first project'
'LOGOUT': 'Logout'
}
export default en
// create a wrapper for a component for easier usage
interface WrappedButtonPropsType {
message: LocalizedString,
onClick: () => void,
}
export function WrappedButton({ message, onClick }: WrappedButtonPropsType) {
return <WrapTranslation
message={message}
renderComponent={(infix) => <button onClick={onClick}>{infix}</button>} />
}
// use it inside your application
export function App() {
return <>
<header>
<WrappedButton message={LL.LOGOUT()} onClick={() => alert('do logout')}>
</header>
<main>
<WrappedButton message={LL.WELCOME({ name: 'John' })} onClick={() => alert('clicked')}>
</main>
<>
}
This is an example written for a react application, but this concept can be used with any kind of framework.
Basically you will need to write a function that splits the translated message and renders a component between the parts. You can define your split characters yourself but you would always need to make sure you add them in any translation since typesafe-i18n
doesn't provide any typesafety for these characters (yet).
Your locale translation files can be any kind of JavaScript object. So you can make object-transformations inside your translation file. The only restriction is: in the end it has to contain a default export with type Translation
. You could do something like this:
create your BaseTranslation
// file 'src/i18n/en/index.ts'
import type { BaseTranslation } from '../i18n-types'
const en: BaseTranslation = {
WELCOME: "Welcome to XYZ",
// ... some other translations
COLOR: "colour"
}
export default en
create your other translation that overrides specific translations
// file 'src/i18n/en-US/index.ts'
import type { Translation } from '../i18n-types'
import en from '../en' // import translations from 'en' locale
const en_US: Translation = {
...en, // use destructuring to copy all translations from your 'en' locale
COLOR: "color" // override specific translations
}
export default en_US
If you are using nested translations, you probably need a function like
lodash/merge
to make a deep merge of your translations.import { merge } from 'lodash' const en_US: Translation = deepMerge(en, { labels: { color: "color" // override specific translations } }) export default en_US
The generated types are really strict. It helps you from making unintentional mistakes. If you want to opt-out for certain translations, you can use the any
keyword.
create your BaseTranslation
with a translation containing a parameter
// file 'src/i18n/en/index.ts'
import type { BaseTranslation } from '../i18n-types'
const en: BaseTranslation = {
HELLO: "Hi {name}!",
}
export default en
create another locale without that parameter by disabling the strict type checking with as any
// file 'src/i18n/de/index.ts'
import type { Translation } from '../i18n-types'
const de: Translation = {
HELLO: "Hallo!" as any // we don't want to output the 'name' variable
}
export default de
WARNING! the usage of 'any' can introduce unintentional mistakes in future. It should only be used when really necessary and you know what you are doing.
A better approach would be to create a custom formatter e.g.
create your translation and add a formatter to your variable
// file 'src/i18n/en/index.ts'
import type { BaseTranslation } from '../i18n-types'
const en: BaseTranslation = {
HELLO: "Hi {name|nameFormatter}!",
}
export default en
// file 'src/i18n/de/index.ts'
import type { Translation } from '../i18n-types'
const de: Translation = {
HELLO: "Hallo {name|nameFormatter}!"
}
export default de
create the formatter based on the locale
// file 'src/i18n/formatters.ts'
import type { FormattersInitializer } from 'typesafe-i18n'
import type { Locales, Formatters } from './i18n-types'
import { identity, ignore } from 'typesafe-i18n/formatters'
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
const nameFormatter =
locale === 'de'
// return an empty string for locale 'de'
? ignore // same as: () => ''
// return the unmodified parameter
: identity // same as: (value) => value
const formatters: Formatters = {
nameFormatter: nameFormatter
}
return formatters
}
LocalizedString
and not the type string
itself?With the help of LocalizedString
you could enforce texts in your application to be translated. Lets take an Error message as example:
const showErrorMessage(message: string) => alert(message)
const createUser = (name: string, password: string) => {
if (name.length === 0) {
showErrorMessage(LL.user.create.nameNotProvided())
return
}
if (isStrongPassword(password)) {
showErrorMessage('Password is to weak')
return
}
// ... create user in DB
}
In this example we can pass in any string, so it can also happen that some parts of your application are not translated. To improve your i18n experience a bit we can take advantage of the LocalizedString
type:
import type { LocalizedString } from 'typesafe-i18n'
const showErrorMessage(message: LocalizedString) => alert(message)
const createUser = (name: string, password: string) => {
if (name.length === 0) {
showErrorMessage(LL.user.create.nameNotProvided())
return
}
if (isStrongPassword(password)) {
showErrorMessage('Password is to weak') // => ERROR: Argument of type 'string' is not assignable to parameter of type 'LocalizedString'.
return
}
// ... create user in DB
}
With the type LocalizedString
you can restrict your functions to only translated strings.
Jest
Unfortunately there are some open issues in the Jest
repository regarding modern package export formats so jest
doesn't know where to load files from.
You need to manually tell jest
where these files should be loaded from, by defining moduleNameMapper
inside your jest.config.js
:
// jest.config.js
module.exports = {
moduleNameMapper: {
"typesafe-i18n/angular": "typesafe-i18n/angular/index.cjs",
"typesafe-i18n/react": "typesafe-i18n/react/index.cjs",
"typesafe-i18n/solid": "typesafe-i18n/solid/index.cjs",
"typesafe-i18n/svelte": "typesafe-i18n/svelte/index.cjs",
"typesafe-i18n/vue": "typesafe-i18n/vue/index.cjs",
"typesafe-i18n/formatters": "typesafe-i18n/formatters/index.cjs",
"typesafe-i18n/detectors": "typesafe-i18n/detectors/index.cjs",
}
};
here is the original issue with some additional information: #140
Intl
package does not work with locales other than 'en'Node.JS, by default, does not come with the full intl
support. To reduce the size of the node installment it will only include 'en' as locale. You would need to add it yourself. The easiest way is to install the intl
package
> npm install intl
and then add following lines on top of your src/i18n/formatters.ts
file:
const intl = require('intl')
intl.__disableRegExpRestore()
globalThis.Intl.DateTimeFormat = intl.DateTimeFormat
Then you should be able to use formatters from the Intl
namespace with all locales.