typed-intl v1.0.3
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-intlDepending 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.tsxDefining 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); // 4We 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,fewandmanyitems. 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 onenumberparameter ans using ICU message syntax.We specify a full German translation. If we would forget to define the
taskStatusmessage 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 ofpluralis of type(n: number) => string. Thus for thetaskStatuskey in every language we must specify something resulting in the same type or the compiler will complain.We define the
TaskListmessages to extend theBasemessages. As a result theTaskListmessages will also contain ourok,cancelandwelcomekeys.
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.tsxbelow.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
taskStatusmessage formatter. Note that the compiler/IDE will complain if we mistype the message's key or pass something else than anumberto it.- As our
TaskListmessages areextending()ourBasemessages we can easily access theoktext 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-CHandsp. - The users language preferences are
it-CH,de-CH,fr-CHanden. - 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.