Storybookでnext-i18next対応
はじめに
多言語対応しているNext.jsのサービスにstorybookを導入しようとしたところ、next-i18next
から提供されているuseTranslation
を使用しているコンポーネントで以下のようなエラーが発生しました。
ERROR in ./node_modules/next-i18next/dist/esm/config/createConfig.js 100:15-28
Module not found: Error: Can't resolve 'fs' in '/home/umyomyomyon/git/next-i18next-storybook/node_modules/next-i18next/dist/esm/config'
こちらの対応方法にについて書いていきます。
動いてるコードだけ見られればいいという方はこちらのリポジトリをご覧ください。
現象
storybookで表示したいコンポーネント内でnext-i18next
のuseTranslate()
を使用していると以下のエラーが発生します。
ERROR in ./node_modules/next-i18next/dist/esm/config/createConfig.js 100:15-28
Module not found: Error: Can't resolve 'fs' in '/home/umyomyomyon/git/next-i18next-storybook/node_modules/next-i18next/dist/esm/config'
実際に再現するために以下のコンポーネントを使用します。
コンポーネント
import { FC } from "react";
import { useTranslation } from "next-i18next";
export const Button: FC = () => {
const { t } = useTranslation();
return <button>{t('click')}</button>;
};
story
import { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
const meta: Meta<typeof Button> = {
title: 'Button',
component: Button,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Default: Story = {};
翻訳ファイル
{
"click": "!CLICK!"
}
{
"click": "!クリック!"
}
上記のコンポーネント、story、翻訳ファイル用意してstorybookを起動するとエラーが発生すると思います。
なお、翻訳ファイルはnext-i18next
のデフォルト設定である以下のcommon.json
を使用するものとします。
.
└── public
└── locales
├── en
| └── common.json
└── ja
└── common.json
対策
storybook上でnext-i18next
のかわりにreact-i18next
使用することによってエラーを回避することができます。
以下のように.storybook/main.ts
でaliasを設定してnext-i18next
のかわりにreact-i18next
を読み込むようにします
また、staticDirs: ['../public']
を追加して翻訳ファイル(/public/locales/en/common.json
, /public/locales/ja/common.json
)を読み取れるようにします。
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
docs: {
autodocs: "tag",
},
+ webpack: (config) => {
+ if (config.resolve && config.resolve.alias) {
+ config.resolve.alias = {
+ ...config.resolve.alias,
+ 'next-i18next': 'react-i18next',
+ };
+ };
+ return config;
+ },
+ staticDirs: ['../public'],
};
export default config;
ここでstorybookを起動してみましょう。
すると、エラーが発生しないことが確認できると思います。
しかし、t('click')
に対応する!クリック!
または!CLICK!
の表示が期待されているにも関わらずclick
という表示のままです。
この原因はreact-i18next
の設定ができていないことにあるのでその設定を行います。
react-i18nextの設定
storybookで使用するreact-i18next
の設定を行っていきます。
まずi18next-http-backend
を入れます。
yarn add -D i18next-http-backend
.storybook/i18n.tsを作成
.storybook/i18n.ts
を新規作成して以下の内容を記述します。
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
i18n
.use(initReactI18next)
.use(Backend)
.init({
lng: 'ja',
fallbackLng: 'ja',
debug: true,
defaultNS: 'common',
ns: 'common',
supportedLngs: ['ja', 'en'],
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
});
export default i18n;
next-i18next
がデフォルトで使用する各言語翻訳ファイルは以下のようなっており
.
└── public
└── locales
├── en
| └── common.json
└── ja
└── common.json
backend.loadPath
を'/locales/{{lng}}/{{ns}}.json'
とすることでreact-i18next
でもnext-i18next
で使用する翻訳ファイルを読み取れるように設定しています。
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
decoratorを定義する
storybook上のすべてのコンポーネントに適用するdecoratorを定義します。
作成したi18n
をdecoratorを通してコンポーネントに提供します。
まず、.storybook/preview.ts
を.storybook/preview.tsx
にリネームします。
(拡張子を.ts
-> .tsx
に変更)
次に.storybook/preview.tsx
内でdecoratorを定義します。
+import React from "react";
import type { Preview } from "@storybook/react";
+import { I18nextProvider } from "react-i18next";
+import i18n from "./i18n";
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
+ decorators: [
+ (Story) => (
+ <I18nextProvider i18n={i18n}>
+ <Story />
+ </I18nextProvider>
+ )
+ ]
};
export default preview;
ここでstorybookを起動すると、ボタンには!クリック!
と表示されているはずです。
しかしこのままではlocaleを切り替えることができないですね。
localeを切り替えられるようにする
こちらを参考に、storybookの画面上からlocaleを切り替えられるようにします。
.storybook/preview.tsx
を以下のように編集します。
-import React from "react";
+import React, { useEffect } from "react";
import type { Preview } from "@storybook/react";
import { I18nextProvider } from "react-i18next";
import i18n from "./i18n";
+const withI18next = (Story, context) => {
+ const { locale } = context.globals;
+
+ useEffect(() => {
+ i18n.changeLanguage(locale);
+ }, [locale]);
+
+ return (
+ <I18nextProvider i18n={i18n}>
+ <Story />
+ </I18nextProvider>
+ );
+};
+
+export const globalTypes = {
+ locale: {
+ name: 'Locale',
+ description: 'Internationalization locale',
+ toolbar: {
+ icon: 'globe',
+ items: [
+ { value: 'ja', title: '日本語' },
+ { value: 'en', title: 'English' },
+ ],
+ showName: true,
+ },
+ },
+};
+
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
- decorators: [
- (Story) => (
- <I18nextProvider i18n={i18n}>
- <Story />
- </I18nextProvider>
- )
- ]
+ decorators: [withI18next]
};
export default preview;
以下の部分でlocale
というグローバル変数を宣言し、storybookにツールバーから変更できるようにしています。
export const globalTypes = {
locale: {
name: 'Locale',
description: 'Internationalization locale',
toolbar: {
icon: 'globe',
items: [
{ value: 'ja', title: '日本語' },
{ value: 'en', title: 'English' },
],
showName: true,
},
},
};
その上で、グローバルなlocale
が変更されたらI18nextProvider
に渡すi18n
のlocaleを変更するようなdecoratorを定義しています。
上記で定義したグローバルなlocaleはcontext.globals.locale
から取得することができます。
const withI18next = (Story, context) => {
const { locale } = context.globals;
useEffect(() => {
i18n.changeLanguage(locale);
}, [locale]);
return (
<I18nextProvider i18n={i18n}>
<Story />
</I18nextProvider>
);
};
ここまでで、storybookを起動するとツールバーにlocaleの変更ボタンが追加されており、locale切り替えもできることが確認できるのではないでしょうか。
Discussion