Shopifyが提供する多言語モジュールについて調べてみた
この記事は、Shopify開発を盛り上げる(Liquid, React, Node.js, Graph QL) Advent Calendar 2020の投稿です。 ShopifyにおけるReactの多言語化 React […]
目次
この記事は、Shopify開発を盛り上げる(Liquid, React, Node.js, Graph QL) Advent Calendar 2020の投稿です。
ShopifyにおけるReactの多言語化
Reactで多言語化と聞くと、react-intlを思い浮かべる方も多いと思います。が、Shopifyでは自前の多言語化モジュールを利用しています。
理由について、react-i18nライブラリのREADMEに記載がありますが、「コンポーネント毎に翻訳を提供したい」「非同期に翻訳ファイルを読み込みたい」など、Shopifyに組み込むにあたって必要だった要件とreact-intlに向いている要件がマッチしなかったことが伺えます。
react-i18nを使った多言語化
ライブラリは、yarn add @shopify/react-i18n
でインストールできます。
Providerの配置
また、Context APIを利用していますので、親要素でI18nContext.Provider
を配置する必要があります。
import {useI18n,I18nContext, I18nManager} from '@shopify/react-i18n';
export function Index() {
const i18nManager = new I18nManager({
locale: 'en'
})
return (
<I18nContext.Provider value={i18nManager}>
<ChildElements />
</I18nContext.Provider>
)
このサンプルでは、ChildElements
またはその子孫要素でreact-i18nのAPIが利用できます。
useI18nによるコンポーネント毎の多言語化
react-intlでは1つの辞書を共有する形で多言語化を実装します。一方でShopifyのreact-i18n
は、コンポーネント単位での多言語化します。
const en = {
"NotFound": {
"heading": "Page not found",
"action": "Back"
}
}
const ja = {
"NotFound": {
"heading": "ページが見つかりませんでした",
"action": "戻る"
}
}
function NotFound() {
const target = "NotFound.heading"
const [i18n] = useI18n({
id: 'NotFoundComponent',
fallback: en,
translations: () => en,
});
if (!i18n.translationKeyExists(target)) {
return <div>None</div>;
}
return (
<div>{i18n.translate(target)}</div>
)
}
react-i18nの特徴として、「翻訳するキーの有無をチェックできる」という点もあげることができます。上のサンプルでは、translationKeyExists
を使って翻訳の有無をチェックしてから翻訳処理を実行しています。react-intlの場合はdefaultMessageを設定する形でしたので、useMemoなどを組み合わせると似たようなことが可能と思われます。
const useTranslate = (target: string, fallbackMessage: string) => {
const [i18n] = useI18n({
id: target,
translations: (locale: string) => {
if (/ja/.test(locale)) return ja
return en;
},
});
const translatedText = useMemo(() => {
if (!i18n.translationKeyExists(target)) {
return fallbackMessage
}
return i18n.translate(target)
}, [i18n, target, fallbackMessage])
return translatedText
}
動的な翻訳
react-i18nの場合、翻訳のキーを動的にすることも可能です。以下の3つは全て同じ動きをします。もちろん変数も使えますので、細かく翻訳を制御できます。
i18n.translate("Polaris.Common.undo")
i18n.translate('undo', {
scope: 'Polaris.Common'
})
i18n.translate('undo', {
scope: ['Polaris', 'Common']
})
HTMLなどの挿入
翻訳辞書側に{プレースホルダー}を用意すると、HTMLタグなども挿入できます。Linkが使えるのは結構ありがたいです。
const ja = {
"NotFound": {
"heading": "ページが見つかりませんでした",
"action": "{link} に戻る",
...
<p>{
i18n.translate('NotFound.action', {
link: <Link>Home</Link>
})
}</p>
フォーマット系は通貨などもサポート
EC向けSaaSが作ったライブラリだけあって、通貨や週初めの取得といった通販で欲しそうな機能もサポートされています。
SSR対応
Next.jsなどのSSRを行うアプリケーションや、GatsbyのようにSSGを行うサイトの場合は@shopify/react-i18n-universal-provider
を使います。
import {I18nUniversalProvider} from '@shopify/react-i18n-universal-provider';
export function Index() {
return (
<I18nUniversalProvider locale="ja">
{/* App contents */}
</I18nUniversalProvider>
とはいえProviderを変えるだけで動きますので、知っていれば困ることもあまりなさそうです。
配布を前提とするならreact-18n、1つのアプリとして管理するならreact-intl
個人的な感想としては、コンポーネント単位で配布する想定のライブラリにはShopifyのreact-i18nがむいていると思います。配布想定の場合、コンポーネントレベルで翻訳を管理できる方が、そのライブラリを利用する側の辞書を更新せずとも新しい翻訳文を提供することができます。
Material UIのようにThemeProviderコンポーネントを提供するように実装し、その中に言語切り替え系のAPI(フック)とI18nContext.Providerコンポーネントを配置すると良いでしょう。
一方で1つのリポジトリの中にある、1つのアプリケーションを運用するという場合には、翻訳文章がコンポーネントレベルで分散してしまうreact-i18nは少し扱いにくいのではないかと思います。
ですので、「複数のサービスを運用し、それらで共通の自作ライブラリを利用している」というケースに該当したときには、このShopifyのreact-i18nを検討しても良さそうです。
余談: Polarisはこれ使ってない模様
と、いろいろ書いたのですが、Shopifyアプリ開発でお世話になるPolaris本体は@shopify/react-i18n
を使っていなかったりします。
ライブラリの中に自前でi18n系のファイルが配置されていますので、Polarisを使って開発する際にreact-i18nのドキュメントを見てしまうとちょっと混乱するかもしれません。実際しました。
とはいえ互換性がないわけでは無い様子で、react-i18nでPolarisの辞書をimportして使うことは可能そうです。
import enTranslations from '@shopify/polaris/locales/en.json';
import jaTranslations from '@shopify/polaris/locales/ja.json';
const App = () => {
const target = "Polaris.Common.undo"
const [i18n] = useI18n({
id: 'PolarisComponent',
translations: (locale: string) => {
if (/ja/.test(locale)) return jaTranslations
return enTranslations;
},
});
const translatedText = useMemo(() => {
if (!i18n.translationKeyExists(target)) {
return 'Fallback'
}
return i18n.translate(target)
}, [i18n, target])