Fluent が発表されてその説明を読んでいると React 用のバインディングがあることがわかります。
大規模開発の関心事(苦労)の一つとしてローカライゼーションがあり、それらがどういう思想・ API になっているか気になったので調査してみました。
Fluent とは
Fluent は Mozilla による新しいローカライゼーション( l10n )システムです。
公式ページによれば、英語との一対一対応で書くタイプの「時代遅れな」システムの縛りから脱出して、各言語の表現力を英語文法に制限されないよう活かすのが一つの目的のようです。
一対一で書くタイプのシステムの問題の例を挙げると、アプリケーション内の UI を英文との一対一対応で書く場合、動的に変わる部分では言語における性(仏語: chat blanc 白い雄猫 / chatte blanche 白い雌猫)や数(亜語: مُدَرِّس 一人の男性教師 / مُدَرِّسَانِ 二人の男性教師 / مُدَرِّسُونَ 複数の男性教師)、格(露語: Иван イワン / Ивана イワンの / etc. )を適切に活用したテキスト・コンテンツを書けないということがあります。
この問題を解決するために生まれた Fluent が持つ文法は FTL (Fluent Translation List) と呼ばれ、あたかもプログラミング言語の関数のような仕組みを持ちます。特に Selectors や Parameterized Terms はパターンマッチングであるかのようです。
詳しい Fluent 自体の解説や活用事例は、今後はおそらく記事が増えていくと信じているので、検索してそちらをご参照ください。
fluent-react とは
fluent-react は、公式の React 用ライブラリです。
公式による使い方の例は https://github.com/projectfluent/fluent.js/tree/master/fluent-react/examples に載っています。
fluent-react で新しく提供している機能は、おそらくフォールバックと「疑似タグ」です。
この記事で載せているコードはこちら。
使い方
準備
l10n リソースの準備は公式サンプルの l10n.js やこの記事向けの l10n.ts に書いたコードそのものですが、イテレータを使ったリソース取得を行います。ここでは generateBundles
という名前で定義しています。
Fetching Translations にあるように、サーバーから取ってくる機能はないので自ら書く必要があります。イテレータということで非同期の優先度付きリソース取得が可能そうです。
次に、 Redux と同じ要領で <LocalizationProvider bundles={generateBundles(navigator.languages)}>
でアプリケーションを囲ってリソースを末端につなげる準備をします。 navigator.languages
はブラウザから言語情報を取得するもので、このコードはアプリケーションはブラウザが指定する言語を利用することを意味します。ブラウザによらずユーザーが任意のタイミングで言語を切り替えられるようにするには、 React の state を利用して generateBundles
に渡すようにすれば良さそうです。(公式サンプルより)
リソースの利用
リソースを利用するところでは <Localized id="???">
を使います。内側( children
) にはフォールバックとなる React ノードを書いておきます。
6 | <Localized id="hello"> |
fluent-react の変なところとして、リソースの内容は <Localized>
直下に書いたコンポーネント・ HTML 要素の children
として注入されます。
上の例では、<h1>こんにちは、世界!</h1>
と描画されます。
直下が空だったり文字列だったりすると、直感に反して escape 済みの文字列だけ表示されます。これは現状の仕様だと思われますが、無理にタグを入れたくない場合でも Fragment を利用すれば回避できます。
Variables
リソースに含まれる Variables に対して値を当てはめるには、 <Localized>
に与えるプロパティにします。すべての型を受け入れますが、リソース側の想定に従うのが無難です。
28 | <Localized id="description2" $user="Bob" $gender="male"> |
プロパティ名は “$” を含めて書きます。
上の例では <>Bob 様に特別オファー。</>
と描画されます。( “en” では <>Special offer for Mr. Bob.</>
)
もし必要な Variable が与えられなかった場合は Variable 名自体かセレクタのデフォルト値が表示されます。
「疑似タグ」
特に SPA において言語毎にマークアップする箇所を変えたい場合、例えば「(一つの)とてもきれいな車」という文章で「とてもきれいな」を強調したいときの a very clean car と une voiture très propre のように文法的な違いが起因で強調すべき箇所・順番が違う場合、単純な key-value では複雑になるかリソース定義が断片化してしまいます。
FTL 自体の機能ではないようですが、 fluent-react の一機能として、こうした問題は XML のような「疑似タグ」を加えて実現できます。
22 | <Localized id="description1" special={<strong />}> |
ここでは <special>
という「疑似タグ」がリソースに入っていて、 <Localized>
には同じ名前のプロパティがコンポーネントを値として(ここでは単なる HTML タグだが…)に渡されています。「疑似タグ」はプロパティに入っているコンポーネントで置き換えられ、囲まれた内容がそのコンポーネントの children
となります。
上の例では、 ついに、<strong>世界中で</strong>大人気の Contoso が日本上陸。
となります。<special>
「疑似タグ」が <strong>
HTML タグに置き換わっています。
ただし、「疑似タグ」をネストしたり、 children
以外のプロパティに指定することはできない模様です。
もし必要なコンポーネントが与えられなかった場合は、「疑似タグ」だけが除かれた状態、例えば ついに、世界中で大人気の Contoso が日本上陸。
と描画されます。
所感
初めて fluent-react の API を見たときはこれを使っても大丈夫なのか心配になりましたが、 Hooks や Tagged Template Literal を使うとどうなるかを想像してみてもうまいアイデアは思い浮かばなかったので、今の API が少し改善されれば期待通りの活躍をしてくれそうです。
型が効かない点も面倒ですが、言語ごとに引数が違うことを考えたら実現しづらいので無理もありません。
結局のところは l10n 自体が難しいのだろうという感想に至りました。
心配なのは fluent-react 用の FTL を書くときで、フロントエンドエンジニアなら疑似タグの管理はいいでしょうけど、デザイナや翻訳者が編集するときも秩序が保たれるのでしょうか。
挿入されていない「疑似タグ」は消されてレンダリングされるものの、 FTL 文法の定義として決まっていない部分は(当然)実装によって挙動が違いそうなので、バックエンド側で l10n 後の文字列を生成したときにどうなるかはわかりません。
まとめ
fluent-react はまた正式バージョンに達していないこと、まだ Fluent がメジャーになっていないことから、今は試しに使う以上のことには難しそうです。ただしフィードバックを送る価値があるかもしれません。
嬉しいことに、プロダクトがまだ l10n 対応していない状況では、コード側もリソース側もそれを利用するよう漸進的に置き換えできそうな API になっています。
少なくとも、 SSR する場合やリソース単位での code-splitting みたいなことがしたい場合には、リソースの一貫性や管理の上で Fetching Translations が必須になります。こうした状況でのリソースの置き場や管理ツールなどをどうすると良いかという事例が多く報告されると、パフォーマンス面での懸念も減ります。