README
typed-intl - Typesafe internationalization for TypeScript/JavaScript Apps
typed-intl is an internationlization (intl/i18n) library for TypeScript/JavaScript apps.
Motivation
I've tried out quite a few internationalization libraries, but none of them made me happy. Some of them only concentrated on picking the right translations but did not provide formatting or plural stuff. Others provided the whole stuff required, but where quite verbose in usage (e.g. Format.js and its React integration react-intl).
And finally none of them provided (type)safe message access and formatting options, so that the compiler would point me towards access to non existing messages or (accidentially) left out translation strings. But as I have a strong Scala background and am using TypeScript for my React apps which has a powerful type system, I wanted to have a solution that does exactly this.
Design Goals & Features
Thus these where my design goals:
- Easy and clean definition of translation messages including
- Defining keys and messages as plain JavaScript objects
- Possibility to place component specific translations near the component's code
- Possibility to define translation objects based on others
- The compiler should remind me if I miss a message for one language
- Simple and checked access to messages, so that the compiler will report an error if I try to reference a non exiting message
- Extensive formatting capabilities based on ICU message syntax
- to define messages with placeholders for strings, numbers, dates and times
- to define pluralized messages
- typesafe format functions, so that the compiler ensures I'm passing in the right message parameters.
- Unopinionated regarding the used UI framework. In my opinion, if accessing translation messages is easy an clean now specific React (or whatever) integration is required.
- Automatic language selection based on
navigator.languages
. - Do the heavy lifting (message formatting) based on well proven libraries. typed-intl uses Yahoos's well proven intl-messageformat here which is a part of the Format.JS stack.
- 100% test coverage.
Installation
To add typed-intl to your project install it with:
npm install typed-intl
Depending on which browsers you target, be sure to install the required polyfills for intl-messageformat as described in it's documentation.
Introduction
Assume the following file structure:
src
├ common
│ └ Base.msg.ts
├ tasklist
│ ├ TaskList.msg.ts
│ └ TaskList.tsx
├ App.tsx
└ index.tsx
Defining Messages
In common/Base.msg.ts
we have some common basic translations defined:
import { translate } from 'typed-intl';
export default translate({ // 1.
ok: 'OK',
cancel: 'Cancel'
welcome: 'Welcome',
}).supporting('de', { // 2.
ok: 'OK',
cancel: 'Abbrechen'
welcome: 'Willkommen',
}).partiallySupporting('de-CH', { // 3.
welcome: 'Grüezi'
});
Lets see what we have here:
- We are defining default messages in English. As we specify them without a language tag, these are the messages that will be used as fallback.
- We provide a German translation. If we would leave off one of the keys for the German translation created using
supporting('de', {...}
the compiler/IDE would report an error. - We define a partial translation for Switzerland using
partiallySupporting('de-CH', {...})
. This states, that only specifying a subset of the messages is okay. But it is not okay to sepcify new translation keys.
Now lets look at tasklist/TaskList.msg.ts
:
import { format, plural, translate } from 'typed-intl';
import base from 'common/Base.msg';
export default translate(lang => {
taskStatus: plural(lang, { // 1
zero: 'You have no tasks',
one: 'You have one task',
other: n => format<number>(lang, 'You have {1, number} tasks')(n) // 2
});
}).supporting('de', lang => {
taskStatus: plural(lang, { // 3
zero: 'Du hast keine Aufgaben',
one: 'Du hast eine Aufgabe',
other: n => format<number>(lang, 'Du hast {1, number} Aufgaben')(n)
});
}).extending(base); // 4
We want a message here, that displays the current status of our tasklist and we want different messages depending on the number of tasks on our list.
To achieve this we use the
plural()
function and specify different messages for zero, one and any other number of tasks.Plurals depend on the locale. For example in Arabic there are further distinctions for
two
,few
andmany
items. Thus we need to pass in the user's language toplural()
. Note that we should not simply pass in'en'
here, because we want the full locale as specified in the user's language preferences. We can retrieve the current language by providing a function of type(lang: LanguageTag) => {}
totranslate()
,supporting()
andpartiallySupporting()
instead of simply the translation object as we did it inBase.msg.ts
.For any case where there is more than one task we use a formatted message to include the number of tasks into the message. We are using the
format()
function here expecting onenumber
parameter ans using ICU message syntax.We specify a full German translation. If we would forget to define the
taskStatus
message or misspell the key, again the compiler/IDE will remind use here.Further on also the type of a message is checked. In the first example all messages were simply of type
string
, but the result ofplural
is of type(n: number) => string
. Thus for thetaskStatus
key in every language we must specify something resulting in the same type or the compiler will complain.We define the
TaskList
messages to extend theBase
messages. As a result theTaskList
messages will also contain ourok
,cancel
andwelcome
keys.
Note: extending
messages is typed-intl's solution for structuring messages in a large application. In contrast typed-intl currently does not support nested messages.
Consuming Messages
Now that we have our messages defined we can use them in our tasklist/TaskList.tsx
component:
import * as React from 'react';
import translations from './TaskList.msg' // 1
const msg = translations.messages(); // 2
...
export class TaskList extends React.PureComponent<...> {
...
render() {
return (
<div>
{msg.taskStatus(this.props.tasks.count())} // 3
</div>
...
<Button ...>{msg.ok}</Button> // 4
);
}
}
We define a React component here, but this will work for every other framework too.
We import our messages.
We retrieve the correct messages for the user's preferred language. See how we initalize this in
index.tsx
below.If you don't want to use the global language mechanism, e.g. because you want allow the user to dynamically switch the language, then you need to explicitly specify the user's language and retrieve the best matching translation using
translations.messagesFor(language: LanguageTag)
.We format the task status by passing the task count to our
taskStatus
message formatter. Note that the compiler/IDE will complain if we mistype the message's key or pass something else than anumber
to it.As our
TaskList
messages areextending()
ourBase
messages we can easily access theok
text here.
Initialization
You can use typed-intl without any initialization if you explicitly specify the language you want to retrieve messages for using messagesFor(lang: LanguageTag)
on a MessageProvider
. Actually you will have to do this if you want some kind of dynamic language switching without reloading the page.
But if you simply want to set the user's language for the whole app based on the user's language preferences, then you can use typed-intl's global language handling.
This is what we do in our example in index.tsx
:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import { selectPreferredLanguage } from 'typed-intl';
selectPreferredLanguage(['en', 'de', 'de-CH']); // 1
ReactDOM.render(
<App />,
document.getElementById('root') as HTMLElement
);
- All we need to do here is calling the
selectPreferredLanguage()
function with the list of languages we are providing translations for. The second parameter to this function is the list of language preferences of the current user and defaults tonavigator.languages
-- thus you will have to specify them manually if you want to target browsers not supporting this property or if your server delivers the language preferences.
This is how selectPreferredLanguage()
works:
- It will go through the list of the user's preferred languages and pick the first one we have a translation for. A matching algorithm is used here to match even complex language tags to the best available translation.
- If no translations match, the users most preferred language will be chosen which will result in the fallback messages being used.
- The chosen language will be set as global language calling
setPreferredLanguage()
.
You can always query the current language using the preferredLanguage()
function. This language will be used when calling the parameterless messages()
function on a MessageProvider
and it will be passed through to your message definitions as shown for TaskList.msg.ts
above.
Lets look at an example:
- Lets assume we have Translations for
en
,de
,fr
,fr-CH
andsp
. - The users language preferences are
it-CH
,de-CH
,fr-CH
anden
. - Then
selectPreferredLanguage()
will setpreferredLanguage()
tode-CH
.
Explanation: Preferred languages are checked in the specified order as the first one is expected to be the most preferred. As we do not have a translation for it-CH
nor for it
, we need to look at de-CH
next. We do not have an exact match for de-CH
, but at least the language de
is supported and thus de-CH
is chosen. fr-CH
would have an exact match but comes later in the list of preferred languages.
API Documentation
This library comes with a full API documentation.