@k8ordo/i18n

テストする

文言やgetLocale()がどのロケールを読むかは、テストが動く環境によって変わります。Nodeで動くテストではサーバーと同じくlocales.runで、ブラウザで動くテストではURLでロケールを決めます。

このページの内容

Nodeで動くテスト

何も指名しなければ、文言は既定のロケールの文を返します。別のロケールで確かめたいときは、locales.runの中で呼びます。

messages/nav.test.ts
import { expect, it } from 'vitest';

import { locales } from '../i18n';
import * as nav from './nav';

it('renders in the default locale', () => {
  expect(nav.home()).toBe('ホーム');
});

it('renders in the locale run names', () => {
  expect(locales.run('en', () => nav.home())).toBe('Home');
});

it('keeps the locale across awaits', async () => {
  const text = await locales.run('en', async () => {
    await Promise.resolve();
    return nav.home();
  });
  expect(text).toBe('Home');
});

asyncの関数を渡しても、awaitをまたいでロケールは保たれます。同時に動くrunどうしが混ざることもありません。

paramsSchemaを確かめる

paramsSchemaがロケールを受け付けると、呼んだ非同期の流れの残り全体に、そのロケールが設定されます。フレームワークはパターンごとに流れを分けますが、テストから直接呼ぶと分かれません。

i18n.test.ts
import { expect, it } from 'vitest';

import { locales } from './i18n';

it('accepts only the listed locales', () => {
  const { validate } = locales.paramsSchema['~standard'];
  expect(
    locales.run('ja', () => validate({ locale: 'en' })),
  ).toStrictEqual({ value: { locale: 'en' } });
  expect(validate({ locale: 'fr' })).toMatchObject({
    issues: [{ path: ['locale'] }],
  });
});

そのため、受け付ける呼び出しはrunで囲みます。設定したロケールはrunを抜けると元に戻るので、後に続くテストに漏れません。一覧に無いロケールは受け付けないので、何も設定しません。

ブラウザで動くテスト

Vitestのブラウザモードのような本物のブラウザでは、URLがロケールです。history.replaceStateでpathnameを変え、終わったら元に戻します。

messages/nav.browser.test.ts
import { afterEach, expect, it } from 'vitest';

import { locales } from '../i18n';
import * as nav from './nav';

const initial = location.pathname;

afterEach(() => {
  history.replaceState(null, '', initial);
});

it('renders in the locale the URL names', () => {
  history.replaceState(null, '', '/en/cart');
  expect(locales.getLocale()).toBe('en');
  expect(nav.home()).toBe('Home');
});

ブラウザでは、runはエラーを投げます。jsdomやhappy-domのようにdocumentを定義する環境も、このパッケージにとってはブラウザなので、同じようにURLでロケールを決めます。

テストの中で集合を定義しない

テストの中でdefineLocalesを呼ぶと、それ以降の文言はその集合を読みます。最後に定義した集合が使われるためです。

テストでは、アプリのlocalesをimportして使います。別の集合を定義するテストがあるなら、beforeEachでアプリの集合を定義し直します。

ts
import { defineLocales } from '@k8ordo/i18n';
import { beforeEach } from 'vitest';

import { locales } from '../i18n';

beforeEach(() => {
  defineLocales(locales.definitions, { default: locales.default });
});

型の保証をテストに残す

文言の引数の型や、ロケールが欠けたときの型エラーは、型のテストで固定できます。

messages/cart.test.ts
import { message } from '@k8ordo/i18n';
import { expectTypeOf, it } from 'vitest';

import * as cart from './cart';

it('types the arguments of a message', () => {
  expectTypeOf(cart.added).parameters.toEqualTypeOf<
    [name: string]
  >();
});

it('refuses a message without every locale', () => {
  // @ts-expect-error
  message({ ja: '保存' });
});

@ts-expect-errorを付けた宣言は、ロケールが欠けて型エラーになる限り通ります。Registerの登録が外れて型エラーにならなくなると、型チェックが失敗します。

k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2