An opinionated, fully type-safe, lightweight localization library for TypeScript and JavaScript projects with no external dependencies.
🐤 lightweight (<1kb)
👌 easy to use syntax
🏃 fast and efficient
🦺 prevents you from making mistakes
💬 supports plural rules
📅 allows formatting of values e.g. locale-dependent date or number formats
🔄 option for asynchronous loading of locales
⏱️ supports SSR (Server-Side Rendering)
🤝 can be used for frontend, backend and API projects
🔍 locale-detection for browser and server environments
⛔ no external dependencies\
$ npm install typesafe-i18n
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:
- Node.js apis, backends, scripts, ...
- Svelte / SvelteKit / Sapper applications
- React / Next.js applications
- Browser (Vanilla JS) projects
- other frameworks like VueJS, Angular and others ...
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 https://formatjs.io/docs/polyfills/intl-pluralrules/.
The typesafe-i18n
package exports a few different objects you can use to localize your applications:
In order to get full typechecking support, you should use the exported functions in i18n-utils.ts
created by the generator. 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'
You can group sections of your application by defining nested translations:
import { i18nObject } from 'typesafe-i18n'
const translations = {
login: {
greeting: 'Welcome to XYZ',
labels: {
'email': 'email-address:',
'pw': 'password:'
},
},
projects: {
count: '{0} project{{s}}',
}
}
const LL = i18nObject('en', translations)
LL.login.labels.email() // => 'email-address:'
LL.projects.count(3) // => '3 projects'
Nesting can be as deep as you want. The only restriction is that you can't use the '.' character for your translation keys.
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 translation 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 generator during development. The generator listens for changes you make to your base locale file and creates the corresponding TypeScript types.
You will also benefit from full typesafe JavaScript code via JSDoc-annotations.
Make sure you have installed node
version > 12.x
and are using a typescript
version > 3.x.x
.
The generator will create 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 three variants to run the generator:
- 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 generator 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 for your production bundle.
Currently implemented optimizations:
- get rid of the arguments type information inside your base-translation:
These types inside your base translations e.g.Hello {name:string}!
are only used from the generator to create types for your translations. 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 generator 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 generator node process in your terminal:
> npx typesafe-i18n
or define a script in your package.json
file:
{
"scripts": {
"typesafe-i18n-generator": "typesafe-i18n"
}
}
Passing options to the generator is possible by creating a .typesafe-i18n.json
file in the root of your workspace e.g.
{
"$schema": "https://unpkg.com/[email protected]/schema/typesafe-i18n.json",
"baseLocale": "de",
"adapter": "svelte"
}
You could use a npm-package like
npm-run-all
in order to start the generator and you development-server in parallel.
Take a look at this demo repository to see how to run the generator node process.
When running tests or scripts you can disable the watcher by passing the argument --no-watch
to the generator node-process:
> npx typesafe-i18n --no-watch
This will only generate types once and not listen to changes in your locale files. The process will throw an TypesafeI18nParseError
if a wrong syntax is detected in your base locale file.
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 generator for the first time, a few files will be created:
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 generator will create 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-information 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 generator 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/' |
outputFormat | 'TypeScript' | 'JavaScript' |
'TypeScript' |
typesFileName | string |
'i18n-types' |
esmImports | boolean |
false |
utilFileName | string |
'i18n-util' |
formattersTemplateFileName | string |
'formatters' |
typesTemplateFileName | string |
'custom-types' |
adapterFileName | string | undefined |
undefined |
generateOnlyTypes | boolean |
false |
tempPath | string |
'./node_modules/typesafe-i18n/temp-output/' |
banner | string |
'/* eslint-disable */' |
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.
The programming language you use inside your code. If 'TypeScript' is selected, the generator will output TypeScript code and types. If you choose 'JavaScript' the generator will output typesafe JavaScript code annotated with JSDoc-comments.
If true
generated files will import other files with the .js
file extension. This makes it compatible with ESM packages that have specified "type": "module"
in their package.json
file.
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]
.
If you don't want to use the auto-generated helpers and instead write your own wrappers, you can set this option to true
.
Folder where the generator 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.
This text will be output on top of all auto-generated files. It is meant to add a custom disable-linter comment. Since every project can have different lint rules, we want to disable linting on those files.
If you want to use typesafe-i18n
inside your JavaScript code, you can get also full typesafety by running the generator
with the outputFormat
option set to 'JavaScript'
. The generator then provides wrappers for the core functions together with JSDoc annotations.
An IDE like VS Code will show you code-completions and errors when you have opened a file in the editor.
In order to get typesafety for your locales files, you need to annotate it like this:
// @ts-check
/**
* @typedef { import('../i18n-types').Translation } Translation
*/
/** @type { Translation } */
module.exports = {
/* your translations go here */
}
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'
Syntax:
{{zero|one|other}}
const LIST_ITEMS = 'The list includes {{ no items | an item | ?? items }}'
LLL(LIST_ITEMS, 0) // => 'The list includes no items'
LLL(LIST_ITEMS, 1) // => 'The list includes an item'
LLL(LIST_ITEMS, 12) // => 'The list includes 12 items'
Syntax:
{{singular|?? plural}}
const BANANAS = '{{ a banana | ?? bananas }}'
LLL(BANANAS, 1) // => 'a banana'
LLL(BANANAS, 3) // => '3 bananas'
Under the hood, typesafe-i18n
uses the Intl.PluralRules for detecting the plural form.
The only small modification made is, that the values 0
and '0'
are always mapped to 'zero'
instead of 'other'
.
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' -->
</div>
You can specify your own formatters, that take an argument as an input and returns another value.
const formatters = {
roiCalculator: (value) => (value * 4.2) - 7
}
LLL('Invest ${0} and get ${0|roiCalculator} in return', 100)
// => 'Invest $100 and get $413 in return'
If you need to apply multiple formatters to the same argument, you can chain them by using the pipe |
operator:
const formatters = {
sqrt: (value) => Math.sqrt(value),
round: (value) => Math.round(value),
}
LLL('Result: {0|sqrt|round}', 5)
// => 'Result: 2'
The formatters get applied from left to right. So in this example
5
will be the input for thesqrt
formatter, that will return2.23606797749979
. This value then gets passed to theround
formatter that will output2
.
You can also use a few builtin formatters:
A wrapper for Intl.DateTimeFormat.
import { date } from 'typesafe-i18n/formatters'
const formatters = {
weekday: date({ weekday: 'long' })
}
LLL('Today is {0|weekday}', new Date()) // => 'Today is friday'
See here if you want to use this formatter in a Node.JS environment.
Same as the date
-formatter
import { time } from 'typesafe-i18n/formatters'
const formatters = {
timeShort: time('en', { timeStyle: 'short' })
}
LLL('Next meeting: {0|timeShort}', meetingTime) // => 'Next meeting: 8:00 AM'
A wrapper for Intl.NumberFormat
import { number } from 'typesafe-i18n/formatters'
const formatters = {
currency: number('en', { style: 'currency', currency: 'EUR' })
}
LLL('your balance is {0|currency}', 12345) // => 'your balance is €12,345.00'
See here if you want to use this formatter in a Node.JS environment.
A wrapper for String.prototype.replace
import { replace } from 'typesafe-i18n/formatters'
const formatters = {
noSpaces: replace(/\s/, '-')
}
LLL('The link is: https://www.xyz.com/{0|noSpaces}', 'super cool product')
// => 'The link is: https://www.xyz.com/super-cool-product'
Returns the variable without modifications
import { identity } from 'typesafe-i18n/formatters'
const formatters = {
myFormatter: identity // (value) => value
}
LLL('Hello {name|myFormatter}', 'John')
// => 'Hello John'
Ignores the variable and returns an empty string.
import { ignore } from 'typesafe-i18n/formatters'
const formatters = {
myFormatter: ignore // () => ''
}
LLL('Hello {name|myFormatter}', 'John')
// => 'Hello '
A wrapper for String.prototype.toUpperCase
import { uppercase } from 'typesafe-i18n/formatters'
const formatters = {
upper: uppercase
}
LLL('I said: {0|upper}', 'hello') // => 'I said: HELLO'
A wrapper for String.prototype.toLowerCase
import { lowercase } from 'typesafe-i18n/formatters'
const formatters = {
lower: lowercase
}
LLL('He said: {0|lower}', 'SOMETHING') // => 'He said: something'
To automatically detect a users locale, you can use the detectLocale
function:
import { detectLocale } from 'typesafe-i18n/detectors'
const detectedLocale = detectLocale(fallbackLocale, availableLocales, detector)
The function expects following parameters:
fallbackLocale: string
When no matching locale is found this value is used.availableLocales: string[]
A list of locales your application supports.detector: () => string[]
A function that returns a list of locales. You can also pass multiple detectors. If the first detector does not find any matching locale, the next detector will be called and so on.detectLocale(fallbackLocale, availableLocales, detector1, detector2, /* ... */ detector7)
Detectors can be used either on the browser or the server side. You can use the built-in detectors or write your own detector
functions:
import { detectLocale } from 'typesafe-i18n/detectors'
const fallbackLocale = 'de'
const availableLocales = ['de', 'en' 'it', 'fr']
const customDetector = () => {
const locale = detectRandomLocale() // your custom locale detector
return [locale]
}
const detectedLocale = detectLocale(fallbackLocale, availableLocales, customDetector)
if you need to access any request-specific variable, you would need to "initialize" the function first, since the detector is called without any arguments. The "initialization" function is needed to bind some values that can be used when executing the function later. The return type of that function is the detector, you need to pass to the
detectLocale
function.const initIpDetector = (req: Request) => { return () => { const locale = detectLocaleFromIpAddress(req) // your custom locale detector return [locale] } } app.use((req: Request, res: Response) => { const ipDetector = initIpDetector(req) const detectedLocale = detectLocale(fallbackLocale, availableLocales, ipDetector) res.json({ locale: detectedLocale, }) })
typesafe-i18n
offers a few built-in detectors you can use:
This detectors are expected to run on a server-environment e.g. an express server or serverless function. These detectors all expect an express
-compatible req
object.
Reads and parses the 'accept-language'
header.
e.g. 'accept-language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
The function needs to be initialized first and expects you to pass the Request
object.
import { detectLocale, initAcceptLanguageHeaderDetector } from 'typesafe-i18n/detectors'
app.use((req: Request, res: Response) => {
const acceptLanguageHeaderDetector = initAcceptLanguageHeaderDetector(req)
// or: const acceptLanguageHeaderDetector = initAcceptLanguageHeaderDetector(req, 'Accept-Language')
const detectedLocale = detectLocale(fallbackLocale, availableLocales, acceptLanguageHeaderDetector)
res.json({
locale: detectedLocale,
})
})
The default header is 'accept-language'
, but you can change it by passing a string
as a second argument to the initAcceptLanguageHeaderDetector
function.
Reads and parses the Request-cookies
.
The function needs to be initialized first and expects you to pass the Request
object.
import { detectLocale, initRequestCookiesDetector } from 'typesafe-i18n/detectors'
app.use((req: Request, res: Response) => {
const requestCookiesDetector = initRequestCookiesDetector(req)
// or: const requestCookies = initRequestCookiesDetector(req, 'user-lang')
const detectedLocale = detectLocale(fallbackLocale, availableLocales, requestCookies)
res.json({
locale: detectedLocale,
})
})
The default cookie name is 'lang'
, but you can change it by passing a string
as a second argument to the initRequestCookiesDetector
function.
Extracts the locale from the request's path.
e.g. /:lang/products
The function needs to be initialized first and expects you to pass the Request
object.
import { detectLocale, initRequestCookiesDetector } from 'typesafe-i18n/detectors'
app.get('/:lang/products', (req: Request, res: Response) => {
const requestParametersDetector = initRequestParametersDetector(req)
// or: const requestParametersDetector = initRequestParametersDetector(req, 'user-lang')
const detectedLocale = detectLocale(fallbackLocale, availableLocales, requestParametersDetector)
res.json({
locale: detectedLocale,
})
})
The default parameter name is 'lang'
, but you can change it by passing a string
as a second argument to the initRequestParametersDetector
function.
This detectors are expected to run in a browser-environment e.g. on the website an user visits.
Detects the browser locales by accessing navigator.languages.
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors'
const detectedLocale = detectLocale(fallbackLocale, availableLocales, navigatorDetector)
Detects the locale on a website by reading the HTML lang
attribute.
<html lang="en">
<!-- your website's content-->
</html>
import { detectLocale, htmlLangAttributeDetector } from 'typesafe-i18n/detectors'
const detectedLocale = detectLocale(fallbackLocale, availableLocales, htmlLangAttributeDetector)
Detects the locale from a websites URL.
e.g. https://www.example.com/product-1/details.html?lang=de
import { detectLocale, queryStringDetector } from 'typesafe-i18n/detectors'
const detectedLocale = detectLocale(fallbackLocale, availableLocales, queryStringDetector)
The default parameter is 'lang'
, but you can change it by using the initQueryStringDetector
function:
import { detectLocale, initQueryStringDetector } from 'typesafe-i18n/detectors'
const queryStringDetector = initQueryStringDetector('user-lang')
const detectedLocale = detectLocale(fallbackLocale, availableLocales, queryStringDetector)
Looks for an entry inside the localStorage
.
import { detectLocale, localStorageDetector } from 'typesafe-i18n/detectors'
const detectedLocale = detectLocale(fallbackLocale, availableLocales, localStorageDetector)
The default item-key is 'lang'
, but you can change it by using the initLocalStorageDetector
function:
import { detectLocale, initLocalStorageDetector } from 'typesafe-i18n/detectors'
const localStorageDetector = initLocalStorageDetector('user-lang')
const detectedLocale = detectLocale(fallbackLocale, availableLocales, localStorageDetector)
Looks for an entry inside the sessionStorage
.
import { detectLocale, sessionStorageDetector } from 'typesafe-i18n/detectors'
const detectedLocale = detectLocale(fallbackLocale, availableLocales, sessionStorageDetector)
The default item-key is 'lang'
, but you can change it by using the initSessionStorageDetector
function:
import { detectLocale, initSessionStorageDetector } from 'typesafe-i18n/detectors'
const sessionStorageDetector = initSessionStorageDetector('user-lang')
const detectedLocale = detectLocale(fallbackLocale, availableLocales, sessionStorageDetector)
Detects the locale by parsing the website's cookies.
Note: this detector can only read cookies that are not marked as secure. If your locale-cookie is set with a secure-context, the detector will not work.
import { detectLocale, documentCookieDetector } from 'typesafe-i18n/detectors'
const detectedLocale = detectLocale(fallbackLocale, availableLocales, documentCookieDetector)
The default cookie name is 'lang'
, but you can change it by using the initSessionStorageDetector
function:
import { detectLocale, initDocumentCookieDetector } from 'typesafe-i18n/detectors'
const documentCookieDetector = initDocumentCookieDetector('user-lang')
const detectedLocale = detectLocale(fallbackLocale, availableLocales, documentCookieDetector)
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:
- string-parser: detects variables, formatters and plural-rules in your localized strings
- translation function: injects arguments, formats 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: 806 bytes gzipped
- i18nObject: 895 bytes gzipped
- i18n: 996 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: 1150 bytes gzipped
- typesafe-i18n react-context: 1171 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 unnecessary 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 doesn't mean, we should stop there. There are planned some possible optimizations, to improve the bundle size even further:
- get rid of the arguments type information inside your base-translation #13
- inline translations for single-locale bundles #14
- transform bundle so no runtime parsing is needed #68
- rewrite keyed to index-based arguments #15
I added a new translation to my locale file, but TypeScript gives me the Error 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.
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
):// file '.typesafe-i18n.json' { "baseLocale": "it" }
-
define the type of your base locale as
BaseTranslation
// file 'src/i18n/it/index.ts' import type { BaseTranslation } from 'typesafe-i18n' 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
.
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
onClick={onClick}
message={message}
renderComponent={(infix) => <button>{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).
I have two similar locales (only a few translations are different) but I don't want to duplicate my translations
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 'typesafe-i18n' 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
For certain locales I don't want to output a variable, but due to the strict typing I have to specify it in my translation
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 'typesafe-i18n' 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 'typesafe-i18n' 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 }
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.
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 i 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.