DEV Community

Masaki Hara for Wantedly, Inc.

Posted on • Originally published at Medium

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

“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)

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

GitHub logo wantedly / hi18n

message internationalization meets immutability and type-safety

hi18n: message internationalization meets immutability and type-safety

Quickstart

Installation:

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

Put the following file named like src/locale/index.ts:

import { Book, Catalog, Message, msg } from "@hi18n/core";

type Vocabulary = {
  "example/greeting": Message<{ name: string }>;
};
const catalogEn = new Catalog<Vocabulary>({
  "example/greeting": msg("Hello, {name}!"),
});
export const book = new Book<Vocabulary>({ en: catalogEn });
Enter fullscreen mode Exit fullscreen mode

And you can use the translation anywhere like:

import React from "react";
import { useI18n } from "@hi18n/react";
import { book } from "../locale";
export const Greeting: React.FC = () => {
  // Locale can be configured via
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

// 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,
});
Enter fullscreen mode Exit fullscreen mode
// 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}!"),
});
Enter fullscreen mode Exit fullscreen mode
// 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}さん!"),
});
Enter fullscreen mode Exit fullscreen mode

Then define a command for synchronizing translations and translation IDs:

// package.json
{
  "scripts": {
    "i18n:sync": "hi18n sync 'src/**/*.ts' 'src/**/*.tsx'"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you can start using hi18n.

If you are using ESLint, we recommend our 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).

// 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>
);
Enter fullscreen mode Exit fullscreen mode

Then use useI18n to start translating everywhere in the tree.

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: "太郎" })}</>;
};
Enter fullscreen mode Exit fullscreen mode

With <Translate>

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

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="太郎" />;
};
Enter fullscreen mode Exit fullscreen mode

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

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>;
};
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Define a task using package.json:

// package.json
{
  "scripts": {
    "i18n:sync": "hi18n sync 'src/**/*.ts' 'src/**/*.tsx'"
  }
}
Enter fullscreen mode Exit fullscreen mode

and use it to update your translations:

npm run i18n:sync

# Or:

yarn i18n:sync
Enter fullscreen mode Exit fullscreen mode

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>.

t.todo("example/new");
<Translate.Todo book={book} id="example/new" />;
Enter fullscreen mode Exit fullscreen mode

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 and translated to English by the author.

Top comments (0)