An opinionated, fully type-safe, lightweight localization library for TypeScript projects with no external dependencies.
🐤 lightweight
👌 easy to use syntax
🏃 fast and efficient
💬 supports plural rules
🔀 allows formatting of values e.g. locale-dependent date or number formats
💪 can be used for frontend, backend and API projects
🦺 prevents you from making mistakes
⛔ no external dependencies
$ npm install --save typesafe-i18n
The package can be used inside JavaScript and TypeScript applications, but you will get a lot of benefits using it together with TypeScript, since the watcher will generate a few wrappers for easier usage.
You can use typesafe-i18n
in a variety of project-setups:
- Node.js apis, backends, scripts, ...
- Svelte/Sapper/SvelteKit applications
- React applications
- Browser projects
- other frameworks like React, VueJS, Angular and others ...
The typesafe-i18n
package exports a few different objects you can use to localize your applications:
In order to get full typechecking support, you sould use the exported functions in i18n-utils.ts
generated by the watcher. It contains fully typed wrappers for the following core functionalities.
The i18nString
contains the core of the localization engine. To initialize it, you need to pass your desired locale
and the formatters
(optional) you want to use.
You will get an object back that can be used to transform your translation strings.
import { i18nString } from 'typesafe-i18n'
const locale = 'en'
const formatters = {
uppercase: (value) => value.toUpperCase()
}
const LLL = i18nString(locale, formatters)
LLL('Hello {name|uppercase}!', { name: 'world' }) // => 'Hello WORLD!'
The i18nObject
wraps your translations for a certain locale. To initialize it, you need to pass your desired locale
, your translations
-object and the formatters
(optional) you want to use.
You will get an object back that can be used to access and apply your translations.
import { i18nObject } from 'typesafe-i18n'
const locale = 'en'
const translations = {
HI: "Hello {name}!",
RESET_PASSWORD: "reset password"
/* ... */
}
const formatters = { /* ... */ }
const LL = i18nObject(locale, translations, formatters)
LL.HI({ name: 'world' }) // => 'Hello world!'
LL.RESET_PASSWORD() // => 'reset password'
Wrap all your locales with i18n
. To initialize it, you need to pass a callback to get the translations
-object for a given locale and a callback to initialize the formatters
you want to use (optional).
You will get an object back that can be used to access all your locales and apply your translations.
import { i18n } from 'typesafe-i18n'
const localeTranslations = {
en: { TODAY: "Today is {date|weekday}" },
de: { TODAY: "Heute ist {date|weekday}" },
it: { TODAY: "Oggi è {date|weekday}" },
}
const loadLocale = (locale) => localeTranslations[locale]
const initFormatters = (locale) => {
const dateFormatter = new Intl.DateTimeFormat(locale, { weekday: 'long' })
return {
weekday: (value) => dateFormatter.format(value)
}
}
const L = i18n(loadLocale, initFormatters)
const now = new Date()
L.en.TODAY({ date: now }) // => 'Today is friday'
L.de.TODAY({ date: now }) // => 'Heute ist Freitag'
L.it.TODAY({ date: now }) // => 'Oggi è venerdì'
A good usecase for this object could be inside your API, when your locale is dynamic e.g. derived from a users session:
function doSomething(session) {
/* ... */
const locale = session.language
return L[locale].SUCCESS_MESSAGE()
}
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 an issue, if you need a guide for a specific framework.
The typesafe-i18n
package allows us to be 100% typesafe for our tranlsation functions and even the translations for other locales itself. It generates TypeScript definitions based on your base locale. Here you can see some examples where the generated types can help you:
In order to get get full typesafety for your locales, you can start the watcher during development. The watcher listens for changes you make to your base locale file and generates the corresponding TypeScript types.
Make sure you have installed node
version > 12.x
and are using a typescript
version > 3.x.x
.
The watcher will generate a different output depending on your TypeScript version. Older versions don't support all the features
typesafe-i18n
need to provide you with the best types. Make sure to use a TypeScript version> 4.1.x
to benefit from all the typechecking features.
You can choose between two variants to run the watcher:
- as a rollup-plugin
- as a webpack-plugin
- as a node-process
If you are already using rollup
as your bundler, you can add the typesafeI18n
-Plugin to your rollup.config.js
.
import typesafeI18n from 'typesafe-i18n/rollup/rollup-plugin-typesafe-i18n'
export default {
input: ...,
output: ...,
plugins: {
...
typescript(),
// looks for changes in your base locale file in development mode & optimizes code in production mode
typesafeI18n({ /* options go here */ })
}
}
You can pass options to the watcher by creating a .typesafe-i18n.json
file in the root of your workspace, or by passing it as an argument to the typesafeI18n
plugin.
The rollup plugin has an advantage over the node-process, since it can also be used to optimize the translations.
Currently implemented optimizations:
- get rid of the arguments type informations inside your base-translation:
These types inside your base translations e.g.Hello {name:string}!
are only used from the watcher to generate types for your translation. The rollup plugin removes these types from the translations in order to reduce bundle size by a few bytes. The example above will be optimized toHello {name}!
inside your production bundle. - include only certain locales:
If you want to create an own bundle per locale. When running rollup to create your production-bundle, you can specify the'locales'
option to include only certain locales. The rollup plugin will remove all other locales from the production bundle.
More optimizations will follow.
If you are already using webpack
as your bundler, you can add the TypesafeI18nPlugin
to your webpack.config.js
.
const TypesafeI18nPlugin = require('typesafe-i18n/webpack/webpack-plugin-typesafe-i18n').default
module.exports = {
entry: ...,
module: ...,
output: ...,
plugins: [
...
// looks for changes in your base locale file in development mode
new TypesafeI18nPlugin({ /* options go here */ })
],
}
You can pass options to the watcher by creating a .typesafe-i18n.json
file in the root of your workspace, or by passing it as an argument to the constructor of TypesafeI18nPlugin
.
This is the fallback option for all developers who aren't using rollup or webpack. Use this option if you bundle your application via parcel, esbuild etc. or if you don't use a bundler at all.
Start the watcher node process in your terminal:
> node ./node_modules/typesafe-i18n/node/watcher.js
Passing options to the watcher is possible by creating a .typesafe-i18n.json
file in the root of your workspace.
You could use a npm-package like
npm-run-all
in order to start the watcher and you development-server in parallel.
Take a look at this demo repository to see how to run the watcher node process.
This project requires you to use an opinionated folder structure for your locales. All your localization files are located inside the src/i18n
folder.
When running the watcher for the first time, a few files will be generated:
src/
i18n/
en/
index.ts
custom-types.ts
formatters.ts
i18n-types.ts
i18n-util.ts
Some files are auto-generated on every change of your base locale file; please don't make manual changes to them, since they will be overwritten.
-
en/index.ts
If 'en' is your base locale, the filesrc/i18n/en/index.ts
will contain your translations. Whenever you make changes to this file, the watcher will generate updated type definitions. -
custom-types.ts
To defining types that are unknown totypesafe-i18n
. -
formatters.ts
In this file, you can configure the formatters to use inside your translations. -
i18n-types.ts
Type definitions are generated in this file. You don't have to understand them. They are just here to help TypeScript understand, how you need to call the translation functions. -
i18n-util.ts
This file contains wrappers with type-informations around the base i18n functions.
Locales must follow a specific file pattern. For each locale, you have to create a folder with the name of the locale inside your src/i18n
folder e.g. 'en', 'en-us', 'en-GB'. The name of the folder is also the name of the locale you use inside your code. Each locales folder needs to have an index.ts
file with a default export. The file should export an object with string key-values pairs and should look something like:
import type { Translation } from '../i18n-types';
const de: Translation = {
/* your translations go here */
}
export default de
make sure to give it the type of
Translation
to get compile-errors, when some translations are missing
If you want to pass arguments with your own types to the translation function, you need to tell typesafe-i18n
how these types look like. In order to do this, you need to create an export with the exact name of that type inside this file.
If you have a translation with e.g. the type Sum
,
const translations: BaseTranslation = {
RESULT: 'The result is: {0:Sum|calculate}'
}
you need to export Sum
as a type in your custom-types.ts
file
export type Sum = {
n1: number
n2: number
n2: number
}
You can set options for the watcher in order to get optimized output for your specific project. The available options are:
key | type | default value |
---|---|---|
baseLocale | string |
'en' |
locales | string[] |
[] |
loadLocalesAsync | boolean |
true |
adapter | 'node' | 'svelte' | 'react' | undefined |
undefined |
outputPath | string |
'./src/i18n/' |
typesFileName | string |
'i18n-types' |
utilFileName | string |
'i18n-util' |
formattersTemplateFileName | string |
'formatters' |
typesTemplateFileName | string |
'custom-types' |
adapterFileName | string | undefined |
undefined |
tempPath | string |
'./node_modules/typesafe-i18n/temp-output/' |
Defines which locale to use for the types generation. You can find more information on how to structure your locales here.
Specifies the locales you want to use. This can be useful if you want to create an own bundle for each locale. If you want to include only certain locales, you need to set the locales you want to use. If empty, all locales will be used.
Note: requires the usage of the rollup-plugin
Whether to generate code that loads the locales asynchronously. If set to true
, a locale will be loaded, when you first access it. If set to false
all locales will be loaded when you init the i18n-functions.
If this config is set, code will be generated that wraps i18n functions into useful helpers for that environment e.g. a svelte
-store.
Folder in which the files should be generated and where your locale files are located.
Name for the file where the types for your locales are generated.
Name for the file where the typesafe i18n-functions are generated.
Name for the file where you can configure your formatters.
Name for the file where you can configure your custom-types.
Name for the file when generating output for an adapter. The default filename is i18n-[adapter]
.
Folder where the watcher can store temporary files. These files are generated when your base locale is analyzed and the types are generated. The folder will be cleared, after the types were generated. So make sure you use an empty folder, if you change this option.
For more information about the LLL
object, read the usage section.
Syntax:
{index}
const APPLES = '{0} apples'
LLL(APPLES, 12) // => '12 apples'
const FRUITS = '{0} apples and {1} bananas'
LLL(FRUITS, 3, 7) // => '3 apples and 7 bananas'
Syntax:
{key}
const FRUITS = '{nrOfApples} apples and {nrOfBananas} bananas'
LLL(FRUITS, { nrOfApples: 3, nrOfBananas: 7 }) // => '3 apples and 7 bananas'
Syntax:
{{singular|plural}}
const APPLES = '{nrOfApples} {{apple|apples}}'
LLL(APPLES, { nrOfApples: 1 }) // => '1 apple'
LLL(APPLES, { nrOfApples: 2 }) // => '2 apples'
Syntax:
{{plural}}
const APPLES = '{nrOfApples} apple{{s}}'
LLL(APPLES, { nrOfApples: 0 }) // => '0 apples'
LLL(APPLES, { nrOfApples: 1 }) // => '1 apple'
LLL(APPLES, { nrOfApples: 5 }) // => '5 apples'
Syntax:
{{singular|}}
const MEMBERS = '{0} weitere{{s|}} Mitglied{{er}}'
LLL(MEMBERS, 0) // => '0 weitere Mitglieder'
LLL(MEMBERS, 1) // => '1 weiteres Mitglied'
LLL(MEMBERS, 9) // => '9 weitere Mitglieder'
Under the hood, typesafe-i18n
uses the Intl.PluralRules for detecting the plural form.
Syntax:
{{zero|one|two|few|many|other}}
// locale set to 'ar-EG'
const PLURAL = 'I have {{zero|one|two|a few|many|a lot}} apple{{s}}'
LLL(PLURAL, 0) // => 'I have zero apples'
LLL(PLURAL, 1) // => 'I have one apple'
LLL(PLURAL, 2) // => 'I have two apples'
LLL(PLURAL, 6) // => 'I have a few apples'
LLL(PLURAL, 18) // => 'I have many apples'
Read the formatters section to learn how you can configure formatters.
const now = Date.now()
LLL('Today is {date|weekday}', { date: now }) // => 'Today is Friday'
LLL('Heute ist {date|weekday}', { date: now }) // => 'Heute ist Freitag'
Allows also to format values by multiple formatters in row. The formatters will be called from left to right.
const now = Date.now()
LLL('Today is {date|weekday}', { date: now }) // => 'Today is Friday'
LLL('Today is {date|weekday|uppercase}', { date: now }) // => 'Today is FRIDAY'
LLL('Today is {date|weekday|uppercase|shorten}', { date: now }) // => 'Today is FRI'
For information about the LL
object, read the usage section.
const translation = {
HI: 'Hello {0}'
}
LL.HI() // => ERROR: Expected 1 arguments, but got 0.
LL.HI('John', 'Jane') // => ERROR: Expected 1 arguments, but got 2.
LL.HI('John') // => 'Hi John'
Syntax:
{key:type}
const translation = {
HI: 'Hello {name:string}'
}
LL.HI('John') // => ERROR: Argument of type 'string' is not assignable to parameter of type '{ name: string; }'.
LL.HI({ name: 'John' }) // => 'Hi John'
const MESSAGE = 'Hi {name:string|uppercase}, I want to buy {nrOfApples:number} apple{{s}}'
LLL(MESSAGE, { name: 'John', nrOfApples: 42 }) // => 'Hi JOHN, I would like to buy 42 apples'
Of course typesafe-i18n
can handle that as well.
LLL('Welcome to my site') // => 'Welcome to my site'
Or if you are using the i18nObject (LL):
<script>
const translation = {
LOGIN: 'login'
}
<script>
<div>
{LL.LOGIN()} <!-- => 'login' -->
<!-- NOTE: *not* supported everywhere -->
{LL.LOGIN} <!-- => 'login' -->
</div>
Some frameworks like 'Svelte' will call the
toString()
method on objects inside the html-template. So if you have translations with no arguments you can omit calling the translation-function. The framework will do that for you.
You can specify your own formatters, that take an argument as an input and returns another value.
const formatters = {
roiCalculator: (value) => {
return (value * 4.2) - 7
}
}
LLL('Invest ${0} and get ${0|roiCalculator} in return', 100)
// => 'Invest $100 and get $413 in return'
You can also use a few builtin formatters:
- date
A wrapper for Intl.DateTimeFormatimport { date } from 'typesafe-i18n/formatters' const formatters = { weekday: date({ weekday: 'long' }) } LLL('Today is {0|weekday}', new Date()) // => 'Today is friday'
- time
A wrapper for Intl.DateTimeFormatimport { time } from 'typesafe-i18n/formatters' const formatters = { timeShort: time('en', { timeStyle: 'short' }) } LLL('Next meeting: {0|timeShort}', meetingTime) // => 'Next meeting: 8:00 AM'
- number
A wrapper for Intl.NumberFormatimport { number } from 'tyoesafe-i18n/formatters' const formatters = { currency: number('en', { style: 'currency', currency: 'EUR' }) } LLL('your balance is {0|currency}', 12345) // => 'your balance is €12,345.00'
- replace
A wrapper for String.prototype.replace
import { replace } from 'typesafe-i18n/formatters' const formatters = { noSpaces: replace(' ', '-') } LLL('The link is: https://www.xyz.com/{0|noSpaces}', 'super cool product') // => 'The link is: https://www.xyz.com/super-cool-product'
- uppercase
A wrapper for String.prototype.toUpperCaseimport { uppercase } from 'typesafe-i18n/formatters' const formatters = { upper: uppercase } LLL('I sayed: {0|upper}', 'hello') // => 'I sayed: HELLO'
- lowercase
A wrapper for String.prototype.toLowerCaseimport { lowercase } from 'typesafe-i18n/formatters' const formatters = { lower: lowercase } LLL('He sayed: {0|lower}', 'SOMETHING') // => 'He sayed: something'
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 watcher generates 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:
- string-parser: detects variables, formatters and plural-rules in your localized strings
- translation function: injects arguments, formattes them and finds the correct plural form for the given arguments
These parts are bundled into the core functions. The sizes of the core functionalities are:
- i18nString: 765 bytes gzipped
- i18nObject: 835 bytes gzipped
- i18n: 944 bytes gzipped
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 svelte-store: 1092 bytes gzipped
- typesafe-i18n react-context: 1068 bytes gzipped
The package was optimized for performance:
- the amount of network traffic is kept small
The translation functions are small. Only the locales that are currently used are loaded. - no unecessary workload
Parsing your translation file for variables and formatters will only be performed when you access a translation for the first time. The result of that parsing process will be stored in an optimized object and kept in memory. - fast translations
Passing variables to the translation function will be fast, because its treated like a simple string concatenation. For formatting values, a single function is called per formatter.
If you use typesafe-i18n
you will get a smaller bundle compared to other i18n solutions. But that does't mean, we should stop there. There are planned some possible optimizations, to improve the bundle size even further: