Simple i18n implementation with TypeScript.
$ npm i
$ npm run build
$ npm start
# visit http://localhost:9000
The page accepts two query parameters, 'name' and 'unread' (http://localhost:9000?name=kimamula&unread=1, for example).
See what happens when you switch the primary language of your browser between English and Japanese and reload the page.
export const messages = {
title: 'Simple i18n implementation with TypeScript',
greeting: (name = 'John Doe') => `Hello, ${name}.`,
unreadNotification: (unread: number) => `You have ${unread === 0 ? 'no' : unread} unread message${unread === 1 ? '' : 's'}.`
};
- Type-safe: the following things result in compilation errors.
- Typo of the key of the messages.
- Typo, type mismatch, or excess or deficiency of the arguments for the messages.
- Any inconsistency of the messages definition among the different languages.
- No special syntax is required for complex formatting of numbers, dates, plural/singular, etc. Just write TypeScript to achieve them (as you do wherever else).
Messages are dynamically required depending on the request's language.
import * as path from 'path';
import * as glob from 'glob';
import { messages as en } from './en';
export const messages = en;
export namespace Server {
// Languages for which messages are defined under this dir are acceptable
export const acceptableLanguages = glob.sync(`${__dirname}/*.js`)
.map((file) => path.basename(file, '.js'))
.filter((language) => language !== 'index');
// require messages for each language and cache
const map = acceptableLanguages.reduce((acc, language) => {
acc[language] = require(`./${language}`).messages;
return acc;
}, {} as {[language: string]: typeof messages});
/**
* Returns a messages object for the specified language
*/
export function messagesOf(language: string): typeof messages {
return map[language];
}
}
Then you can get a messages object as follows (using Express req.acceptsLanguages
, for example):
import * as express from 'express';
import { Server } from './messages';
const
app = express(),
DEFAULT_LANGUAGE = 'en';
app.get('/', (req: express.Request, res: express.Response) => {
const
language = (req.acceptsLanguages(Server.acceptableLanguages) || DEFAULT_LANGUAGE) as string,
messages = Server.messagesOf(language);
// ...
});
require('./path/to/messages')
is converted to require('./path/to/messages/{language}')
for each language in the build time by webpack.NormalModuleReplacementPlugin
.
const webpack = require('webpack');
const { Server } = require('./src/messages');
module.exports = Server.acceptableLanguages.map((language) => ({
entry: './src/client.js',
output: {
path: './build',
filename: `bundle_${language}.js` // outputs bundled JS for each language
},
plugins: [
new webpack.NormalModuleReplacementPlugin(/^\..*\/messages$/, (result) => result.request += `/${language}`)
]
}));
Assign appropriate bundled JS for each request in the server side template:
const language = (req.acceptsLanguages(Server.acceptableLanguages) || DEFAULT_LANGUAGE) as string;
// ...
<script type='text/javascript' charSet='utf-8' src={`bundle_${language}.js`}></script>
Then you can get a messages object on the client side as follows:
import { messages } from './path/to/messages';
You cannot directly import/require a messages object from within components that are rendered on both the server side and the client side, as they are using different messages resolving strategies.
Instead, define a function which accepts a messages object as an argument and returns the top level component for your application.
import * as React from 'react';
import { messages } from './messages';
export const createApp = (_messages: typeof messages) =>
class extends React.Component<{ name?: string; unread?: number; }, void> {
render(): JSX.Element {
return <div>
<h1>{_messages.title}</h1>
<p>{_messages.greeting(this.props.name)}</p>
<p>{_messages.unreadNotification(this.props.unread)}</p>
</div>;
}
}
;
Then, call the function with a messages object that are resolved in either of the server side and client side ways to obtain the top level component.
The descendants components can access to the messages via props or contexts (or any other mechanism depending on your view library) that are propagated from the top level component.