“hi18n”, a TypeScript-first internationalization library — getting started guide
Tags: javascript, typescript, webdev, tutorial
Published: 2022-06-24 03:42:18


## “hi18n”, a TypeScript-first internationalization library — getting started guide

![Photo by [Etienne Girardet](https://unsplash.com/es/@etiennegirardet?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/@etiennegirardet?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5g7n1gzczda1ybu0j5w0.jpg)
 

"hi18n" is an internationalization library (more specifically, translation management library) for JavaScript/TypeScript and is currently under active development in Wantedly.

{%embed https://github.com/wantedly/hi18n %}

It roughly has the following features:

- TypeScript compiler ensures that you are using correct translation ids and translation parameters.
- Designed to match declarative paradigms such as React.
- Integrates well with existing ecosystems (such as Webpack) without extra configuration. For example, Webpack’s hot reloading and chunk splitting work out-of-the-box with hi18n. This happens just thanks to its modern architecture and requires no bundler-specific code.

We’ll describe design principles in detail in the coming blog posts. In this article, however, we provide a **quick start for those interested in our library**.

## Setting it up

In this article, we assume you have a React + TypeScript project. First, install the following packages:

```
npm install @hi18n/core @hi18n/react-context @hi18n/react
npm install -D @hi18n/cli

# Or:
yarn add @hi18n/core @hi18n/react-context @hi18n/react
yarn add -D @hi18n/cli
```

Then create files for translation data. Depending on your preference, you can configure it in two ways: **single-file** or **multi-files**. We use the latter in this guide.

In the multi-file configuration, you have N+1 files where N is the number of languages.

```tsx
// src/locale/index.ts
// (other names are fine)
// This file defines the list of translatable strings.

import { Book, Message } from "@hi18n/core";
import catalogEn from "./en";
import catalogJa from "./ja";

export type Vocabulary = {
  // "<Translation ID>": Message<{ <Parameters> }>; (or simply Message; if you don't need parameters)
  "example/greeting": Message<{ name: string }>;
};

// This is a bundle of translations for all languages (English and Japanese in this case).
export const book = new Book<Vocabulary>({
  en: catalogEn,
  ja: catalogJa,
});
```

```tsx
// src/locale/en.ts
// (other names are fine)
// This file contains translated messages in a specific language (i.e. English).

import { Catalog, msg } from "@hi18n/core";
import type { Vocabulary } from ".";

export default new Catalog<Vocabulary>({
  // "<Translation ID>": msg(<translated message>),
  "example/greeting": msg("Hello, {name}!"),
});
```

```tsx
// src/locale/ja.ts (in the same manner as en.ts)

import { Catalog, msg } from "@hi18n/core";
import type { Vocabulary } from ".";

export default new Catalog<Vocabulary>({
  "example/greeting": msg("こんにちは、{name}さん!"),
});
```

Then define a command for synchronizing translations and translation IDs:

```json
// package.json
{
  "scripts": {
    "i18n:sync": "hi18n sync 'src/**/*.ts' 'src/**/*.tsx'"
  }
}
```

Then you can start using hi18n.

If you are using ESLint, we recommend [our ESLint plugin](http://npmjs.com/package/@hi18n/eslint-plugin). Just install the package and extend the preset called `plugin:@hi18n/recommended`.

## Using translated messages

There are several ways to use the translated messages in your application code.

### With useI18n

This is the most standard way if you are using React. To use `useI18n`, you first need to configure a locale (usually at the root of the tree).

```tsx
// An example with explicit ReactDOM call.
// You may need to place it in a different place (like _app.ts).
import { LocaleProvider } from "@hi18n/react";


const root = ReactDOMClient.createRoot(/* ... */);
root.render(
  <LocaleProvider locales="ja">
    {/* ... */}
  </LocaleProvider>
);
```

Then use useI18n to start translating everywhere in the tree.

```tsx
import { useI18n } from "@hi18n/react";
// You need to explicitly import the translation data (which we call a book).
import { book } from "../../locale";

const Greet: React.FC = () => {
  // It returns a function for translation using the current locale (from the context) and the book you supplied.
  const { t } = useI18n(book);
  return <>{t("example/greeting", { name: "太郎" })}</>;
};
```

### With `<Translate>`

The `<Translate>` component is suitable for translating messages interleaved with markups or React elements.

```tsx
import { Translate } from "@hi18n/react";
// You need to explicitly import the translation data (which we call a book).
import { book } from "../../locale";

const Greet: React.FC = () => {
  return <Translate book={book} id="example/greeting" name="太郎" />;
};
```

Special to `<Translate>`, you can pass a React element as a parameter to the translation:

```tsx
const UnreadMessages: React.FC = () => {
  const unreadCount = 2;
  if (unreadCount === 0) return null;

  // en: "You have <link>{count,plural,one{# unread message}other{# unread messages}}</link>"
  // ja: "<link>{count,number}通の未読メッセージ</link>があります"
  return <Translate book={book} id="example/unread" count={unreadCount}>
    <a key="link" href="https://example.com/inbox" />
  </Translate>;
};
```

## Synchronizing translations and translation IDs

Use hi18n sync command to synchronize translation IDs between the application and the translation data.

```
hi18n sync <globs...> [--exclude <glob>] [--check | -c]
```

Define a task using package.json:

```json
// package.json
{
  "scripts": {
    "i18n:sync": "hi18n sync 'src/**/*.ts' 'src/**/*.tsx'"
  }
}
```

and use it to update your translations:

```
npm run i18n:sync

# Or:

yarn i18n:sync
```

Watch out for your unsaved changes to the translations; it may rewrite TypeScript/JavaScript files containing the translations.

This command does the following:

- it comments out unused translations, and
- it generates a boilerplate for translations required by the application, but not defined yet.

Sometimes translations that are still in use are accidentally commented out. This is likely caused by a mistake in the application:

- You may have used hi18n in a way that the tool cannot detect translation usages.
- Or you may have commented out some portion of the application for debugging, and forgot to uncomment them before running the sync command.

In any case, just fix the problem in the application and rerun the synchronization. Then the command undoes the comment-out.

## Adding a new translatable message

To add a new message, you can follow the steps below:

First, add a message using `t.todo` or `<Translate.Todo>` instead of `t` or `<Translate>`.

```tsx
t.todo("example/new");
<Translate.Todo book={book} id="example/new" />;
```

Then use the `hi18n sync` command to generate the boilerplate. Fill in the actual translations and remove `.todo` or `.Todo`.

You may also get the same result by editing everything by hand.

## Other things you can do with hi18n

These things will be covered in later posts.

- Plural forms and number formats. The `Intl` API must be available in the browser to use them.
- `t.dynamic`, `Translate.Dynamic`, and `translationId` API to dynamically select messages.
- Our ESLint plugin `@hi18n/eslint-plugin` ensures the correct use of hi18n. It also has helper rules such as migration from Lingui.
- You can simply create multiple Books if you need to split translation data. This is useful if you need to reduce bundle size or if you need to use hi18n in a library.

## Colophon

This article was originally [written in Japanese](https://www.wantedly.com/companies/wantedly/post_articles/399501) and translated to English by the author.